| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275 |
- /**
- * 流程进度布局工具
- *
- * 根据流程定义的 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<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 ancestors = new Set<string>()
- if (currentNodeId && nodeMap.has(currentNodeId)) {
- const reverse: Map<string, string[]> = 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<string>()
- 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<string>()
- 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
- }
|