fix(def-editor): 空出口分支(直接通过)往返支持——解析不再造无审批人占位卡(曾致打开即报"未选择科室映射"且保存凭空加节点改语义);序列化空分支=起点→汇合点命名+条件直连边(Tail 携带出口属性);校验空分支降为 warn(并行分支保持 error:后端禁 SPLIT 直连 JOIN);ConditionBox 空分支渲染"直达汇合点"提示。convert 往返 64/64(新增空分支/空泳道 6 例+修正 1 处过期负例文案)
build / build (push) Successful in 1m53s
build / build (push) Successful in 1m53s
This commit is contained in:
@@ -78,6 +78,7 @@ function editCondition(i: number, v: string): void {
|
|||||||
@input="editCondition(i, ($event.target as HTMLInputElement).value)"
|
@input="editCondition(i, ($event.target as HTMLInputElement).value)"
|
||||||
/>
|
/>
|
||||||
<NodeList :items="branch" :container="`${node.uid}:${i}`" />
|
<NodeList :items="branch" :container="`${node.uid}:${i}`" />
|
||||||
|
<div v-if="!branch.length" class="cb-empty">该出口直达汇合点(直接通过)</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button v-if="canAddBranch" class="cb-add" size="small" long @click="ctx.addBranch(node.uid)">
|
<Button v-if="canAddBranch" class="cb-add" size="small" long @click="ctx.addBranch(node.uid)">
|
||||||
@@ -118,6 +119,16 @@ function editCondition(i: number, v: string): void {
|
|||||||
color: #9254de;
|
color: #9254de;
|
||||||
margin: 6px 0 10px;
|
margin: 6px 0 10px;
|
||||||
}
|
}
|
||||||
|
.cb-empty {
|
||||||
|
margin: 4px 0 8px;
|
||||||
|
padding: 8px 6px;
|
||||||
|
border: 1px dashed #d3c6e8;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #9254de;
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
.cb-cols {
|
.cb-cols {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
|
|||||||
@@ -42,25 +42,42 @@ export function toContentJson(model: EditorModel): DefJson {
|
|||||||
/**
|
/**
|
||||||
* 条件分支(2026-09-24)无 JOIN 语义:分支尾不互连,隐式汇合到容器后继——cursor 置 null 后
|
* 条件分支(2026-09-24)无 JOIN 语义:分支尾不互连,隐式汇合到容器后继——cursor 置 null 后
|
||||||
* 下一个节点(或 END)只接收各分支尾的连线;firstEdgeName 只作用于分支首边(出口名)。
|
* 下一个节点(或 END)只接收各分支尾的连线;firstEdgeName 只作用于分支首边(出口名)。
|
||||||
* walk 返回"尾集合"(普通序列=末节点,末尾含条件分支=各分支尾)。
|
|
||||||
* firstEdgeCondition(2026-10-10):EXPRESSION 模式分支首边的条件表达式,与出口名同位写入。
|
* firstEdgeCondition(2026-10-10):EXPRESSION 模式分支首边的条件表达式,与出口名同位写入。
|
||||||
|
* 空出口分支(2026-10-12,"直接通过"):items 为空时 walk 原样返回起点作为尾并携带出口名/条件,
|
||||||
|
* 汇合连线即"起点→汇合点"的命名直连边——与引擎 cond→汇合点直连边双向保真。
|
||||||
*/
|
*/
|
||||||
|
interface Tail {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
condition?: string;
|
||||||
|
}
|
||||||
|
const tailEdge = (from: string, to: string, t: Tail): DefEdgeJson => {
|
||||||
|
const e: DefEdgeJson = { from, to };
|
||||||
|
if (t.name) e.name = t.name;
|
||||||
|
if (t.condition !== undefined && t.condition !== null && t.condition !== "") {
|
||||||
|
e.condition = t.condition;
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
};
|
||||||
const walk = (items: EditorItem[], prev: string, firstEdgeName?: string,
|
const walk = (items: EditorItem[], prev: string, firstEdgeName?: string,
|
||||||
firstEdgeCondition?: string): string[] => {
|
firstEdgeCondition?: string): Tail[] => {
|
||||||
let cursor: string | null = prev;
|
let cursor: string | null = prev;
|
||||||
let pending: string[] = [];
|
let pending: Tail[] = [];
|
||||||
let first = true;
|
let first = true;
|
||||||
const linkTo = (toId: string): void => {
|
const linkTo = (toId: string): void => {
|
||||||
if (cursor !== null) {
|
if (cursor !== null) {
|
||||||
if (first && firstEdgeName) {
|
if (first && firstEdgeName) {
|
||||||
const e: DefEdgeJson = { from: cursor, to: toId, name: firstEdgeName };
|
const e: DefEdgeJson = { from: cursor, to: toId, name: firstEdgeName };
|
||||||
if (firstEdgeCondition !== undefined) e.condition = firstEdgeCondition;
|
if (firstEdgeCondition !== undefined && firstEdgeCondition !== null
|
||||||
|
&& firstEdgeCondition !== "") {
|
||||||
|
e.condition = firstEdgeCondition;
|
||||||
|
}
|
||||||
edges.push(e);
|
edges.push(e);
|
||||||
} else {
|
} else {
|
||||||
edges.push({ from: cursor, to: toId });
|
edges.push({ from: cursor, to: toId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pending.forEach((t) => edges.push({ from: t, to: toId }));
|
pending.forEach((t) => edges.push(tailEdge(t.id, toId, t)));
|
||||||
pending = [];
|
pending = [];
|
||||||
first = false;
|
first = false;
|
||||||
};
|
};
|
||||||
@@ -80,7 +97,7 @@ export function toContentJson(model: EditorModel): DefJson {
|
|||||||
if (item.outlets && item.outlets.length >= 2 && item.branches) {
|
if (item.outlets && item.outlets.length >= 2 && item.branches) {
|
||||||
// 多出口(2026-10-03 P2):APPROVAL 的多条命名出边即按钮选路,
|
// 多出口(2026-10-03 P2):APPROVAL 的多条命名出边即按钮选路,
|
||||||
// 分支尾隐式汇合到后继(与条件分支容器完全同构)
|
// 分支尾隐式汇合到后继(与条件分支容器完全同构)
|
||||||
const tails: string[] = [];
|
const tails: Tail[] = [];
|
||||||
item.branches.forEach((branch, bi) => {
|
item.branches.forEach((branch, bi) => {
|
||||||
tails.push(...walk(branch, id, item.outlets![bi]));
|
tails.push(...walk(branch, id, item.outlets![bi]));
|
||||||
});
|
});
|
||||||
@@ -116,7 +133,7 @@ export function toContentJson(model: EditorModel): DefJson {
|
|||||||
if (item.judgeMode === "EXPRESSION") node.judgeMode = "EXPRESSION";
|
if (item.judgeMode === "EXPRESSION") node.judgeMode = "EXPRESSION";
|
||||||
nodes.push(node);
|
nodes.push(node);
|
||||||
linkTo(condId);
|
linkTo(condId);
|
||||||
const tails: string[] = [];
|
const tails: Tail[] = [];
|
||||||
const expression = item.judgeMode === "EXPRESSION";
|
const expression = item.judgeMode === "EXPRESSION";
|
||||||
item.branches.forEach((branch, bi) => {
|
item.branches.forEach((branch, bi) => {
|
||||||
const cond = expression ? (item.conditions || [])[bi] || "" : undefined;
|
const cond = expression ? (item.conditions || [])[bi] || "" : undefined;
|
||||||
@@ -132,17 +149,27 @@ export function toContentJson(model: EditorModel): DefJson {
|
|||||||
nodes.push({ id: join, type: "PARALLEL_JOIN", waitFor: "ALL" });
|
nodes.push({ id: join, type: "PARALLEL_JOIN", waitFor: "ALL" });
|
||||||
linkTo(split);
|
linkTo(split);
|
||||||
item.branches.forEach((branch) => {
|
item.branches.forEach((branch) => {
|
||||||
walk(branch, split).forEach((tail) => edges.push({ from: tail, to: join }));
|
walk(branch, split).forEach((tail) => edges.push(tailEdge(tail.id, join, tail)));
|
||||||
});
|
});
|
||||||
cursor = join;
|
cursor = join;
|
||||||
});
|
});
|
||||||
const tails: string[] = [];
|
const tails: Tail[] = [];
|
||||||
if (cursor !== null) tails.push(cursor);
|
// 空出口分支:无任何节点消费(first 未翻转、cursor 停在起点)——出口名/条件随尾携带,
|
||||||
|
// 由汇合点连线落成命名直连边(非空分支的 first 边已由 linkTo 写过,此处 first=false 不受影响)
|
||||||
|
if (cursor !== null) {
|
||||||
|
const t: Tail = { id: cursor };
|
||||||
|
if (first && firstEdgeName) t.name = firstEdgeName;
|
||||||
|
if (first && firstEdgeCondition !== undefined && firstEdgeCondition !== null
|
||||||
|
&& firstEdgeCondition !== "") {
|
||||||
|
t.condition = firstEdgeCondition;
|
||||||
|
}
|
||||||
|
tails.push(t);
|
||||||
|
}
|
||||||
tails.push(...pending);
|
tails.push(...pending);
|
||||||
return tails;
|
return tails;
|
||||||
};
|
};
|
||||||
|
|
||||||
walk(model.list, "start").forEach((tail) => edges.push({ from: tail, to: "end" }));
|
walk(model.list, "start").forEach((tail) => edges.push(tailEdge(tail.id, "end", tail)));
|
||||||
const endNode: DefNodeJson = { id: "end", type: "END" };
|
const endNode: DefNodeJson = { id: "end", type: "END" };
|
||||||
if (model.endName) endNode.name = model.endName;
|
if (model.endName) endNode.name = model.endName;
|
||||||
nodes.push(endNode);
|
nodes.push(endNode);
|
||||||
@@ -334,7 +361,9 @@ export function fromContentJson(def: any): ParsedDef {
|
|||||||
judgeMode: n.judgeMode === "EXPRESSION" ? "EXPRESSION" : "SERVICE",
|
judgeMode: n.judgeMode === "EXPRESSION" ? "EXPRESSION" : "SERVICE",
|
||||||
conditions,
|
conditions,
|
||||||
outlets,
|
outlets,
|
||||||
branches: branches.map((b) => (b.length ? b : [createApproval()])),
|
// 空出口分支(2026-10-12):保留空数组("直接通过"直达汇合点),
|
||||||
|
// 不再造无审批人占位卡(曾导致打开即报"未选择科室映射"、保存还会凭空改语义)
|
||||||
|
branches,
|
||||||
},
|
},
|
||||||
mergeId,
|
mergeId,
|
||||||
};
|
};
|
||||||
@@ -377,7 +406,7 @@ export function fromContentJson(def: any): ParsedDef {
|
|||||||
}
|
}
|
||||||
const node = toApprovalNode(n);
|
const node = toApprovalNode(n);
|
||||||
node.outlets = outlets;
|
node.outlets = outlets;
|
||||||
node.branches = branches.map((b) => (b.length ? b : [createApproval()]));
|
node.branches = branches; // 空出口泳道保留(直达汇合点),不造占位卡
|
||||||
list.push(node);
|
list.push(node);
|
||||||
cur = mergeId;
|
cur = mergeId;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ export function validateModel(model: EditorModel, depts: DeptOption[], routers:
|
|||||||
(item.branches || []).forEach((branch, i) => {
|
(item.branches || []).forEach((branch, i) => {
|
||||||
const p = `${path}·出口「${item.outlets![i] || i + 1}」`;
|
const p = `${path}·出口「${item.outlets![i] || i + 1}」`;
|
||||||
if (!branch.length) {
|
if (!branch.length) {
|
||||||
push(item.uid, "error", p, "出口泳道为空,请添加节点");
|
// 空出口泳道(2026-10-12):合法语义=该出口直达汇合点(直接通过),仅提示不阻断
|
||||||
|
push(item.uid, "warn", p, "出口泳道为空:该出口直达汇合点(直接通过)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
rec(branch, p);
|
rec(branch, p);
|
||||||
@@ -136,7 +137,8 @@ export function validateModel(model: EditorModel, depts: DeptOption[], routers:
|
|||||||
item.branches.forEach((branch, i) => {
|
item.branches.forEach((branch, i) => {
|
||||||
const branchPath = `${path}·出口「${item.outlets[i] || BRANCH_LABELS[i] || i + 1}」`;
|
const branchPath = `${path}·出口「${item.outlets[i] || BRANCH_LABELS[i] || i + 1}」`;
|
||||||
if (!branch.length) {
|
if (!branch.length) {
|
||||||
push(item.uid, "error", branchPath, "分支为空,请添加节点");
|
// 空出口分支(2026-10-12):合法语义=该出口直达汇合点(直接通过),仅提示不阻断
|
||||||
|
push(item.uid, "warn", branchPath, "分支为空:该出口直达汇合点(直接通过)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
rec(branch, branchPath);
|
rec(branch, branchPath);
|
||||||
|
|||||||
Reference in New Issue
Block a user