优化任务详情界面

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 { .hero-meta-grid {
display: grid; display: flex;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); flex-wrap: wrap;
gap: var(--space-3); 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 { .hero-metric {
padding: 14px var(--space-4); flex: 1;
border-radius: 18px; min-width: 120px;
border: 1px solid var(--border-default); padding: 10px var(--space-4);
background: rgba(255, 255, 255, 0.82); background: #fff;
display: flex;
align-items: center;
gap: var(--space-3);
} }
.hero-metric span { .hero-metric span {
display: block; display: block;
margin-bottom: var(--space-2);
color: var(--text-secondary); color: var(--text-secondary);
font-size: var(--text-xs); font-size: var(--text-xs);
white-space: nowrap;
flex-shrink: 0;
} }
.hero-metric strong { .hero-metric strong {
color: var(--text-primary); color: var(--text-primary);
font-size: var(--text-lg); font-size: var(--text-sm);
font-weight: 600;
line-height: 1.3; line-height: 1.3;
word-break: break-word; word-break: break-all;
}
.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);
} }
.hero-inline, .hero-inline,
@@ -372,35 +374,202 @@
gap: var(--space-3); gap: var(--space-3);
} }
.checklist-grid { /* ===== Flow Timeline (Checklist) ===== */
.flow-timeline {
display: grid; display: grid;
gap: var(--space-3); gap: 0;
margin-top: var(--space-4); margin-top: var(--space-4);
padding-left: 2px;
} }
.checklist-item { .flow-timeline-item {
padding: 14px var(--space-4); display: grid;
border-radius: var(--radius-md); grid-template-columns: 20px 1fr;
background: rgba(255, 255, 255, 0.84); gap: 0 var(--space-3);
border: 1px solid var(--border-default); 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; display: flex;
align-items: center; align-items: center;
justify-content: space-between; gap: var(--space-2);
gap: var(--space-3); margin-bottom: 2px;
margin-bottom: var(--space-2);
} }
.checklist-item-head span { .timeline-label {
font-size: var(--text-xs);
font-weight: 500;
color: var(--text-secondary); 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); font-size: var(--text-xs);
} }
.checklist-item strong { /* ===== Role Enhancements ===== */
color: var(--text-primary); .role-verified-badge {
line-height: 1.5; 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 { .role-hero {
@@ -476,8 +645,11 @@
.flow-card-head, .flow-card-head,
.flow-head-actions, .flow-head-actions,
.spotlight-head, .spotlight-head,
.spotlight-actions, .spotlight-actions {
.checklist-item-head {
display: grid; display: grid;
} }
.timeline-head {
flex-wrap: wrap;
}
} }
@@ -13,23 +13,22 @@ defineProps<Props>()
<template> <template>
<el-card shadow="never" class="section-card"> <el-card shadow="never" class="section-card">
<h3>任务事件</h3> <h3>任务事件</h3>
<el-table :data="detail.events" size="small"> <div class="event-timeline">
<el-table-column label="事件" width="180"> <div
<template #default="{ row: event }"> v-for="event in detail.events"
{{ formatTaskEventType(event.eventType) }} :key="event.eventType + event.createdAt"
</template> class="event-tl-item"
</el-table-column> >
<el-table-column label="内容"> <div class="event-tl-gutter">
<template #default="{ row: event }"> <div class="event-tl-dot"></div>
<div class="event-copy"> </div>
{{ formatTaskEventPayload(event.payload) }} <div class="event-tl-body">
</div> <div class="event-tl-type">{{ formatTaskEventType(event.eventType) }}</div>
<div class="event-time"> <div class="event-tl-payload">{{ formatTaskEventPayload(event.payload) }}</div>
{{ formatAdminDateTime(event.createdAt) }} <div class="event-tl-time">{{ formatAdminDateTime(event.createdAt) }}</div>
</div> </div>
</template> </div>
</el-table-column> </div>
</el-table>
</el-card> </el-card>
</template> </template>
@@ -80,12 +80,13 @@ function resolveCustomerStageLabel(status: string) {
v-for="card in summaryCards" v-for="card in summaryCards"
:key="card.label" :key="card.label"
shadow="never" shadow="never"
:class="['hero-metric', `hero-metric--${card.tone}`]" class="hero-metric"
> >
<span>{{ card.label }}</span> <span>{{ card.label }}</span>
<strong>{{ card.value }}</strong> <strong>{{ card.value }}</strong>
</el-card> </el-card>
</div> </div>
<div class="hero-inline"> <div class="hero-inline">
<span>资源 {{ resolveResourceStageLabel(detail.task.resourceStatus) }}</span> <span>资源 {{ resolveResourceStageLabel(detail.task.resourceStatus) }}</span>
<span>客户 {{ resolveCustomerStageLabel(detail.task.customerStatus) }}</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(' / ') : '-' 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> </script>
<template> <template>
@@ -98,6 +150,7 @@ function formatAccountNames(labels: string[] | undefined, keys: string[] | undef
</div> </div>
<div class="kuaishou-focus-grid"> <div class="kuaishou-focus-grid">
<!-- 客户角色卡片 -->
<section class="flow-block flow-block--spotlight"> <section class="flow-block flow-block--spotlight">
<div class="spotlight-head"> <div class="spotlight-head">
<div> <div>
@@ -133,30 +186,60 @@ function formatAccountNames(labels: string[] | undefined, keys: string[] | undef
<strong>{{ formatAdminDateTime(flow.role?.refreshedAt) }}</strong> <strong>{{ formatAdminDateTime(flow.role?.refreshedAt) }}</strong>
</div> </div>
</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"> <p v-if="flow.role?.errorMessage" class="role-warning">
{{ flow.role.errorMessage }} {{ flow.role.errorMessage }}
</p> </p>
</section> </section>
<!-- 流程进度 Timeline -->
<section class="flow-block flow-block--checklist"> <section class="flow-block flow-block--checklist">
<h4>流程进度</h4> <h4>流程进度</h4>
<p class="flow-copy"> <p class="flow-copy">
当前任务只需要按这个顺序推进异常时再展开下方明细 当前任务只需要按这个顺序推进异常时再展开下方明细
</p> </p>
<div class="checklist-grid"> <div class="flow-timeline">
<div v-for="item in checklist" :key="item.key" class="checklist-item"> <div
<div class="checklist-item-head"> v-for="(item, idx) in checklist"
<span>{{ item.label }}</span> :key="item.key"
<AdminStatusTag :status="item.status" /> 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> </div>
<strong>{{ item.detail }}</strong>
</div> </div>
</div> </div>
</section> </section>
</div> </div>
<el-collapse :model-value="collapsePanels" class="flow-collapse"> <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 class="flow-meta-list flow-meta-list--two-column">
<div> <div>
<span>客户链接</span><strong>{{ claimUrl || '-' }}</strong> <span>客户链接</span><strong>{{ claimUrl || '-' }}</strong>
@@ -189,7 +272,15 @@ function formatAccountNames(labels: string[] | undefined, keys: string[] | undef
</p> </p>
</el-collapse-item> </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 class="flow-meta-list flow-meta-list--two-column">
<div> <div>
<span>核销码</span><strong>{{ flow.ticket.code || '-' }}</strong> <span>核销码</span><strong>{{ flow.ticket.code || '-' }}</strong>
@@ -209,7 +300,15 @@ function formatAccountNames(labels: string[] | undefined, keys: string[] | undef
</p> </p>
</el-collapse-item> </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 class="flow-meta-list flow-meta-list--two-column">
<div> <div>
<span>账号优先级</span> <span>账号优先级</span>
@@ -257,7 +356,15 @@ function formatAccountNames(labels: string[] | undefined, keys: string[] | undef
</div> </div>
</el-collapse-item> </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 class="flow-meta-list flow-meta-list--two-column">
<div> <div>
<span>退号时间</span <span>退号时间</span