feat(embed): 新增办件进度嵌入页与无鉴权旁路
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
<script setup lang="ts">
|
||||
// 办件进度嵌入页(iframe 免签嵌入,2026-10-03 方案):子应用 Drawer 里以 iframe 打开本页,
|
||||
// 展示"进度 + 流程示意图",零样式复制、单点维护。全程零 CAS 交互——数据走 /open/flow/embed/data
|
||||
// 免签端点(只认 token),示意图 token 两用(/open/flow/diagram/img 同一 token 出图)。
|
||||
// 本页只读(方案 §9):不做任何审批动作;办理入口仍是待办站内信 → /task/:taskId 既有链路。
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { INSTANCE_STATUS, INSTANCE_STATUS_COLOR } from "@/api/module/flow";
|
||||
import TraceTimeline from "@/views/flow/components/TraceTimeline.vue";
|
||||
import config from "@/assets/config/host.config";
|
||||
|
||||
const route = useRoute();
|
||||
const detail = ref<any>(null);
|
||||
const loading = ref(true);
|
||||
/** 兜底空态:缺 token / 过期 / 加载失败,全页同一文案,不留白 */
|
||||
const expired = ref(false);
|
||||
|
||||
/** 显示口径:dept=按科室 / user=按人员(缺省)。纯前端本地状态——嵌入页 URL 归子应用管,
|
||||
* 改 query 会破坏 token 语义,不落 URL(与管理面 InstanceDetail 写 query 的动机不同) */
|
||||
const traceMode = ref<string>("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 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 [];
|
||||
});
|
||||
|
||||
/** 业务表单数据键值平铺(同 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)),
|
||||
}));
|
||||
}
|
||||
return [{ k: "表单原文", v: String(raw) }];
|
||||
});
|
||||
|
||||
function statusName(s: number) {
|
||||
return INSTANCE_STATUS[s] || s;
|
||||
}
|
||||
|
||||
function statusColor(s: number) {
|
||||
return INSTANCE_STATUS_COLOR[s] || "gray";
|
||||
}
|
||||
|
||||
// 免签端点不带任何会话,alova 拦截器(token 头/信封解析)一概用不上,原生 fetch 直取
|
||||
onMounted(async () => {
|
||||
if (!token.value) {
|
||||
expired.value = true;
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`${config.serviceHost}/open/flow/embed/data?token=${token.value}`);
|
||||
if (!res.ok) {
|
||||
// 404=过期/伪造,与缺 token 同空态(不区分,不诱导探测)
|
||||
expired.value = true;
|
||||
return;
|
||||
}
|
||||
detail.value = await res.json();
|
||||
} catch {
|
||||
expired.value = true;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="embed-page" v-if="detail">
|
||||
<div class="head">
|
||||
<span class="title">{{ detail.bizTitle }}</span>
|
||||
<Tag :color="statusColor(detail.status)">{{ statusName(detail.status) }}</Tag>
|
||||
</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>
|
||||
<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>
|
||||
</Card>
|
||||
|
||||
<Card class="block" :bordered="false" dis-hover>
|
||||
<template #title>流程示意图</template>
|
||||
<img class="diagram-img" :src="diagramSrc" alt="流程示意图" />
|
||||
</Card>
|
||||
|
||||
<Card class="block" :bordered="false" dis-hover>
|
||||
<template #title>
|
||||
流程轨迹
|
||||
<RadioGroup class="trace-mode" :model-value="traceMode" size="small" type="button" @on-change="switchMode">
|
||||
<Radio label="user">按人员</Radio>
|
||||
<Radio label="dept">按科室</Radio>
|
||||
</RadioGroup>
|
||||
</template>
|
||||
<TraceTimeline :trace="detail.trace || []" :mode="traceMode" />
|
||||
</Card>
|
||||
|
||||
<Card class="block" :bordered="false" dis-hover v-if="formRows.length">
|
||||
<template #title>业务表单数据</template>
|
||||
<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>
|
||||
</Card>
|
||||
</div>
|
||||
<div v-else-if="loading" class="embed-page muted">加载中…</div>
|
||||
<div v-else class="embed-page muted expired">链接已过期,请回到来源应用重新打开</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 根类不用 .page:UnoCSS 原子类同名会以更高优先级盖掉 scoped 的 padding(裸渲染无 layout 壳时暴露) */
|
||||
.embed-page { padding: 12px 16px; }
|
||||
.head { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
|
||||
.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; }
|
||||
.expired { padding: 48px 16px; text-align: center; }
|
||||
.diagram-img { max-width: 100%; }
|
||||
.trace-mode { float: right; font-weight: normal; margin-right: 16px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user