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 色映射统一导出
174 lines
7.2 KiB
Vue
174 lines
7.2 KiB
Vue
<script setup lang="ts">
|
|
// 办件详情:状态 + 业务摘要 + 流程轨迹时间线 + 分支会签进度(概览 §9.4 进度图数据源)
|
|
// 轨迹两档显示(2026-09-18):mode=user(人员,缺省)|dept(科室),写进 query 便于分享/刷新保持,
|
|
// 并随"流程示意图"跳转带给图示页(两边口径一致)。
|
|
import { ref, computed, onMounted } from "vue";
|
|
import { useRoute, useRouter } from "vue-router";
|
|
import { flowApi, INSTANCE_STATUS } from "@/api/module/flow";
|
|
import TraceTimeline from "@/views/flow/components/TraceTimeline.vue";
|
|
import config from "@/assets/config/host.config";
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const loading = ref(false);
|
|
const detail = ref<any>(null);
|
|
|
|
/** 显示口径:dept=按科室 / user=按人员(不传=人员) */
|
|
const mode = ref<string>(route.query.mode === "dept" ? "dept" : "user");
|
|
|
|
function switchMode(val: string) {
|
|
mode.value = val;
|
|
router.replace({ query: { ...route.query, mode: val } });
|
|
}
|
|
|
|
const snapshotRows = computed(() => {
|
|
const raw = detail.value?.bizSnapshot;
|
|
if (!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") {
|
|
return Object.entries(obj).map(([k, v]) => ({ k, v: v == null ? "-" : String(v) }));
|
|
}
|
|
return [];
|
|
});
|
|
|
|
/** 业务表单数据(§3.4 只读渲染;解析失败降级一行原文;无 formData 时整块隐藏) */
|
|
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)),
|
|
}));
|
|
}
|
|
return [{ k: "表单原文", v: String(raw) }];
|
|
});
|
|
|
|
const branchText = computed(() => {
|
|
const branches = detail.value?.branches || [];
|
|
if (!branches.length) return "";
|
|
const arrived = branches.filter((b: any) => b.status !== 0).length;
|
|
return `${arrived}/${branches.length} 分支已到达汇聚点`;
|
|
});
|
|
|
|
/** 流程示意图图片直嵌(2026-09-16 "CAS 验证码式":后端 /diagram.png 返回 PNG 流,img 直显)。
|
|
* img 不走 alova(无 baseUrl 拼接),须显式带 serviceHost——/{ctx}/front/ 子路径部署下
|
|
* 裸 /api 会落到网关根导致 404。mode 跟随本页选择,缩略图与轨迹口径一致。 */
|
|
const diagramSrc = computed(
|
|
() => `${config.serviceHost}/api/flow/diagram.png?instanceId=${route.params.instanceId}&mode=${mode.value}`,
|
|
);
|
|
|
|
/** 跳流程图时带上当前口径(图示页读 mode,缺省人员) */
|
|
function toDiagram() {
|
|
router.push({ path: "/diagram", query: { instanceId: String(route.params.instanceId), mode: mode.value } });
|
|
}
|
|
|
|
function statusName(s: number) {
|
|
return INSTANCE_STATUS[s] || s;
|
|
}
|
|
|
|
function statusColor(s: number) {
|
|
return s === 10 ? "blue" : s === 20 ? "green" : s === 30 ? "red" : "gray";
|
|
}
|
|
|
|
onMounted(async () => {
|
|
loading.value = true;
|
|
try {
|
|
detail.value = await flowApi.detail(String(route.params.instanceId)).send();
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="page" v-if="detail">
|
|
<div class="head">
|
|
<span class="title">{{ detail.bizTitle }}</span>
|
|
<Tag :color="statusColor(detail.status)">{{ statusName(detail.status) }}</Tag>
|
|
<a @click="toDiagram()">流程示意图</a>
|
|
<span class="muted" v-if="branchText">{{ branchText }}</span>
|
|
</div>
|
|
|
|
<Card class="block" :bordered="false" dis-hover>
|
|
<template #title>基本信息</template>
|
|
<div class="grid">
|
|
<div class="row"><span class="k">流程</span><span>{{ detail.defCode }} v{{ detail.defVersion }}</span></div>
|
|
<div class="row"><span class="k">提交人</span><span>{{ detail.submitterName }}</span></div>
|
|
<div class="row"><span class="k">提交时间</span><span>{{ (detail.submitTime || "").replace("T", " ") }}</span></div>
|
|
<div class="row"><span class="k">办结时间</span><span>{{ (detail.finishTime || "").replace("T", " ") || "-" }}</span></div>
|
|
<div class="row" v-if="detail.bizUrl"><span class="k">业务详情</span>
|
|
<a :href="detail.bizUrl" target="_blank">查看业务详情</a></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>
|
|
</template>
|
|
<template v-if="formRows.length">
|
|
<Divider size="small">业务表单数据</Divider>
|
|
<div class="snapshot">
|
|
<div v-for="row in formRows" :key="row.k" class="row">
|
|
<span class="k">{{ row.k }}</span>
|
|
<span class="v">{{ row.v }}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
|
|
<Card class="block" :bordered="false" dis-hover>
|
|
<template #title>
|
|
流程示意图
|
|
<a class="diagram-more" @click="toDiagram()">查看大图</a>
|
|
</template>
|
|
<img class="diagram-img" :src="diagramSrc" alt="流程示意图" />
|
|
</Card>
|
|
|
|
<Card class="block" :bordered="false" dis-hover>
|
|
<template #title>
|
|
流程轨迹
|
|
<RadioGroup class="trace-mode" :model-value="mode" size="small" type="button" @on-change="switchMode">
|
|
<Radio label="user">按人员</Radio>
|
|
<Radio label="dept">按科室</Radio>
|
|
</RadioGroup>
|
|
</template>
|
|
<TraceTimeline :trace="detail.trace || []" :mode="mode" />
|
|
</Card>
|
|
</div>
|
|
<div v-else class="page muted">加载中…</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.page { padding: 12px 16px; }
|
|
.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; }
|
|
</style>
|