/** * 流程进度布局工具 * * 后端进度接口(/flow/instance/{id}/progress)返回的是按 BFS 拓扑排序后的"拍平"节点列表, * 分支(条件节点)会全部排在一条线上,不易看出分支结构。 * * 这里根据流程定义的 modelJson(nodes + edges)重建真实拓扑, * 将流程组织为若干"行"(FlowRow): * - start / end:起点/终点标记 * - node / cc:主流程上的单个节点 * - branch:条件分支区(含多个分支列,分支节点并行展示) * 分支区之后若各分支汇聚到同一节点,则继续主流程。 */ export interface FlowLayoutNode { id: string name: string type: string // start / approval / cc / condition / end status: 'completed' | 'current' | 'pending' | 'neutral' isMyTurn: boolean /** 审批节点的完整进度数据(tasks / subNodes 等) */ nodeProgress?: any /** 该节点下的审批记录 */ records: any[] /** 所在分支的名称(分支节点内展示) */ branchLabel?: string conditionText?: string isDefault?: boolean } export interface FlowBranch { label: string condition: string isDefault: boolean nodes: FlowLayoutNode[] } export interface FlowRow { kind: 'start' | 'node' | 'cc' | 'branch' | 'end' /** 单节点行的节点列表(通常 1 个);分支行无此字段 */ nodes?: FlowLayoutNode[] /** 分支区标题(条件节点名称) */ title?: string branches?: FlowBranch[] } const MAX_DEPTH = 200 /** * 根据流程模型与进度数据构建布局行 * @param modelNodes 模型节点 [{id,type,name,properties}] * @param modelEdges 模型连线 [{sourceNodeId,targetNodeId,condition:{condition,isDefault,branchName}}] * @param progress 进度节点列表(仅审批节点,NodeProgress[]) * @param records 审批记录列表(ApprovalRecord[]) */ export function buildFlowLayout( modelNodes: any[], modelEdges: any[], progress: any[], records: any[] ): 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 startNode = modelNodes.find((n) => n.type === 'start') const endNode = modelNodes.find((n) => n.type === 'end') const startId = startNode?.id const endId = endNode?.id const mkNode = (n: any, branchMeta?: { branchName?: string; condition?: string; isDefault?: boolean }): FlowLayoutNode => { const p = progressMap.get(n.id) let status: FlowLayoutNode['status'] = 'neutral' let isMyTurn = false if (p) { status = p.status isMyTurn = !!p.isMyTurn } else if (n.type === 'cc') { // 抄送节点自动推进,视为已完成 status = 'completed' } return { id: n.id, name: n.name || n.id, type: n.type, status, isMyTurn, nodeProgress: p, records: recordsMap.get(n.id) || [], branchLabel: branchMeta?.branchName, conditionText: branchMeta?.condition, isDefault: branchMeta?.isDefault, } } const rows: FlowRow[] = [] const visited = new Set() let curId: string | undefined = startId // 起点 if (startId && nodeMap.has(startId)) { rows.push({ kind: 'start', nodes: [mkNode(nodeMap.get(startId))] }) visited.add(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' || curId === endId) { rows.push({ kind: 'end', nodes: [mkNode(cur)] }) } else { // 游离节点(不应出现),兜底展示 rows.push({ kind: 'node', nodes: [mkNode(cur)] }) } break } // 多出边 → 分支节点(条件节点等) if (outs.length > 1) { const branches: FlowBranch[] = [] let mergeId: string | null = null for (const e of outs) { const cond = e.condition || {} const branchName = cond.branchName || '' const condition = cond.condition || '' const isDefault = !!cond.isDefault const chain: FlowLayoutNode[] = [] 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 && chain.length > 0)) { branchMerge = bCurId break } // 分支标签已展示在分支列头部,节点卡片上不再重复 chain.push(mkNode(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 } branches.push({ label: branchName || condition || (isDefault ? '默认分支' : '分支'), condition, isDefault, nodes: chain, }) if (branchMerge) { if (mergeId === null) mergeId = branchMerge else if (mergeId !== branchMerge) mergeId = null // 各分支汇聚点不一致 } } rows.push({ kind: 'branch', title: cur.name || cur.id, branches }) if (mergeId && nodeMap.has(mergeId)) { 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)] }) } else { rows.push({ kind: 'node', nodes: [mkNode(cur)] }) } } if (visited.has(nextId)) break // 防环 visited.add(nextId) curId = nextId } // 结束节点兜底:若未在遍历中输出,则补在末尾 if (endId && nodeMap.has(endId) && !rows.some((r) => r.kind === 'end' && r.nodes?.[0]?.id === endId)) { rows.push({ kind: 'end', nodes: [mkNode(nodeMap.get(endId))] }) } return rows }