[fix] 办理页联调修复:业务信息只认 summaryFields / 业务详情用 bizUrlAbs / bare 态不叠遮罩层
build / build (push) Successful in 3m50s

- 业务摘要 → 业务信息:只渲染 bizSnapshot.summaryFields(label+value,口径同 InstanceBody),
  不再平铺英文键(summaryFields 曾渲染成 [object Object]);老数据仅回退 标题/摘要/附件
- 查看业务详情:独立打开改用后端解析的 bizUrlAbs(裸路径不再落到本应用 origin),拿不到不渲染
- bare 态(embed=1):同意改行内确认条、退回/移交改行内面板(Modal 在 iframe 内会与父页面弹窗套娃)
- bare 态流程轨迹默认折叠(父页面已有审批进度,同一份 trace 不同屏铺开两次)
This commit is contained in:
chenxing
2026-10-19 20:09:41 +08:00
parent ba448fc7a0
commit 8853c0faa3
2 changed files with 156 additions and 25 deletions
+145 -23
View File
@@ -47,6 +47,18 @@ const showTransfer = ref(false);
const transferUser = ref("");
const transferName = ref("");
/**
* 业务信息行:唯一内容 = 约定键 bizSnapshot.summaryFields(子应用决定展示哪些字段、用什么中文 label,
* 口径同 InstanceBody「业务信息」)。不再逐字平铺 bizSnapshot 原始键——那会把 title/summaryFields 这类
* 英文 key 直接摆给办理人看,对象值还会渲染成 [object Object]。
* 无 summaryFields(老数据/未接入约定的子应用)只回退已知的人类可读键,未知键一律不外露。
*/
const SUMMARY_FALLBACK_LABELS: Array<[string, string]> = [
["title", "标题"],
["summary", "摘要"],
["attachmentNames", "附件"],
];
const snapshotRows = computed(() => {
const raw = detail.value?.bizSnapshot;
if (!raw) return [];
@@ -56,10 +68,22 @@ const snapshotRows = computed(() => {
} catch {
return [{ k: "摘要", v: String(raw) }];
}
if (obj && typeof obj === "object") {
return Object.entries(obj).map(([k, v]) => ({ k, v: v == null ? "-" : String(v) }));
if (!obj || typeof obj !== "object") return [];
const sf = obj.summaryFields;
if (Array.isArray(sf) && sf.length) {
return sf
.filter((x: any) => x && x.label !== null && x.label !== undefined)
.slice(0, 6)
.map((x: any) => ({
k: String(x.label),
v: Array.isArray(x.value)
? (x.value.join("、") || "-")
: (x.value === null || x.value === undefined || x.value === "" ? "-" : String(x.value)),
}));
}
return [];
return SUMMARY_FALLBACK_LABELS
.filter(([key]) => obj[key] !== null && obj[key] !== undefined && obj[key] !== "")
.map(([key, label]) => ({ k: label, v: formValueText(obj[key]) }));
});
/** 业务表单数据(§3.4):formDataJson 逐键值只读渲染;数组值按文件名/项连接;解析失败降级一行原文 */
@@ -100,15 +124,22 @@ async function load() {
}
}
/** 业务详情绝对地址(后端按 bizUrl + applyAppCode 查应用注册表 pc_url 拼出)。
* bizUrl 本身是裸相对路径,流内直开时没有宿主页面可借基址,直接 window.open 会落到本应用 origin 上。 */
const bizUrlAbs = computed(() => detail.value?.bizUrlAbs || "");
/** 入口可见性:bare 由父页面按自己路由接管(原样传裸路径),独立打开必须拿到绝对地址 */
const canOpenBiz = computed(() => !!detail.value?.bizUrl && (isBare.value || !!bizUrlAbs.value));
function openBizUrl() {
const url = detail.value?.bizUrl;
if (!url) return;
// bare:由父页面决定怎么开(它才知道自家业务详情的路由),iframe 里 window.open 会白跳一个中心站地址
if (isBare.value) {
postToParent({ type: "open-biz", bizId: detail.value?.bizId ?? null, bizUrl: url });
postToParent({ type: "open-biz", bizId: detail.value?.bizId ?? null, bizUrl: detail.value?.bizUrl || "" });
return;
}
window.open(url, "_blank");
if (!bizUrlAbs.value) {
Message.warning("业务详情地址暂不可用,请在业务系统内查看");
return;
}
window.open(bizUrlAbs.value, "_blank");
}
/** 流程图仍要能看,但 bare 下不能用 router.push——/diagram 不是 /task/ 前缀,会在 iframe 里长出菜单壳 */
@@ -162,17 +193,33 @@ async function submit(approved: boolean) {
}
}
/**
* 二次确认:独立打开用 Modal.confirm(原生体验);bare 态一律行内确认。
* iframe 内弹 Modal 会在父页面弹窗之上再叠一层遮罩——父页遮罩盖不到 iframe,视觉上是"弹窗套弹窗",
* 且弹出位置只在本页顶部(长表单下离按钮很远)。行内确认条没有这个问题。
*/
const confirming = ref(false);
function confirmApprove() {
if (outlets.value.length > 1 && !chosenEdge.value) {
Message.warning("该节点有多个办理去向,请先选择");
return;
}
if (isBare.value) {
confirming.value = true;
return;
}
Modal.confirm({
title: "同意该任务?",
onOk: () => submit(true),
});
}
function doApprove() {
confirming.value = false;
submit(true);
}
// ===== 退回 =====
function openReturn() {
const targets = detail.value?.returnTargets || [];
@@ -266,17 +313,18 @@ onBeforeUnmount(() => {
<Tag v-if="myTask && myTask.status === 0" color="warning">待本人办理</Tag>
</div>
<!-- 业务摘要区块(M1 办理页核心;富文本正文留在业务方,经 bizUrl 查看) -->
<!-- 业务信息区块(M1 办理页核心):只渲染子应用给的 summaryFields 中文标签行;
富文本正文留在业务方,经 bizUrl 查看 -->
<Card class="block" :bordered="false" dis-hover>
<template #title>业务摘要</template>
<div v-if="snapshotRows.length === 0" class="muted">(业务方未提供摘要)</div>
<template #title>业务信息</template>
<div v-if="snapshotRows.length === 0" class="muted">(业务方未提供业务信息)</div>
<div v-else 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>
<Button v-if="detail.bizUrl" size="small" type="primary" ghost class="biz-link" @click="openBizUrl">
<Button v-if="canOpenBiz" size="small" type="primary" ghost class="biz-link" @click="openBizUrl">
查看业务详情
</Button>
</Card>
@@ -306,16 +354,49 @@ onBeforeUnmount(() => {
<Input v-model="opinion" type="textarea" :rows="3" maxlength="500" show-word-limit
placeholder="同意可选填意见;拒绝必须填写意见" />
<div class="actions" v-if="myTaskId">
<Button type="primary" :loading="submitting" @click="confirmApprove">同意</Button>
<Button :loading="submitting" @click="openReturn">退回</Button>
<Button :loading="submitting" @click="showTransfer = true">移交</Button>
<Button type="error" :loading="submitting" @click="submit(false)">拒绝</Button>
<template v-if="confirming">
<span class="confirm-tip">同意该任务?</span>
<Button type="primary" :loading="submitting" @click="doApprove">确定</Button>
<Button @click="confirming = false">取消</Button>
</template>
<template v-else>
<Button type="primary" :loading="submitting" @click="confirmApprove">同意</Button>
<Button :loading="submitting" @click="openReturn">退回</Button>
<Button :loading="submitting" @click="showTransfer = true">移交</Button>
<Button type="error" :loading="submitting" @click="submit(false)">拒绝</Button>
</template>
</div>
<div v-else class="muted">当前任务不可办理(不存在/非本人/已办理)</div>
<!-- bare 态退回/移交:行内面板(iframe 里再弹 Modal 会与父页面弹窗叠成三层) -->
<div v-if="isBare && showReturn" class="inline-panel">
<div class="modal-row">
<span class="modal-label">退回至:</span>
<Select v-model="returnTarget" size="small" style="width: 240px"
placeholder="缺省=最近一个已办节点">
<Option value="">(最近一个已办节点)</Option>
<Option v-for="t in detail.returnTargets || []" :key="t.nodeId" :value="t.nodeId"
:label="t.nodeName || t.nodeId" />
</Select>
<Button size="small" type="primary" :loading="submitting" @click="doReturn">确认退回</Button>
<Button size="small" @click="showReturn = false">取消</Button>
</div>
<div class="muted modal-tip">退回后目标节点重新生成待办;并行分支内节点不可作为退回目标。</div>
</div>
<div v-if="isBare && showTransfer" class="inline-panel">
<div class="modal-row">
<span class="modal-label">移交对象工号:</span>
<Input v-model="transferUser" size="small" style="width: 200px" placeholder="对方工号(数字)" />
<Button size="small" type="primary" :loading="submitting" @click="doTransfer">确认移交</Button>
<Button size="small" @click="showTransfer = false">取消</Button>
</div>
<div class="muted modal-tip">移交后由对方接手本任务办理;意见可在下方填写交接说明。</div>
</div>
</Card>
<!-- 流程轨迹 -->
<Card class="block" :bordered="false" dis-hover>
<!-- 流程轨迹(独立打开):中心侧唯一上下文,常显 -->
<Card v-if="!isBare" class="block" :bordered="false" dis-hover>
<template #title>
流程轨迹
<RadioGroup class="trace-mode" :model-value="mode" size="small" type="button" @on-change="switchMode">
@@ -327,8 +408,27 @@ onBeforeUnmount(() => {
<Button size="small" ghost type="primary" @click="openDiagram">查看流程图</Button>
</Card>
<!-- 退回弹窗 -->
<Modal v-model="showReturn" title="退回" :width="480">
<!-- 流程轨迹(bare 态):父页面已有"审批进度"卡片,同一份 trace 不再整块铺开——
默认收起,需要核对明细(路由判断/抄送/退回留痕)时再展开 -->
<Card v-else class="block" :bordered="false" dis-hover>
<details class="trace-fold">
<summary>
<span class="trace-fold-title">流程轨迹({{ (detail.trace || []).length }} 条)</span>
<RadioGroup class="trace-mode" :model-value="mode" size="small" type="button"
@click.stop @on-change="switchMode">
<Radio label="user">按人员</Radio>
<Radio label="dept">按科室</Radio>
</RadioGroup>
</summary>
<div class="trace-fold-b">
<TraceTimeline :trace="detail.trace || []" :mode="mode" />
<Button size="small" ghost type="primary" @click="openDiagram">查看流程图</Button>
</div>
</details>
</Card>
<!-- 退回弹窗(独立打开;bare 态走行内面板,见办理区) -->
<Modal v-if="!isBare" v-model="showReturn" title="退回" :width="480">
<div class="modal-row">
<span class="modal-label">退回至:</span>
<Select v-model="returnTarget" size="small" style="width: 240px"
@@ -345,8 +445,8 @@ onBeforeUnmount(() => {
</template>
</Modal>
<!-- 移交弹窗 -->
<Modal v-model="showTransfer" title="移交" :width="480">
<!-- 移交弹窗(独立打开;bare 态走行内面板) -->
<Modal v-if="!isBare" v-model="showTransfer" title="移交" :width="480">
<div class="modal-row">
<span class="modal-label">移交对象工号:</span>
<Input v-model="transferUser" size="small" style="width: 200px" placeholder="对方工号(数字)" />
@@ -373,7 +473,15 @@ onBeforeUnmount(() => {
.snapshot .k { width: 140px; color: #888; flex-shrink: 0; }
.snapshot .v { white-space: pre-wrap; word-break: break-all; }
.biz-link { margin-top: 8px; }
.actions { margin-top: 10px; display: flex; gap: 8px; }
.actions { margin-top: 10px; display: flex; gap: 8px; align-items: center; }
.confirm-tip { color: #515a6e; font-size: 13px; }
/* bare 态行内动作面板:与按钮行同宽,浅底描边,不遮挡父页面 */
.inline-panel {
margin-top: 10px; padding: 10px 12px;
border: 1px solid #e8eaec; border-radius: 6px; background: #fafbfc;
}
.inline-panel .modal-row { margin-bottom: 4px; }
.inline-panel .modal-row .ivu-btn { margin-left: 8px; }
.muted { color: #999; }
.outlet { margin-bottom: 8px; display: flex; align-items: center; }
.outlet-label { color: #515a6e; margin-right: 6px; font-size: 13px; }
@@ -384,4 +492,18 @@ onBeforeUnmount(() => {
.opinion { color: #666; }
.time { color: #aaa; font-size: 12px; }
.trace-mode { float: right; font-weight: normal; margin-right: 16px; }
/* bare 态轨迹折叠:summary 自绘(去默认三角),展开后内容与独立页一致 */
.trace-fold > summary {
cursor: pointer; list-style: none; display: flex; align-items: center; gap: 10px;
font-size: 14px; font-weight: 600; color: #17233d;
}
.trace-fold > summary::-webkit-details-marker { display: none; }
.trace-fold > summary::after { content: "▾"; margin-left: auto; color: #808695; font-size: 12px; transition: .2s; }
.trace-fold[open] > summary::after { transform: rotate(180deg); }
.trace-fold-title { position: relative; padding-left: 9px; }
.trace-fold-title::before {
content: ""; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
width: 3px; height: 13px; border-radius: 2px; background: var(--brand);
}
.trace-fold-b { padding-top: 12px; }
</style>
+11 -2
View File
@@ -108,6 +108,15 @@ const formRows = computed(() => {
return [{ label: "表单原文", value: String(raw) }];
});
/** 业务详情链接(仅管理面):bizUrl 是裸相对路径,必须用后端解析好的绝对地址(bizUrlAbs),
* 否则会落到流程中心自己的 origin 上;解析不出就不给入口,不给打不开的链接。 */
const bizUrlAbs = computed(() => {
const abs = props.detail?.bizUrlAbs;
if (abs) return abs;
const raw = props.detail?.bizUrl;
return /^https?:\/\//i.test(raw || "") ? raw : "";
});
// ===== 事件类型口径(与 TraceTimeline.vue 同源:改一处必须同步另一处) =====
/** 事件类型 → 动作标签主题色(逐字照抄 TraceTimeline.vue 的 KIND_TAG) */
@@ -356,9 +365,9 @@ onMounted(async () => {
<div class="embed-meta-k">{{ m.k }}</div>
<div class="embed-meta-v" :title="m.v">{{ m.v }}</div>
</div>
<div v-if="full && detail.bizUrl" class="embed-meta-i">
<div v-if="full && bizUrlAbs" class="embed-meta-i">
<div class="embed-meta-k">业务详情</div>
<div class="embed-meta-v"><a :href="detail.bizUrl" target="_blank">查看业务详情</a></div>
<div class="embed-meta-v"><a :href="bizUrlAbs" target="_blank">查看业务详情</a></div>
</div>
</div>
</div>