Files
geo/apps/desktop-client/src/renderer/views/TasksView.vue
T
root b59435dec5 refactor(desktop/renderer): centralize platform label and short-name lookup
Move the per-platform translation map out of HomeView/TasksView and the
titleCaseToken fallback out of PublishManagementView into shared helpers
on media-catalog. Single source of truth keeps renderer views consistent
with the binding catalog when new platforms ship.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 17:12:05 +08:00

332 lines
6.7 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 } from "../lib/formatters";
import { translateDesktopPlatform } from "../lib/media-catalog";
import { taskTone } from "../lib/runtime-ui";
function translatePlatform(platform: string) {
return translateDesktopPlatform(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>