- 修复办理页「查看流程图」双 hash URL:改 router.resolve + toAppHashUrl 单 hash
- 修复管理端办件抽屉「查看大图」域名尾点(origin + BASE_URL("./") 拼接)
- 新增 flowError 口径层与办理页内联失败条,审批失败提示可读、不泄漏堆栈
- 审批节点抽屉新增「预览候选人」真入口 + AssigneePreviewModal(只读,不写回定义)
- 线级停发/恢复失败改为单条错误面,避免同错双 toast
- 审批抽屉核心区/高级设置分层;插入菜单常用/高级分组
- 新增 FlowAssignEmbed 选人 iframe 父侧宿主(origin/source/信封/context 四校验)
- 修复 /diagram、/assign-preview 面包屑误显示「流程编辑器」
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* FL 侧选人 iframe 宿主(验收 P2-2 协议收口)。
|
||||
*
|
||||
* 用法(业务页,不是 FL 自己的产品入口):
|
||||
* <FlowAssignEmbed ref="picker" :flow-guid="guid" @confirmed="onPicked" @cancelled="onCancel" />
|
||||
* picker.value.open()
|
||||
*
|
||||
* 语义(与既定架构一致):
|
||||
* - 确认=把 iframe 内选择的最终结果交回宿主页面内存;宿主何时提交仍由业务调 FL 正式接口决定,
|
||||
* 前端选择结果不是授权凭据,最终资格由 FL 服务端复核。
|
||||
* - 取消/直接关框=只通知宿主,不改宿主已有的旧选择。
|
||||
* - 重新打开后确认=以最近一次为准(B 替换 A)。
|
||||
* - 收消息四道校验:event.origin、event.source(必须是本组件那个 iframe 的窗口)、
|
||||
* 协议信封(proto/v/type)、本次 contextId;任一不过直接丢弃(旧会话迟到消息不会串台)。
|
||||
* - 关闭即摘监听,重开换新 contextId。
|
||||
*/
|
||||
import { onBeforeUnmount, ref, watch } from "vue";
|
||||
import { Modal } from "view-ui-plus";
|
||||
import { isValidAssignMessage } from "@/utils/assignEmbed";
|
||||
import type { AssignEmbedMessage } from "@/utils/assignEmbed";
|
||||
import { toAppHashUrl } from "@/utils/appUrl";
|
||||
|
||||
const props = defineProps<{
|
||||
/** 要预览/选择的流程线 */
|
||||
flowGuid: string;
|
||||
/** 弹框标题 */
|
||||
title?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
/** 用户在 iframe 内点了确认:assigns 为最终选择,defVersion 为预览时的版本锚 */
|
||||
confirmed: [payload: { flowGuid: string; defVersion?: number; assigns: { nodeId: string; userIds: number[] }[] }];
|
||||
/** 取消或宿主关框:宿主应保持原有选择,不做任何覆盖 */
|
||||
cancelled: [];
|
||||
}>();
|
||||
|
||||
const visible = ref(false);
|
||||
const frameUrl = ref("");
|
||||
const contextId = ref("");
|
||||
const frameEl = ref<HTMLIFrameElement | null>(null);
|
||||
/** 本次打开锁定的父↔子通道条件;非响应式:只在 open 时刷新 */
|
||||
let expectedOrigin = "";
|
||||
let listening = false;
|
||||
/** 本次会话是否已经由 iframe 明确答复过(确认/取消),用于区分"宿主自己关框" */
|
||||
let settled = false;
|
||||
|
||||
function newContextId(): string {
|
||||
return `c${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
function closeListener(): void {
|
||||
if (listening) {
|
||||
window.removeEventListener("message", onMessage);
|
||||
listening = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onMessage(ev: MessageEvent): void {
|
||||
const msg: AssignEmbedMessage | null = isValidAssignMessage(
|
||||
ev,
|
||||
expectedOrigin,
|
||||
frameEl.value?.contentWindow ?? null,
|
||||
contextId.value,
|
||||
);
|
||||
if (!msg) return;
|
||||
settled = true;
|
||||
closeListener();
|
||||
visible.value = false;
|
||||
if (msg.type === "assign-cancelled") {
|
||||
emit("cancelled");
|
||||
return;
|
||||
}
|
||||
emit("confirmed", {
|
||||
flowGuid: msg.flowGuid || props.flowGuid,
|
||||
defVersion: msg.defVersion,
|
||||
assigns: msg.assigns ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
function open(): void {
|
||||
if (!props.flowGuid) return;
|
||||
// 重开:先摘掉上一轮监听,再换一个新的 contextId——旧 frame 的迟到消息按 context 失效
|
||||
closeListener();
|
||||
settled = false;
|
||||
contextId.value = newContextId();
|
||||
const query = `flowGuid=${encodeURIComponent(props.flowGuid)}&contextId=${encodeURIComponent(contextId.value)}`;
|
||||
frameUrl.value = toAppHashUrl(`#/assign-preview?${query}`);
|
||||
expectedOrigin = new URL(frameUrl.value, window.location.href).origin;
|
||||
visible.value = true;
|
||||
window.addEventListener("message", onMessage);
|
||||
listening = true;
|
||||
}
|
||||
|
||||
/** 宿主主动收起(等价于取消):同样摘监听,避免后续消息继续影响本页 */
|
||||
function close(): void {
|
||||
closeListener();
|
||||
if (visible.value && !settled) emit("cancelled");
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
// 点遮罩/X 关闭走这里;已 settled 的关闭由 iframe 自己回过话,不再补发 cancelled
|
||||
watch(visible, (val) => {
|
||||
if (!val) closeListener();
|
||||
});
|
||||
|
||||
onBeforeUnmount(closeListener);
|
||||
|
||||
defineExpose({ open, close, contextId });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal v-model="visible" :title="title || '选择办理人'" :width="760" :footer-hide="true" class="assign-embed-modal">
|
||||
<iframe
|
||||
v-if="frameUrl"
|
||||
ref="frameEl"
|
||||
:src="frameUrl"
|
||||
class="assign-embed-frame"
|
||||
frameborder="0"
|
||||
title="选择办理人"
|
||||
/>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.assign-embed-frame {
|
||||
width: 100%;
|
||||
height: 460px;
|
||||
border: 0;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@@ -76,10 +76,15 @@ const routeChange = (to: any) => {
|
||||
const navArr: any[] = [];
|
||||
|
||||
// 优先按 path 匹配菜单(平台模块 id 与路由 meta.modId 不保证一致),
|
||||
// 匹配不到再退回 modId 对照
|
||||
// 匹配不到再退回 modId 对照。
|
||||
// modId 为空时必须短路:深链页(/diagram、/assign-preview、/task/*)不带 modId,
|
||||
// 原先 `x.modId === undefined` 会命中第一条同样没有 modId 的菜单,面包屑于是显示成
|
||||
// 不相干的「流程编辑器」(验收报告 P3)。
|
||||
const find: any =
|
||||
menuArr.value.find((x: any) => x.page && x.page === to.path) ||
|
||||
menuArr.value.find((x: any) => x.modId === modId);
|
||||
(modId !== undefined && modId !== null
|
||||
? menuArr.value.find((x: any) => x.modId === modId)
|
||||
: null);
|
||||
if (find) {
|
||||
currPath.value = find.page || "";
|
||||
if (find.pid) {
|
||||
@@ -109,7 +114,10 @@ const routeChange = (to: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
const r = router.getRoutes().find((rt: any) => (rt.meta as any)?.modId === modId);
|
||||
// 同上:modId 缺失时不做这条兜底,否则又是"第一条无 modId 路由"中奖
|
||||
const r = (modId === undefined || modId === null)
|
||||
? undefined
|
||||
: router.getRoutes().find((rt: any) => (rt.meta as any)?.modId === modId);
|
||||
if (r) {
|
||||
currPath.value = r.path;
|
||||
navPath.value = [{ name: (r.meta as any).title, page: r.path }];
|
||||
@@ -117,7 +125,10 @@ const routeChange = (to: any) => {
|
||||
}
|
||||
|
||||
currPath.value = "";
|
||||
navPath.value = [];
|
||||
// 深链页(/diagram 流程示意图、/assign-preview 选择办理人、/task/*)不挂菜单:
|
||||
// 面包屑退回平台应用名(FL 配置的是「流程编辑器」),与实际页面标题不符(验收报告 P3)。
|
||||
// 菜单匹配不上时至少给出本页 meta.title,父级缺失不再伪造。
|
||||
navPath.value = title ? [{ name: title }] : [];
|
||||
};
|
||||
|
||||
const linkMenu = (item: any) => {
|
||||
|
||||
Reference in New Issue
Block a user