- 「办理进度」三视图(按节点默认 / 时间线 / 列表),节点内办理人保持后端顺序 - 「流程示意图」「业务信息」各为折叠块、默认收缩;业务信息只渲染 bizSnapshot.summaryFields,无值整块不渲染 - 删「业务表单数据」Card 与 bizSnapshot/formDataJson 平铺 - 办理人行动作用页面内展示层映射(已提交/已同意/待审核…),不改全站共用的 TASK_STATUS - TraceTimeline 增可选 statusText prop(不传=原行为,管理面零影响)
This commit is contained in:
@@ -3,10 +3,13 @@
|
|||||||
// 展示"进度 + 流程示意图",零样式复制、单点维护。全程零 CAS 交互——数据走 /open/flow/embed/data
|
// 展示"进度 + 流程示意图",零样式复制、单点维护。全程零 CAS 交互——数据走 /open/flow/embed/data
|
||||||
// 免签端点(只认 token),示意图 token 两用(/open/flow/diagram/img 同一 token 出图)。
|
// 免签端点(只认 token),示意图 token 两用(/open/flow/diagram/img 同一 token 出图)。
|
||||||
// 本页只读(方案 §9):不做任何审批动作;办理入口仍是待办站内信 → /task/:taskId 既有链路。
|
// 本页只读(方案 §9):不做任何审批动作;办理入口仍是待办站内信 → /task/:taskId 既有链路。
|
||||||
|
// v2.4 版式(2026-10-10):原「流程轨迹」+「审批记录」合并为「办理进度」三视图(按节点/时间线/列表),
|
||||||
|
// 流程示意图与业务信息改为默认收缩的折叠块;业务信息只渲染约定键 bizSnapshot.summaryFields,
|
||||||
|
// 无值整块不显示;bizSnapshot 其余键与 formDataJson 一律不再平铺(原始数据不外露)。
|
||||||
import { ref, computed, onMounted, nextTick } from "vue";
|
import { ref, computed, onMounted, nextTick } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { Tag } from "view-ui-plus";
|
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 TraceTimeline from "@/views/flow/components/TraceTimeline.vue";
|
||||||
import { fitDiagram } from "@/utils/diagramImg";
|
import { fitDiagram } from "@/utils/diagramImg";
|
||||||
import config from "@/assets/config/host.config";
|
import config from "@/assets/config/host.config";
|
||||||
@@ -17,6 +20,12 @@ const loading = ref(true);
|
|||||||
/** 兜底空态:缺 token / 过期 / 加载失败,全页同一文案,不留白 */
|
/** 兜底空态:缺 token / 过期 / 加载失败,全页同一文案,不留白 */
|
||||||
const expired = ref(false);
|
const expired = ref(false);
|
||||||
|
|
||||||
|
/** 办理进度视图:node=按节点(缺省)/ timeline=时间线 / list=列表 */
|
||||||
|
const viewMode = ref<string>("node");
|
||||||
|
function switchView(val: string) {
|
||||||
|
viewMode.value = val;
|
||||||
|
}
|
||||||
|
|
||||||
/** 显示口径:dept=按科室 / user=按人员(缺省)。纯前端本地状态——嵌入页 URL 归子应用管,
|
/** 显示口径:dept=按科室 / user=按人员(缺省)。纯前端本地状态——嵌入页 URL 归子应用管,
|
||||||
* 改 query 会破坏 token 语义,不落 URL(与管理面 InstanceDetail 写 query 的动机不同) */
|
* 改 query 会破坏 token 语义,不落 URL(与管理面 InstanceDetail 写 query 的动机不同) */
|
||||||
const traceMode = ref<string>("user");
|
const traceMode = ref<string>("user");
|
||||||
@@ -34,47 +43,37 @@ const diagramEl = ref<HTMLImageElement | null>(null);
|
|||||||
function onDiagramLoad() {
|
function onDiagramLoad() {
|
||||||
fitDiagram(diagramEl.value);
|
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;
|
const raw = detail.value?.bizSnapshot;
|
||||||
if (!raw) return [];
|
if (!raw) return [];
|
||||||
let obj: any = null;
|
let obj: any;
|
||||||
try {
|
try {
|
||||||
obj = typeof raw === "string" ? JSON.parse(raw) : raw;
|
obj = typeof raw === "string" ? JSON.parse(raw) : raw;
|
||||||
} catch {
|
} catch {
|
||||||
return [{ k: "摘要", v: String(raw) }];
|
|
||||||
}
|
|
||||||
if (obj && typeof obj === "object") {
|
|
||||||
return Object.entries(obj).map(([k, v]) => ({ k, v: v == null ? "-" : String(v) }));
|
|
||||||
}
|
|
||||||
return [];
|
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)) {
|
const sf = obj?.summaryFields;
|
||||||
return Object.entries(obj).map(([k, v]) => ({
|
if (!Array.isArray(sf) || !sf.length) return [];
|
||||||
k,
|
return sf
|
||||||
v: v == null ? "-"
|
.filter((x: any) => x && x.label !== null && x.label !== undefined)
|
||||||
: Array.isArray(v)
|
.slice(0, 6)
|
||||||
? (v.map((item: any) => item && typeof item === "object" ? item.fileName || JSON.stringify(item) : String(item)).join("、") || "-")
|
.map((x: any) => ({
|
||||||
: (typeof v === "object" ? JSON.stringify(v) : String(v)),
|
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<string, string> = {
|
const RECORD_KIND_TAG: Record<string, string> = {
|
||||||
SUBMIT: "green",
|
SUBMIT: "green",
|
||||||
RESUBMIT: "green",
|
RESUBMIT: "green",
|
||||||
@@ -89,6 +88,9 @@ const RECORD_KIND_TAG: Record<string, string> = {
|
|||||||
ROUTE: "purple",
|
ROUTE: "purple",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 留痕事件行:不参与节点聚合,每行独立成一步(口径同流程中心 toProgressVo) */
|
||||||
|
const EVENT_KINDS = ["SUBMIT", "RESUBMIT", "WITHDRAW", "CC", "ROUTE"];
|
||||||
|
|
||||||
/** 多轮才显示"轮次"列(单轮不打扰,口径同 TraceTimeline 的 multiRound) */
|
/** 多轮才显示"轮次"列(单轮不打扰,口径同 TraceTimeline 的 multiRound) */
|
||||||
const multiRound = computed(() =>
|
const multiRound = computed(() =>
|
||||||
(detail.value?.trace || []).some((t: any) => (t.round || 1) > 1));
|
(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+$/, "") : "";
|
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 档不泄露人名,科室节点显科室、指定人显"指定办理人") */
|
/** 操作者(口径=TraceTimeline.actor:dept 档不泄露人名,科室节点显科室、指定人显"指定办理人") */
|
||||||
function recordActor(t: any): string {
|
function recordActor(t: any): string {
|
||||||
if (t.kind === "ROUTE") return "流程引擎";
|
if (t.kind === "ROUTE") return "流程引擎";
|
||||||
@@ -114,14 +121,124 @@ function recordActor(t: any): string {
|
|||||||
: (t.assigneeName || "-");
|
: (t.assigneeName || "-");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 动作标签(口径=TraceTimeline.actionText:CC/ROUTE 专用文案,任务行走 TASK_STATUS 码表) */
|
/** 嵌入页展示层状态文案(设计图口径:已提交 / 已同意 / 待审核…)。
|
||||||
|
* ⚠️ 刻意不复用全局 TASK_STATUS(0 待办 / 1 同意 / …)——它被 TraceTimeline 与管理面详情页共用,
|
||||||
|
* 改它会把全站文案一起改掉;此处只做本页的展示层映射,三视图共用一份。 */
|
||||||
|
const TRACE_STATUS_TEXT: Record<number, string> = {
|
||||||
|
0: "待审核", 1: "已同意", 2: "已拒绝", 3: "失效",
|
||||||
|
4: "已退回", 5: "已移交", 6: "已提交", 7: "已撤回",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 动作标签(口径同 TraceTimeline.actionText:CC/ROUTE 专用文案,任务行查展示层映射) */
|
||||||
function recordAction(t: any): string {
|
function recordAction(t: any): string {
|
||||||
if (t.kind === "CC") return "已抄送";
|
if (t.kind === "CC") return "已抄送";
|
||||||
if (t.kind === "ROUTE") 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<string, any> = {};
|
||||||
|
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 recordColumns = computed(() => {
|
||||||
const cols: any[] = [
|
const cols: any[] = [
|
||||||
{ title: "时间", width: 150, render: (h: any, p: any) => h("span", fmtTime(p.row.time)) },
|
{ title: "时间", width: 150, render: (h: any, p: any) => h("span", fmtTime(p.row.time)) },
|
||||||
@@ -206,58 +323,88 @@ onMounted(async () => {
|
|||||||
<Tag :color="statusColor(detail.status)">{{ statusName(detail.status) }}</Tag>
|
<Tag :color="statusColor(detail.status)">{{ statusName(detail.status) }}</Tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card class="block" :bordered="false" dis-hover>
|
<div class="embed-sec">
|
||||||
<template #title>基本信息</template>
|
<div class="embed-sec-h"><span>基本信息</span></div>
|
||||||
<div class="grid">
|
<div class="embed-meta">
|
||||||
<div class="row"><span class="k">流程</span><span>{{ detail.defCode }} v{{ detail.defVersion }}</span></div>
|
<div v-for="m in metaItems" :key="m.k" class="embed-meta-i">
|
||||||
<div class="row"><span class="k">提交人</span><span>{{ detail.submitterName }}</span></div>
|
<div class="embed-meta-k">{{ m.k }}</div>
|
||||||
<div class="row"><span class="k">提交时间</span><span>{{ (detail.submitTime || "").replace("T", " ") }}</span></div>
|
<div class="embed-meta-v" :title="m.v">{{ m.v }}</div>
|
||||||
<div class="row"><span class="k">办结时间</span><span>{{ (detail.finishTime || "").replace("T", " ") || "-" }}</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
<template v-if="snapshotRows.length">
|
|
||||||
<Divider size="small" />
|
|
||||||
<div class="snapshot">
|
|
||||||
<div v-for="row in snapshotRows" :key="row.k" class="row">
|
|
||||||
<span class="k">{{ row.k }}</span>
|
|
||||||
<span class="v">{{ row.v }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card class="block" :bordered="false" dis-hover>
|
<div class="embed-sec">
|
||||||
<template #title>流程示意图</template>
|
<div class="embed-sec-h">
|
||||||
<img class="diagram-img" ref="diagramEl" :src="diagramSrc" alt="流程示意图" @load="onDiagramLoad" />
|
<span>办理进度</span>
|
||||||
</Card>
|
<RadioGroup class="embed-view-seg" :model-value="viewMode" size="small" type="button" @on-change="switchView">
|
||||||
|
<Radio label="node">按节点</Radio>
|
||||||
|
<Radio label="timeline">时间线</Radio>
|
||||||
|
<Radio label="list">列表</Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card class="block" :bordered="false" dis-hover>
|
<!-- ① 按节点:同节点多任务每人一行,步内保持后端任务序 -->
|
||||||
<template #title>
|
<template v-if="viewMode === 'node'">
|
||||||
流程轨迹
|
<div class="embed-mode-row">
|
||||||
<RadioGroup class="trace-mode" :model-value="traceMode" size="small" type="button" @on-change="switchMode">
|
<RadioGroup :model-value="traceMode" size="small" type="button" @on-change="switchMode">
|
||||||
<Radio label="user">按人员</Radio>
|
<Radio label="user">按人员</Radio>
|
||||||
<Radio label="dept">按科室</Radio>
|
<Radio label="dept">按科室</Radio>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</template>
|
</div>
|
||||||
<TraceTimeline :trace="detail.trace || []" :mode="traceMode" />
|
<div class="embed-steps" v-if="nodeSteps.length">
|
||||||
</Card>
|
<div v-for="(s, i) in nodeSteps" :key="`${s.type}-${s.nodeId || 'x'}-${i}`"
|
||||||
|
class="embed-step" :class="stepClass(s)">
|
||||||
<Card class="block" :bordered="false" dis-hover>
|
<span class="embed-step-dot"></span>
|
||||||
<template #title>
|
<div class="embed-step-body">
|
||||||
审批记录
|
<div class="embed-step-head">
|
||||||
<span class="record-tip">整个流程周期的完整事件流水</span>
|
<span class="embed-step-name">{{ s.nodeName || s.nodeId || "—" }}</span>
|
||||||
</template>
|
<Tag size="small" :color="stepTagColor(s)">{{ s.label }}</Tag>
|
||||||
<Table size="small" :columns="recordColumns" :data="detail.trace || []" />
|
<Tag v-if="s.active" size="small" color="blue">办理中</Tag>
|
||||||
</Card>
|
</div>
|
||||||
|
<div v-for="(r, j) in s.rows" :key="r.rowKey || j" class="embed-assignee" :class="rowClass(r)">
|
||||||
<Card class="block" :bordered="false" dis-hover v-if="formRows.length">
|
<span class="embed-an">{{ recordActor(r) }}</span>
|
||||||
<template #title>业务表单数据</template>
|
<span class="embed-am">{{ recordAction(r) }}</span>
|
||||||
<div class="snapshot">
|
<span class="embed-am">{{ rowTimeText(r) }}</span>
|
||||||
<div v-for="row in formRows" :key="row.k" class="row">
|
<span class="embed-ao" v-if="r.opinion">
|
||||||
<span class="k">{{ row.k }}</span>
|
{{ r.kind === "REJECT" ? "拒绝意见" : "意见" }}:{{ r.opinion }}
|
||||||
<span class="v">{{ row.v }}</span>
|
</span>
|
||||||
|
<span class="embed-ao" v-if="r.edgeName">去向:{{ r.edgeName }}</span>
|
||||||
|
<span class="embed-ao route-hung" v-if="r.kind === 'ROUTE' && !r.outlet">
|
||||||
|
判断失败已挂起(第 {{ r.attempt || 1 }} 次),等待管理端重跑路由
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="muted" v-else>暂无轨迹</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ② 时间线:扁平行 + 多轮分隔(复用共用组件,口径不变) -->
|
||||||
|
<TraceTimeline v-else-if="viewMode === 'timeline'" :trace="detail.trace || []" :mode="traceMode"
|
||||||
|
:status-text="TRACE_STATUS_TEXT" />
|
||||||
|
|
||||||
|
<!-- ③ 列表:整个流程周期的完整事件流水 -->
|
||||||
|
<Table v-else size="small" :columns="recordColumns" :data="detail.trace || []" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details class="embed-fold" @toggle="onDiagramToggle">
|
||||||
|
<summary>流程示意图</summary>
|
||||||
|
<div class="embed-fold-b">
|
||||||
|
<img class="diagram-img" ref="diagramEl" :src="diagramSrc" alt="流程示意图" @load="onDiagramLoad" />
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details v-if="summaryFields.length" class="embed-fold">
|
||||||
|
<summary>业务信息</summary>
|
||||||
|
<div class="embed-fold-b">
|
||||||
|
<div class="sf">
|
||||||
|
<div v-for="f in summaryFields" :key="f.label" class="sf-row">
|
||||||
|
<span class="sf-k">{{ f.label }}</span>
|
||||||
|
<span class="sf-v">{{ f.value }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="loading" class="embed-page muted">加载中…</div>
|
<div v-else-if="loading" class="embed-page muted">加载中…</div>
|
||||||
<div v-else class="embed-page muted expired">链接已过期,请回到来源应用重新打开</div>
|
<div v-else class="embed-page muted expired">链接已过期,请回到来源应用重新打开</div>
|
||||||
@@ -267,23 +414,84 @@ onMounted(async () => {
|
|||||||
/* 根类不用 .page:UnoCSS 原子类同名会以更高优先级盖掉 scoped 的 padding(裸渲染无 layout 壳时暴露) */
|
/* 根类不用 .page:UnoCSS 原子类同名会以更高优先级盖掉 scoped 的 padding(裸渲染无 layout 壳时暴露) */
|
||||||
/* 嵌入页自身即滚动容器:全局 html/body overflow:hidden(App.vue 壳用),裸渲染页无 layout 滚动壳,
|
/* 嵌入页自身即滚动容器:全局 html/body overflow:hidden(App.vue 壳用),裸渲染页无 layout 滚动壳,
|
||||||
不在此定高滚动的话内容超一屏被裁且无滚动条(2026-10-10 用户反馈) */
|
不在此定高滚动的话内容超一屏被裁且无滚动条(2026-10-10 用户反馈) */
|
||||||
.embed-page { height: 100vh; overflow-y: auto; padding: 12px 16px; box-sizing: border-box; }
|
.embed-page { height: 100vh; overflow-y: auto; padding: 0 20px 28px; box-sizing: border-box; }
|
||||||
.head { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
|
/* 标题区吸顶(贴合设计基线):左右负外边距让白色背景铺满滚动容器,滚动时内容不从两侧留白露出 */
|
||||||
.title { font-size: 16px; font-weight: 600; }
|
.head {
|
||||||
.block { margin-bottom: 12px; }
|
position: sticky; top: 0; z-index: 5; background: #fff;
|
||||||
.grid .row { display: flex; padding: 3px 0; }
|
display: flex; align-items: center; gap: 8px;
|
||||||
.k { width: 120px; color: #888; flex-shrink: 0; }
|
margin: 0 -20px 16px; padding: 14px 20px 12px; border-bottom: 1px solid #f2f4f7;
|
||||||
.v { white-space: pre-wrap; word-break: break-all; }
|
}
|
||||||
|
.title { font-size: 16px; font-weight: 600; flex: 1; min-width: 0; }
|
||||||
.muted { color: #999; }
|
.muted { color: #999; }
|
||||||
.expired { padding: 48px 16px; text-align: center; }
|
.expired { padding: 48px 16px; text-align: center; }
|
||||||
.diagram-img { max-width: 100%; height: auto; display: block; margin: 0 auto; }
|
|
||||||
.trace-mode { float: right; font-weight: normal; margin-right: 16px; }
|
/* 区块标题:左侧蓝条 + 右侧控件 */
|
||||||
/* 类名带 record- 前缀:防 UnoCSS 原子类撞名(嵌入页裸渲染无 layout 壳,撞名实测会盖样式) */
|
.embed-sec { margin-bottom: 18px; }
|
||||||
.record-tip {
|
.embed-sec-h { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; font-size: 14px; font-weight: 600; }
|
||||||
float: right;
|
.embed-sec-h > span:first-child { position: relative; padding-left: 9px; }
|
||||||
font-size: 12px;
|
.embed-sec-h > span:first-child::before {
|
||||||
font-weight: normal;
|
content: ""; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
|
||||||
margin-right: 16px;
|
width: 3px; height: 13px; border-radius: 2px; background: #2d8cf0;
|
||||||
color: #999;
|
|
||||||
}
|
}
|
||||||
|
.embed-view-seg { margin-left: auto; }
|
||||||
|
|
||||||
|
/* 基本信息:四列等宽(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-mode-row { display: flex; justify-content: flex-end; margin-bottom: 8px; }
|
||||||
|
.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: #2d8cf0; 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; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ const props = defineProps<{
|
|||||||
trace: any[];
|
trace: any[];
|
||||||
/** user=按人员(缺省)|dept=按科室 */
|
/** user=按人员(缺省)|dept=按科室 */
|
||||||
mode?: string;
|
mode?: string;
|
||||||
|
/** 展示层状态文案(缺省用全局 TASK_STATUS);嵌入页传自己的口径,管理面不传即维持原样 */
|
||||||
|
statusText?: Record<number, string>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
/** 事件类型 → 时间线圆点颜色 */
|
/** 事件类型 → 时间线圆点颜色 */
|
||||||
@@ -84,7 +86,8 @@ function actor(t: any): string {
|
|||||||
function actionText(t: any): string {
|
function actionText(t: any): string {
|
||||||
if (t.kind === "CC") return "已抄送";
|
if (t.kind === "CC") return "已抄送";
|
||||||
if (t.kind === "ROUTE") return "系统判断";
|
if (t.kind === "ROUTE") return "系统判断";
|
||||||
return TASK_STATUS[t.status] || String(t.status ?? "");
|
const text = props.statusText || TASK_STATUS;
|
||||||
|
return text[t.status] || String(t.status ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmtTime(v: string | null | undefined): string {
|
function fmtTime(v: string | null | undefined): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user