feat(@vben/web-antd): 工单详情取消状态进度条改造及代码质量优化
- 取消状态下进度条展示 timeline 实际节点,末尾追加"已取消"终态 - 已走过节点灰色实心,取消终态节点深灰,进度线灰色 - 移除底部红色 cancelled-banner,改为进度条内联展示 - 进行中节点蓝色标签改为显示到达时间 - 提取 isCurrentActive / getStepTime 简化模板逻辑 - timeline 去重防御异常数据,取消终态改用索引判断 - 移除 progress-steps-wrapper 的 overflow:hidden 避免裁切光晕 - 清理无引用的 cancelled-banner CSS Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -343,8 +343,51 @@ const orderImages = computed(() => {
|
||||
return images;
|
||||
});
|
||||
|
||||
/** 是否已取消 */
|
||||
const isCancelled = computed(() => order.value.status === 'CANCELLED');
|
||||
|
||||
/** 是否为当前进行中(非终态) */
|
||||
const isCurrentActive = computed(
|
||||
() =>
|
||||
!isCancelled.value &&
|
||||
!['COMPLETED', 'CANCELLED'].includes(order.value.status),
|
||||
);
|
||||
|
||||
/** 根据 step.key 查找 timeline 中对应的时间 */
|
||||
function getStepTime(key: string): string {
|
||||
const t = timeline.value.find((item) => item.status === key);
|
||||
if (t?.time) return formatRelativeTime(t.time);
|
||||
if (key === 'PENDING') return formatRelativeTime(order.value.createTime);
|
||||
if (key === 'COMPLETED' && order.value.endTime)
|
||||
return formatRelativeTime(order.value.endTime);
|
||||
return '';
|
||||
}
|
||||
|
||||
/** 动态生成应该显示的状态步骤(根据 timeline 过滤) */
|
||||
const visibleSteps = computed(() => {
|
||||
// 取消状态:只展示 timeline 中已发生的节点(按时间顺序,去重)
|
||||
if (isCancelled.value) {
|
||||
const seen = new Set<string>();
|
||||
return timeline.value
|
||||
.filter((t) => {
|
||||
if (seen.has(t.status)) return false;
|
||||
seen.add(t.status);
|
||||
return true;
|
||||
})
|
||||
.map((t) => {
|
||||
const step = STATUS_STEPS.find((s) => s.key === t.status);
|
||||
return {
|
||||
key: t.status,
|
||||
title:
|
||||
t.status === 'CANCELLED'
|
||||
? '已取消'
|
||||
: (step?.title || t.statusName || t.status),
|
||||
icon: step?.icon || 'solar:close-circle-bold-duotone',
|
||||
desc: t.description || step?.desc || '',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// 必须显示的节点(主流程)
|
||||
const requiredSteps = new Set([
|
||||
'ARRIVED',
|
||||
@@ -377,7 +420,10 @@ const visibleSteps = computed(() => {
|
||||
|
||||
/** 计算当前状态步骤索引 */
|
||||
const currentStepIndex = computed(() => {
|
||||
if (order.value.status === 'CANCELLED') return -1;
|
||||
// 取消状态:所有节点都视为"已走过",索引指向最后一个
|
||||
if (isCancelled.value) {
|
||||
return visibleSteps.value.length - 1;
|
||||
}
|
||||
if (order.value.status === 'PAUSED') {
|
||||
// 暂停状态显示在到岗后
|
||||
return visibleSteps.value.findIndex((s) => s.key === 'ARRIVED');
|
||||
@@ -625,10 +671,15 @@ onUnmounted(stopPolling);
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-base font-semibold">{{ order.title }}</span>
|
||||
<!-- 工单类型标签 -->
|
||||
<Tag
|
||||
v-if="order.orderType"
|
||||
:color="ORDER_TYPE_COLOR_MAP[order.orderType]"
|
||||
class="status-tag"
|
||||
<span
|
||||
v-if="
|
||||
order.orderType && ORDER_TYPE_COLOR_MAP[order.orderType]
|
||||
"
|
||||
class="status-tag inline-flex items-center rounded px-2 py-0.5 text-xs font-medium"
|
||||
:style="{
|
||||
backgroundColor: ORDER_TYPE_COLOR_MAP[order.orderType]?.bg,
|
||||
color: ORDER_TYPE_COLOR_MAP[order.orderType]?.text,
|
||||
}"
|
||||
>
|
||||
<IconifyIcon
|
||||
:icon="
|
||||
@@ -641,18 +692,25 @@ onUnmounted(stopPolling);
|
||||
class="mr-1"
|
||||
/>
|
||||
{{ ORDER_TYPE_TEXT_MAP[order.orderType] }}
|
||||
</Tag>
|
||||
</span>
|
||||
<!-- 状态标签 -->
|
||||
<Tag :color="STATUS_COLOR_MAP[order.status]" class="status-tag">
|
||||
<span
|
||||
v-if="STATUS_COLOR_MAP[order.status]"
|
||||
class="status-tag inline-flex items-center rounded px-2 py-0.5 text-xs font-medium"
|
||||
:style="{
|
||||
backgroundColor: STATUS_COLOR_MAP[order.status]?.bg,
|
||||
color: STATUS_COLOR_MAP[order.status]?.text,
|
||||
}"
|
||||
>
|
||||
<IconifyIcon
|
||||
:icon="STATUS_ICON_MAP[order.status] || 'solar:circle-bold'"
|
||||
class="mr-1"
|
||||
/>
|
||||
{{ STATUS_TEXT_MAP[order.status] }}
|
||||
</Tag>
|
||||
</span>
|
||||
<!-- 优先级标签 -->
|
||||
<span
|
||||
v-if="order.priority != null"
|
||||
v-if="order.priority !== null && order.priority !== undefined"
|
||||
class="priority-badge inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium"
|
||||
:class="{
|
||||
'animate-pulse': Number(order.priority) === 0,
|
||||
@@ -789,7 +847,10 @@ onUnmounted(stopPolling);
|
||||
<div class="progress-line">
|
||||
<div
|
||||
class="progress-line-fill"
|
||||
:class="{ 'line-all-completed': order.status === 'COMPLETED' }"
|
||||
:class="{
|
||||
'line-all-completed': order.status === 'COMPLETED',
|
||||
'line-cancelled': isCancelled,
|
||||
}"
|
||||
:style="{
|
||||
width: `${Math.max(0, (currentStepIndex / (visibleSteps.length - 1)) * 100)}%`,
|
||||
}"
|
||||
@@ -800,14 +861,20 @@ onUnmounted(stopPolling);
|
||||
:key="step.key"
|
||||
class="progress-node"
|
||||
:class="{
|
||||
'node-completed': index < currentStepIndex,
|
||||
'node-completed':
|
||||
!isCancelled && index < currentStepIndex,
|
||||
'node-current':
|
||||
index === currentStepIndex &&
|
||||
!['CANCELLED', 'COMPLETED'].includes(order.status),
|
||||
isCurrentActive && index === currentStepIndex,
|
||||
'node-completed-current':
|
||||
index === currentStepIndex && order.status === 'COMPLETED',
|
||||
'node-pending': index > currentStepIndex,
|
||||
'node-cancelled': order.status === 'CANCELLED',
|
||||
!isCancelled &&
|
||||
index === currentStepIndex &&
|
||||
order.status === 'COMPLETED',
|
||||
'node-pending':
|
||||
!isCancelled && index > currentStepIndex,
|
||||
'node-cancelled-done':
|
||||
isCancelled && index < currentStepIndex,
|
||||
'node-cancelled-end':
|
||||
isCancelled && index === currentStepIndex,
|
||||
}"
|
||||
>
|
||||
<Tooltip :title="step.desc">
|
||||
@@ -816,46 +883,28 @@ onUnmounted(stopPolling);
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div class="node-label">{{ step.title }}</div>
|
||||
<!-- 已到达节点的时间(进行中节点除外,由 badge 显示) -->
|
||||
<div
|
||||
v-if="
|
||||
index < currentStepIndex ||
|
||||
(index === currentStepIndex && order.status === 'COMPLETED')
|
||||
index <= currentStepIndex &&
|
||||
!(isCurrentActive && index === currentStepIndex)
|
||||
"
|
||||
class="node-time"
|
||||
>
|
||||
{{
|
||||
timeline.find((t) => t.status === step.key)?.time
|
||||
? formatRelativeTime(
|
||||
timeline.find((t) => t.status === step.key)?.time || '',
|
||||
)
|
||||
: step.key === 'PENDING'
|
||||
? formatRelativeTime(order.createTime)
|
||||
: step.key === 'COMPLETED' && order.endTime
|
||||
? formatRelativeTime(order.endTime)
|
||||
: ''
|
||||
}}
|
||||
{{ getStepTime(step.key) }}
|
||||
</div>
|
||||
<!-- 当前进行中节点:蓝色标签显示时间 -->
|
||||
<div
|
||||
v-else-if="
|
||||
index === currentStepIndex &&
|
||||
order.status !== 'CANCELLED' &&
|
||||
order.status !== 'COMPLETED'
|
||||
"
|
||||
v-if="isCurrentActive && index === currentStepIndex"
|
||||
class="node-badge"
|
||||
>
|
||||
<span class="badge-dot"></span>
|
||||
进行中
|
||||
{{ getStepTime(step.key) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 已取消状态提示 -->
|
||||
<div v-if="order.status === 'CANCELLED'" class="cancelled-banner">
|
||||
<IconifyIcon icon="solar:close-circle-bold-duotone" />
|
||||
<span>工单已取消</span>
|
||||
</div>
|
||||
|
||||
<!-- 展开的业务日志列表 -->
|
||||
<div v-if="showLogs" class="logs-expand-section">
|
||||
<div v-if="businessLogs.length > 0" class="logs-simple-list">
|
||||
@@ -1318,7 +1367,6 @@ onUnmounted(stopPolling);
|
||||
.progress-steps-wrapper {
|
||||
position: relative;
|
||||
padding: 0 72px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-nodes {
|
||||
@@ -1347,6 +1395,10 @@ onUnmounted(stopPolling);
|
||||
&.line-all-completed {
|
||||
background: #52c41a;
|
||||
}
|
||||
|
||||
&.line-cancelled {
|
||||
background: #d9d9d9;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-node {
|
||||
@@ -1403,10 +1455,37 @@ onUnmounted(stopPolling);
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.node-cancelled .node-icon {
|
||||
/* 取消流程 - 已走过的节点(灰色实心) */
|
||||
.node-cancelled-done .node-icon {
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%);
|
||||
border-color: #ff4d4f;
|
||||
background: #bfbfbf;
|
||||
border-color: #bfbfbf;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.node-cancelled-done .node-label {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.node-cancelled-done .node-time {
|
||||
color: #bfbfbf;
|
||||
}
|
||||
|
||||
/* 取消流程 - 最终取消节点(红灰色调) */
|
||||
.node-cancelled-end .node-icon {
|
||||
color: #fff;
|
||||
background: #8c8c8c;
|
||||
border-color: #8c8c8c;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.node-cancelled-end .node-label {
|
||||
font-weight: 600;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.node-cancelled-end .node-time {
|
||||
color: #bfbfbf;
|
||||
}
|
||||
|
||||
.node-label {
|
||||
@@ -1461,21 +1540,6 @@ onUnmounted(stopPolling);
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.cancelled-banner {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #ff4d4f;
|
||||
background: #fff2f0;
|
||||
border: 1px solid #ffccc7;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* ========== 基础信息行:左右等高 ========== */
|
||||
.info-row :deep(> .ant-col) {
|
||||
display: flex;
|
||||
@@ -1811,10 +1875,6 @@ onUnmounted(stopPolling);
|
||||
color: rgb(255 255 255 / 75%);
|
||||
}
|
||||
|
||||
.cancelled-banner {
|
||||
background: rgb(255 77 79 / 10%);
|
||||
border-color: rgb(255 77 79 / 30%);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 通用卡片样式 ========== */
|
||||
|
||||
Reference in New Issue
Block a user