feat(admin): 抄送(NOTIFY)与条件分支(CONDITION)全栈接入编辑器/轨迹/管理面
build / build (push) Successful in 1m55s

NOTIFY(2026-09-23):编辑器抄送卡片+抽屉+画布 bpmn:ServiceTask 映射、工作台「抄送我的」页签、轨迹 kind=CC 行、示意图青框卡片
CONDITION(2026-09-24):ConditionBox 互斥分支容器(分支列出口名就地编辑、无 JOIN 语义分支尾隐式汇合)、ConditionDrawer 判断服务下拉(flw_router)、convert 双向序列化(CONDITION+命名出边、uid+c 尾缀往返收敛)、validate 镜像后端口径、画布 bpmn:ExclusiveGateway 映射、轨迹「系统判断→出口」行(失败挂起提示)、示意图紫菱形(SVG 与服务端 PNG 同配色)、管理面 flw_router 维护页(/admin/routers)与办件记录「重跑路由」入口
convert 往返 54 项全过、npm run build 通过
This commit is contained in:
chenxing
2026-09-24 01:19:15 +08:00
parent cdf04f0930
commit e30e7ab665
20 changed files with 1614 additions and 84 deletions
+66 -2
View File
@@ -5,7 +5,15 @@
* 结构类规则(id 唯一/度约束/孤儿/环/START·END 唯一)由结构化生成保证,前端无需校验。
* 后端 DefValidator 仍是发布期权威,此处仅作体验前置。
*/
import { BRANCH_LABELS, DeptOption, EditorItem, EditorModel, MAX_BRANCHES, MIN_BRANCHES } from "./model";
import {
BRANCH_LABELS,
DeptOption,
EditorItem,
EditorModel,
MAX_BRANCHES,
MIN_BRANCHES,
RouterOption,
} from "./model";
export type IssueLevel = "error" | "warn";
@@ -20,7 +28,7 @@ export interface Issue {
text: string;
}
export function validateModel(model: EditorModel, depts: DeptOption[]): Issue[] {
export function validateModel(model: EditorModel, depts: DeptOption[], routers: RouterOption[] = []): Issue[] {
const issues: Issue[] = [];
const push = (uid: string, level: IssueLevel, path: string, msg: string): void => {
issues.push({ uid, level, path, msg, text: `${path}:${msg}` });
@@ -50,6 +58,62 @@ export function validateModel(model: EditorModel, depts: DeptOption[]): Issue[]
return;
}
if (item.kind === "notify") {
// 抄送节点(运行时宽容:解析不出人只跳过不阻塞,但发布期后端仍拦配置缺失)
if (!item.name.trim()) push(item.uid, "error", path, "节点名称不能为空");
const r = item.receivers;
if (!r) {
push(item.uid, "error", path, "未配置抄送对象");
} else if (r.type === "DEPT_KEY") {
if (!r.deptKey) {
push(item.uid, "error", path, "未选择抄送科室");
} else {
const d = depts.find((x) => x.deptKey === r.deptKey);
if (!d) {
push(item.uid, "warn", path, `科室映射「${r.deptKey}」不存在,发布会被拦截`);
} else if (!d.count) {
push(item.uid, "warn", path, `科室「${d.deptName || r.deptKey}」无启用人员,抄送会落空`);
}
}
} else if (!r.userIds.length) {
push(item.uid, "error", path, "抄送用户不能为空");
}
return;
}
if (item.kind === "condition") {
// 条件分支(2026-09-24):判断服务必选(未注册仅 warn,发布期后端权威拦截);
// 出口名即判断接口 outlets 契约,非空唯一(镜像后端 CONDITION 校验口径)
if (!item.name.trim()) push(item.uid, "error", path, "节点名称不能为空");
if (!item.routerKey) {
push(item.uid, "error", path, "未选择判断服务(发布后运行到该节点会挂起)");
} else if (routers.length && !routers.some((r) => r.routerKey === item.routerKey && r.status === 1)) {
push(item.uid, "warn", path, `判断服务「${item.routerKey}」未注册或已停用,发布会被拦截`);
}
if (item.branches.length < MIN_BRANCHES) {
push(item.uid, "error", path, `条件分支至少保留 ${MIN_BRANCHES} 个`);
}
if (item.branches.length > MAX_BRANCHES) {
push(item.uid, "error", path, `条件分支最多 ${MAX_BRANCHES} 个`);
}
item.outlets.forEach((o, i) => {
if (!o.trim()) {
push(item.uid, "error", path, `第 ${i + 1} 条分支的出口名不能为空`);
} else if (item.outlets.indexOf(o) !== i) {
push(item.uid, "error", path, `出口名重复:「${o}」(出口名即判断依据,必须唯一)`);
}
});
item.branches.forEach((branch, i) => {
const branchPath = `${path}·出口「${item.outlets[i] || BRANCH_LABELS[i] || i + 1}」`;
if (!branch.length) {
push(item.uid, "error", branchPath, "分支为空,请添加节点");
return;
}
rec(branch, branchPath);
});
return;
}
if (item.branches.length < MIN_BRANCHES) {
push(item.uid, "error", path, `并行分支至少保留 ${MIN_BRANCHES} 个`);
}