/** * 流程进度布局工具 * * 根据流程定义的 modelJson(nodes + edges)重建真实拓扑,将流程组织为若干"行"(FlowRow): * - start / end:起点/终点标记 * - node / cc:主流程上的单个节点(线性主线) * - branch:条件分支区(在该处列出全部可能的分支路线,并标记实际选择的分支) * * 状态语义: * - completed:节点已有处理任务或审批记录(历史已处理) * - current:节点处于进行中(有待处理任务,或为实例当前节点) * - passed:节点位于当前节点之前且无任务/记录(导入数据直接跳到中间时,前面的流程视为已走过) * - pending:节点尚未到达 / 未被选择的分支 * - neutral:起点/终点/条件节点等非审批节点 */ export interface FlowLayoutNode { id: string name: string type: string // start / approval / cc / condition / end status: 'completed' | 'current' | 'passed' | 'pending' | 'neutral' isMyTurn: boolean /** 审批节点的完整进度数据(tasks / subNodes 等) */ nodeProgress?: any /** 该节点下的审批记录 */ records: any[] } export interface FlowBranch { label: string condition: string isDefault: boolean /** 该分支是否被实际选择(分支内存在已处理任务/记录,或当前节点位于该分支) */ isChosen: boolean nodes: FlowLayoutNode[] } export interface FlowRow { kind: 'start' | 'node' | 'cc' | 'branch' | 'end' /** 单节点行的节点列表(通常 1 个);分支行无此字段 */ nodes?: FlowLayoutNode[] /** 分支区标题(条件节点名称) */ title?: string branches?: FlowBranch[] } const MAX_DEPTH = 300 /** * 根据流程模型与进度数据构建布局行 * @param modelNodes 模型节点 [{id,type,name,properties}] * @param modelEdges 模型连线 [{sourceNodeId,targetNodeId,condition:{condition,isDefault,branchName}}] * @param progress 进度节点列表(仅审批节点,NodeProgress[]) * @param records 审批记录列表(ApprovalRecord[]) * @param currentNodeId 当前节点 ID(用于判断"导入跳过"的历史节点与已选择分支) */ export function buildFlowLayout( modelNodes: any[], modelEdges: any[], progress: any[], records: any[], currentNodeId?: string | null ): FlowRow[] { if (!Array.isArray(modelNodes) || modelNodes.length === 0) return [] const nodeMap = new Map() for (const n of modelNodes) { if (n && n.id) nodeMap.set(n.id, n) } const outgoing = new Map() const incomingCount = new Map() for (const e of modelEdges || []) { if (!e || !e.sourceNodeId || !e.targetNodeId) continue if (!nodeMap.has(e.targetNodeId)) continue if (!outgoing.has(e.sourceNodeId)) outgoing.set(e.sourceNodeId, []) outgoing.get(e.sourceNodeId)!.push(e) incomingCount.set(e.targetNodeId, (incomingCount.get(e.targetNodeId) || 0) + 1) } const progressMap = new Map() for (const p of progress || []) { if (p && p.nodeId) progressMap.set(p.nodeId, p) } const recordsMap = new Map() for (const r of records || []) { if (!r || !r.nodeId) continue if (!recordsMap.has(r.nodeId)) recordsMap.set(r.nodeId, []) recordsMap.get(r.nodeId)!.push(r) } // 计算"当前节点的祖先"(从当前节点沿反向边可达的节点)——用于识别导入跳过的主线历史节点 const ancestors = new Set() if (currentNodeId && nodeMap.has(currentNodeId)) { const reverse: Map = new Map() for (const e of modelEdges || []) { if (!e?.sourceNodeId || !e?.targetNodeId) continue if (!nodeMap.has(e.targetNodeId)) continue if (!reverse.has(e.targetNodeId)) reverse.set(e.targetNodeId, []) reverse.get(e.targetNodeId)!.push(e.sourceNodeId) } const stack = [currentNodeId] while (stack.length) { const nid = stack.pop()! for (const pre of reverse.get(nid) || []) { if (pre && pre !== currentNodeId && !ancestors.has(pre)) { ancestors.add(pre) stack.push(pre) } } } } const startNode = modelNodes.find((n) => n.type === 'start') const endNodes = modelNodes.filter((n) => n.type === 'end') const startId = startNode?.id /** 节点是否有实际活动(任务或记录) */ const isNodeActive = (n: any): boolean => { const p = progressMap.get(n.id) if (p && p.status && p.status !== 'pending') return true if ((recordsMap.get(n.id) || []).length) return true return false } const mkNode = (n: any, status?: FlowLayoutNode['status']): FlowLayoutNode => { const p = progressMap.get(n.id) return { id: n.id, name: n.name || n.id, type: n.type, status: status ?? (n.type === 'start' || n.type === 'end' || n.type === 'condition' ? 'neutral' : 'pending'), isMyTurn: !!p?.isMyTurn, nodeProgress: p, records: recordsMap.get(n.id) || [], } } /** 主线节点状态:优先进度,其次"祖先=已走过",否则待处理 */ const mainLineStatus = (n: any): FlowLayoutNode['status'] => { const p = progressMap.get(n.id) if (p) { if (p.status === 'current') return 'current' if (p.status === 'completed') return 'completed' return 'pending' } if (n.id === currentNodeId) return 'current' if (ancestors.has(n.id)) return 'passed' if (n.type === 'cc') return 'completed' return 'pending' } const rows: FlowRow[] = [] const visited = new Set() if (startId && nodeMap.has(startId)) { rows.push({ kind: 'start', nodes: [mkNode(nodeMap.get(startId))] }) visited.add(startId) } let curId: string | undefined = startId let guard = 0 while (curId && nodeMap.has(curId) && guard++ < MAX_DEPTH) { const cur = nodeMap.get(curId) const outs = (outgoing.get(curId) || []).filter((e) => e.targetNodeId && nodeMap.has(e.targetNodeId)) if (cur.type === 'end' || outs.length === 0) { if (cur.type === 'end') rows.push({ kind: 'end', nodes: [mkNode(cur)] }) else rows.push({ kind: 'node', nodes: [mkNode(cur, mainLineStatus(cur))] }) break } // 多出边 → 分支节点 if (outs.length > 1) { // 第一遍:收集每条分支的节点链(原始模型节点,不含汇聚点/终点) const rawChains: { edge: any; nodes: any[]; merge: string | null }[] = [] let mergeId: string | null = null for (const e of outs) { const chainNodes: any[] = [] let bCurId = e.targetNodeId let bGuard = 0 let branchMerge: string | null = null while (bCurId && nodeMap.has(bCurId) && bGuard++ < MAX_DEPTH) { const bn = nodeMap.get(bCurId) const bin = incomingCount.get(bCurId) || 0 if (bn.type === 'end' || bin > 1) { branchMerge = bCurId break } chainNodes.push(bn) const bNext = (outgoing.get(bCurId) || []).filter((be) => be.targetNodeId && nodeMap.has(be.targetNodeId)) const nextIn = bNext.length === 1 ? incomingCount.get(bNext[0].targetNodeId) || 0 : 0 if (bNext.length !== 1 || nextIn > 1 || bn.type === 'condition') { if (bNext.length === 1 && nextIn > 1) branchMerge = bNext[0].targetNodeId break } bCurId = bNext[0].targetNodeId } rawChains.push({ edge: e, nodes: chainNodes, merge: branchMerge }) if (branchMerge) { if (mergeId === null) mergeId = branchMerge else if (mergeId !== branchMerge) mergeId = null } } // 第二遍:判定各分支是否被选择,再解析分支节点状态 const branches: FlowBranch[] = rawChains.map((rc) => { const cond = rc.edge.condition || {} const branchName = cond.branchName || '' const condition = cond.condition || '' const isDefault = !!cond.isDefault // 分支被选择:分支内有活动节点(已处理/进行中/有记录),或当前节点就在该分支 const isChosen = rc.nodes.some((n) => isNodeActive(n) || n.id === currentNodeId) const nodes = rc.nodes.map((n) => { if (n.id === currentNodeId) return mkNode(n, 'current') const p = progressMap.get(n.id) if (p) { if (p.status === 'current') return mkNode(n, 'current') if (p.status === 'completed') return mkNode(n, 'completed') return mkNode(n, 'pending') } // 无任务/记录:所在分支被选择视为已走过(历史),否则为未选择 return mkNode(n, isChosen ? 'passed' : 'pending') }) return { label: branchName || condition || (isDefault ? '默认分支' : '分支'), condition, isDefault, isChosen, nodes, } }) rows.push({ kind: 'branch', title: cur.name || cur.id, branches }) // 所有分支汇聚到同一非终点节点则继续主线;否则流程已在分支内结束 if (mergeId && nodeMap.has(mergeId) && mergeId !== endNodes[0]?.id) { curId = mergeId } else { curId = undefined } continue } // 单出边 → 正常推进 const nextId = outs[0].targetNodeId if (cur.type !== 'start') { if (cur.type === 'cc') rows.push({ kind: 'cc', nodes: [mkNode(cur, 'completed')] }) else rows.push({ kind: 'node', nodes: [mkNode(cur, mainLineStatus(cur))] }) } if (visited.has(nextId)) break // 防环 visited.add(nextId) curId = nextId } // 兜底:主遍历未覆盖到的节点追加到末尾,保证所有节点都可见 const emittedIds = new Set() for (const r of rows) { r.nodes?.forEach((n) => emittedIds.add(n.id)) r.branches?.forEach((b) => b.nodes.forEach((n) => emittedIds.add(n.id))) } for (const n of modelNodes) { if (n.type === 'start' || n.type === 'end' || n.type === 'condition') continue if (emittedIds.has(n.id)) continue rows.push({ kind: n.type === 'cc' ? 'cc' : 'node', nodes: [mkNode(n, mainLineStatus(n))] }) } // 结束节点兜底:补上所有尚未展示的终点 for (const en of endNodes) { if (en && nodeMap.has(en.id) && !emittedIds.has(en.id)) { rows.push({ kind: 'end', nodes: [mkNode(en)] }) } } return rows }