fix(def-editor): 空出口分支(直接通过)往返支持——解析不再造无审批人占位卡(曾致打开即报"未选择科室映射"且保存凭空加节点改语义);序列化空分支=起点→汇合点命名+条件直连边(Tail 携带出口属性);校验空分支降为 warn(并行分支保持 error:后端禁 SPLIT 直连 JOIN);ConditionBox 空分支渲染"直达汇合点"提示。convert 往返 64/64(新增空分支/空泳道 6 例+修正 1 处过期负例文案)
build / build (push) Successful in 1m53s

This commit is contained in:
chenxing
2026-10-12 11:29:07 +08:00
parent 2a1ec48b25
commit ecd082bb99
3 changed files with 57 additions and 15 deletions
@@ -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 -13
View File
@@ -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 {
+4 -2
View File
@@ -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);