/** * 文件说明:旧流程数据与 Vue Flow 状态、操作 API 之间的兼容适配层。 * 作用范围:仅服务于流程设计器模块。 */ import { nextTick } from "vue"; import { v4 as uuid } from "uuid"; import { MarkerType } from "@vue-flow/core"; /** * 克隆 clone 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} value 待处理的新值 */ const clone = (value) => JSON.parse(JSON.stringify(value ?? {})); const GROUP_TYPES = new Set(["loop", "selectArea"]); const TARGET_ONLY_TYPES = new Set(["end", "stopLoop", "subEnd"]); const SOURCE_ONLY_TYPES = new Set(["start", "subStart"]); const LOOP_BODY_INSET = { left: 24, top: 112, right: 24, bottom: 24 }; const GROUP_BODY_INSET = { left: 24, top: 120, right: 24, bottom: 24 }; const LOOP_MIN_SIZE = { width: 520, height: 360 }; /** * 处理 loopChildExtent 对应的数据或交互;作用范围仅限画布适配层。 */ const loopChildExtent = () => ({ range: "parent", padding: [ LOOP_BODY_INSET.top, LOOP_BODY_INSET.right, LOOP_BODY_INSET.bottom, LOOP_BODY_INSET.left, ], }); /** * 编组 groupChildExtent 对应的数据或交互;作用范围仅限画布适配层。 */ const groupChildExtent = () => ({ range: "parent", padding: [ GROUP_BODY_INSET.top, GROUP_BODY_INSET.right, GROUP_BODY_INSET.bottom, GROUP_BODY_INSET.left, ], }); /** * 处理 nodeSize 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} node 当前流程节点 */ const nodeSize = (node) => { // 分支节点采用统一的标准卡片尺寸,忽略旧流程数据中遗留的大尺寸记录。 if (node.type === "branch") return { width: 372, height: 180 }; const width = Number(node.properties?.width || node.width); const height = Number(node.properties?.height || node.height); if (Number.isFinite(width) && Number.isFinite(height)) { return { width, height }; } if (GROUP_TYPES.has(node.type)) return { width: 720, height: 460 }; if (TARGET_ONLY_TYPES.has(node.type) || SOURCE_ONLY_TYPES.has(node.type)) { return { width: 328, height: 120 }; } return { width: 372, height: 180 }; }; /** * 处理 edgeId 对应的数据或交互;作用范围仅限画布适配层。 */ const edgeId = () => `edge_${uuid().replace(/-/g, "")}`; /** * 处理 nodeId 对应的数据或交互;作用范围仅限画布适配层。 */ const nodeId = () => uuid().replace(/-/g, ""); /** * 处理 toVueNode 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} node 当前流程节点 * @param {*} sizeOverride 调用方传入的 sizeOverride 参数 * @param {*} positionOverride 调用方传入的 positionOverride 参数 */ const toVueNode = (node, sizeOverride, positionOverride) => { const normalized = clone(node); normalized.id ||= nodeId(); normalized.properties ||= {}; const overrideWidth = Number(sizeOverride?.width); const overrideHeight = Number(sizeOverride?.height); const size = Number.isFinite(overrideWidth) && overrideWidth > 0 && Number.isFinite(overrideHeight) && overrideHeight > 0 ? { width: overrideWidth, height: overrideHeight } : nodeSize(normalized); const center = { x: Number(normalized.x) || 0, y: Number(normalized.y) || 0, }; const overrideX = Number(positionOverride?.x); const overrideY = Number(positionOverride?.y); const position = Number.isFinite(overrideX) && Number.isFinite(overrideY) ? { x: overrideX, y: overrideY } : { x: center.x - size.width / 2, y: center.y - size.height / 2, }; const dimensions = GROUP_TYPES.has(normalized.type) ? { width: size.width, height: size.height } : { width: size.width }; return { id: normalized.id, type: "flow-node", position, ...dimensions, draggable: normalized.draggable !== false, selectable: true, deletable: !SOURCE_ONLY_TYPES.has(normalized.type) && normalized.type !== "end", connectable: true, zIndex: GROUP_TYPES.has(normalized.type) ? 0 : 2, data: { flowType: normalized.type, properties: normalized.properties, children: Array.isArray(normalized.children) ? normalized.children : [], text: normalized.text, backend: normalized, originalCenter: center, originalPosition: position, size, }, }; }; /** * 处理 toVueEdge 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} edge 调用方传入的 edge 参数 */ const toVueEdge = (edge) => { const normalized = clone(edge); normalized.id ||= edgeId(); return { id: normalized.id, source: normalized.sourceNodeId, target: normalized.targetNodeId, sourceHandle: normalized.sourceAnchorId || null, targetHandle: normalized.targetAnchorId || null, type: "flow-edge", animated: true, selectable: true, focusable: true, deletable: true, interactionWidth: 32, markerEnd: { type: MarkerType.ArrowClosed, color: "#8b4513", width: 20, height: 20, }, style: { stroke: "#8b4513", strokeWidth: 2 }, data: { backend: normalized }, }; }; /** * 关联 attachChildrenToGroups 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} nodes 调用方传入的 nodes 参数 */ const attachChildrenToGroups = (nodes) => { // 先建立节点索引和显式父子关系,再用旧数据中的 children 补齐缺失关系。 // 最后统一换算相对坐标,避免父节点尚未就绪时提前计算造成位置漂移。 const nodeLookup = new Map(nodes.map((node) => [node.id, node])); const declaredParents = new Map(); nodes.filter((node) => GROUP_TYPES.has(node.data.flowType)).forEach((group) => { (group.data.children || []).forEach((childId) => { if (!declaredParents.has(childId)) declaredParents.set(childId, group.id); }); }); nodes.forEach((child) => { const parentId = child.data.properties?.parentId || declaredParents.get(child.id); const parent = nodeLookup.get(parentId); if (!parent || !GROUP_TYPES.has(parent.data.flowType) || GROUP_TYPES.has(child.data.flowType)) return; const isLoop = parent.data.flowType === "loop"; const relativePosition = { x: isLoop ? Math.max(child.position.x - parent.position.x, LOOP_BODY_INSET.left) : child.position.x - parent.position.x, y: isLoop ? Math.max(child.position.y - parent.position.y, LOOP_BODY_INSET.top) : child.position.y - parent.position.y, }; child.parentNode = parentId; child.extent = isLoop ? loopChildExtent() : groupChildExtent(); child.expandParent = false; child.position = relativePosition; child.zIndex = 5; child.data.properties = { ...child.data.properties, parentId }; parent.data.children = Array.from(new Set([...(parent.data.children || []), child.id])); }); nodes.filter((node) => node.data.flowType === "loop").forEach((group) => { const children = nodes.filter((node) => node.parentNode === group.id); if (!children.length) return; const requiredWidth = Math.max(...children.map((child) => ( child.position.x + Number(child.data.size?.width || 0) + LOOP_BODY_INSET.right ))); const requiredHeight = Math.max(...children.map((child) => ( child.position.y + Number(child.data.size?.height || 0) + LOOP_BODY_INSET.bottom ))); const width = Math.max(Number(group.data.size?.width || 0), LOOP_MIN_SIZE.width, requiredWidth); const height = Math.max(Number(group.data.size?.height || 0), LOOP_MIN_SIZE.height, requiredHeight); group.width = width; group.height = height; group.data.size = { width, height }; group.data.properties = { ...group.data.properties, width, height }; }); return nodes.sort((left, right) => Number(Boolean(left.parentNode)) - Number(Boolean(right.parentNode))); }; export const toVueFlowData = (graph = {}, options = {}) => { const nodes = attachChildrenToGroups((graph.nodes || []).map((node) => ( toVueNode(node, options.nodeSizes?.get(node.id), options.nodePositions?.get(node.id)) ))); const nodeLookup = new Map(nodes.map((node) => [node.id, node])); const edges = (graph.edges || []).map((edge) => { const vueEdge = toVueEdge(edge); const source = nodeLookup.get(vueEdge.source); const target = nodeLookup.get(vueEdge.target); if (source?.parentNode || target?.parentNode) vueEdge.zIndex = 4; return vueEdge; }); return { nodes, edges }; }; export const createFlowFacade = (store) => { const handlers = new Map(); const componentInstances = new Map(); const loopLayoutStates = new Map(); let historySuspended = false; let history = []; let historyIndex = -1; /** * 获取 getNodes 对应的数据或交互;作用范围仅限画布适配层。 */ const getNodes = () => store.getNodes.value || []; /** * 获取 getEdges 对应的数据或交互;作用范围仅限画布适配层。 */ const getEdges = () => store.getEdges.value || []; /** * 处理 backendNode 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} node 当前流程节点 */ const backendNode = (node) => { const result = clone(node.data.backend); const size = { width: Number(node.width || node.dimensions?.width || node.data.size?.width || 0), height: Number(node.height || node.dimensions?.height || node.data.size?.height || 0), }; const parent = node.parentNode ? store.findNode(node.parentNode) : null; const parentPosition = parent?.position || parent?.computedPosition || { x: 0, y: 0 }; const absolutePosition = parent ? { x: parentPosition.x + node.position.x, y: parentPosition.y + node.position.y } : (node.position || node.computedPosition); result.id = node.id; result.type = node.data.flowType; if (GROUP_TYPES.has(result.type)) { result.x = absolutePosition.x + size.width / 2; result.y = absolutePosition.y + size.height / 2; } else { const originalCenter = node.data.originalCenter || { x: absolutePosition.x + size.width / 2, y: absolutePosition.y + size.height / 2, }; const originalPosition = node.data.originalPosition || absolutePosition; result.x = originalCenter.x + absolutePosition.x - originalPosition.x; result.y = originalCenter.y + absolutePosition.y - originalPosition.y; } result.properties = clone(node.data.properties || {}); if (parent?.data?.flowType === "selectArea") delete result.properties.parentId; if (GROUP_TYPES.has(result.type)) { result.properties.width = size.width; result.properties.height = size.height; } if (node.data.children?.length || GROUP_TYPES.has(result.type)) { result.children = [...(node.data.children || [])]; } else { delete result.children; } return result; }; /** * 处理 backendEdge 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} edge 调用方传入的 edge 参数 */ const backendEdge = (edge) => ({ ...clone(edge.data?.backend), id: edge.id, type: edge.data?.backend?.type || "bezier", sourceNodeId: edge.source, targetNodeId: edge.target, sourceAnchorId: edge.sourceHandle || `${edge.source}_1`, targetAnchorId: edge.targetHandle || `${edge.target}_3`, }); /** * 处理 emit 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} name 名称或事件名称 * @param {*} payload 事件数据 */ const emit = (name, payload) => { (handlers.get(name) || []).forEach((handler) => handler(payload)); }; /** * 处理 recordHistory 对应的数据或交互;作用范围仅限画布适配层。 */ const recordHistory = () => { if (historySuspended) return; const graph = facade.getGraphData(); const serialized = JSON.stringify(graph); if (history[historyIndex]?.serialized === serialized) return; history = history.slice(0, historyIndex + 1); history.push({ serialized, graph: clone(graph) }); if (history.length > 100) history.shift(); historyIndex = history.length - 1; }; /** * 更新 changed 对应的数据或交互;作用范围仅限画布适配层。 */ const changed = () => { if (historySuspended) return; recordHistory(); emit("history:change", { data: facade.getGraphData() }); }; /** * 获取 getNodeRect 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} node 当前流程节点 */ const getNodeRect = (node) => { const parent = node.parentNode ? store.findNode(node.parentNode) : null; const parentPosition = parent?.position || parent?.computedPosition || { x: 0, y: 0 }; const position = parent ? { x: parentPosition.x + node.position.x, y: parentPosition.y + node.position.y } : (node.position || node.computedPosition); return { x: Number(position?.x || 0), y: Number(position?.y || 0), width: Number(node.width || node.dimensions?.width || node.data.size?.width || 0), height: Number(node.height || node.dimensions?.height || node.data.size?.height || 0), }; }; /** * 获取 getLoopLayoutState 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} group 调用方传入的 group 参数 */ const getLoopLayoutState = (group) => { const current = getNodeRect(group); const existing = loopLayoutStates.get(group.id); /** * 处理 if 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} !existing 调用方传入的 !existing 参数 */ if (!existing) { const state = { base: { ...current }, last: { ...current } }; loopLayoutStates.set(group.id, state); return state; } const sizeChanged = Math.abs(current.width - existing.last.width) > 1 || Math.abs(current.height - existing.last.height) > 1; /** * 处理 if 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} sizeChanged 调用方传入的 sizeChanged 参数 */ if (sizeChanged) { existing.base = { ...current }; } else { const dx = current.x - existing.last.x; const dy = current.y - existing.last.y; existing.base.x += dx; existing.base.y += dy; } existing.last = { ...current }; return existing; }; /** * 布局 layoutLoop 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} group 调用方传入的 group 参数 * @param {*} activeChildId 调用方传入的 activeChildId 参数 * @param {*} activePosition 调用方传入的 activePosition 参数 */ const layoutLoop = (group, activeChildId, activePosition) => { const state = getLoopLayoutState(group); const children = getNodes().filter((node) => node.parentNode === group.id); if (!children.length) return false; const snapshots = children.map((child) => { const rect = getNodeRect(child); const position = child.id === activeChildId && activePosition ? { x: activePosition.x, y: activePosition.y } : { x: rect.x, y: rect.y }; return { child, position, width: rect.width, height: rect.height }; }); const contentLeft = Math.min(...snapshots.map(({ position }) => position.x)); const contentTop = Math.min(...snapshots.map(({ position }) => position.y)); const contentRight = Math.max(...snapshots.map(({ position, width }) => position.x + width)); const contentBottom = Math.max(...snapshots.map(({ position, height }) => position.y + height)); const left = Math.min(state.base.x, contentLeft - LOOP_BODY_INSET.left); const top = Math.min(state.base.y, contentTop - LOOP_BODY_INSET.top); const right = Math.max( state.base.x + Math.max(state.base.width, LOOP_MIN_SIZE.width), contentRight + LOOP_BODY_INSET.right, ); const bottom = Math.max( state.base.y + Math.max(state.base.height, LOOP_MIN_SIZE.height), contentBottom + LOOP_BODY_INSET.bottom, ); const width = right - left; const height = bottom - top; const properties = { ...group.data.properties, width, height }; store.updateNode(group.id, { position: { x: left, y: top }, computedPosition: { ...group.computedPosition, x: left, y: top }, dimensions: { width, height }, width, height, }); store.updateNodeData(group.id, { ...group.data, properties, size: { width, height }, }, { replace: true }); snapshots.forEach(({ child, position }) => { store.updateNode(child.id, { position: { x: position.x - left, y: position.y - top }, computedPosition: { ...child.computedPosition, x: position.x, y: position.y }, extent: loopChildExtent(), expandParent: false, }); }); state.last = { x: left, y: top, width, height }; nextTick(() => store.updateNodeInternals([group.id, ...children.map((child) => child.id)])); return true; }; /** * 获取 getAnchors 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} node 当前流程节点 */ const getAnchors = (node) => { const type = node.data.flowType; /** * 处理 if 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} type 目标类型 */ if (type === "branch") { return [ { id: `${node.id}_entry`, name: "left" }, ...(node.data.properties?.anchor || []).map((item) => ({ ...item, name: "right" })), ]; } if (TARGET_ONLY_TYPES.has(type)) return [{ id: `${node.id}_3`, name: "left" }]; if (SOURCE_ONLY_TYPES.has(type)) return [{ id: `${node.id}_1`, name: "right" }]; if (type === "selectArea") return []; return [ { id: `${node.id}_1`, name: "right" }, { id: `${node.id}_3`, name: "left" }, ]; }; /** * 处理 modelFor 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} id 目标元素 ID */ const modelFor = (id) => { const node = store.findNode(id); if (!node) return null; const model = { id, type: node.data.flowType, get x() { return backendNode(node).x; }, get y() { return backendNode(node).y; }, get width() { return node.dimensions?.width || node.data.size?.width || 0; }, get height() { return node.dimensions?.height || node.data.size?.height || 0; }, get left() { return getNodeRect(node).x; }, get top() { return getNodeRect(node).y; }, get parentId() { return node.parentNode || null; }, get anchors() { return getAnchors(node); }, get children() { return new Set(node.data.children || []); }, set children(value) { const children = Array.from(value || []); store.updateNodeData(id, { ...node.data, children }, { replace: true }); changed(); }, get draggable() { return node.draggable !== false; }, set draggable(value) { store.updateNode(id, { draggable: Boolean(value) }); }, getProperties: () => facade.getProperties(id), setProperties: (properties) => facade.setProperties(id, properties), setComponentInstance: (instance) => componentInstances.set(id, instance), closePopover: () => componentInstances.get(id)?.closePopover?.(), updateSize: () => store.updateNodeInternals([id]), setCustomProperties: () => store.updateNodeInternals([id]), }; return model; }; const facade = { /** * 订阅 on 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} name 名称或事件名称 * @param {*} handler 事件处理函数 */ on(name, handler) { const list = handlers.get(name) || []; list.push(handler); handlers.set(name, list); return () => facade.off(name, handler); }, /** * 订阅 once 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} name 名称或事件名称 * @param {*} handler 事件处理函数 */ once(name, handler) { /** * 处理 wrapped 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} payload 事件数据 */ const wrapped = (payload) => { facade.off(name, wrapped); handler(payload); }; facade.on(name, wrapped); }, /** * 取消订阅 off 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} name 名称或事件名称 * @param {*} handler 事件处理函数 */ off(name, handler) { if (!handler) handlers.delete(name); else handlers.set(name, (handlers.get(name) || []).filter((item) => item !== handler)); }, emit, markChanged: changed, /** * 获取 getGraphData 对应的数据或交互;作用范围仅限画布适配层。 */ getGraphData() { return { nodes: getNodes().map(backendNode), edges: getEdges().map(backendEdge), }; }, /** * 渲染 render 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} graph 调用方传入的 graph 参数 * @param {*} options 行为配置 */ render(graph, options = {}) { historySuspended = true; loopLayoutStates.clear(); const nodeSizes = options.preserveNodeSizes === true ? new Map(getNodes().map((node) => [node.id, { width: Number(node.dimensions?.width || node.width || node.data.size?.width || 0), height: Number(node.dimensions?.height || node.height || node.data.size?.height || 0), }])) : options.nodeSizes; const nodePositions = options.preserveNodePositions === true ? new Map(getNodes().map((node) => { const rect = getNodeRect(node); return [node.id, { x: rect.x, y: rect.y }]; })) : options.nodePositions; const data = toVueFlowData(graph, { nodeSizes, nodePositions }); /** * 处理 finishRender 对应的数据或交互;作用范围仅限画布适配层。 */ const finishRender = () => { historySuspended = false; if (options.resetHistory !== false) { history = []; historyIndex = -1; recordHistory(); } nextTick(() => store.updateNodeInternals()); }; /** * 处理 applyData 对应的数据或交互;作用范围仅限画布适配层。 */ const applyData = () => { store.setNodes(data.nodes); store.setEdges(data.edges); finishRender(); }; if (options.replaceNodes === true) { // Changing parentNode is structural in Vue Flow. Unmount in one tick // and remount in the next so node wrappers and edges cannot retain // different graph-node instances for the same id. store.setEdges([]); store.setNodes([]); nextTick(applyData); } else { applyData(); } }, /** * 新增 addNode 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} node 当前流程节点 */ addNode(node) { const vueNode = toVueNode(node); store.addNodes(vueNode); changed(); return backendNode(vueNode); }, /** * 删除 deleteNode 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} id 目标元素 ID */ deleteNode(id) { const target = store.findNode(id); if (target?.data?.flowType === "selectArea") { facade.ungroup(id); return; } const parentId = target?.parentNode; const childIds = target?.data?.children || []; const removeChildren = target?.data?.flowType === "loop" && childIds.length > 0; const nodeIds = removeChildren ? [id, ...childIds] : id; const removedIds = new Set(Array.isArray(nodeIds) ? nodeIds : [nodeIds]); store.removeNodes(nodeIds, true, removeChildren); getNodes().forEach((node) => { if (node.data.children?.includes(id) && !removedIds.has(node.id)) { store.updateNodeData(node.id, { ...node.data, children: node.data.children.filter((childId) => childId !== id), }, { replace: true }); } }); componentInstances.delete(id); childIds.forEach((childId) => componentInstances.delete(childId)); changed(); if (parentId) nextTick(() => facade.resizeGroupToChildren(parentId)); }, /** * 新增 addEdge 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} edge 调用方传入的 edge 参数 */ addEdge(edge) { const vueEdge = toVueEdge(edge); const source = store.findNode(vueEdge.source); const target = store.findNode(vueEdge.target); if (source?.parentNode || target?.parentNode) vueEdge.zIndex = 4; store.addEdges(vueEdge); changed(); return backendEdge(vueEdge); }, /** * 删除 deleteEdge 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} id 目标元素 ID */ deleteEdge(id) { store.removeEdges(id); changed(); }, /** * 删除 deleteEdgeByNodeId 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} id 目标元素 ID */ deleteEdgeByNodeId(id) { store.removeEdges(getEdges().filter((edge) => edge.source === id || edge.target === id)); changed(); }, /** * 获取 getProperties 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} id 目标元素 ID */ getProperties(id) { return clone(store.findNode(id)?.data?.properties || {}); }, /** * 设置 setProperties 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} id 目标元素 ID * @param {*} properties 调用方传入的 properties 参数 */ setProperties(id, properties) { const node = store.findNode(id); if (!node) return; store.updateNodeData(id, { ...node.data, properties: clone(properties) }, { replace: true }); nextTick(() => store.updateNodeInternals([id])); changed(); }, getNodeModelById: modelFor, /** * 获取 getSelectElements 对应的数据或交互;作用范围仅限画布适配层。 */ getSelectElements() { return { nodes: (store.getSelectedNodes.value || []).map(backendNode), edges: (store.getSelectedEdges.value || []).map(backendEdge), }; }, /** * 清理 clearSelectElements 对应的数据或交互;作用范围仅限画布适配层。 */ clearSelectElements() { store.removeSelectedElements(); }, /** * 处理 selectElementById 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} id 目标元素 ID */ selectElementById(id) { store.removeSelectedElements(); if (!id) return; const node = store.findNode(id); if (node) store.addSelectedNodes([node]); const edge = store.findEdge(id); if (edge) store.addSelectedEdges([edge]); }, /** * 处理 selectElementsByIds 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} ids 调用方传入的 ids 参数 */ selectElementsByIds(ids = []) { store.removeSelectedElements(); const nodes = ids.map((id) => store.findNode(id)).filter(Boolean); if (nodes.length) store.addSelectedNodes(nodes); }, /** * 处理 focusOn 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} { id } 调用方传入的 { id } 参数 */ focusOn({ id }) { const node = store.findNode(id); if (!node) return; const center = backendNode(node); store.setCenter(center.x, center.y, { zoom: Math.max(store.viewport.value?.zoom || 1, 0.7), duration: 300 }); }, /** * 处理 fitView 对应的数据或交互;作用范围仅限画布适配层。 */ fitView() { return store.fitView({ padding: 0.18, duration: 300 }); }, /** * 处理 translateCenter 对应的数据或交互;作用范围仅限画布适配层。 */ translateCenter() { return facade.fitView(); }, /** * 缩放 zoom 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} scale 缩放比例 * @param {*} center 缩放中心 */ zoom(scale, center) { if (Array.isArray(center) && center.length === 2) { return store.setCenter(center[0], center[1], { zoom: scale, duration: 250 }); } return store.zoomTo(scale, { duration: 250 }); }, /** * 获取 getPointByClient 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} x 客户端横坐标 * @param {*} y 客户端纵坐标 */ getPointByClient(x, y) { const point = store.screenToFlowCoordinate({ x, y }); return { canvasOverlayPosition: point, domOverlayPosition: point }; }, /** * 新增 addToGroup 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} groupId 分组节点 ID * @param {*} childId 子节点 ID * @param {*} dropCenter 拖放中心坐标 */ addToGroup(groupId, childId, dropCenter) { const group = store.findNode(groupId); const child = store.findNode(childId); if (!group || !child || GROUP_TYPES.has(child.data.flowType)) return; const previousParentId = child.parentNode || child.data?.properties?.parentId; const graph = facade.getGraphData(); const groupData = graph.nodes.find((node) => node.id === groupId); const childData = graph.nodes.find((node) => node.id === childId); if (!groupData || !childData) return; // 节点从循环体外进入循环体后,原连线的两端已处于不同执行作用域,必须全部断开。 // 在重新渲染父子结构前直接过滤图数据,可保证入边、出边和层级变更在同一次更新中完成。 const enteringLoop = group.data.flowType === "loop" && previousParentId !== groupId; if (enteringLoop) { graph.edges = graph.edges.filter((edge) => ( edge.sourceNodeId !== childId && edge.targetNodeId !== childId )); } groupData.children = Array.from(new Set([...(groupData.children || []), childId])); childData.properties = { ...(childData.properties || {}), parentId: groupId }; if (dropCenter) { childData.x = dropCenter.x; childData.y = dropCenter.y; } facade.render(graph, { resetHistory: false, replaceNodes: true, preserveNodeSizes: true, preserveNodePositions: true, }); nextTick(() => { store.updateNodeInternals([groupId, childId]); changed(); }); }, /** * 编组 groupNodes 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} groupNode 调用方传入的 groupNode 参数 * @param {*} childIds 子节点 ID 集合 */ groupNodes(groupNode, childIds = []) { // 先计算选中节点的外接矩形确定容器尺寸,再把子节点坐标转换为容器内相对坐标。 // 与分组外节点相连的边保持不变,此处仅更新节点层级和几何信息。 const graph = facade.getGraphData(); const selectedIds = new Set(childIds); const children = graph.nodes.filter((node) => ( selectedIds.has(node.id) && !GROUP_TYPES.has(node.type) && !store.findNode(node.id)?.parentNode )); if (!children.length) return null; const group = clone(groupNode); group.id ||= nodeId(); group.type = "selectArea"; group.properties = { ...(group.properties || {}), action: "NONE" }; group.children = children.map((node) => node.id); children.forEach((node) => { node.properties = { ...(node.properties || {}), parentId: group.id }; }); graph.nodes.push(group); facade.render(graph, { resetHistory: false, replaceNodes: true, preserveNodeSizes: true, preserveNodePositions: true, }); nextTick(() => { store.updateNodeInternals([group.id, ...group.children]); changed(); }); return clone(group); }, /** * 处理 ungroup 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} groupId 分组节点 ID */ ungroup(groupId) { const graph = facade.getGraphData(); const group = graph.nodes.find((node) => node.id === groupId && node.type === "selectArea"); if (!group) return false; const childIds = new Set([ ...(group.children || []), ...graph.nodes.filter((node) => node.properties?.parentId === groupId).map((node) => node.id), ]); graph.nodes = graph.nodes.filter((node) => node.id !== groupId); graph.nodes.forEach((node) => { if (!childIds.has(node.id)) return; const properties = { ...(node.properties || {}) }; delete properties.parentId; node.properties = properties; }); facade.render(graph, { resetHistory: false, replaceNodes: true, preserveNodeSizes: true, preserveNodePositions: true, }); nextTick(() => { store.updateNodeInternals([...childIds]); changed(); }); return true; }, /** * 准备 prepareLoopChildDrag 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} childId 子节点 ID */ prepareLoopChildDrag(childId) { const child = store.findNode(childId); const group = child?.parentNode ? store.findNode(child.parentNode) : null; if (!group || group.data.flowType !== "loop") return; store.updateNode(childId, { extent: loopChildExtent(), expandParent: false, }); getLoopLayoutState(group); const rect = getNodeRect(child); return { groupId: group.id, position: { x: rect.x, y: rect.y } }; }, /** * 布局 layoutLoopDuringDrag 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} groupId 分组节点 ID * @param {*} childId 子节点 ID * @param {*} position 调用方传入的 position 参数 */ layoutLoopDuringDrag(groupId, childId, position) { const group = store.findNode(groupId); if (!group || group.data.flowType !== "loop" || !position) return; layoutLoop(group, childId, position); }, /** * 调整 resizeGroupToChildren 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} groupId 分组节点 ID * @param {*} options 行为配置 */ resizeGroupToChildren(groupId, options = {}) { // 循环容器沿用统一的内边距和最小尺寸约束,并按配置决定是否记录变更。 // 拖拽过程可关闭变更标记,避免高频移动产生多余历史记录。 const group = store.findNode(groupId); if (!group || group.data.flowType !== "loop") return; if (layoutLoop(group) && options.markChanged !== false) changed(); }, /** * 移动 moveNodeBy 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} id 目标元素 ID * @param {*} dx 横向位移 * @param {*} dy 纵向位移 */ moveNodeBy(id, dx, dy) { const node = store.findNode(id); if (!node) return; store.updateNode(id, { position: { x: node.position.x + dx, y: node.position.y + dy, }, }); changed(); }, /** * 撤销 undo 对应的数据或交互;作用范围仅限画布适配层。 */ undo() { if (historyIndex <= 0) return false; historyIndex -= 1; const snapshot = clone(history[historyIndex].graph); facade.render(snapshot, { resetHistory: false, replaceNodes: true }); nextTick(() => emit("history:change", { data: facade.getGraphData() })); return true; }, /** * 重做 redo 对应的数据或交互;作用范围仅限画布适配层。 */ redo() { if (historyIndex >= history.length - 1) return false; historyIndex += 1; const snapshot = clone(history[historyIndex].graph); facade.render(snapshot, { resetHistory: false, replaceNodes: true }); nextTick(() => emit("history:change", { data: facade.getGraphData() })); return true; }, /** * 注册 registerComponentInstance 对应的数据或交互;作用范围仅限画布适配层。 * @param {*} id 目标元素 ID * @param {*} instance 组件实例 */ registerComponentInstance(id, instance) { if (instance) componentInstances.set(id, instance); }, extension: { selectionSelect: { openSelectionSelect: () => emit("selection:open"), closeSelectionSelect: () => emit("selection:close"), }, }, }; return facade; };