flowLayout.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. /**
  2. * 流程进度布局工具
  3. *
  4. * 后端进度接口(/flow/instance/{id}/progress)返回的是按 BFS 拓扑排序后的"拍平"节点列表,
  5. * 分支(条件节点)会全部排在一条线上,不易看出分支结构。
  6. *
  7. * 这里根据流程定义的 modelJson(nodes + edges)重建真实拓扑,
  8. * 将流程组织为若干"行"(FlowRow):
  9. * - start / end:起点/终点标记
  10. * - node / cc:主流程上的单个节点
  11. * - branch:条件分支区(含多个分支列,分支节点并行展示)
  12. * 分支区之后若各分支汇聚到同一节点,则继续主流程。
  13. */
  14. export interface FlowLayoutNode {
  15. id: string
  16. name: string
  17. type: string // start / approval / cc / condition / end
  18. status: 'completed' | 'current' | 'pending' | 'neutral'
  19. isMyTurn: boolean
  20. /** 审批节点的完整进度数据(tasks / subNodes 等) */
  21. nodeProgress?: any
  22. /** 该节点下的审批记录 */
  23. records: any[]
  24. /** 所在分支的名称(分支节点内展示) */
  25. branchLabel?: string
  26. conditionText?: string
  27. isDefault?: boolean
  28. }
  29. export interface FlowBranch {
  30. label: string
  31. condition: string
  32. isDefault: boolean
  33. nodes: FlowLayoutNode[]
  34. }
  35. export interface FlowRow {
  36. kind: 'start' | 'node' | 'cc' | 'branch' | 'end'
  37. /** 单节点行的节点列表(通常 1 个);分支行无此字段 */
  38. nodes?: FlowLayoutNode[]
  39. /** 分支区标题(条件节点名称) */
  40. title?: string
  41. branches?: FlowBranch[]
  42. }
  43. const MAX_DEPTH = 200
  44. /**
  45. * 根据流程模型与进度数据构建布局行
  46. * @param modelNodes 模型节点 [{id,type,name,properties}]
  47. * @param modelEdges 模型连线 [{sourceNodeId,targetNodeId,condition:{condition,isDefault,branchName}}]
  48. * @param progress 进度节点列表(仅审批节点,NodeProgress[])
  49. * @param records 审批记录列表(ApprovalRecord[])
  50. */
  51. export function buildFlowLayout(
  52. modelNodes: any[],
  53. modelEdges: any[],
  54. progress: any[],
  55. records: any[]
  56. ): FlowRow[] {
  57. if (!Array.isArray(modelNodes) || modelNodes.length === 0) return []
  58. const nodeMap = new Map<string, any>()
  59. for (const n of modelNodes) {
  60. if (n && n.id) nodeMap.set(n.id, n)
  61. }
  62. const outgoing = new Map<string, any[]>()
  63. const incomingCount = new Map<string, number>()
  64. for (const e of modelEdges || []) {
  65. if (!e || !e.sourceNodeId || !e.targetNodeId) continue
  66. if (!nodeMap.has(e.targetNodeId)) continue
  67. if (!outgoing.has(e.sourceNodeId)) outgoing.set(e.sourceNodeId, [])
  68. outgoing.get(e.sourceNodeId)!.push(e)
  69. incomingCount.set(e.targetNodeId, (incomingCount.get(e.targetNodeId) || 0) + 1)
  70. }
  71. const progressMap = new Map<string, any>()
  72. for (const p of progress || []) {
  73. if (p && p.nodeId) progressMap.set(p.nodeId, p)
  74. }
  75. const recordsMap = new Map<string, any[]>()
  76. for (const r of records || []) {
  77. if (!r || !r.nodeId) continue
  78. if (!recordsMap.has(r.nodeId)) recordsMap.set(r.nodeId, [])
  79. recordsMap.get(r.nodeId)!.push(r)
  80. }
  81. const startNode = modelNodes.find((n) => n.type === 'start')
  82. const endNode = modelNodes.find((n) => n.type === 'end')
  83. const startId = startNode?.id
  84. const endId = endNode?.id
  85. const mkNode = (n: any, branchMeta?: { branchName?: string; condition?: string; isDefault?: boolean }): FlowLayoutNode => {
  86. const p = progressMap.get(n.id)
  87. let status: FlowLayoutNode['status'] = 'neutral'
  88. let isMyTurn = false
  89. if (p) {
  90. status = p.status
  91. isMyTurn = !!p.isMyTurn
  92. } else if (n.type === 'cc') {
  93. // 抄送节点自动推进,视为已完成
  94. status = 'completed'
  95. }
  96. return {
  97. id: n.id,
  98. name: n.name || n.id,
  99. type: n.type,
  100. status,
  101. isMyTurn,
  102. nodeProgress: p,
  103. records: recordsMap.get(n.id) || [],
  104. branchLabel: branchMeta?.branchName,
  105. conditionText: branchMeta?.condition,
  106. isDefault: branchMeta?.isDefault,
  107. }
  108. }
  109. const rows: FlowRow[] = []
  110. const visited = new Set<string>()
  111. let curId: string | undefined = startId
  112. // 起点
  113. if (startId && nodeMap.has(startId)) {
  114. rows.push({ kind: 'start', nodes: [mkNode(nodeMap.get(startId))] })
  115. visited.add(startId)
  116. }
  117. let guard = 0
  118. while (curId && nodeMap.has(curId) && guard++ < MAX_DEPTH) {
  119. const cur = nodeMap.get(curId)
  120. const outs = (outgoing.get(curId) || []).filter((e) => e.targetNodeId && nodeMap.has(e.targetNodeId))
  121. if (cur.type === 'end' || outs.length === 0) {
  122. if (cur.type === 'end' || curId === endId) {
  123. rows.push({ kind: 'end', nodes: [mkNode(cur)] })
  124. } else {
  125. // 游离节点(不应出现),兜底展示
  126. rows.push({ kind: 'node', nodes: [mkNode(cur)] })
  127. }
  128. break
  129. }
  130. // 多出边 → 分支节点(条件节点等)
  131. if (outs.length > 1) {
  132. const branches: FlowBranch[] = []
  133. let mergeId: string | null = null
  134. for (const e of outs) {
  135. const cond = e.condition || {}
  136. const branchName = cond.branchName || ''
  137. const condition = cond.condition || ''
  138. const isDefault = !!cond.isDefault
  139. const chain: FlowLayoutNode[] = []
  140. let bCurId = e.targetNodeId
  141. let bGuard = 0
  142. let branchMerge: string | null = null
  143. while (bCurId && nodeMap.has(bCurId) && bGuard++ < MAX_DEPTH) {
  144. const bn = nodeMap.get(bCurId)
  145. const bin = incomingCount.get(bCurId) || 0
  146. // 汇聚点(多个分支指向同一节点)或结束节点:不并入分支链
  147. if (bn.type === 'end' || (bin > 1 && chain.length > 0)) {
  148. branchMerge = bCurId
  149. break
  150. }
  151. // 分支标签已展示在分支列头部,节点卡片上不再重复
  152. chain.push(mkNode(bn))
  153. const bNext = (outgoing.get(bCurId) || []).filter((be) => be.targetNodeId && nodeMap.has(be.targetNodeId))
  154. // 若下一节点是汇聚点,则当前链到此为止(下一轮主循环处理汇聚点)
  155. const nextIn = bNext.length === 1 ? incomingCount.get(bNext[0].targetNodeId) || 0 : 0
  156. if (bNext.length !== 1 || nextIn > 1 || bn.type === 'condition') {
  157. if (bNext.length === 1 && nextIn > 1) {
  158. branchMerge = bNext[0].targetNodeId
  159. }
  160. break
  161. }
  162. bCurId = bNext[0].targetNodeId
  163. }
  164. branches.push({
  165. label: branchName || condition || (isDefault ? '默认分支' : '分支'),
  166. condition,
  167. isDefault,
  168. nodes: chain,
  169. })
  170. if (branchMerge) {
  171. if (mergeId === null) mergeId = branchMerge
  172. else if (mergeId !== branchMerge) mergeId = null // 各分支汇聚点不一致
  173. }
  174. }
  175. rows.push({ kind: 'branch', title: cur.name || cur.id, branches })
  176. if (mergeId && nodeMap.has(mergeId)) {
  177. curId = mergeId
  178. } else {
  179. curId = undefined // 分支后流程结束(各分支分别结束)
  180. }
  181. continue
  182. }
  183. // 单出边 → 正常推进
  184. const nextId = outs[0].targetNodeId
  185. if (cur.type !== 'start') {
  186. if (cur.type === 'cc') {
  187. rows.push({ kind: 'cc', nodes: [mkNode(cur)] })
  188. } else {
  189. rows.push({ kind: 'node', nodes: [mkNode(cur)] })
  190. }
  191. }
  192. if (visited.has(nextId)) break // 防环
  193. visited.add(nextId)
  194. curId = nextId
  195. }
  196. // 结束节点兜底:若未在遍历中输出,则补在末尾
  197. if (endId && nodeMap.has(endId) && !rows.some((r) => r.kind === 'end' && r.nodes?.[0]?.id === endId)) {
  198. rows.push({ kind: 'end', nodes: [mkNode(nodeMap.get(endId))] })
  199. }
  200. return rows
  201. }