feat(ui): 应用页规范全量重构 + 工作台与流程轨迹优化
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 色映射统一导出
This commit is contained in:
chenxing
2026-09-18 16:52:40 +08:00
parent 0083246039
commit cdf04f0930
16 changed files with 1172 additions and 481 deletions
@@ -0,0 +1,80 @@
<script setup lang="ts">
// 流程源码抽屉(2026-09-18):只读展示流程定义的 contentJson,支持一键复制(文本复用)。
// 数据来源由调用方给(图示页走 /api/flow/def/source 或 /admin/def/get;列表页直接用行上的 contentJson),
// 本组件只管展示与复制,不做任何解析/回写(编辑走流程编辑器的"源码"高级模式)。
import { Message } from "view-ui-plus";
import Utils from "@/utils";
const props = defineProps<{
modelValue: boolean;
/** 展示文本(已格式化的 JSON 字符串) */
text: string;
/** 标题补充信息,如「流程名(code v1)」 */
subtitle?: string;
}>();
const emit = defineEmits<{
"update:modelValue": [boolean];
}>();
async function onCopy(): Promise<void> {
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(props.text);
Message.success("已复制");
return;
}
await Utils.copyToClipboard(props.text);
Message.success("已复制");
} catch {
Message.error("复制失败,请手动选择文本复制");
}
}
</script>
<template>
<Drawer
:model-value="modelValue"
title="流程源码"
:width="680"
:mask-closable="false"
@update:model-value="emit('update:modelValue', $event)"
>
<div class="tip">
<span v-if="subtitle">{{ subtitle }} · </span>
以下是流程定义的 JSON(后端运行时格式:nodes/edges/settings/layout)。只读展示;
需要改结构请在流程编辑器里用「源码」高级模式,或以此文本为底稿新建流程。
</div>
<Input :model-value="text" type="textarea" :rows="26" readonly class="json-area" spellcheck="false" />
<div class="dw-foot">
<span style="flex: 1" />
<Button @click="emit('update:modelValue', false)">关闭</Button>
<Button type="primary" @click="onCopy">复制</Button>
</div>
</Drawer>
</template>
<style scoped>
.tip {
color: #808695;
font-size: 12px;
line-height: 1.7;
margin-bottom: 10px;
}
.json-area :deep(textarea) {
font-family: Consolas, Menlo, monospace;
font-size: 12px;
line-height: 1.5;
}
.dw-foot {
position: sticky;
bottom: -16px;
display: flex;
align-items: center;
gap: 8px;
padding: 12px 0;
margin-top: 8px;
background: #fff;
border-top: 1px solid #f0f0f0;
}
</style>
+127
View File
@@ -0,0 +1,127 @@
<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>