refactor(flow-editor)!: 删除画布模式,卡片树成为唯一编辑形态
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
// 流程编辑器(方案 §3):钉钉式卡片流——纵向节点卡片 + 节点间"+"插入 + 点卡片右侧抽屉配置,免连线。
|
||||
// 流程编辑器(飞书流派,2026-10-03 画布退役):卡片树是唯一编辑形态与数据源;
|
||||
// 多出口=审批节点出口按钮+泳道(OutletLanes),结构域封死 → 出图确定性布局。
|
||||
//
|
||||
// 入口三态(query):无参=新建;?id={defId}=编辑草稿;?id={defId}&baseGuid={flowGuid}=升版本(载入该版本内容为底稿)。
|
||||
// 保存只产草稿,走既有 POST /api/flow/admin/def/save;发布仍留在流程定义列表页(后端 DefValidator 终审)。
|
||||
@@ -39,7 +40,6 @@ import NotifyDrawer from "./components/NotifyDrawer.vue";
|
||||
import ConditionDrawer from "./components/ConditionDrawer.vue";
|
||||
import SettingsDrawer from "./components/SettingsDrawer.vue";
|
||||
import JsonDrawer from "./components/JsonDrawer.vue";
|
||||
import BpmnCanvas from "./components/BpmnCanvas.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -64,174 +64,9 @@ const userNames = reactive<Record<number, string>>({});
|
||||
/** 条件分支的判断服务注册(flw_router;拉取失败为空数组,校验降级提示) */
|
||||
const routers = reactive<RouterOption[]>([]);
|
||||
|
||||
// ==================== 双模式统一(2026-09-17):卡片树 = 唯一数据源,画布 = 其可视化投影 ====================
|
||||
/**
|
||||
* 旧实现画布/卡片各持一份基准(sourceJson vs model),切换后互相读不到对方改动,"切回去数据变初始化"。
|
||||
* 现统一为:EditorModel 永远持有业务配置(审批人/会签/时限)与卡片结构;
|
||||
* - 进画布:canvasSource = toContentJson(model),节点 id 直接用卡片 uid(convert.ts),天然一一对应;
|
||||
* - 出画布(切卡片/保存/源码应用后):serializeUnified 以 model 为 base 让 bpmn 画布结构性改动
|
||||
* (新增节点/改连线/布局)叠加其上,再经 fromContentJson 反解回卡片树;卡片树不支持的结构(多出口
|
||||
* DAG 等)拒绝切卡片,留在画布编辑。
|
||||
*/
|
||||
/** 编辑形态:canvas=bpmn-js 画布(支持多出口/跨段连线),cards=卡片流 */
|
||||
const editMode = ref<"canvas" | "cards">("cards");
|
||||
/** 画布侧数据是否可反解为卡片树(载入时探测,决定卡片单选是否可用) */
|
||||
const cardsBroken = ref(false);
|
||||
const bpmnWrap = ref<InstanceType<typeof BpmnCanvas> | null>(null);
|
||||
/** 画布数据源:载入用原始 contentJson;由卡片切回画布时用卡片树投影覆盖 */
|
||||
const canvasSource = ref("");
|
||||
/** 载入时的原始 contentJson(画布上"卡片树不存在"的新画节点配置兜底) */
|
||||
const canvasBase = ref<any>(null);
|
||||
/** 画布上新画节点的配置快照(element id → ApprovalNode 配置),保存时并入 nodes */
|
||||
const canvasConfigs = reactive<Record<string, any>>({});
|
||||
/** 原始 contentJson 快照(画布裁剪兜底与 JsonDrawer 应用基准) */
|
||||
const sourceJson = ref("");
|
||||
|
||||
function onCanvasNode(_el: any): void {
|
||||
// selection.changed 自画布 emit:选中审批/抄送/条件分支节点 → 打开对应配置抽屉
|
||||
const isApproval = _el && _el.type === "bpmn:UserTask";
|
||||
const isNotify = _el && _el.type === "bpmn:ServiceTask";
|
||||
const isCondition = _el && _el.type === "bpmn:ExclusiveGateway";
|
||||
if (!isApproval && !isNotify && !isCondition) return;
|
||||
const id = String(_el.businessObject.id || _el.id);
|
||||
// 条件分支的画布元素 id 是 JSON id(uid+"c" 后缀),卡片树按 uid 查(uid 由本编辑器生成,不带 c 尾)
|
||||
const lookupId = isCondition && id.endsWith("c") ? id.slice(0, -1) : id;
|
||||
const item = findItem(model, lookupId);
|
||||
if (item && (item.kind === "approval" || item.kind === "notify" || item.kind === "condition")) {
|
||||
// 卡片树已有节点:抽屉直接编辑卡片本体(id=uid 天然对应)
|
||||
openNode(item.uid);
|
||||
return;
|
||||
}
|
||||
// 画布上新画的节点(卡片树里没有):配置进 canvasConfigs,保存时并入
|
||||
const kind = isNotify ? "notify" : isCondition ? "condition" : "approval";
|
||||
const baseNode = (canvasBase.value?.nodes || []).find((n: any) => n.id === id) || {};
|
||||
if (!canvasConfigs[id]) {
|
||||
canvasConfigs[id] =
|
||||
kind === "notify"
|
||||
? {
|
||||
uid: id,
|
||||
kind: "notify",
|
||||
name: _el.businessObject.name || baseNode.name || "抄送",
|
||||
receivers: baseNode.receivers || { type: "USERS", userIds: [] },
|
||||
}
|
||||
: kind === "condition"
|
||||
? {
|
||||
uid: id,
|
||||
kind: "condition",
|
||||
name: _el.businessObject.name || baseNode.name || "条件判断",
|
||||
routerKey: baseNode.routerKey || "",
|
||||
outlets: [],
|
||||
branches: [],
|
||||
}
|
||||
: {
|
||||
uid: id,
|
||||
kind: "approval",
|
||||
name: _el.businessObject.name || baseNode.name || "审批",
|
||||
assignee: baseNode.assignee || null,
|
||||
countersign: baseNode.countersign || "ANY",
|
||||
deadlineHours: baseNode.deadlineHours || null,
|
||||
};
|
||||
}
|
||||
editingUid.value = id;
|
||||
if (canvasConfigs[id].kind === "notify") {
|
||||
notifyDrawer.value = true;
|
||||
} else if (canvasConfigs[id].kind === "condition") {
|
||||
conditionDrawer.value = true;
|
||||
} else {
|
||||
nodeDrawer.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function canvasRemoveNode(uid: string): void {
|
||||
bpmnWrap.value?.removeNode(uid);
|
||||
delete canvasConfigs[uid];
|
||||
}
|
||||
|
||||
/** 画布序列化 → contentJson 字符串:结构性内容(节点/连线/布局)出自画布,
|
||||
* 业务配置(审批人/会签/时限/抄送对象)以卡片树为 base 按 id 继承,settings(含催办)随 base 带出 */
|
||||
function serializeUnified(): string {
|
||||
const canvasJson = snapshotCanvasObject();
|
||||
// canvasConfigs(画布新画节点的抽屉配置)按 id 并入
|
||||
(canvasJson.nodes || []).forEach((n: any) => {
|
||||
const cfg = canvasConfigs[String(n.id)];
|
||||
if (!cfg) return;
|
||||
if (cfg.kind === "notify" && n.type === "NOTIFY") {
|
||||
n.name = cfg.name || n.name;
|
||||
if (cfg.receivers) n.receivers = cfg.receivers;
|
||||
} else if (cfg.kind === "approval" && n.type === "APPROVAL") {
|
||||
n.name = cfg.name || n.name;
|
||||
if (cfg.assignee) n.assignee = cfg.assignee;
|
||||
if (cfg.countersign) n.countersign = cfg.countersign;
|
||||
if (cfg.deadlineHours) n.deadlineHours = cfg.deadlineHours;
|
||||
} else if (cfg.kind === "condition" && n.type === "CONDITION") {
|
||||
n.name = cfg.name || n.name;
|
||||
if (cfg.routerKey) n.routerKey = cfg.routerKey;
|
||||
}
|
||||
});
|
||||
return JSON.stringify(canvasJson);
|
||||
}
|
||||
|
||||
const issues = computed<Issue[]>(() => validateModel(model, depts, routers));
|
||||
const defJson = computed(() => toContentJson(model));
|
||||
|
||||
/** 序列化 base:原始 contentJson 为底(保住卡片树不支持结构/新画节点的既有业务配置),
|
||||
* code/name/settings 与卡片树审批/抄送节点配置(审批人/会签/时限/抄送对象)以卡片树为准覆盖 */
|
||||
function buildSerializeBase(): any {
|
||||
const modelDef = toContentJson(model);
|
||||
const base = JSON.parse(JSON.stringify(canvasBase.value || {}));
|
||||
base.code = model.code.trim();
|
||||
base.name = model.name.trim();
|
||||
base.settings = JSON.parse(JSON.stringify(modelDef.settings));
|
||||
const byId: Record<string, any> = {};
|
||||
(base.nodes || []).forEach((n: any) => {
|
||||
byId[String(n.id)] = n;
|
||||
});
|
||||
// 全层级遍历(含并行分支内节点):只覆盖 id 能对上的,画布独有节点不受影响
|
||||
walkItems(model.list, (item) => {
|
||||
if (item.kind === "approval") {
|
||||
const merged: any = { ...(byId[item.uid] || {}) };
|
||||
merged.id = item.uid;
|
||||
merged.type = "APPROVAL";
|
||||
merged.name = item.name || "审批";
|
||||
if (item.assignee) merged.assignee = JSON.parse(JSON.stringify(item.assignee));
|
||||
if (item.countersign === "ANY") merged.countersign = "ANY";
|
||||
else delete merged.countersign; // 后端缺省即会签,不回写
|
||||
if (item.deadlineHours) merged.deadlineHours = item.deadlineHours;
|
||||
byId[item.uid] = merged;
|
||||
} else if (item.kind === "notify") {
|
||||
const merged: any = { ...(byId[item.uid] || {}) };
|
||||
merged.id = item.uid;
|
||||
merged.type = "NOTIFY";
|
||||
merged.name = item.name || "抄送";
|
||||
if (item.receivers) merged.receivers = JSON.parse(JSON.stringify(item.receivers));
|
||||
byId[item.uid] = merged;
|
||||
} else if (item.kind === "condition") {
|
||||
// 条件分支的 JSON id 带 c 后缀(convert.ts 生成约定),routerKey/名称随卡片树覆盖
|
||||
const jsonId = `${item.uid}c`;
|
||||
const merged: any = { ...(byId[jsonId] || {}) };
|
||||
merged.id = jsonId;
|
||||
merged.type = "CONDITION";
|
||||
merged.name = item.name || "条件判断";
|
||||
merged.routerKey = item.routerKey || "";
|
||||
byId[jsonId] = merged;
|
||||
}
|
||||
});
|
||||
base.nodes = Object.values(byId);
|
||||
return base;
|
||||
}
|
||||
|
||||
/** 画布未挂载(载入中/刚切换未挂好)时,快照退回卡片树投影 */
|
||||
function snapshotCanvasObject(): any {
|
||||
if (!bpmnWrap.value) return JSON.parse(JSON.stringify(defJson.value));
|
||||
const canvasJson = bpmnWrap.value?.serialize(
|
||||
buildSerializeBase(),
|
||||
model.code.trim(),
|
||||
model.name.trim()
|
||||
);
|
||||
if (!canvasJson) throw new Error("画布不可用");
|
||||
return canvasJson;
|
||||
}
|
||||
|
||||
/** 归一化(去 layout 坐标、节点按 id 排序):卡片树投影与画布序列化表示不同但内容等价,脏比较必须同表示 */
|
||||
function canonicalJson(content: any): string {
|
||||
const copy: any = JSON.parse(JSON.stringify(content));
|
||||
@@ -250,13 +85,9 @@ function canonicalJson(content: any): string {
|
||||
return JSON.stringify(copy);
|
||||
}
|
||||
|
||||
/** 脏检查基准(载入/保存后刷新);随编辑形态取同一内容的对应投影,切换模式后不因表示差异误报 */
|
||||
/** 脏检查基准(载入/保存后刷新) */
|
||||
const savedSnapshot = ref("");
|
||||
const currentSnapshot = computed(() =>
|
||||
editMode.value === "cards"
|
||||
? canonicalJson(JSON.parse(JSON.stringify(defJson.value)))
|
||||
: canonicalJson(snapshotCanvasObject())
|
||||
);
|
||||
const currentSnapshot = computed(() => canonicalJson(JSON.parse(JSON.stringify(defJson.value))));
|
||||
const isDirty = computed(() => !loading.value && currentSnapshot.value !== savedSnapshot.value);
|
||||
|
||||
const nodeDrawer = ref(false);
|
||||
@@ -267,34 +98,16 @@ const jsonDrawer = ref(false);
|
||||
const editingUid = ref("");
|
||||
|
||||
const editingNode = computed<ApprovalNode | null>(() => {
|
||||
if (editMode.value === "canvas") {
|
||||
const item = editingUid.value ? findItem(model, editingUid.value) : null;
|
||||
if (item && item.kind === "approval") return item;
|
||||
const cfg = canvasConfigs[editingUid.value];
|
||||
return cfg && cfg.kind === "approval" ? cfg : null;
|
||||
}
|
||||
const item = editingUid.value ? findItem(model, editingUid.value) : null;
|
||||
return item && item.kind === "approval" ? item : null;
|
||||
});
|
||||
|
||||
const editingNotify = computed<NotifyNode | null>(() => {
|
||||
if (editMode.value === "canvas") {
|
||||
const item = editingUid.value ? findItem(model, editingUid.value) : null;
|
||||
if (item && item.kind === "notify") return item;
|
||||
const cfg = canvasConfigs[editingUid.value];
|
||||
return cfg && cfg.kind === "notify" ? cfg : null;
|
||||
}
|
||||
const item = editingUid.value ? findItem(model, editingUid.value) : null;
|
||||
return item && item.kind === "notify" ? item : null;
|
||||
});
|
||||
|
||||
const editingCondition = computed<ConditionNode | null>(() => {
|
||||
if (editMode.value === "canvas") {
|
||||
const item = editingUid.value ? findItem(model, editingUid.value) : null;
|
||||
if (item && item.kind === "condition") return item;
|
||||
const cfg = canvasConfigs[editingUid.value];
|
||||
return cfg && cfg.kind === "condition" ? cfg : null;
|
||||
}
|
||||
const item = editingUid.value ? findItem(model, editingUid.value) : null;
|
||||
return item && item.kind === "condition" ? item : null;
|
||||
});
|
||||
@@ -367,8 +180,7 @@ async function load(): Promise<void> {
|
||||
if (queryId && !queryGuid) editId.value = queryId;
|
||||
|
||||
if (!queryId) {
|
||||
// 新建:画布源取卡片树投影(默认模板的审批节点 uid 与卡片一一对应),不用通用骨架
|
||||
canvasSource.value = JSON.stringify(defJson.value);
|
||||
// 新建:卡片树默认模板起步
|
||||
savedSnapshot.value = currentSnapshot.value;
|
||||
return;
|
||||
}
|
||||
@@ -378,18 +190,17 @@ async function load(): Promise<void> {
|
||||
sourceVersion.value = d?.version ?? 0;
|
||||
model.code = d?.code || "";
|
||||
model.name = d?.name || "";
|
||||
sourceJson.value = d?.contentJson || "{}";
|
||||
canvasBase.value = safeJson(sourceJson.value);
|
||||
canvasSource.value = sourceJson.value;
|
||||
let parsedOk = true;
|
||||
try {
|
||||
applyParsed(fromContentJson(JSON.parse(d?.contentJson || "{}")));
|
||||
} catch (e: any) {
|
||||
// 卡片树反解失败(多出口等结构):留在画布模式,卡片单选禁用
|
||||
parsedOk = false;
|
||||
void e;
|
||||
Modal.error({
|
||||
title: "该定义无法用卡片编辑器打开",
|
||||
content: `内容包含卡片视图不支持的结构(${e?.message || e})。此类结构多为历史画布草稿,请从流程定义列表处理。`,
|
||||
onOk: () => router.replace("/admin/defs"),
|
||||
});
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
cardsBroken.value = !parsedOk;
|
||||
await resolveUserNames();
|
||||
savedSnapshot.value = currentSnapshot.value;
|
||||
} finally {
|
||||
@@ -397,66 +208,6 @@ async function load(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function safeJson(s: string): any {
|
||||
try {
|
||||
return JSON.parse(s || "{}");
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 模式切换(双向互通) ====================
|
||||
|
||||
/** 卡片 → 画布:画布源换成卡片树投影(画布重挂载即拿到卡片最新结构/配置) */
|
||||
function toCanvas(): void {
|
||||
canvasSource.value = JSON.stringify(defJson.value);
|
||||
editMode.value = "canvas";
|
||||
bumpSnapshot();
|
||||
}
|
||||
|
||||
/** 画布 → 卡片:画布结构经 fromContentJson 反解回卡片树;不支持的结构拒绝切换 */
|
||||
function toCards(): void {
|
||||
let content = "";
|
||||
try {
|
||||
content = serializeUnified();
|
||||
} catch (e: any) {
|
||||
Message.error(`画布数据读取失败:${e?.message || e}`);
|
||||
return;
|
||||
}
|
||||
if (!content) {
|
||||
Message.warning("画布为空,无法切换到卡片视图");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
applyParsed(fromContentJson(JSON.parse(content)));
|
||||
} catch (e: any) {
|
||||
Message.error(`画布含卡片视图不支持的结构(${e?.message || e}),请继续用画布编辑或先简化结构`);
|
||||
return;
|
||||
}
|
||||
Object.keys(canvasConfigs).forEach((k) => delete canvasConfigs[k]);
|
||||
editMode.value = "cards";
|
||||
bumpSnapshot();
|
||||
}
|
||||
|
||||
/** 切换后按"切换前是否已脏"重置脏基准:干净切换 representations 等价,直接对齐;脏切换保留脏态 */
|
||||
function bumpSnapshot(): void {
|
||||
savedSnapshot.value = currentSnapshot.value;
|
||||
}
|
||||
|
||||
function onModeSwitch(next: string | number | boolean): void {
|
||||
if (loading.value) return;
|
||||
if (next === editMode.value) return;
|
||||
if (next === "cards" && cardsBroken.value) {
|
||||
Message.warning("该定义包含卡片视图不支持的结构(如多出口/退回环路),请用画布编辑");
|
||||
return;
|
||||
}
|
||||
if (next === "canvas") {
|
||||
toCanvas();
|
||||
} else {
|
||||
toCards();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 编辑操作(画布子树经 ctx 共享) ====================
|
||||
|
||||
/** 定位某节点所在的兄弟列表(删除前的"至少保留一个"判断用) */
|
||||
@@ -486,12 +237,6 @@ function openNode(uid: string): void {
|
||||
}
|
||||
|
||||
function removeNode(uid: string): void {
|
||||
if (editMode.value === "canvas") {
|
||||
// 卡片树是唯一数据源:卡片已有节点从两侧一起删;画布新画节点只删画布+配置快照
|
||||
if (findItem(model, uid)) removeItem(model, uid);
|
||||
canvasRemoveNode(uid);
|
||||
return;
|
||||
}
|
||||
const siblings = containerOfItem(uid);
|
||||
if (siblings && siblings.length <= 1) {
|
||||
Message.warning("每个流程/分支至少保留一个节点");
|
||||
@@ -590,30 +335,9 @@ function onUrgeSave(urge: UrgeSetting): void {
|
||||
model.settings.urge = urge;
|
||||
}
|
||||
|
||||
/** 抽屉关闭(确定/删除)后,画布上的节点名回写(配置改的是卡片树本体) */
|
||||
watch([nodeDrawer, notifyDrawer, conditionDrawer], ([aOpen, nOpen, cOpen], [aWas, nWas, cWas]) => {
|
||||
const closed = (open: boolean, was: boolean) => !open && was;
|
||||
if (!closed(aOpen, aWas) && !closed(nOpen, nWas) && !closed(cOpen, cWas)) return;
|
||||
if (editMode.value !== "canvas" || !bpmnWrap.value) return;
|
||||
const item = findItem(model, editingUid.value);
|
||||
if (item && item.kind === "approval") {
|
||||
bpmnWrap.value.renameNode(item.uid, item.name);
|
||||
}
|
||||
if (item && item.kind === "notify") {
|
||||
bpmnWrap.value.renameNode(item.uid, item.name);
|
||||
}
|
||||
if (item && item.kind === "condition") {
|
||||
bpmnWrap.value.renameNode(`${item.uid}c`, item.name);
|
||||
}
|
||||
});
|
||||
|
||||
/** 源码抽屉应用:写回卡片树;画布模式下画布源同步换投影,避免看到旧结构 */
|
||||
/** 源码抽屉应用:写回卡片树(应用源码后 isDirty 自然为 true,预期行为) */
|
||||
function onJsonApplied(p: ParsedDef): void {
|
||||
applyParsed(p);
|
||||
if (editMode.value === "canvas") {
|
||||
canvasSource.value = JSON.stringify(defJson.value);
|
||||
bumpSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
async function onSave(): Promise<void> {
|
||||
@@ -626,36 +350,11 @@ async function onSave(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
const errors = countErrors(issues.value);
|
||||
if (errors.length && editMode.value === "cards") {
|
||||
if (errors.length) {
|
||||
Message.error(`校验未通过:${errors[0].text}`);
|
||||
return;
|
||||
}
|
||||
let content: string;
|
||||
if (editMode.value === "canvas") {
|
||||
try {
|
||||
content = serializeUnified();
|
||||
} catch (e: any) {
|
||||
Message.error(`画布数据读取失败:${e?.message || e}`);
|
||||
return;
|
||||
}
|
||||
const problems = checkCanvas(content);
|
||||
if (problems.length) {
|
||||
Message.error(problems[0]);
|
||||
return;
|
||||
}
|
||||
if (!content || content === "{}") {
|
||||
Message.warning("画布为空,无法保存");
|
||||
return;
|
||||
}
|
||||
// 保存内容同步回卡片树(画布=数据源投影,保存后两侧一致,脏基准重算不误报)
|
||||
try {
|
||||
applyParsed(fromContentJson(JSON.parse(content)));
|
||||
} catch {
|
||||
// 理论不可达(checkCanvas 通过即可反解);失败不阻断保存
|
||||
}
|
||||
} else {
|
||||
content = JSON.stringify(toContentJson(model));
|
||||
}
|
||||
const content = JSON.stringify(toContentJson(model));
|
||||
saving.value = true;
|
||||
try {
|
||||
// code/name 同时出现在 body 与 contentJson 且取值同源,规避 contentJson 静默覆盖行字段的既有坑
|
||||
@@ -674,98 +373,6 @@ async function onSave(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/** 画布产物轻校验(保存前拦截;结构完整校验仍以发布期 DefValidator 为准) */
|
||||
function checkCanvas(content: string): string[] {
|
||||
const problems: string[] = [];
|
||||
let def: any;
|
||||
try {
|
||||
def = JSON.parse(content);
|
||||
} catch {
|
||||
return ["画布数据序列化失败,请重试"];
|
||||
}
|
||||
const nodes = def.nodes || [];
|
||||
const edges = def.edges || [];
|
||||
const nodeIds = new Set(nodes.map((n: any) => n.id));
|
||||
if (nodes.filter((n: any) => n.type === "START").length !== 1) {
|
||||
problems.push("画布必须有且仅有 1 个开始节点");
|
||||
}
|
||||
if (nodes.filter((n: any) => n.type === "END").length !== 1) {
|
||||
problems.push("画布必须有且仅有 1 个结束节点");
|
||||
}
|
||||
nodes.forEach((n: any) => {
|
||||
if (n.type === "APPROVAL") {
|
||||
if (!n.name) problems.push("审批节点未命名");
|
||||
if (!n.assignee) problems.push("审批节点「" + (n.name || n.id) + "」缺少审批人配置");
|
||||
}
|
||||
if (n.type === "NOTIFY") {
|
||||
if (!n.name) problems.push("抄送节点未命名");
|
||||
if (!n.receivers) problems.push("抄送节点「" + (n.name || n.id) + "」缺少抄送对象配置");
|
||||
else if (n.receivers.type === "USERS" && !(n.receivers.userIds || []).length) {
|
||||
problems.push("抄送节点「" + (n.name || n.id) + "」未选择用户");
|
||||
}
|
||||
}
|
||||
if (n.type === "CONDITION") {
|
||||
// 条件分支(镜像后端 DefValidator 的 CONDITION 口径):routerKey 必填 + 出口命名唯一 + 出边 ≥2
|
||||
if (!n.routerKey) problems.push("条件分支「" + (n.name || n.id) + "」未配置判断服务(routerKey)");
|
||||
const outs = edges.filter((e: any) => e.from === n.id);
|
||||
if (outs.length < 2) {
|
||||
problems.push("条件分支「" + (n.name || n.id) + "」至少 2 条出边(当前 " + outs.length + ")");
|
||||
} else {
|
||||
const names = new Set<string>();
|
||||
outs.forEach((e: any) => {
|
||||
const label = e.name?.trim() || "";
|
||||
if (!label) {
|
||||
problems.push(`条件分支「${n.name || n.id}」每个出口必须命名(出口名即判断依据)`);
|
||||
} else if (names.has(label)) {
|
||||
problems.push(`条件分支「${n.name || n.id}」出口名重复:「${label}」`);
|
||||
} else {
|
||||
names.add(label);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
edges.forEach((e: any) => {
|
||||
if (!nodeIds.has(e.to) || !nodeIds.has(e.from)) {
|
||||
problems.push("存在指向缺失节点的连线,请放置后重画");
|
||||
}
|
||||
});
|
||||
// 多出口命名(镜像后端 DefValidator.validateOutlets 口径)
|
||||
const outNames: Record<string, Set<string>> = {};
|
||||
edges.forEach((e: any) => {
|
||||
(outNames[e.from] = outNames[e.from] || new Set()).add(e.name || "");
|
||||
});
|
||||
nodes.forEach((n: any) => {
|
||||
const outs = edges.filter((e: any) => e.from === n.id);
|
||||
if (n.type === "APPROVAL" && outs.length > 1) {
|
||||
outs.forEach((e: any) => {
|
||||
if (!e.name?.trim()) problems.push(`多出口节点「${n.name || n.id}」每个出口必须命名(按钮文案)`);
|
||||
});
|
||||
outNames[n.id]?.forEach((x) => {
|
||||
// 重复名展示一次即可
|
||||
});
|
||||
}
|
||||
});
|
||||
// 可达性(simple BFS from start,无孤儿)
|
||||
const start = def.start;
|
||||
if (start) {
|
||||
const seen = new Set([start]);
|
||||
const queue = [start];
|
||||
while (queue.length) {
|
||||
const cur = queue.shift()!;
|
||||
edges.forEach((e: any) => {
|
||||
if (e.from === cur && nodeIds.has(e.to) && !seen.has(e.to)) {
|
||||
seen.add(e.to);
|
||||
queue.push(e.to);
|
||||
}
|
||||
});
|
||||
}
|
||||
nodes.forEach((n: any) => {
|
||||
if (!seen.has(n.id)) problems.push(`从开始不可达的节点:${n.name || n.id}`);
|
||||
});
|
||||
}
|
||||
return [...new Set(problems)];
|
||||
}
|
||||
|
||||
function onOpenDiagram(): void {
|
||||
if (!editId.value) return;
|
||||
@@ -841,10 +448,6 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
||||
</label>
|
||||
</div>
|
||||
<div class="hd-right">
|
||||
<RadioGroup :model-value="editMode" size="small" type="button" @on-change="onModeSwitch">
|
||||
<Radio label="cards">卡片(推荐)</Radio>
|
||||
<Radio label="canvas" :disabled="cardsBroken">画布(高级)</Radio>
|
||||
</RadioGroup>
|
||||
<Button @click="settingsDrawer = true">流程设置</Button>
|
||||
<Button @click="jsonDrawer = true">源码</Button>
|
||||
<Button :disabled="!editId" @click="onOpenDiagram">图示预览</Button>
|
||||
@@ -852,11 +455,7 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 画布模式:bpmn-js 建模(多出口/并联/退回环路均可画) -->
|
||||
<BpmnCanvas v-if="!loading && editMode === 'canvas'" ref="bpmnWrap"
|
||||
:source="sourceJson" @node-config="onCanvasNode" />
|
||||
|
||||
<main v-if="!loading && editMode === 'cards'" class="ed-canvas">
|
||||
<main v-if="!loading" class="ed-canvas">
|
||||
<div class="ed-hint">点击卡片配置节点 · 悬停虚线处 + 插入节点(审批 / 抄送 / 并行分支)</div>
|
||||
<div class="ed-flow">
|
||||
<div class="capsule start">发起开始</div>
|
||||
|
||||
Reference in New Issue
Block a user