feat(admin): 画布↔卡片数据打通——卡片树为唯一数据源(uid 即 contentJson 节点 id, 画布=contentJson 投影/反解); 多人审批默认或签; 选人弹窗换 /admin/user/teacherTree 教师树; 示意图页三视图(示意图/画布视图/进度图PNG)
build / build (push) Successful in 2m3s

This commit is contained in:
chenxing
2026-09-17 14:52:55 +08:00
parent f9d52d2f5d
commit b76c419a6f
7 changed files with 288 additions and 52 deletions
+30 -6
View File
@@ -22,14 +22,13 @@ import {
ParallelNode,
createApproval,
newUid,
resetUidSeq,
} from "./model";
/** 编辑树 → 后端 contentJson(code/name 恒取表单值,规避 contentJson 覆盖行字段的既有坑) */
export function toContentJson(model: EditorModel): DefJson {
const nodes: DefNodeJson[] = [];
const edges: DefEdgeJson[] = [];
let seq = 1;
const nid = (p: string) => `${p}${seq++}`;
// 端点 name 仅在既有定义带值时回写(新建流程不生成,保持最小 JSON)
const startNode: DefNodeJson = { id: "start", type: "START" };
@@ -40,7 +39,7 @@ export function toContentJson(model: EditorModel): DefJson {
let cursor = prev;
items.forEach((item) => {
if (item.kind === "approval") {
const id = nid("n");
const id = item.uid;
const node: DefNodeJson = {
id,
type: "APPROVAL",
@@ -54,8 +53,8 @@ export function toContentJson(model: EditorModel): DefJson {
cursor = id;
return;
}
const split = nid("split");
const join = nid("join");
const split = `${item.uid}s`;
const join = `${item.uid}j`;
nodes.push({ id: split, type: "PARALLEL_SPLIT" });
nodes.push({ id: join, type: "PARALLEL_JOIN", waitFor: "ALL" });
edges.push({ from: cursor, to: split });
@@ -87,6 +86,11 @@ export function toContentJson(model: EditorModel): DefJson {
};
}
/**
* toContentJson 的节点 id 直接取编辑态 uid(u 段 / uid 后缀 +s、+j):id 稳定可回溯,
* bpmn 画布与卡片共用同一套 id——画布上选中元素的 id 即卡片 uid,配置抽屉无需二次映射;
* id 仅要求"非空且唯一"(DefValidator 口径)。
*/
function cloneAssignee(a: Assignee | null): Assignee {
if (!a) return { type: "DEPT_KEY", deptKey: "" };
return a.type === "USERS" ? { type: "USERS", userIds: [...a.userIds] } : { type: "DEPT_KEY", deptKey: a.deptKey };
@@ -106,6 +110,8 @@ export interface ParsedDef {
* contentJson → 编辑树。抛错即为"不可还原为卡片视图",调用方展示错误并建议源码模式。
*/
export function fromContentJson(def: any): ParsedDef {
UsedIds.set.clear();
resetUidSeq();
if (!def || !Array.isArray(def.nodes) || !Array.isArray(def.edges)) {
throw new Error("JSON 需包含 nodes 与 edges 数组");
}
@@ -240,8 +246,10 @@ export function fromContentJson(def: any): ParsedDef {
function toApprovalNode(n: any): ApprovalNode {
const countersign: Countersign = n.countersign === "ANY" ? "ANY" : "ALL";
// uid 复用 JSON 原节点 id(如 n3):载入→卡片→画布→保存全程同一套 id,卡片树与画布
// 节点一一对应(业务配置按 id 继承的通道依赖这一点);缺 id/撞 id 才退回新生成 uid。
return {
uid: newUid(),
uid: UsedIds.take(n.id),
kind: "approval",
name: n.name || "审批",
assignee: n.assignee ? cloneAssignee(n.assignee as Assignee) : null,
@@ -250,6 +258,22 @@ function toApprovalNode(n: any): ApprovalNode {
};
}
/** 载入会话内已占用的 uid 集(防重复/缺 id 撞车) */
const UsedIds = {
set: new Set<string>(),
take(raw: unknown): string {
const s = raw == null ? "" : String(raw).trim();
if (s && !this.set.has(s)) {
this.set.add(s);
return s;
}
let u = newUid();
while (this.set.has(u)) u = newUid();
this.set.add(u);
return u;
},
};
/** 还原时保留真实分支结构(含 1 分支的非常规定义,交给校验提示);仅空分支补占位,避免画布出现空容器 */
function makeParallel(branches: EditorItem[][]): ParallelNode {
return {