build / build (push) Successful in 3m17s
应用页规范(bsch)重构: - 6 个列表页迁规范骨架: page-wrapper/tabs-wrapper + search-wrap 搜索区(关键字 200px 即输即搜, 去查询按钮) + link-table table-bottom-box 定高表格 + page-panel 分页栏 - 分页列表统一接 TablePageAlovaNew(页长 [10,20,30,40]/customPageCount 记忆/增删智能翻页); dataApi 须返回 Method, 调 .send() 会导致表格静默空数据 - 操作列统一图标式 operatingHandle(Tooltip+fixed right 居中); 各列表补序号列; 危险操作统一 MyConfirm 二次确认 - 弹窗表单必填红星 + 统一提示「请填写红色星号内容!」 - TablePageAlovaNew 补页长记忆与重载后清空勾选; 新增 src/types OperatorEnum 工作台与流程轨迹优化: - 已办列表去重: 每办件一行, doneCount>1 显示「共办理 N 次」标签 - 新增 TraceTimeline 轨迹时间流(时间升序/kind/轮次, 按人员/科室切换写回 URL), 接入详情/办理页 - 新增流程源码只读抽屉 SourceDrawer, 接入定义列表与图示页 - 办理人映射/流程定义新增「启用」回路(/admin/map/enable, /admin/def/enable) - 图示视图口径统一 dept/user 两档, 缺省人员视图; 状态列 Tag 色映射统一导出
128 lines
4.6 KiB
Vue
128 lines
4.6 KiB
Vue
<script setup lang="ts">
|
||
// 流程轨迹时间线(2026-09-18 新增,办件详情页与办理页共用一个实现):
|
||
// ① 时间流:后端已按"该行实际发生的时间"升序返回(time 字段),此处不再排序,只按 round 分轮展示;
|
||
// ② 两档显示:user=按人员(默认,显示办理人姓名);dept=按科室(DEPT_KEY 节点显科室名、USERS 节点显
|
||
// "指定办理人"、发起/撤回/重提显"提交人"),口径与流程示意图的科室视图一致;
|
||
// ③ 撤回重走留痕:kind=WITHDRAW/RESUBMIT 的行由后端留痕事件行提供,重提处插入"第 N 轮"分隔。
|
||
import { computed } from "vue";
|
||
import { TASK_STATUS } from "@/api/module/flow";
|
||
|
||
const props = defineProps<{
|
||
/** 后端 detail.trace(已按时间升序,含 kind/time/round/科室富化字段) */
|
||
trace: any[];
|
||
/** user=按人员(缺省)|dept=按科室 */
|
||
mode?: string;
|
||
}>();
|
||
|
||
/** 事件类型 → 时间线圆点颜色 */
|
||
const KIND_DOT: Record<string, string> = {
|
||
SUBMIT: "green",
|
||
RESUBMIT: "blue",
|
||
WITHDRAW: "orange",
|
||
APPROVE: "green",
|
||
REJECT: "red",
|
||
RETURN: "orange",
|
||
TRANSFER: "cyan",
|
||
INVALID: "gray",
|
||
PENDING: "blue",
|
||
};
|
||
|
||
/** 事件类型 → 动作标签主题色(标签文案取 TASK_STATUS,避免两处口径漂移) */
|
||
const KIND_TAG: Record<string, string> = {
|
||
SUBMIT: "green",
|
||
RESUBMIT: "green",
|
||
WITHDRAW: "orange",
|
||
APPROVE: "green",
|
||
REJECT: "red",
|
||
RETURN: "orange",
|
||
TRANSFER: "cyan",
|
||
INVALID: "default",
|
||
PENDING: "blue",
|
||
};
|
||
|
||
const rows = computed(() => props.trace || []);
|
||
|
||
/** 多轮才有轮次分隔(单轮不打扰) */
|
||
const multiRound = computed(() => rows.value.some((t: any) => (t.round || 1) > 1));
|
||
|
||
/** 轮次分隔标题:第 N 轮 + 该轮重提时间 */
|
||
function roundTitle(round: number): string {
|
||
let at = "";
|
||
for (const t of rows.value) {
|
||
if ((t.round || 1) === round && t.kind === "RESUBMIT") {
|
||
at = fmtTime(t.time);
|
||
break;
|
||
}
|
||
}
|
||
return at ? `第 ${round} 轮审批(重新提交于 ${at})` : `第 ${round} 轮审批`;
|
||
}
|
||
|
||
/** 操作者:按人员=姓名;按科室=科室名/指定办理人/提交人(不暴露具体人名) */
|
||
function actor(t: any): string {
|
||
if (props.mode !== "dept") {
|
||
return t.assigneeName || t.assigneeId || "-";
|
||
}
|
||
if (t.assigneeType === "DEPT_KEY") {
|
||
return t.deptName || t.deptKey || t.assigneeName || "-";
|
||
}
|
||
if (t.assigneeType === "USERS") {
|
||
return "指定办理人";
|
||
}
|
||
// 留痕事件行(发起/撤回/重新提交)与定义里已找不到的老节点:不泄露人名,统一称"提交人"
|
||
return t.kind === "SUBMIT" || t.kind === "RESUBMIT" || t.kind === "WITHDRAW"
|
||
? "提交人"
|
||
: (t.assigneeName || "-");
|
||
}
|
||
|
||
/** 动作标签:失效行标"失效"而不是笼统的"提交" */
|
||
function actionText(t: any): string {
|
||
return TASK_STATUS[t.status] || String(t.status ?? "");
|
||
}
|
||
|
||
function fmtTime(v: string | null | undefined): string {
|
||
if (!v) return "";
|
||
return String(v).replace("T", " ").replace(/\.\d+$/, "");
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div>
|
||
<Timeline>
|
||
<template v-for="(t, i) in rows" :key="t.rowKey || i">
|
||
<div v-if="multiRound && (t.round || 1) !== (rows[i - 1]?.round || 1)" class="round">
|
||
{{ roundTitle(t.round || 1) }}
|
||
</div>
|
||
<TimelineItem :color="KIND_DOT[t.kind] || 'gray'">
|
||
<div class="line">
|
||
<span class="node">{{ t.nodeName || t.nodeId }}</span>
|
||
<Tag size="small">{{ actor(t) }}</Tag>
|
||
<Tag size="small" :color="KIND_TAG[t.kind] || 'default'">{{ actionText(t) }}</Tag>
|
||
<span class="opinion" v-if="t.opinion">「{{ t.opinion }}」</span>
|
||
<span class="opinion" v-if="t.edgeName">去向:{{ t.edgeName }}</span>
|
||
</div>
|
||
<div class="time">{{ fmtTime(t.time || t.handleTime || t.arriveTime) }}</div>
|
||
</TimelineItem>
|
||
</template>
|
||
</Timeline>
|
||
<div class="muted" v-if="!rows.length">暂无轨迹</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.round {
|
||
margin: 4px 0 10px 6px;
|
||
padding: 2px 8px;
|
||
display: inline-block;
|
||
font-size: 12px;
|
||
color: #2d8cf0;
|
||
background: #f0f7ff;
|
||
border-left: 3px solid #2d8cf0;
|
||
border-radius: 2px;
|
||
}
|
||
.line { display: flex; align-items: center; flex-wrap: wrap; gap: 4px; }
|
||
.node { font-weight: 600; }
|
||
.opinion { color: #666; }
|
||
.time { color: #aaa; font-size: 12px; }
|
||
.muted { color: #999; }
|
||
</style>
|