diff --git a/src/views/flow/EmbedInstance.vue b/src/views/flow/EmbedInstance.vue index b345f34..cc3c12c 100644 --- a/src/views/flow/EmbedInstance.vue +++ b/src/views/flow/EmbedInstance.vue @@ -3,17 +3,14 @@ // 展示"进度 + 流程示意图",零样式复制、单点维护。全程零 CAS 交互——数据走 /open/flow/embed/data // 免签端点(只认 token),示意图 token 两用(/open/flow/diagram/img 同一 token 出图)。 // 本页只读(方案 §9):不做任何审批动作;办理入口仍是待办站内信 → /task/:taskId 既有链路。 -// v2.4 版式(2026-10-10):原「流程轨迹」+「审批记录」合并为「办理进度」三视图(按节点/时间线/列表), -// 流程示意图与业务信息改为默认收缩的折叠块;业务信息只渲染约定键 bizSnapshot.summaryFields, -// 无值整块不显示;bizSnapshot 其余键与 formDataJson 一律不再平铺(原始数据不外露)。 -// v2.5(2026-10-10):口径切换器(人员视图/科室视图)与视图切换器同排在标题行、三视图常驻——口径实际对 -// 三个视图都生效(时间线走 :mode、列表走办理人列),原先只在「按节点」模板内会切视图即消失。 -import { ref, computed, onMounted, nextTick } from "vue"; +// 2026-10-11 整合:正文(基本信息/办理进度三视图/流程示意图折叠/业务信息摘要)抽至共用组件 +// InstanceBody.vue,与管理面办件详情 /instance/:instanceId 同源同款;本页只留 token 取数、 +// 标题吸顶壳与空态——嵌入口径(只认 summaryFields、无流程信息/表单数据)由组件缺省 variant 保证。 +import { ref, computed, onMounted } from "vue"; import { useRoute } from "vue-router"; import { Tag } from "view-ui-plus"; import { INSTANCE_STATUS, INSTANCE_STATUS_COLOR } from "@/api/module/flow"; -import TraceTimeline from "@/views/flow/components/TraceTimeline.vue"; -import { fitDiagram } from "@/utils/diagramImg"; +import InstanceBody from "@/views/flow/components/InstanceBody.vue"; import config from "@/assets/config/host.config"; const route = useRoute(); @@ -22,265 +19,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"); -function switchMode(val: string) { - traceMode.value = val; -} - const token = computed(() => String(route.query.token || "")); /** 示意图直嵌:同一 token 第二处消费(换图端点零改动即认领嵌入 token) */ const diagramSrc = computed( () => `${config.serviceHost}/open/flow/diagram/img?token=${token.value}`, ); -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); -} - -/** 业务信息唯一内容 = 约定键 bizSnapshot.summaryFields;形状错 / 无值 ⇒ [] ⇒ 整块不渲染 */ -const summaryFields = computed(() => { - const raw = detail.value?.bizSnapshot; - if (!raw) return []; - let obj: any; - try { - obj = typeof raw === "string" ? JSON.parse(raw) : raw; - } catch { - return []; - } - 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)), - })); -}); - -// ===== 事件类型口径(与 TraceTimeline.vue 同源:改一处必须同步另一处) ===== - -/** 事件类型 → 动作标签主题色(逐字照抄 TraceTimeline.vue 的 KIND_TAG) */ -const RECORD_KIND_TAG: Record = { - SUBMIT: "green", - RESUBMIT: "green", - WITHDRAW: "orange", - APPROVE: "green", - REJECT: "red", - RETURN: "orange", - TRANSFER: "cyan", - INVALID: "default", - PENDING: "blue", - CC: "cyan", - 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)); - -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 "流程引擎"; - if (traceMode.value !== "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 || "-"); -} - -/** 嵌入页展示层状态文案(设计图口径:已提交 / 已同意 / 待审核…)。 - * ⚠️ 刻意不复用全局 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 TRACE_STATUS_TEXT[t.status] || String(t.status ?? ""); -} - -/** 节点步结论:任一拒绝 > 任一同意 > 全失效 > 待审核(口径=流程中心 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)) }, - ]; - if (multiRound.value) { - cols.push({ - title: "轮次", width: 80, align: "center", - render: (h: any, p: any) => h("span", `第 ${p.row.round || 1} 轮`), - }); - } - cols.push( - { - title: "节点", minWidth: 120, - render: (h: any, p: any) => h("span", p.row.nodeName || p.row.nodeId || "-"), - }, - { title: "办理人", width: 110, render: (h: any, p: any) => h("span", recordActor(p.row)) }, - { - title: "动作", width: 90, align: "center", - render: (h: any, p: any) => - h(Tag, { size: "small", color: RECORD_KIND_TAG[p.row.kind] || "default" }, - () => recordAction(p.row)), - }, - { - title: "意见 / 去向", minWidth: 180, - render: (h: any, p: any) => { - const parts: any[] = []; - if (p.row.opinion) parts.push(h("span", `「${p.row.opinion}」`)); - if (p.row.edgeName) parts.push(h("span", `去向:${p.row.edgeName}`)); - if (p.row.kind === "ROUTE") { - // outlet 空=判断失败挂起(同 TraceTimeline 的 route-hung 文案),等管理端重跑路由 - parts.push(p.row.outlet - ? h("span", `→ ${p.row.outlet}`) - : h("span", { style: "color:#ed4014" }, `判断失败已挂起(第 ${p.row.attempt || 1} 次)`)); - } - return parts.length ? h("span", parts) : h("span", { style: "color:#999" }, "-"); - }, - }, - ); - return cols; -}); function statusName(s: number) { return INSTANCE_STATUS[s] || s; @@ -310,11 +54,6 @@ onMounted(async () => { } finally { loading.value = false; } - // 缓存命中的图可能在监听器挂上之前就 complete,补一次适配 - await nextTick(); - if (diagramEl.value?.complete) { - fitDiagram(diagramEl.value); - } }); @@ -325,88 +64,7 @@ onMounted(async () => { {{ statusName(detail.status) }} -
-
基本信息
-
-
-
{{ m.k }}
-
{{ m.v }}
-
-
-
- -
-
- 办理进度 - - 按节点 - 时间线 - 列表 - - - - 人员视图 - 科室视图 - -
- - - - - - - - - - - -
- 流程示意图 -
- 流程示意图 -
-
- -
- 业务信息 -
-
-
- {{ f.label }} - {{ f.value }} -
-
-
-
+
加载中…
链接已过期,请回到来源应用重新打开
@@ -426,78 +84,4 @@ onMounted(async () => { .title { font-size: 16px; font-weight: 600; flex: 1; min-width: 0; } .muted { color: #999; } .expired { padding: 48px 16px; text-align: center; } - -/* 区块标题:左侧蓝条 + 右侧控件 */ -.embed-sec { margin-bottom: 18px; } -.embed-sec-h { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; font-size: 14px; font-weight: 600; } -.embed-sec-h > span:first-child { position: relative; padding-left: 9px; } -.embed-sec-h > span:first-child::before { - content: ""; position: absolute; left: 0; top: 50%; transform: translateY(-50%); - width: 3px; height: 13px; border-radius: 2px; background: var(--brand); -} -/* 两处切换器(视图 / 口径)现同排在标题行,同为 iView small button group、几何与配色一致 - (活动态 #0d9488、非活动 #515a6e/#dcdee2)。⚠️ 两者都落在 .embed-sec-h(600) 内 ⇒ 会继承粗体, - 必须显式回常规(管理面同款控件 InstanceDetail/TaskHandle 的 .trace-mode 也是这个写法)。 */ -.embed-view-seg { margin-left: auto; font-weight: normal; } -/* 口径紧随视图切换器:与 .embed-sec-h 的 10px gap 叠加为 24px,避免两组按钮被看成一排五选一 */ -.embed-mode-seg { margin-left: 14px; font-weight: normal; } - -/* 基本信息:四列等宽(740 抽屉实测放得下时间戳,超出省略号 + title 兜底) */ -.embed-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border: 1px solid #f2f4f7; border-radius: 8px; overflow: hidden; } -.embed-meta-i { padding: 11px 14px; border-right: 1px solid #e8eaec; min-width: 0; } -.embed-meta-i:last-child { border-right: none; } -.embed-meta-k { font-size: 12px; color: #808695; margin-bottom: 4px; } -.embed-meta-v { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - -/* 按节点视图 */ -.embed-steps { position: relative; } -.embed-step { display: flex; gap: 11px; padding-bottom: 16px; position: relative; } -.embed-step:last-child { padding-bottom: 2px; } -.embed-step:not(:last-child)::before { - content: ""; position: absolute; left: 5px; top: 16px; bottom: 0; width: 2px; background: #f2f4f7; -} -.embed-step-dot { - width: 12px; height: 12px; border-radius: 50%; background: #c8ced8; margin-top: 5px; flex-shrink: 0; - z-index: 1; border: 2px solid #fff; box-shadow: 0 0 0 1px #e8eaec; -} -.embed-step.is-done .embed-step-dot { background: #19be6b; box-shadow: 0 0 0 1px #b7ebc9; } -.embed-step.is-reject .embed-step-dot { background: #ed4014; box-shadow: 0 0 0 1px #ffd2c9; } -.embed-step.is-active .embed-step-dot { background: var(--brand); box-shadow: 0 0 0 3px rgba(45, 140, 240, .18); } -.embed-step-body { flex: 1; min-width: 0; } -.embed-step-head { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; flex-wrap: wrap; } -.embed-step-name { font-size: 14px; font-weight: 600; } -.embed-assignee { - display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 13px; - padding: 6px 10px; margin-bottom: 5px; border-radius: 6px; background: #fafbfc; border: 1px solid #f0f2f5; -} -.embed-assignee:last-child { margin-bottom: 0; } -.embed-an { font-weight: 600; } -.embed-assignee.is-done .embed-an { color: #19be6b; } -.embed-assignee.is-reject .embed-an { color: #ed4014; } -.embed-am { color: #808695; font-size: 12px; } -.embed-ao { color: #515a6e; width: 100%; padding-top: 2px; } -.embed-ao.route-hung { color: #ed4014; } -.embed-assignee.is-invalid { opacity: .5; } -.embed-assignee.is-invalid .embed-an { text-decoration: line-through; text-decoration-color: #b9bcc4; } - -/* 折叠块:流程示意图 / 业务信息,默认收缩 */ -.embed-fold { border: 1px solid #f2f4f7; border-radius: 8px; background: #fff; margin-bottom: 18px; } -.embed-fold > summary { - cursor: pointer; list-style: none; padding: 12px 16px; display: flex; align-items: center; gap: 10px; - font-size: 14px; font-weight: 600; color: #17233d; -} -.embed-fold > summary::-webkit-details-marker { display: none; } -.embed-fold > summary::after { content: "▾"; margin-left: auto; color: #808695; transition: .2s; font-size: 12px; } -.embed-fold[open] > summary::after { transform: rotate(180deg); } -.embed-fold-b { padding: 2px 16px 16px; } -.diagram-img { - max-width: 100%; height: auto; display: block; margin: 0 auto; - border: 1px solid #f2f4f7; border-radius: 6px; -} -/* 业务摘要:折叠块内再收一层卡,与摘要行成组(设计基线口径) */ -.sf { border: 1px solid #e6eefb; border-radius: 8px; overflow: hidden; background: #fcfdff; } -.sf-row { display: flex; align-items: flex-start; gap: 12px; padding: 10px 14px; border-bottom: 1px solid #eef3fb; font-size: 13px; } -.sf-row:last-child { border-bottom: none; } -.sf-k { width: 84px; flex-shrink: 0; color: #808695; } -.sf-v { flex: 1; min-width: 0; word-break: break-all; font-weight: 500; } diff --git a/src/views/flow/InstanceDetail.vue b/src/views/flow/InstanceDetail.vue index 65b009a..39ad379 100644 --- a/src/views/flow/InstanceDetail.vue +++ b/src/views/flow/InstanceDetail.vue @@ -1,12 +1,16 @@ @@ -115,54 +72,12 @@ onMounted(async () => { {{ branchText }} - - -
-
流程{{ detail.defCode }} v{{ detail.defVersion }}
-
提交人{{ detail.submitterName }}
-
提交时间{{ (detail.submitTime || "").replace("T", " ") }}
-
办结时间{{ (detail.finishTime || "").replace("T", " ") || "-" }}
-
业务详情 - 查看业务详情
-
- @@ -172,13 +87,7 @@ onMounted(async () => { .head { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; } .head a { cursor: pointer; } .title { font-size: 16px; font-weight: 600; } -.block { margin-bottom: 12px; } -.grid .row { display: flex; padding: 3px 0; } -.k { width: 120px; color: #888; flex-shrink: 0; } -.v { white-space: pre-wrap; word-break: break-all; } .muted { color: #999; } -.diagram-box { display: flex; justify-content: center; } -.diagram-img { max-width: 100%; } -.diagram-more { float: right; font-size: 12px; font-weight: normal; margin-right: 16px; } -.trace-mode { float: right; font-weight: normal; margin-right: 16px; } +/* 折叠块 summary 右侧入口:summary 本体 600 粗体,链接回常规 */ +.fold-link { font-size: 12px; font-weight: normal; } diff --git a/src/views/flow/components/InstanceBody.vue b/src/views/flow/components/InstanceBody.vue new file mode 100644 index 0000000..7d23279 --- /dev/null +++ b/src/views/flow/components/InstanceBody.vue @@ -0,0 +1,540 @@ + + + + +