|
|
@@ -1,37 +1,37 @@
|
|
|
/**
|
|
|
* 流程进度布局工具
|
|
|
*
|
|
|
- * 后端进度接口(/flow/instance/{id}/progress)返回的是按 BFS 拓扑排序后的"拍平"节点列表,
|
|
|
- * 分支(条件节点)会全部排在一条线上,不易看出分支结构。
|
|
|
- *
|
|
|
- * 这里根据流程定义的 modelJson(nodes + edges)重建真实拓扑,
|
|
|
- * 将流程组织为若干"行"(FlowRow):
|
|
|
+ * 根据流程定义的 modelJson(nodes + edges)重建真实拓扑,将流程组织为若干"行"(FlowRow):
|
|
|
* - start / end:起点/终点标记
|
|
|
- * - node / cc:主流程上的单个节点
|
|
|
- * - branch:条件分支区(含多个分支列,分支节点并行展示)
|
|
|
- * 分支区之后若各分支汇聚到同一节点,则继续主流程。
|
|
|
+ * - 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' | 'pending' | 'neutral'
|
|
|
+ status: 'completed' | 'current' | 'passed' | '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
|
|
|
+ /** 该分支是否被实际选择(分支内存在已处理任务/记录,或当前节点位于该分支) */
|
|
|
+ isChosen: boolean
|
|
|
nodes: FlowLayoutNode[]
|
|
|
}
|
|
|
|
|
|
@@ -44,7 +44,7 @@ export interface FlowRow {
|
|
|
branches?: FlowBranch[]
|
|
|
}
|
|
|
|
|
|
-const MAX_DEPTH = 200
|
|
|
+const MAX_DEPTH = 300
|
|
|
|
|
|
/**
|
|
|
* 根据流程模型与进度数据构建布局行
|
|
|
@@ -52,12 +52,14 @@ const MAX_DEPTH = 200
|
|
|
* @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[]
|
|
|
+ records: any[],
|
|
|
+ currentNodeId?: string | null
|
|
|
): FlowRow[] {
|
|
|
if (!Array.isArray(modelNodes) || modelNodes.length === 0) return []
|
|
|
|
|
|
@@ -88,111 +90,154 @@ export function buildFlowLayout(
|
|
|
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 endNode = modelNodes.find((n) => n.type === 'end')
|
|
|
+ const endNodes = modelNodes.filter((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 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)
|
|
|
- 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,
|
|
|
+ status: status ?? (n.type === 'start' || n.type === 'end' || n.type === 'condition' ? 'neutral' : 'pending'),
|
|
|
+ isMyTurn: !!p?.isMyTurn,
|
|
|
nodeProgress: p,
|
|
|
records: recordsMap.get(n.id) || [],
|
|
|
- branchLabel: branchMeta?.branchName,
|
|
|
- conditionText: branchMeta?.condition,
|
|
|
- isDefault: branchMeta?.isDefault,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /** 主线节点状态:优先进度,其次"祖先=已走过",否则待处理 */
|
|
|
+ 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>()
|
|
|
- let curId: string | undefined = startId
|
|
|
|
|
|
- // 起点
|
|
|
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' || curId === endId) {
|
|
|
- rows.push({ kind: 'end', nodes: [mkNode(cur)] })
|
|
|
- } else {
|
|
|
- // 游离节点(不应出现),兜底展示
|
|
|
- rows.push({ kind: 'node', nodes: [mkNode(cur)] })
|
|
|
- }
|
|
|
+ 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 branches: FlowBranch[] = []
|
|
|
+ // 第一遍:收集每条分支的节点链(原始模型节点,不含汇聚点/终点)
|
|
|
+ const rawChains: { edge: any; nodes: any[]; merge: string | null }[] = []
|
|
|
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[] = []
|
|
|
+ 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 && chain.length > 0)) {
|
|
|
+ if (bn.type === 'end' || bin > 1) {
|
|
|
branchMerge = bCurId
|
|
|
break
|
|
|
}
|
|
|
- // 分支标签已展示在分支列头部,节点卡片上不再重复
|
|
|
- chain.push(mkNode(bn))
|
|
|
+ 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
|
|
|
- }
|
|
|
+ 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,
|
|
|
- })
|
|
|
+ rawChains.push({ edge: e, nodes: chainNodes, merge: branchMerge })
|
|
|
if (branchMerge) {
|
|
|
if (mergeId === null) mergeId = branchMerge
|
|
|
- else if (mergeId !== branchMerge) mergeId = null // 各分支汇聚点不一致
|
|
|
+ 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)) {
|
|
|
+ // 所有分支汇聚到同一非终点节点则继续主线;否则流程已在分支内结束
|
|
|
+ if (mergeId && nodeMap.has(mergeId) && mergeId !== endNodes[0]?.id) {
|
|
|
curId = mergeId
|
|
|
} else {
|
|
|
- curId = undefined // 分支后流程结束(各分支分别结束)
|
|
|
+ curId = undefined
|
|
|
}
|
|
|
continue
|
|
|
}
|
|
|
@@ -200,20 +245,31 @@ export function buildFlowLayout(
|
|
|
// 单出边 → 正常推进
|
|
|
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 (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
|
|
|
}
|
|
|
|
|
|
- // 结束节点兜底:若未在遍历中输出,则补在末尾
|
|
|
- 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))] })
|
|
|
+ // 兜底:主遍历未覆盖到的节点追加到末尾,保证所有节点都可见
|
|
|
+ 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
|
|
|
}
|