allinssl/frontend/apps/vue-flow/store/transformFlowData.ts

687 lines
20 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { v4 as uuidv4 } from 'uuid'
import { WorkflowNode, WorkflowEdge, NodeData } from '../types'
import { MarkerType } from '@vue-flow/core'
// 节点类型映射
const NODE_TYPE_MAP = {
start: 'start',
apply: 'apply',
branch: 'normal', // 使用普通节点作为分支节点
condition: 'normal', // 条件节点
deploy: 'deploy',
execute_result_branch: 'normal', // 执行结果分支节点
execute_result_condition: 'normal', // 执行结果条件节点
notify: 'notify',
}
// 节点标签映射
const NODE_LABEL_MAP = {
start: '开始',
apply: '申请证书',
branch: '分支节点',
condition: '条件节点',
deploy: '部署证书',
execute_result_branch: '执行结果',
execute_result_condition: '执行结果条件',
notify: '通知',
}
// 布局配置
const LAYOUT_CONFIG = {
// 节点尺寸
nodeWidth: 180,
nodeHeight: 40,
// 节点间距
horizontalGap: 220, // 水平间距,增加防止重叠
verticalGap: 150, // 垂直间距,增加更多空间
// 分支节点的水平间距
branchHorizontalGap: 280, // 增加分支节点间距
// 多分支的间距调整系数
multiBranchSpacingFactor: 0.9, // 多分支时适当减小间距,确保视野内能容纳
// 初始位置
initialX: 600, // 增加初始X坐标使整个图更居中
initialY: 80,
}
interface NestedNode {
id: string
name: string
type: string
config?: any
childNode?: NestedNode
conditionNodes?: NestedNode[]
inputs?: Array<{ name: string; fromNodeId: string }>
}
interface ProcessResult {
nodes: WorkflowNode[]
edges: WorkflowEdge[]
}
/**
* 节点子树的大小信息
*/
interface SubtreeSize {
width: number // 子树宽度
height: number // 子树高度
childCount: number // 子节点数量
}
/**
* 将嵌套节点数据结构转换为VueFlow所需的节点和边
* @param data 嵌套的节点数据
* @returns VueFlow的节点和边
*/
export function transformNestedFlowData(data: NestedNode): ProcessResult {
const result: ProcessResult = {
nodes: [],
edges: [],
}
if (!data) return result
// 垂直对齐布局的实现
const nodePositions = new Map<string, { x: number; y: number }>()
const yLevels = new Map<number, number>() // 每个层级的y坐标
// 计算层级
const nodeLevels = new Map<string, number>()
const nodeParents = new Map<string, string>() // 保存节点的父节点
const nodeTrees = new Map<string, NestedNode>() // 保存所有节点引用,便于访问
const nodeSubtreeSize = new Map<string, SubtreeSize>() // 保存每个节点子树的大小信息
const conditionEndNodes = new Map<string, string[]>() // 保存分支节点的所有结束节点IDs
// 第一步:遍历节点树,确定每个节点的层级和父节点,并收集所有节点
function assignLevels(node: NestedNode | undefined, level: number = 0, parentId: string | null = null) {
if (!node) return
// 保存节点信息
nodeLevels.set(node.id, level)
nodeTrees.set(node.id, node)
if (parentId) {
nodeParents.set(node.id, parentId)
}
// 处理条件分支节点
if (node.conditionNodes && node.conditionNodes.length > 0) {
const hasChildNode = node.childNode !== undefined
// 条件节点层级 +1
for (const condNode of node.conditionNodes) {
assignLevels(condNode, level + 1, node.id)
// 递归处理条件节点的子节点
if (condNode.childNode) {
// 条件节点的子节点层级 +2
assignLevels(condNode.childNode, level + 2, condNode.id)
// 如果条件分支有子节点,要递归找到所有末端节点
findAllEndNodes(condNode.childNode, node.id)
} else {
// 如果条件节点没有子节点,它自己就是终止节点
if (!conditionEndNodes.has(node.id)) {
conditionEndNodes.set(node.id, [])
}
conditionEndNodes.get(node.id)?.push(condNode.id)
}
}
// 处理普通子节点 - 如果同时存在conditionNodes和childNode
if (hasChildNode && node.childNode) {
// 如果节点同时有条件分支和子节点,子节点级别设为独立的,不直接关联到父节点
// 放在比所有条件分支末端节点更低的层级
const maxConditionLevel = findMaxConditionEndLevel(node)
assignLevels(node.childNode, maxConditionLevel + 1, null)
}
} else if (node.childNode) {
// 普通子节点,层级+1
assignLevels(node.childNode, level + 1, node.id)
}
}
// 查找条件分支的最大末端层级
function findMaxConditionEndLevel(node: NestedNode): number {
let maxLevel = nodeLevels.get(node.id) || 0
// 获取当前分支的所有末端节点
const endNodeIds = conditionEndNodes.get(node.id) || []
// 找出最大层级
for (const endId of endNodeIds) {
const level = nodeLevels.get(endId) || 0
maxLevel = Math.max(maxLevel, level)
}
// 至少比父节点高2个层级
return Math.max(maxLevel, nodeLevels.get(node.id)! + 2)
}
// 递归查找分支下所有末端节点
function findAllEndNodes(node: NestedNode, branchParentId: string) {
// 初始化终止节点集合
if (!conditionEndNodes.has(branchParentId)) {
conditionEndNodes.set(branchParentId, [])
}
// 如果有条件分支,则不是末端节点,需要继续递归
if (node.conditionNodes && node.conditionNodes.length > 0) {
for (const condNode of node.conditionNodes) {
if (condNode.childNode) {
findAllEndNodes(condNode.childNode, branchParentId)
} else {
// 条件节点没有子节点,它是末端节点
conditionEndNodes.get(branchParentId)?.push(condNode.id)
}
}
// 如果还有子节点,继续递归查找
if (node.childNode) {
findAllEndNodes(node.childNode, branchParentId)
}
}
// 如果还有子节点,不是末端节点,继续递归
else if (node.childNode) {
findAllEndNodes(node.childNode, branchParentId)
}
// 没有子节点,也没有条件分支,它是末端节点
else {
conditionEndNodes.get(branchParentId)?.push(node.id)
}
}
// 第二步:计算每个节点子树的大小,自底向上
function calculateSubtreeSizes(nodeId: string): SubtreeSize {
const node = nodeTrees.get(nodeId)
if (!node) {
return { width: 0, height: 0, childCount: 0 }
}
// 如果已经计算过,直接返回
if (nodeSubtreeSize.has(nodeId)) {
return nodeSubtreeSize.get(nodeId)!
}
// 默认大小
let subtreeWidth = LAYOUT_CONFIG.nodeWidth
let maxChildWidth = 0
let totalChildWidth = 0
let childCount = 0
// 计算条件分支的子树大小
if (node.conditionNodes && node.conditionNodes.length > 0) {
for (const condNode of node.conditionNodes) {
// 计算条件节点子树大小
const condSize = calculateSubtreeSizes(condNode.id)
totalChildWidth += condSize.width
maxChildWidth = Math.max(maxChildWidth, condSize.width)
childCount += condSize.childCount + 1 // +1 是条件节点本身
// 如果条件节点有子节点,也要计算
if (condNode.childNode) {
const childSize = calculateSubtreeSizes(condNode.childNode.id)
totalChildWidth += childSize.width
maxChildWidth = Math.max(maxChildWidth, childSize.width)
childCount += childSize.childCount + 1 // +1 是子节点本身
}
}
// 多个条件分支的总宽度
if (node.conditionNodes.length > 1) {
// 条件分支节点之间需要间距
subtreeWidth = Math.max(
subtreeWidth,
totalChildWidth + (node.conditionNodes.length - 1) * LAYOUT_CONFIG.branchHorizontalGap,
)
} else {
subtreeWidth = Math.max(subtreeWidth, maxChildWidth)
}
}
// 计算普通子节点的子树大小
if (node.childNode) {
const childSize = calculateSubtreeSizes(node.childNode.id)
subtreeWidth = Math.max(subtreeWidth, childSize.width)
childCount += childSize.childCount + 1 // +1 是子节点本身
}
// 保存并返回结果
const result = { width: subtreeWidth, height: 0, childCount }
nodeSubtreeSize.set(nodeId, result)
return result
}
// 第三步计算每个层级的Y坐标
function calculateYCoordinates() {
const maxLevel = Math.max(...Array.from(nodeLevels.values()))
for (let i = 0; i <= maxLevel; i++) {
yLevels.set(i, LAYOUT_CONFIG.initialY + i * LAYOUT_CONFIG.verticalGap)
}
}
// 第四步计算节点的X坐标位置考虑子树宽度避免重叠
function positionNodes(nodeId: string, leftBoundary: number = 0): number {
const node = nodeTrees.get(nodeId)
if (!node) return leftBoundary
const level = nodeLevels.get(nodeId) || 0
const subtreeSize = nodeSubtreeSize.get(nodeId) || { width: LAYOUT_CONFIG.nodeWidth, height: 0, childCount: 0 }
// 计算节点的x坐标 - 居中于其子树
const x = leftBoundary + subtreeSize.width / 2
// 保存节点位置
nodePositions.set(nodeId, {
x: x,
y: yLevels.get(level)!,
})
// 初始左边界,用于子节点布局
let childLeftBoundary = leftBoundary
// 处理条件分支
if (node.conditionNodes && node.conditionNodes.length > 0) {
const conditionCount = node.conditionNodes.length
// 计算所有条件分支以及它们子节点的总宽度
let totalConditionWidth = 0
const conditionSizes: { nodeId: string; width: number }[] = []
// 先收集所有条件分支的宽度信息
for (const condNode of node.conditionNodes) {
const condSize = nodeSubtreeSize.get(condNode.id) || {
width: LAYOUT_CONFIG.nodeWidth,
height: 0,
childCount: 0,
}
// 计算这个条件分支的总宽度(包括所有子节点)
let branchWidth = condSize.width
// 如果有子节点,需要考虑子节点的宽度
if (condNode.childNode) {
// 递归计算子树的宽度
const childTreeWidth = calculateBranchWidth(condNode.childNode)
branchWidth = Math.max(branchWidth, childTreeWidth)
}
conditionSizes.push({ nodeId: condNode.id, width: branchWidth })
totalConditionWidth += branchWidth
}
// 根据分支数量动态调整间距
let adjustedGap = LAYOUT_CONFIG.branchHorizontalGap
// 当分支数量大于2时适当减小间距
if (conditionCount > 2) {
adjustedGap =
LAYOUT_CONFIG.branchHorizontalGap * Math.pow(LAYOUT_CONFIG.multiBranchSpacingFactor, conditionCount - 2)
}
// 添加分支之间的间距
totalConditionWidth += (conditionCount - 1) * adjustedGap
// 计算条件分支区域的起始位置,确保条件分支居中于父节点
childLeftBoundary = x - totalConditionWidth / 2
// 布局每个条件分支
for (const condInfo of conditionSizes) {
const condNode = nodeTrees.get(condInfo.nodeId)
if (!condNode) continue
// 定位条件节点
const condX = childLeftBoundary + condInfo.width / 2
// 保存条件节点位置
nodePositions.set(condNode.id, {
x: condX,
y: yLevels.get(level + 1)!,
})
// 处理条件子节点
if (condNode.childNode) {
// 递归布局条件子节点及其子树
positionBranch(condNode.childNode, condX, level + 2, condInfo.width)
}
// 更新下一个条件分支的位置
childLeftBoundary += condInfo.width + adjustedGap
}
// 如果节点同时有条件分支和普通子节点,独立处理子节点的位置
if (node.childNode) {
const childNodeId = node.childNode.id
// 获取子节点的层级已经在assignLevels中计算为条件分支末端之后
const childLevel = nodeLevels.get(childNodeId) || level + 3
// 保存子节点位置 - 与父节点垂直对齐
nodePositions.set(childNodeId, {
x: x,
y: yLevels.get(childLevel)!,
})
// 递归处理子节点的子节点
if (node.childNode.childNode || (node.childNode.conditionNodes && node.childNode.conditionNodes.length > 0)) {
positionNodes(childNodeId, x - subtreeSize.width / 2)
}
}
} else if (node.childNode) {
// 处理普通子节点 - 垂直对齐于父节点下方
const childNodeId = node.childNode.id
const childLevel = level + 1
// 保存节点位置
nodePositions.set(childNodeId, {
x: x, // 与父节点垂直对齐
y: yLevels.get(childLevel)!,
})
// 递归处理其子节点
if (node.childNode.childNode || (node.childNode.conditionNodes && node.childNode.conditionNodes.length > 0)) {
positionNodes(childNodeId, leftBoundary)
}
}
return leftBoundary + subtreeSize.width
}
// 计算分支的总宽度(包括所有子节点)
function calculateBranchWidth(node: NestedNode): number {
const nodeSize = nodeSubtreeSize.get(node.id) || {
width: LAYOUT_CONFIG.nodeWidth,
height: 0,
childCount: 0,
}
let totalWidth = nodeSize.width
// 如果有条件分支,计算所有分支的总宽度
if (node.conditionNodes && node.conditionNodes.length > 0) {
let branchesWidth = 0
const branchCount = node.conditionNodes.length
// 获取每个分支的宽度
const branchWidths: number[] = []
for (const condNode of node.conditionNodes) {
const condWidth = calculateBranchWidth(condNode)
branchWidths.push(condWidth)
branchesWidth += condWidth
}
// 根据分支数量动态调整间距
let adjustedGap = LAYOUT_CONFIG.branchHorizontalGap
// 当分支数量大于2时适当减小间距避免超出视野
if (branchCount > 2) {
adjustedGap =
LAYOUT_CONFIG.branchHorizontalGap * Math.pow(LAYOUT_CONFIG.multiBranchSpacingFactor, branchCount - 2)
}
// 加上分支间距
if (branchCount > 1) {
branchesWidth += (branchCount - 1) * adjustedGap
}
totalWidth = Math.max(totalWidth, branchesWidth)
}
// 如果有子节点,递归计算
if (node.childNode) {
const childWidth = calculateBranchWidth(node.childNode)
totalWidth = Math.max(totalWidth, childWidth)
}
return totalWidth
}
// 递归定位分支中的所有节点
function positionBranch(node: NestedNode, centerX: number, level: number, availableWidth: number): void {
// 保存节点位置 - 垂直对齐于父节点
nodePositions.set(node.id, {
x: centerX,
y: yLevels.get(level)!,
})
// 如果有条件分支,递归处理
if (node.conditionNodes && node.conditionNodes.length > 0) {
// 类似于positionNodes中的处理逻辑
const conditionCount = node.conditionNodes.length
let totalWidth = 0
const condSizes: { nodeId: string; width: number }[] = []
// 计算所有条件分支的宽度
for (const condNode of node.conditionNodes) {
const width = calculateBranchWidth(condNode)
condSizes.push({ nodeId: condNode.id, width })
totalWidth += width
}
// 根据分支数量动态调整间距
let adjustedGap = LAYOUT_CONFIG.branchHorizontalGap
// 当分支数量大于2时适当减小间距
if (conditionCount > 2) {
adjustedGap =
LAYOUT_CONFIG.branchHorizontalGap * Math.pow(LAYOUT_CONFIG.multiBranchSpacingFactor, conditionCount - 2)
}
// 添加分支间距
totalWidth += (conditionCount - 1) * adjustedGap
// 计算起始位置,确保居中
let startX = centerX - totalWidth / 2
// 处理每个条件分支
for (const condInfo of condSizes) {
const condNode = nodeTrees.get(condInfo.nodeId)
if (!condNode) continue
// 分支节点居中
const condX = startX + condInfo.width / 2
// 保存位置
nodePositions.set(condNode.id, {
x: condX,
y: yLevels.get(level + 1)!,
})
// 处理子节点
if (condNode.childNode) {
positionBranch(condNode.childNode, condX, level + 2, condInfo.width)
}
// 更新下一个分支的位置
startX += condInfo.width + adjustedGap
}
// 处理子节点
if (node.childNode) {
// 获取子节点层级
const childLevel =
Math.max(
...Array.from(nodeLevels.entries())
.filter(([id]) => nodeParents.get(id) === node.id)
.map(([, level]) => level),
) + 1
// 定位子节点
positionBranch(node.childNode, centerX, childLevel, availableWidth)
}
}
// 如果有子节点但没有条件分支
else if (node.childNode) {
positionBranch(node.childNode, centerX, level + 1, availableWidth)
}
}
// 第五步:创建节点和边
function createNodesAndEdges(node: NestedNode | undefined, parentId: string | null = null) {
if (!node) return
// 获取节点位置
const position = nodePositions.get(node.id)
if (!position) return // 跳过没有位置信息的节点
// 节点类型和标签
const nodeType = NODE_TYPE_MAP[node.type as keyof typeof NODE_TYPE_MAP] || 'normal'
const nodeLabel = node.name || NODE_LABEL_MAP[node.type as keyof typeof NODE_LABEL_MAP] || '未知节点'
// 创建节点数据
const nodeData: NodeData = {
id: node.id,
type: nodeType as any,
label: nodeLabel,
canMove: true,
canDelete: true,
canChangeType: true,
}
// 添加配置信息
if (node.config) {
Object.assign(nodeData, { config: node.config })
}
// 创建节点
const flowNode: WorkflowNode = {
id: node.id,
type: nodeType,
position: { x: position.x, y: position.y },
data: nodeData,
}
// 添加节点
result.nodes.push(flowNode)
// 添加从父节点到当前节点的边(如果有父节点)
if (parentId) {
const edgeId = `${parentId}-${node.id}`
const parentPosition = nodePositions.get(parentId)
if (parentPosition) {
// 计算边缘路径的偏移量,避免与节点重叠
const offset = LAYOUT_CONFIG.nodeHeight / 2 + 10 // 节点高度的一半加上额外间距
const sourceY = parentPosition.y + offset
const targetY = position.y - offset
const edge: WorkflowEdge = {
id: edgeId,
source: parentId,
target: node.id,
type: 'step',
style: {
strokeWidth: 2,
strokeDasharray: 5,
},
animated: true,
// 添加箭头标记
markerEnd: {
type: MarkerType.ArrowClosed,
width: 15,
height: 15,
color: '#b1b1b7',
},
// 添加路径偏移
sourceY,
targetY,
}
result.edges.push(edge)
}
}
// 特殊处理 - 同时有条件分支和子节点的情况
const hasConditionNodesAndChildNode = node.conditionNodes && node.conditionNodes.length > 0 && node.childNode
// 处理条件分支节点
if (node.conditionNodes && node.conditionNodes.length > 0) {
// 处理每个条件分支
for (const condNode of node.conditionNodes) {
// 创建条件节点及其子节点
createNodesAndEdges(condNode, node.id)
if (condNode.childNode) {
createNodesAndEdges(condNode.childNode, condNode.id)
}
}
// 如果同时存在子节点,将所有条件分支末端节点连接到该子节点
if (hasConditionNodesAndChildNode) {
const childNodeId = node.childNode!.id
const endNodeIds = conditionEndNodes.get(node.id) || []
// 为每个条件分支的末端节点创建到子节点的连接
for (const endNodeId of endNodeIds) {
// 避免重复创建边
const edgeId = `${endNodeId}-${childNodeId}`
// 检查是否已经存在此边
const edgeExists = result.edges.some((edge) => edge.id === edgeId)
if (!edgeExists) {
const endNodePosition = nodePositions.get(endNodeId)
const childNodePosition = nodePositions.get(childNodeId)
if (endNodePosition && childNodePosition) {
// 计算边缘路径的偏移量
const offset = LAYOUT_CONFIG.nodeHeight / 2 + 10
const sourceY = endNodePosition.y + offset
const targetY = childNodePosition.y - offset
const edge: WorkflowEdge = {
id: edgeId,
source: endNodeId,
target: childNodeId,
type: 'step',
style: {
strokeWidth: 2,
strokeDasharray: 5,
},
animated: true,
markerEnd: {
type: MarkerType.ArrowClosed,
width: 15,
height: 15,
color: '#b1b1b7',
},
// 添加路径偏移
sourceY,
targetY,
}
result.edges.push(edge)
}
}
}
// 独立创建子节点,不从父节点连接
createNodesAndEdges(node.childNode, null)
}
}
// 处理常规子节点(如果不是与条件分支共存的情况)
if (node.childNode && !hasConditionNodesAndChildNode) {
createNodesAndEdges(node.childNode, node.id)
}
}
// 执行布局算法
assignLevels(data)
// 计算子树大小(从根节点开始)
calculateSubtreeSizes(data.id)
calculateYCoordinates()
// 计算节点位置从根节点开始初始左边界为0
positionNodes(data.id, 0)
createNodesAndEdges(data)
return result
}
/**
* 处理工作流数据,从 JSON 结构转换为 VueFlow 所需的节点和边
* @param workflowData 工作流 JSON 数据
* @returns VueFlow的节点和边
*/
export function processWorkflowData(workflowData: any): ProcessResult {
if (!workflowData) {
return { nodes: [], edges: [] }
}
// 处理嵌套数据结构
return transformNestedFlowData(workflowData)
}