Files
geo/apps/desktop-client/src/renderer/views/TasksView.vue
T
root dc68ad044c refactor(publish): collapse pending_review/unknown task states into failed
Treat ambiguous terminal states as failures end-to-end: server
normalizes unknown desktop task completions and views to failed, drops
the pending_review aggregate branch, and remaps pending_review to
failed in publish record conversion. admin-web removes the
pending_review status option from article list filters, pushes
pending_review through the failed tone, and folds lingering
publishing/pending entries under the publishing bucket. desktop-client
relabels unknown as 发送失败 in PublishManagement/TasksView, rewrites
the scaffold adapter result to a proper failed error envelope, and
sanitises legacy scaffold-only error messages to a user-friendly note.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:44:47 +08:00

341 lines
6.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed } from "vue";
import StatusBadge from "../components/StatusBadge.vue";
import SurfaceCard from "../components/SurfaceCard.vue";
import { useDesktopRuntime } from "../composables/useDesktopRuntime";
import { formatRelativeTime, titleCaseToken } from "../lib/formatters";
import { taskTone } from "../lib/runtime-ui";
function translatePlatform(platform: string) {
const map: Record<string, string> = {
toutiaohao: "头条号",
bilibili: "哔哩哔哩",
xiaohongshu: "小红书",
douyin: "抖音",
kuaishou: "快手",
wechat_oa: "微信公众号",
baijiahao: "百家号",
zhihu: "知乎",
};
return map[platform?.toLowerCase()] || titleCaseToken(platform);
}
function translateTaskStatus(status: string) {
const map: Record<string, string> = {
queued: "等待排队",
in_progress: "执行中",
succeeded: "成功",
failed: "失败",
unknown: "失败",
aborted: "已取消",
};
return map[status?.toLowerCase()] || status;
}
const { snapshot } = useDesktopRuntime();
const columns = computed(() => {
const tasks = snapshot.value?.tasks ?? [];
return [
{
key: "queued",
title: "等待派发 (Queued)",
description: "已经落库,等待 RabbitMQ 唤醒或客户端主动 lease。",
items: tasks.filter((item) => item.status === "queued"),
},
{
key: "in_progress",
title: "正在执行 (In Progress)",
description: "当前被 client 持有活动租约,结果回写必须带 lease_token。",
items: tasks.filter((item) => item.status === "in_progress"),
},
{
key: "exception",
title: "异常与终态 (Terminal)",
description: "failed、succeeded、aborted 等终态任务。",
items: tasks.filter((item) => !["queued", "in_progress"].includes(item.status)),
},
];
});
</script>
<template>
<section class="page">
<header class="hero">
<div class="hero-copy">
<p class="eyebrow">WORKFLOW CENTER</p>
<h2>任务调度中心</h2>
<p class="hero-summary">
基于 lease 的可靠端云协议泳道这里更偏底层调度视角用来观察 MQ 下发租约占用与 fallback 消费的运行阶段
</p>
</div>
</header>
<div class="lane-grid">
<SurfaceCard
v-for="column in columns"
:key="column.key"
eyebrow="Task Lane"
:title="column.title"
:description="column.description"
>
<div class="lane-stack" :class="{ 'scrollable-lane': column.items.length > 0 }">
<article v-for="task in column.items" :key="task.id" class="task-card">
<div class="task-head">
<div class="task-identity-copy">
<strong>{{ task.title }}</strong>
<p>{{ task.accountName }} · {{ translatePlatform(task.platform) }}</p>
</div>
<StatusBadge :tone="taskTone(task.status)" :label="translateTaskStatus(task.status)" class="shrink-badge" />
</div>
<div class="task-flags">
<StatusBadge tone="brand" :label="task.routing" subtle />
<StatusBadge tone="info" :label="task.kind" subtle />
</div>
<p class="task-summary">{{ task.summary }}</p>
<footer class="task-footer">
<span>
{{
task.leaseExpiresAt
? `${formatRelativeTime(task.leaseExpiresAt)} 过期`
: "已释放"
}}
</span>
<small>{{ formatRelativeTime(task.updatedAt) }}</small>
</footer>
</article>
<p v-if="column.items.length === 0" class="empty-text">当前泳道为空</p>
</div>
</SurfaceCard>
</div>
</section>
</template>
<style scoped>
.page,
.lane-grid {
display: grid;
gap: 18px;
}
.lane-stack {
display: flex;
flex-direction: column;
gap: 16px;
}
.scrollable-lane {
max-height: 480px;
overflow-y: auto;
padding-right: 6px;
}
.scrollable-lane::-webkit-scrollbar {
width: 6px;
}
.scrollable-lane::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 999px;
}
.hero {
display: block;
}
.hero-copy {
padding: 24px;
border-radius: 12px;
border: 1px solid #e6edf5;
background: #ffffff;
}
.eyebrow,
.hero-summary,
h2 {
margin: 0;
}
.eyebrow {
color: #8c8c8c;
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
}
h2 {
margin-top: 8px;
font-size: 24px;
font-weight: 600;
color: #1a1a1a;
line-height: 1.4;
}
.hero-summary {
margin-top: 12px;
max-width: 800px;
color: #8c8c8c;
line-height: 1.6;
font-size: 13px;
}
.lane-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.task-card {
padding: 16px;
border-radius: 8px;
border: 1px solid #e6edf5;
background: #fafafb;
transition: all 0.2s;
}
.task-card:hover {
background: #ffffff;
border-color: #91caff;
}
.task-head,
.task-footer {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.task-identity-copy {
flex: 1;
min-width: 0;
padding-right: 12px;
}
.task-head strong {
display: block;
font-size: 14px;
font-weight: 600;
color: #1a1a1a;
word-wrap: break-word;
}
.shrink-badge {
flex-shrink: 0;
}
.task-head p,
.task-summary {
margin: 6px 0 0;
color: #8c8c8c;
line-height: 1.6;
font-size: 13px;
}
.task-flags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.task-summary {
margin-top: 14px;
}
.task-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 16px;
}
.action-button {
border: 1px solid transparent;
border-radius: 6px;
padding: 6px 14px;
font-size: 12px;
cursor: pointer;
background: #ffffff;
transition: all 0.2s;
}
.action-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.action-button.secondary {
border-color: #d9d9d9;
color: #595959;
}
.action-button.secondary:hover:not(:disabled) {
border-color: #1677ff;
color: #1677ff;
}
.action-button.success {
border-color: #52c41a;
color: #52c41a;
}
.action-button.success:hover:not(:disabled) {
background: #f6ffed;
}
.action-button.danger {
border-color: #ff4d4f;
color: #ff4d4f;
}
.action-button.danger:hover:not(:disabled) {
background: #fff2f0;
}
.task-footer {
margin-top: 16px;
color: #8c8c8c;
font-size: 12px;
}
.empty-text {
margin: 0;
color: #8c8c8c;
font-size: 13px;
}
.error-text {
margin: 8px 0 0;
color: #b91c1c;
}
html[data-theme="dark"] .task-card {
background: rgba(255, 255, 255, 0.03);
}
html[data-theme="dark"] .action-button.secondary {
background: rgba(255, 255, 255, 0.08);
}
html[data-theme="dark"] .action-button.success {
color: #6ee7b7;
}
html[data-theme="dark"] .action-button.danger {
color: #fca5a5;
}
@media (max-width: 1120px) {
.hero,
.lane-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.task-head,
.task-footer {
flex-direction: column;
}
}
</style>