Files
geo/apps/desktop-client/src/renderer/views/TasksView.vue
T
root 162abdc97c
Backend CI / Backend (push) Has been cancelled
Frontend CI / Frontend (push) Failing after 1m39s
chore(frontend): introduce prettier + eslint and prune unused code
- Add Prettier 3 with prettier-plugin-organize-imports (sorts/removes unused imports)
- Add ESLint 10 flat config with typescript-eslint + eslint-plugin-vue + eslint-config-prettier
- Add root scripts: format, format:check, lint, lint:fix
- Reformat 257 files across admin-web, ops-web, desktop-client, packages
- Remove unused locals/exports flagged by --noUnusedLocals/--noUnusedParameters
- Fix duplicate localTabLabel key, surrogate-pair regex u-flag, NBSP literal in regex
- Skip server/ (Go) and apps/browser-extension/ (deprecated per ADR)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:39:09 +08:00

335 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>