优化任务详情界面

This commit is contained in:
yml
2026-05-27 14:44:58 +08:00
parent 837e1c6e66
commit db777d0491
4 changed files with 347 additions and 68 deletions
@@ -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;
}
}
@@ -13,23 +13,22 @@ defineProps<Props>()
<template>
<el-card shadow="never" class="section-card">
<h3>任务事件</h3>
<el-table :data="detail.events" size="small">
<el-table-column label="事件" width="180">
<template #default="{ row: event }">
{{ formatTaskEventType(event.eventType) }}
</template>
</el-table-column>
<el-table-column label="内容">
<template #default="{ row: event }">
<div class="event-copy">
{{ formatTaskEventPayload(event.payload) }}
</div>
<div class="event-time">
{{ formatAdminDateTime(event.createdAt) }}
</div>
</template>
</el-table-column>
</el-table>
<div class="event-timeline">
<div
v-for="event in detail.events"
:key="event.eventType + event.createdAt"
class="event-tl-item"
>
<div class="event-tl-gutter">
<div class="event-tl-dot"></div>
</div>
<div class="event-tl-body">
<div class="event-tl-type">{{ formatTaskEventType(event.eventType) }}</div>
<div class="event-tl-payload">{{ formatTaskEventPayload(event.payload) }}</div>
<div class="event-tl-time">{{ formatAdminDateTime(event.createdAt) }}</div>
</div>
</div>
</div>
</el-card>
</template>
@@ -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"
>
<span>{{ card.label }}</span>
<strong>{{ card.value }}</strong>
</el-card>
</div>
<div class="hero-inline">
<span>资源 {{ resolveResourceStageLabel(detail.task.resourceStatus) }}</span>
<span>客户 {{ resolveCustomerStageLabel(detail.task.customerStatus) }}</span>
@@ -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)
</script>
<template>
@@ -98,6 +150,7 @@ function formatAccountNames(labels: string[] | undefined, keys: string[] | undef
</div>
<div class="kuaishou-focus-grid">
<!-- 客户角色卡片 -->
<section class="flow-block flow-block--spotlight">
<div class="spotlight-head">
<div>
@@ -133,30 +186,60 @@ function formatAccountNames(labels: string[] | undefined, keys: string[] | undef
<strong>{{ formatAdminDateTime(flow.role?.refreshedAt) }}</strong>
</div>
</div>
<!-- 已识别徽章 -->
<div v-if="roleHasIdentity(flow)" class="role-verified-badge">
角色已识别
</div>
<!-- 等待识别引导 -->
<div v-else class="role-pending-hint">
客户完成绑定后点击右上角刷新按钮获取角色信息确认角色名与 ID 无误后再执行发货
</div>
<p v-if="flow.role?.errorMessage" class="role-warning">
{{ flow.role.errorMessage }}
</p>
</section>
<!-- 流程进度 Timeline -->
<section class="flow-block flow-block--checklist">
<h4>流程进度</h4>
<p class="flow-copy">
当前任务只需要按这个顺序推进异常时再展开下方明细
</p>
<div class="checklist-grid">
<div v-for="item in checklist" :key="item.key" class="checklist-item">
<div class="checklist-item-head">
<span>{{ item.label }}</span>
<AdminStatusTag :status="item.status" />
<div class="flow-timeline">
<div
v-for="(item, idx) in checklist"
:key="item.key"
class="flow-timeline-item"
:class="resolveTimelineItemClass(item.status, idx === findCurrentStepIndex(checklist))"
>
<div class="timeline-gutter">
<div class="timeline-dot"></div>
</div>
<div class="timeline-body">
<div class="timeline-head">
<span class="timeline-label">{{ item.label }}</span>
<span
v-if="idx === findCurrentStepIndex(checklist)"
class="timeline-current-badge"
>当前</span>
</div>
<div class="timeline-detail">{{ item.detail }}</div>
</div>
<strong>{{ item.detail }}</strong>
</div>
</div>
</section>
</div>
<el-collapse :model-value="collapsePanels" class="flow-collapse">
<el-collapse-item name="claim" title="客户领取与绑定明细">
<el-collapse-item name="claim">
<template #title>
<span class="collapse-title-wrap">
客户领取与绑定明细
<span class="collapse-badge">
{{ badgeLabel(resolveClaimBadge(flow, claimUrl)) }}
</span>
</span>
</template>
<div class="flow-meta-list flow-meta-list--two-column">
<div>
<span>客户链接</span><strong>{{ claimUrl || '-' }}</strong>
@@ -189,7 +272,15 @@ function formatAccountNames(labels: string[] | undefined, keys: string[] | undef
</p>
</el-collapse-item>
<el-collapse-item name="ticket" title="核销与商品明细">
<el-collapse-item name="ticket">
<template #title>
<span class="collapse-title-wrap">
核销与商品明细
<span class="collapse-badge">
{{ badgeLabel(resolveTicketBadge(flow)) }}
</span>
</span>
</template>
<div class="flow-meta-list flow-meta-list--two-column">
<div>
<span>核销码</span><strong>{{ flow.ticket.code || '-' }}</strong>
@@ -209,7 +300,15 @@ function formatAccountNames(labels: string[] | undefined, keys: string[] | undef
</p>
</el-collapse-item>
<el-collapse-item name="cloud" title="Cloud 资源与发货明细">
<el-collapse-item name="cloud">
<template #title>
<span class="collapse-title-wrap">
Cloud 资源与发货明细
<span class="collapse-badge">
{{ badgeLabel(resolveCloudBadge(flow)) }}
</span>
</span>
</template>
<div class="flow-meta-list flow-meta-list--two-column">
<div>
<span>账号优先级</span>
@@ -257,7 +356,15 @@ function formatAccountNames(labels: string[] | undefined, keys: string[] | undef
</div>
</el-collapse-item>
<el-collapse-item name="closure" title="流程收口">
<el-collapse-item name="closure">
<template #title>
<span class="collapse-title-wrap">
流程收口
<span class="collapse-badge">
{{ badgeLabel(resolveClosureBadge(flow)) }}
</span>
</span>
</template>
<div class="flow-meta-list flow-meta-list--two-column">
<div>
<span>退号时间</span