flowLayout.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. /**
  2. * 流程进度布局工具
  3. *
  4. * 根据流程定义的 modelJson(nodes + edges)重建真实拓扑,将流程组织为若干"行"(FlowRow):
  5. * - start / end:起点/终点标记
  6. * - node / cc:主流程上的单个节点(线性主线)
  7. * - branch:条件分支区(在该处列出全部可能的分支路线,并标记实际选择的分支)
  8. *
  9. * 状态语义:
  10. * - completed:节点已有处理任务或审批记录(历史已处理)
  11. * - current:节点处于进行中(有待处理任务,或为实例当前节点)
  12. * - passed:节点位于当前节点之前且无任务/记录(导入数据直接跳到中间时,前面的流程视为已走过)
  13. * - pending:节点尚未到达 / 未被选择的分支
  14. * - neutral:起点/终点/条件节点等非审批节点
  15. */
  16. export interface FlowLayoutNode {
  17. id: string
  18. name: string
  19. type: string // start / approval / cc / condition / end
  20. status: 'completed' | 'current' | 'passed' | 'pending' | 'neutral'
  21. isMyTurn: boolean
  22. /** 审批节点的完整进度数据(tasks / subNodes 等) */
  23. nodeProgress?: any
  24. /** 该节点下的审批记录 */
  25. records: any[]
  26. }
  27. export interface FlowBranch {
  28. label: string
  29. condition: string
  30. isDefault: boolean
  31. /** 该分支是否被实际选择(分支内存在已处理任务/记录,或当前节点位于该分支) */
  32. isChosen: 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 = 300
  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. * @param currentNodeId 当前节点 ID(用于判断"导入跳过"的历史节点与已选择分支)
  51. */
  52. export function buildFlowLayout(
  53. modelNodes: any[],
  54. modelEdges: any[],
  55. progress: any[],
  56. records: any[],
  57. currentNodeId?: string | null
  58. ): FlowRow[] {
  59. if (!Array.isArray(modelNodes) || modelNodes.length === 0) return []
  60. const nodeMap = new Map<string, any>()
  61. for (const n of modelNodes) {
  62. if (n && n.id) nodeMap.set(n.id, n)
  63. }
  64. const outgoing = new Map<string, any[]>()
  65. const incomingCount = new Map<string, number>()
  66. for (const e of modelEdges || []) {
  67. if (!e || !e.sourceNodeId || !e.targetNodeId) continue
  68. if (!nodeMap.has(e.targetNodeId)) continue
  69. if (!outgoing.has(e.sourceNodeId)) outgoing.set(e.sourceNodeId, [])
  70. outgoing.get(e.sourceNodeId)!.push(e)
  71. incomingCount.set(e.targetNodeId, (incomingCount.get(e.targetNodeId) || 0) + 1)
  72. }
  73. const progressMap = new Map<string, any>()
  74. for (const p of progress || []) {
  75. if (p && p.nodeId) progressMap.set(p.nodeId, p)
  76. }
  77. const recordsMap = new Map<string, any[]>()
  78. for (const r of records || []) {
  79. if (!r || !r.nodeId) continue
  80. if (!recordsMap.has(r.nodeId)) recordsMap.set(r.nodeId, [])
  81. recordsMap.get(r.nodeId)!.push(r)
  82. }
  83. // 计算"当前节点的祖先"(从当前节点沿反向边可达的节点)——用于识别导入跳过的主线历史节点
  84. const ancestors = new Set<string>()
  85. if (currentNodeId && nodeMap.has(currentNodeId)) {
  86. const reverse: Map<string, string[]> = new Map()
  87. for (const e of modelEdges || []) {
  88. if (!e?.sourceNodeId || !e?.targetNodeId) continue
  89. if (!nodeMap.has(e.targetNodeId)) continue
  90. if (!reverse.has(e.targetNodeId)) reverse.set(e.targetNodeId, [])
  91. reverse.get(e.targetNodeId)!.push(e.sourceNodeId)
  92. }
  93. const stack = [currentNodeId]
  94. while (stack.length) {
  95. const nid = stack.pop()!
  96. for (const pre of reverse.get(nid) || []) {
  97. if (pre && pre !== currentNodeId && !ancestors.has(pre)) {
  98. ancestors.add(pre)
  99. stack.push(pre)
  100. }
  101. }
  102. }
  103. }
  104. const startNode = modelNodes.find((n) => n.type === 'start')
  105. const endNodes = modelNodes.filter((n) => n.type === 'end')
  106. const startId = startNode?.id
  107. /** 节点是否有实际活动(任务或记录) */
  108. const isNodeActive = (n: any): boolean => {
  109. const p = progressMap.get(n.id)
  110. if (p && p.status && p.status !== 'pending') return true
  111. if ((recordsMap.get(n.id) || []).length) return true
  112. return false
  113. }
  114. const mkNode = (n: any, status?: FlowLayoutNode['status']): FlowLayoutNode => {
  115. const p = progressMap.get(n.id)
  116. return {
  117. id: n.id,
  118. name: n.name || n.id,
  119. type: n.type,
  120. status: status ?? (n.type === 'start' || n.type === 'end' || n.type === 'condition' ? 'neutral' : 'pending'),
  121. isMyTurn: !!p?.isMyTurn,
  122. nodeProgress: p,
  123. records: recordsMap.get(n.id) || [],
  124. }
  125. }
  126. /** 主线节点状态:优先进度,其次"祖先=已走过",否则待处理 */
  127. const mainLineStatus = (n: any): FlowLayoutNode['status'] => {
  128. const p = progressMap.get(n.id)
  129. if (p) {
  130. if (p.status === 'current') return 'current'
  131. if (p.status === 'completed') return 'completed'
  132. return 'pending'
  133. }
  134. if (n.id === currentNodeId) return 'current'
  135. if (ancestors.has(n.id)) return 'passed'
  136. if (n.type === 'cc') return 'completed'
  137. return 'pending'
  138. }
  139. const rows: FlowRow[] = []
  140. const visited = new Set<string>()
  141. if (startId && nodeMap.has(startId)) {
  142. rows.push({ kind: 'start', nodes: [mkNode(nodeMap.get(startId))] })
  143. visited.add(startId)
  144. }
  145. let curId: string | undefined = startId
  146. let guard = 0
  147. while (curId && nodeMap.has(curId) && guard++ < MAX_DEPTH) {
  148. const cur = nodeMap.get(curId)
  149. const outs = (outgoing.get(curId) || []).filter((e) => e.targetNodeId && nodeMap.has(e.targetNodeId))
  150. if (cur.type === 'end' || outs.length === 0) {
  151. if (cur.type === 'end') rows.push({ kind: 'end', nodes: [mkNode(cur)] })
  152. else rows.push({ kind: 'node', nodes: [mkNode(cur, mainLineStatus(cur))] })
  153. break
  154. }
  155. // 多出边 → 分支节点
  156. if (outs.length > 1) {
  157. // 第一遍:收集每条分支的节点链(原始模型节点,不含汇聚点/终点)
  158. const rawChains: { edge: any; nodes: any[]; merge: string | null }[] = []
  159. let mergeId: string | null = null
  160. for (const e of outs) {
  161. const chainNodes: any[] = []
  162. let bCurId = e.targetNodeId
  163. let bGuard = 0
  164. let branchMerge: string | null = null
  165. while (bCurId && nodeMap.has(bCurId) && bGuard++ < MAX_DEPTH) {
  166. const bn = nodeMap.get(bCurId)
  167. const bin = incomingCount.get(bCurId) || 0
  168. if (bn.type === 'end' || bin > 1) {
  169. branchMerge = bCurId
  170. break
  171. }
  172. chainNodes.push(bn)
  173. const bNext = (outgoing.get(bCurId) || []).filter((be) => be.targetNodeId && nodeMap.has(be.targetNodeId))
  174. const nextIn = bNext.length === 1 ? incomingCount.get(bNext[0].targetNodeId) || 0 : 0
  175. if (bNext.length !== 1 || nextIn > 1 || bn.type === 'condition') {
  176. if (bNext.length === 1 && nextIn > 1) branchMerge = bNext[0].targetNodeId
  177. break
  178. }
  179. bCurId = bNext[0].targetNodeId
  180. }
  181. rawChains.push({ edge: e, nodes: chainNodes, merge: branchMerge })
  182. if (branchMerge) {
  183. if (mergeId === null) mergeId = branchMerge
  184. else if (mergeId !== branchMerge) mergeId = null
  185. }
  186. }
  187. // 第二遍:判定各分支是否被选择,再解析分支节点状态
  188. const branches: FlowBranch[] = rawChains.map((rc) => {
  189. const cond = rc.edge.condition || {}
  190. const branchName = cond.branchName || ''
  191. const condition = cond.condition || ''
  192. const isDefault = !!cond.isDefault
  193. // 分支被选择:分支内有活动节点(已处理/进行中/有记录),或当前节点就在该分支
  194. const isChosen = rc.nodes.some((n) => isNodeActive(n) || n.id === currentNodeId)
  195. const nodes = rc.nodes.map((n) => {
  196. if (n.id === currentNodeId) return mkNode(n, 'current')
  197. const p = progressMap.get(n.id)
  198. if (p) {
  199. if (p.status === 'current') return mkNode(n, 'current')
  200. if (p.status === 'completed') return mkNode(n, 'completed')
  201. return mkNode(n, 'pending')
  202. }
  203. // 无任务/记录:所在分支被选择视为已走过(历史),否则为未选择
  204. return mkNode(n, isChosen ? 'passed' : 'pending')
  205. })
  206. return {
  207. label: branchName || condition || (isDefault ? '默认分支' : '分支'),
  208. condition,
  209. isDefault,
  210. isChosen,
  211. nodes,
  212. }
  213. })
  214. rows.push({ kind: 'branch', title: cur.name || cur.id, branches })
  215. // 所有分支汇聚到同一非终点节点则继续主线;否则流程已在分支内结束
  216. if (mergeId && nodeMap.has(mergeId) && mergeId !== endNodes[0]?.id) {
  217. curId = mergeId
  218. } else {
  219. curId = undefined
  220. }
  221. continue
  222. }
  223. // 单出边 → 正常推进
  224. const nextId = outs[0].targetNodeId
  225. if (cur.type !== 'start') {
  226. if (cur.type === 'cc') rows.push({ kind: 'cc', nodes: [mkNode(cur, 'completed')] })
  227. else rows.push({ kind: 'node', nodes: [mkNode(cur, mainLineStatus(cur))] })
  228. }
  229. if (visited.has(nextId)) break // 防环
  230. visited.add(nextId)
  231. curId = nextId
  232. }
  233. // 兜底:主遍历未覆盖到的节点追加到末尾,保证所有节点都可见
  234. const emittedIds = new Set<string>()
  235. for (const r of rows) {
  236. r.nodes?.forEach((n) => emittedIds.add(n.id))
  237. r.branches?.forEach((b) => b.nodes.forEach((n) => emittedIds.add(n.id)))
  238. }
  239. for (const n of modelNodes) {
  240. if (n.type === 'start' || n.type === 'end' || n.type === 'condition') continue
  241. if (emittedIds.has(n.id)) continue
  242. rows.push({ kind: n.type === 'cc' ? 'cc' : 'node', nodes: [mkNode(n, mainLineStatus(n))] })
  243. }
  244. // 结束节点兜底:补上所有尚未展示的终点
  245. for (const en of endNodes) {
  246. if (en && nodeMap.has(en.id) && !emittedIds.has(en.id)) {
  247. rows.push({ kind: 'end', nodes: [mkNode(en)] })
  248. }
  249. }
  250. return rows
  251. }