diff --git a/src/views/flow/EmbedInstance.vue b/src/views/flow/EmbedInstance.vue index d8668db..16627bb 100644 --- a/src/views/flow/EmbedInstance.vue +++ b/src/views/flow/EmbedInstance.vue @@ -3,10 +3,13 @@ // 展示"进度 + 流程示意图",零样式复制、单点维护。全程零 CAS 交互——数据走 /open/flow/embed/data // 免签端点(只认 token),示意图 token 两用(/open/flow/diagram/img 同一 token 出图)。 // 本页只读(方案 §9):不做任何审批动作;办理入口仍是待办站内信 → /task/:taskId 既有链路。 +// v2.4 版式(2026-10-10):原「流程轨迹」+「审批记录」合并为「办理进度」三视图(按节点/时间线/列表), +// 流程示意图与业务信息改为默认收缩的折叠块;业务信息只渲染约定键 bizSnapshot.summaryFields, +// 无值整块不显示;bizSnapshot 其余键与 formDataJson 一律不再平铺(原始数据不外露)。 import { ref, computed, onMounted, nextTick } from "vue"; import { useRoute } from "vue-router"; import { Tag } from "view-ui-plus"; -import { INSTANCE_STATUS, INSTANCE_STATUS_COLOR, TASK_STATUS } from "@/api/module/flow"; +import { INSTANCE_STATUS, INSTANCE_STATUS_COLOR } from "@/api/module/flow"; import TraceTimeline from "@/views/flow/components/TraceTimeline.vue"; import { fitDiagram } from "@/utils/diagramImg"; import config from "@/assets/config/host.config"; @@ -17,6 +20,12 @@ const loading = ref(true); /** 兜底空态:缺 token / 过期 / 加载失败,全页同一文案,不留白 */ const expired = ref(false); +/** 办理进度视图:node=按节点(缺省)/ timeline=时间线 / list=列表 */ +const viewMode = ref("node"); +function switchView(val: string) { + viewMode.value = val; +} + /** 显示口径:dept=按科室 / user=按人员(缺省)。纯前端本地状态——嵌入页 URL 归子应用管, * 改 query 会破坏 token 语义,不落 URL(与管理面 InstanceDetail 写 query 的动机不同) */ const traceMode = ref("user"); @@ -34,47 +43,37 @@ const diagramEl = ref(null); function onDiagramLoad() { fitDiagram(diagramEl.value); } +// 折叠时容器 clientWidth=0,fitDiagram 取不到收缩上限;展开这一刻再适配一次 +function onDiagramToggle(e: Event) { + if ((e.target as HTMLDetailsElement).open) fitDiagram(diagramEl.value); +} -const snapshotRows = computed(() => { +/** 业务信息唯一内容 = 约定键 bizSnapshot.summaryFields;形状错 / 无值 ⇒ [] ⇒ 整块不渲染 */ +const summaryFields = computed(() => { const raw = detail.value?.bizSnapshot; if (!raw) return []; - let obj: any = null; + let obj: any; try { obj = typeof raw === "string" ? JSON.parse(raw) : raw; } catch { - return [{ k: "摘要", v: String(raw) }]; + return []; } - if (obj && typeof obj === "object") { - return Object.entries(obj).map(([k, v]) => ({ k, v: v == null ? "-" : String(v) })); - } - return []; -}); - -/** 业务表单数据键值平铺(同 InstanceDetail 现状,不做精细表单渲染) */ -const formRows = computed(() => { - const raw = detail.value?.formDataJson; - if (raw === null || raw === undefined || raw === "") return []; - let obj: any = null; - try { - obj = typeof raw === "string" ? JSON.parse(raw) : raw; - } catch { - return [{ k: "表单原文", v: String(raw) }]; - } - if (obj && typeof obj === "object" && !Array.isArray(obj)) { - return Object.entries(obj).map(([k, v]) => ({ - k, - v: v == null ? "-" - : Array.isArray(v) - ? (v.map((item: any) => item && typeof item === "object" ? item.fileName || JSON.stringify(item) : String(item)).join("、") || "-") - : (typeof v === "object" ? JSON.stringify(v) : String(v)), + const sf = obj?.summaryFields; + if (!Array.isArray(sf) || !sf.length) return []; + return sf + .filter((x: any) => x && x.label !== null && x.label !== undefined) + .slice(0, 6) + .map((x: any) => ({ + label: String(x.label), + value: Array.isArray(x.value) + ? (x.value.join("、") || "-") + : (x.value === null || x.value === undefined || x.value === "" ? "-" : String(x.value)), })); - } - return [{ k: "表单原文", v: String(raw) }]; }); -// ===== 审批记录流水表(整个流程周期,与"流程轨迹"时间线互补:时间线给进度感,表格给逐条可查流水) ===== +// ===== 事件类型口径(与 TraceTimeline.vue 同源:改一处必须同步另一处) ===== -/** 事件类型 → 动作标签主题色(逐字照抄 TraceTimeline.vue 的 KIND_TAG:两处口径,改一处必须同步另一处) */ +/** 事件类型 → 动作标签主题色(逐字照抄 TraceTimeline.vue 的 KIND_TAG) */ const RECORD_KIND_TAG: Record = { SUBMIT: "green", RESUBMIT: "green", @@ -89,6 +88,9 @@ const RECORD_KIND_TAG: Record = { ROUTE: "purple", }; +/** 留痕事件行:不参与节点聚合,每行独立成一步(口径同流程中心 toProgressVo) */ +const EVENT_KINDS = ["SUBMIT", "RESUBMIT", "WITHDRAW", "CC", "ROUTE"]; + /** 多轮才显示"轮次"列(单轮不打扰,口径同 TraceTimeline 的 multiRound) */ const multiRound = computed(() => (detail.value?.trace || []).some((t: any) => (t.round || 1) > 1)); @@ -97,6 +99,11 @@ function fmtTime(v: string | null | undefined) { return v ? String(v).replace("T", " ").replace(/\.\d+$/, "") : ""; } +/** 行发生时间:留痕行取 time,任务行取 handleTime,未办取 arriveTime */ +function rowTime(r: any): string { + return r.time || r.handleTime || r.arriveTime || ""; +} + /** 操作者(口径=TraceTimeline.actor:dept 档不泄露人名,科室节点显科室、指定人显"指定办理人") */ function recordActor(t: any): string { if (t.kind === "ROUTE") return "流程引擎"; @@ -114,14 +121,124 @@ function recordActor(t: any): string { : (t.assigneeName || "-"); } -/** 动作标签(口径=TraceTimeline.actionText:CC/ROUTE 专用文案,任务行走 TASK_STATUS 码表) */ +/** 嵌入页展示层状态文案(设计图口径:已提交 / 已同意 / 待审核…)。 + * ⚠️ 刻意不复用全局 TASK_STATUS(0 待办 / 1 同意 / …)——它被 TraceTimeline 与管理面详情页共用, + * 改它会把全站文案一起改掉;此处只做本页的展示层映射,三视图共用一份。 */ +const TRACE_STATUS_TEXT: Record = { + 0: "待审核", 1: "已同意", 2: "已拒绝", 3: "失效", + 4: "已退回", 5: "已移交", 6: "已提交", 7: "已撤回", +}; + +/** 动作标签(口径同 TraceTimeline.actionText:CC/ROUTE 专用文案,任务行查展示层映射) */ function recordAction(t: any): string { if (t.kind === "CC") return "已抄送"; if (t.kind === "ROUTE") return "系统判断"; - return TASK_STATUS[t.status] || String(t.status ?? ""); + return TRACE_STATUS_TEXT[t.status] || String(t.status ?? ""); } -/** 表格列:Drawer iframe 宽约 828px,列宽按此设计;轮次列仅多轮时出现 */ +/** 节点步结论:任一拒绝 > 任一同意 > 全失效 > 待审核(口径=流程中心 OapiFlowService.stepLabel) */ +function stepLabel(rows: any[]): string { + let denied = false, approved = false, invalid = true; + rows.forEach((r) => { + const s = r.status === null || r.status === undefined ? -1 : r.status; + if (s === 2) denied = true; + if (s === 1) approved = true; + if (s === 0 || s === 1 || s === 2 || s === 4 || s === 5) invalid = false; + }); + if (denied) return "已拒绝"; + if (approved) return "已同意"; + return invalid ? "失效" : "待审核"; +} + +function labelTagClass(label: string): string { + if (label === "已同意") return "green"; + if (label === "已拒绝") return "red"; + if (label === "待审核") return "blue"; + return "default"; +} + +/** + * trace 扁平行 → 步骤序列:同 nodeId 的审批行归一步,留痕事件行独立成步,步骤按时间升序。 + * ⚠️ 步内办理人行序 = 后端返回顺序(任务 id 序),不得按时间重排。 + */ +const nodeSteps = computed(() => { + const trace: any[] = detail.value?.trace || []; + const act: string[] = String(detail.value?.currentNodes || "").split(",").filter(Boolean); + const rows = trace.map((r) => ({ ...r, _t: rowTime(r) })); + const steps: any[] = []; + const byNode: Record = {}; + rows.forEach((r) => { + if (EVENT_KINDS.includes(r.kind)) { + steps.push({ type: "event", nodeId: r.nodeId, nodeName: r.nodeName, rows: [r], t: r._t }); + return; + } + let s = byNode[r.nodeId]; + if (!s) { + s = { type: "node", nodeId: r.nodeId, nodeName: r.nodeName, rows: [], t: r._t }; + byNode[r.nodeId] = s; + steps.push(s); + } + s.rows.push(r); + if ((r._t || "") > (s.t || "")) s.t = r._t; + }); + steps.forEach((s) => { + if (s.type === "node") { + s.active = act.includes(s.nodeId); + s.label = s.active ? "待审核" : stepLabel(s.rows); + } else { + s.label = recordAction(s.rows[0]); + } + }); + return steps.sort((a, b) => (a.t || "").localeCompare(b.t || "")); +}); + +function stepClass(s: any): string { + if (s.type !== "node") return ""; + if (s.active) return "is-active"; + if (s.label === "已拒绝") return "is-reject"; + if (s.label === "已同意") return "is-done"; + return ""; +} + +function stepTagColor(s: any): string { + return s.type === "node" ? labelTagClass(s.label) : (RECORD_KIND_TAG[s.rows[0].kind] || "default"); +} + +/** 办理人行色:1 绿 / 2 红 / 3 失效置灰删除线 / 0 待办 */ +function rowClass(r: any): string { + if (r.status === 1) return "is-done"; + if (r.status === 2) return "is-reject"; + if (r.status === 3) return "is-invalid"; + return ""; +} + +function rowTimeText(r: any): string { + if (r.handleTime) return `办理于 ${fmtTime(r.handleTime)}`; + if (r.arriveTime) return `到达 ${fmtTime(r.arriveTime)}`; + return fmtTime(rowTime(r)); +} + +/** 基本信息「当前节点」:currentNodes 命中的节点名 + 办理中后缀 */ +const currentNodeText = computed(() => { + const act = String(detail.value?.currentNodes || "").split(",").filter(Boolean); + const names = nodeSteps.value + .filter((s) => s.type === "node" && act.includes(s.nodeId)) + .map((s) => s.nodeName || s.nodeId); + return names.length ? `${names.join("、")}(办理中)` : "-"; +}); + +const metaItems = computed(() => { + const d = detail.value || {}; + const items = [ + { k: "提交人", v: d.submitterName || "-" }, + { k: "提交时间", v: fmtTime(d.submitTime) || "-" }, + { k: "办结时间", v: fmtTime(d.finishTime) || "-" }, + ]; + if (d.currentNodes) items.push({ k: "当前节点", v: currentNodeText.value }); + return items; +}); + +/** 表格列:Drawer iframe 宽约 708px(740 抽屉),列宽按此设计;轮次列仅多轮时出现 */ const recordColumns = computed(() => { const cols: any[] = [ { title: "时间", width: 150, render: (h: any, p: any) => h("span", fmtTime(p.row.time)) }, @@ -206,58 +323,88 @@ onMounted(async () => { {{ statusName(detail.status) }} - - -
-
流程{{ detail.defCode }} v{{ detail.defVersion }}
-
提交人{{ detail.submitterName }}
-
提交时间{{ (detail.submitTime || "").replace("T", " ") }}
-
办结时间{{ (detail.finishTime || "").replace("T", " ") || "-" }}
+
+
基本信息
+
+
+
{{ m.k }}
+
{{ m.v }}
+
-