From db777d04910aedae513a1d7f4bcd1f8e1e5d6aac Mon Sep 17 00:00:00 2001 From: yml Date: Wed, 27 May 2026 14:44:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=BB=E5=8A=A1=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/views/admin/tasks/AdminTaskDetail.css | 252 +++++++++++++++--- .../components/AdminTaskEventsSection.vue | 33 ++- .../tasks/components/AdminTaskHeroSection.vue | 3 +- .../AdminTaskKuaishouCloudPanel.vue | 127 ++++++++- 4 files changed, 347 insertions(+), 68 deletions(-) diff --git a/apps/frontend/src/views/admin/tasks/AdminTaskDetail.css b/apps/frontend/src/views/admin/tasks/AdminTaskDetail.css index 2ec66c8b..b1c38a58 100644 --- a/apps/frontend/src/views/admin/tasks/AdminTaskDetail.css +++ b/apps/frontend/src/views/admin/tasks/AdminTaskDetail.css @@ -66,43 +66,45 @@ } .hero-meta-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: var(--space-3); + display: flex; + flex-wrap: wrap; + gap: 1px; + background: var(--border-default); + border: 1px solid var(--border-default); + border-radius: var(--radius-md); + overflow: hidden; +} + +@media (max-width: 640px) { + .hero-meta-grid { + flex-direction: column; + } } .hero-metric { - padding: 14px var(--space-4); - border-radius: 18px; - border: 1px solid var(--border-default); - background: rgba(255, 255, 255, 0.82); + flex: 1; + min-width: 120px; + padding: 10px var(--space-4); + background: #fff; + display: flex; + align-items: center; + gap: var(--space-3); } .hero-metric span { display: block; - margin-bottom: var(--space-2); color: var(--text-secondary); font-size: var(--text-xs); + white-space: nowrap; + flex-shrink: 0; } .hero-metric strong { color: var(--text-primary); - font-size: var(--text-lg); + font-size: var(--text-sm); + font-weight: 600; line-height: 1.3; - word-break: break-word; -} - -.hero-metric--blue { - box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.08); -} -.hero-metric--green { - box-shadow: inset 0 0 0 1px rgba(16, 185, 129, 0.08); -} -.hero-metric--amber { - box-shadow: inset 0 0 0 1px rgba(245, 158, 11, 0.08); -} -.hero-metric--slate { - box-shadow: inset 0 0 0 1px rgba(100, 116, 139, 0.08); + word-break: break-all; } .hero-inline, @@ -372,35 +374,202 @@ gap: var(--space-3); } -.checklist-grid { +/* ===== Flow Timeline (Checklist) ===== */ +.flow-timeline { display: grid; - gap: var(--space-3); + gap: 0; margin-top: var(--space-4); + padding-left: 2px; } -.checklist-item { - padding: 14px var(--space-4); - border-radius: var(--radius-md); - background: rgba(255, 255, 255, 0.84); - border: 1px solid var(--border-default); +.flow-timeline-item { + display: grid; + grid-template-columns: 20px 1fr; + gap: 0 var(--space-3); + position: relative; } -.checklist-item-head { +/* Vertical connector line */ +.flow-timeline-item:not(:last-child) .timeline-gutter::after { + content: ''; + display: block; + position: absolute; + left: 9px; + top: 24px; + bottom: 0; + width: 1px; + background: var(--border-default); +} + +.timeline-gutter { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + padding-top: 14px; +} + +.timeline-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + z-index: 1; + background: var(--border-default); + margin-top: 4px; +} + +.flow-timeline-item--done .timeline-dot { + background: var(--text-muted); +} + +.flow-timeline-item--current .timeline-dot { + background: var(--text-primary); + width: 10px; + height: 10px; + margin-top: 3px; +} + +.flow-timeline-item--error .timeline-dot { + background: var(--color-danger); +} + +.timeline-body { + padding: 8px 0 14px; +} + +.timeline-head { display: flex; align-items: center; - justify-content: space-between; - gap: var(--space-3); - margin-bottom: var(--space-2); + gap: var(--space-2); + margin-bottom: 2px; } -.checklist-item-head span { +.timeline-label { + font-size: var(--text-xs); + font-weight: 500; color: var(--text-secondary); +} + +.flow-timeline-item--current .timeline-label { + color: var(--text-primary); + font-weight: 600; +} + +.timeline-current-badge { + font-size: 10px; + font-weight: 600; + padding: 0 5px; + border-radius: var(--radius-sm); + background: var(--bg-subtle); + color: var(--text-secondary); +} + +.timeline-detail { + color: var(--text-primary); + font-size: var(--text-sm); + line-height: 1.5; +} + +.flow-timeline-item--done .timeline-detail { + color: var(--text-muted); +} + +/* ===== Collapse Badge ===== */ +.collapse-title-wrap { + display: flex; + align-items: center; + gap: var(--space-2); +} + +.collapse-badge { + font-size: 10px; + font-weight: 500; + padding: 0 5px; + border-radius: var(--radius-sm); + background: rgba(100, 116, 139, 0.08); + color: var(--text-muted); +} + +/* ===== Event Timeline ===== */ +.event-timeline { + display: grid; + gap: 0; + padding-left: 2px; +} + +.event-tl-item { + display: grid; + grid-template-columns: 20px 1fr; + gap: 0 var(--space-3); + position: relative; +} + +.event-tl-item:not(:last-child) .event-tl-gutter::after { + content: ''; + display: block; + position: absolute; + left: 9px; + top: 24px; + bottom: 0; + width: 1px; + background: var(--border-default); +} + +.event-tl-gutter { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + padding-top: 14px; +} + +.event-tl-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--text-muted); + margin-top: 6px; + flex-shrink: 0; + z-index: 1; +} + +.event-tl-body { + padding: 8px 0 14px; +} + +.event-tl-type { + font-size: var(--text-xs); + font-weight: 500; + color: var(--text-secondary); + margin-bottom: 2px; +} + +.event-tl-payload { + color: var(--text-primary); + font-size: var(--text-sm); + line-height: 1.5; +} + +.event-tl-time { + margin-top: 2px; + color: var(--text-muted); font-size: var(--text-xs); } -.checklist-item strong { - color: var(--text-primary); - line-height: 1.5; +/* ===== Role Enhancements ===== */ +.role-verified-badge { + display: inline-block; + margin-top: var(--space-2); + font-size: var(--text-xs); + color: var(--text-muted); +} + +.role-pending-hint { + margin-top: var(--space-3); + color: var(--text-secondary); + font-size: var(--text-xs); + line-height: 1.6; } .role-hero { @@ -476,8 +645,11 @@ .flow-card-head, .flow-head-actions, .spotlight-head, - .spotlight-actions, - .checklist-item-head { + .spotlight-actions { display: grid; } + + .timeline-head { + flex-wrap: wrap; + } } diff --git a/apps/frontend/src/views/admin/tasks/components/AdminTaskEventsSection.vue b/apps/frontend/src/views/admin/tasks/components/AdminTaskEventsSection.vue index ca7dedf4..1b993374 100644 --- a/apps/frontend/src/views/admin/tasks/components/AdminTaskEventsSection.vue +++ b/apps/frontend/src/views/admin/tasks/components/AdminTaskEventsSection.vue @@ -13,23 +13,22 @@ defineProps() diff --git a/apps/frontend/src/views/admin/tasks/components/AdminTaskHeroSection.vue b/apps/frontend/src/views/admin/tasks/components/AdminTaskHeroSection.vue index 77cb98ab..4cf95fb5 100644 --- a/apps/frontend/src/views/admin/tasks/components/AdminTaskHeroSection.vue +++ b/apps/frontend/src/views/admin/tasks/components/AdminTaskHeroSection.vue @@ -80,12 +80,13 @@ function resolveCustomerStageLabel(status: string) { v-for="card in summaryCards" :key="card.label" shadow="never" - :class="['hero-metric', `hero-metric--${card.tone}`]" + class="hero-metric" > {{ card.label }} {{ card.value }} +
资源 {{ resolveResourceStageLabel(detail.task.resourceStatus) }} 客户 {{ resolveCustomerStageLabel(detail.task.customerStatus) }} diff --git a/apps/frontend/src/views/admin/tasks/components/AdminTaskKuaishouCloudPanel.vue b/apps/frontend/src/views/admin/tasks/components/AdminTaskKuaishouCloudPanel.vue index ac994b9b..e4a4fa55 100644 --- a/apps/frontend/src/views/admin/tasks/components/AdminTaskKuaishouCloudPanel.vue +++ b/apps/frontend/src/views/admin/tasks/components/AdminTaskKuaishouCloudPanel.vue @@ -58,6 +58,58 @@ function formatAccountNames(labels: string[] | undefined, keys: string[] | undef return fallbackKeys.length > 0 ? fallbackKeys.join(' / ') : '-' } + +/** 根据 status 决定每个流程步骤的 modifier class 和图标 */ +function resolveTimelineItemClass(status: string, isCurrent: boolean) { + if (isCurrent) return 'flow-timeline-item--current' + const s = String(status || '').trim() + if (['verified', 'ready', 'completed', 'done'].includes(s)) return 'flow-timeline-item--done' + if (['error', 'failed', 'exception', 'resource_exception'].includes(s)) + return 'flow-timeline-item--error' + return '' +} + + +/** 计算 checklist 中第一个「非完成」的步骤索引,即当前卡点 */ +function findCurrentStepIndex(checklist: ChecklistItem[]) { + const doneStatuses = ['verified', 'ready', 'completed', 'done'] + const idx = checklist.findIndex((item) => !doneStatuses.includes(String(item.status || '').trim())) + return idx +} + +/** Collapse badge 状态推导 */ +function resolveClaimBadge(flow: KuaishouCloudFlow, claimUrl: string) { + if (flow.ticket.verifiedAt && flow.binding.bindPreparedAt) return 'ok' + if (claimUrl) return 'pending' + return 'pending' +} + +function resolveTicketBadge(flow: KuaishouCloudFlow) { + if (flow.ticket.verifiedAt) return 'ok' + if (flow.ticket.code) return 'warn' + return 'pending' +} + +function resolveCloudBadge(flow: KuaishouCloudFlow) { + if (flow.dispatch.dispatchAt) return 'ok' + if (flow.binding.resolvedSourceKey) return 'pending' + return 'pending' +} + +function resolveClosureBadge(flow: KuaishouCloudFlow) { + if (flow.consume.consumedAt) return 'ok' + if (flow.consume.errorMessage) return 'warn' + return 'pending' +} + +function badgeLabel(tone: string) { + if (tone === 'ok') return '已完成' + if (tone === 'warn') return '需关注' + return '待处理' +} + +const roleHasIdentity = (flow: KuaishouCloudFlow) => + !!(flow.role?.name || flow.role?.rid)