| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- /**
- * 流程进度布局工具
- *
- * 后端进度接口(/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<string, any>()
- for (const n of modelNodes) {
- if (n && n.id) nodeMap.set(n.id, n)
- }
- const outgoing = new Map<string, any[]>()
- const incomingCount = new Map<string, number>()
- 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<string, any>()
- for (const p of progress || []) {
- if (p && p.nodeId) progressMap.set(p.nodeId, p)
- }
- const recordsMap = new Map<string, any[]>()
- 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<string>()
- 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
- }
|