feat: 增强节点验证与保存逻辑,优化循环体节点的拖放与布局处理
This commit is contained in:
parent
c81c10634b
commit
2d5266c497
@ -217,7 +217,11 @@ watch(
|
|||||||
|
|
||||||
// 保存验证
|
// 保存验证
|
||||||
/**
|
/**
|
||||||
* 校验 validateAndSave 对应的数据或交互;作用范围仅限当前组件或模块。
|
* 校验并保存通用节点的输入、输出和执行目标。
|
||||||
|
*
|
||||||
|
* 实现思路:先运行 Element Plus 的输入输出规则,再校验独立执行目标组件中的角色和设备用途;
|
||||||
|
* 执行目标不属于当前两个 el-form,因此失败时需要主动给出提示,避免用户只看到节点标红却
|
||||||
|
* 无法判断缺少哪个字段。无论成功与否都保留当前编辑值,便于用户继续修正。
|
||||||
*/
|
*/
|
||||||
const validateAndSave = async () => {
|
const validateAndSave = async () => {
|
||||||
let inputValid = true
|
let inputValid = true
|
||||||
@ -228,8 +232,10 @@ const validateAndSave = async () => {
|
|||||||
if (outputFormRef.value) {
|
if (outputFormRef.value) {
|
||||||
await outputFormRef.value.validate((valid) => { if (!valid) outputValid = false })
|
await outputFormRef.value.validate((valid) => { if (!valid) outputValid = false })
|
||||||
}
|
}
|
||||||
if (isRobotNode.value && !formData.executionTarget.roleKey) inputValid = false
|
if (isRobotNode.value && !formData.executionTarget.roleKey) {
|
||||||
if (isRobotNode.value
|
inputValid = false
|
||||||
|
ElMessage.warning('请选择执行机器人角色')
|
||||||
|
} else if (isRobotNode.value
|
||||||
&& actionRequiresDevice(props.data.properties.action)
|
&& actionRequiresDevice(props.data.properties.action)
|
||||||
&& !formData.executionTarget.deviceSlotKey) {
|
&& !formData.executionTarget.deviceSlotKey) {
|
||||||
inputValid = false
|
inputValid = false
|
||||||
|
|||||||
@ -79,6 +79,15 @@ const handleRoleChange = () => {
|
|||||||
emitTarget()
|
emitTarget()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将节点保存的执行目标同步到表单,并补齐界面自动选中的默认角色。
|
||||||
|
*
|
||||||
|
* 实现思路:下拉框在缺少有效 roleKey 时会展示第一个角色,因此必须把同一个默认值
|
||||||
|
* 通过 v-model 回写给父表单,避免出现“界面已选择、实际待保存数据仍为空”的状态。
|
||||||
|
* 仅在规范化后的关键字段与原值不同时触发更新,防止深度 watch 反复回写。
|
||||||
|
*
|
||||||
|
* 注意:没有可用角色时保持空值,交由保存校验给出明确提示。
|
||||||
|
*/
|
||||||
const syncFromModel = () => {
|
const syncFromModel = () => {
|
||||||
roles.value = getResourceRoles()
|
roles.value = getResourceRoles()
|
||||||
const value = props.modelValue || {}
|
const value = props.modelValue || {}
|
||||||
@ -88,6 +97,12 @@ const syncFromModel = () => {
|
|||||||
localTarget.dispatchPolicy = value.dispatchPolicy || 'ONE'
|
localTarget.dispatchPolicy = value.dispatchPolicy || 'ONE'
|
||||||
deviceSlotKey.value = value.deviceSlotKey || value.deviceBindings?.[0]?.slotKey || ''
|
deviceSlotKey.value = value.deviceSlotKey || value.deviceBindings?.[0]?.slotKey || ''
|
||||||
selectDefaultSlot()
|
selectDefaultSlot()
|
||||||
|
|
||||||
|
const normalizedDeviceSlotKey = hasDeviceParameter.value ? deviceSlotKey.value : ''
|
||||||
|
const targetChanged = String(value.roleKey || '') !== String(localTarget.roleKey || '')
|
||||||
|
|| String(value.dispatchPolicy || 'ONE') !== 'ONE'
|
||||||
|
|| String(value.deviceSlotKey || '') !== String(normalizedDeviceSlotKey || '')
|
||||||
|
if (targetChanged) emitTarget()
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@ -624,7 +624,6 @@ function handler(params) {
|
|||||||
name: "视频智能分析",
|
name: "视频智能分析",
|
||||||
type: "mediaAnalysis",
|
type: "mediaAnalysis",
|
||||||
desc: "按分析场景理解测试录像并输出结构化结论",
|
desc: "按分析场景理解测试录像并输出结构化结论",
|
||||||
show: __PROJECT_NAME__ === 'aima',
|
|
||||||
action: "VIDEO_ANALYZE",
|
action: "VIDEO_ANALYZE",
|
||||||
nodeType: "LLM",
|
nodeType: "LLM",
|
||||||
nodeParams: [
|
nodeParams: [
|
||||||
@ -658,7 +657,6 @@ function handler(params) {
|
|||||||
name: "通用图片分析",
|
name: "通用图片分析",
|
||||||
type: "imageAnalysis",
|
type: "imageAnalysis",
|
||||||
desc: "按照提示词分析一张或多张图片并返回指定内容",
|
desc: "按照提示词分析一张或多张图片并返回指定内容",
|
||||||
show: __PROJECT_NAME__ === 'aima',
|
|
||||||
action: 'IMAGE_ANALYZE',
|
action: 'IMAGE_ANALYZE',
|
||||||
nodeType: "LLM",
|
nodeType: "LLM",
|
||||||
nodeParams: [
|
nodeParams: [
|
||||||
|
|||||||
@ -543,8 +543,12 @@ const dragover = (e) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理 drop 对应的数据或交互;作用范围仅限当前组件或模块。
|
* 将组件库节点创建到画布,并在落点位于循环体时建立父子关系。
|
||||||
* @param {*} e 触发操作的浏览器事件
|
*
|
||||||
|
* 实现思路:先按绝对坐标创建节点,再交给适配层完成相对坐标转换;loop 可以进入 loop,
|
||||||
|
* 但开始、结束和普通分组仍保持顶层语义。
|
||||||
|
*
|
||||||
|
* @param {DragEvent} e 组件库拖放事件
|
||||||
*/
|
*/
|
||||||
const drop = (e) => {
|
const drop = (e) => {
|
||||||
let node = e.dataTransfer.getData("flowNode");
|
let node = e.dataTransfer.getData("flowNode");
|
||||||
@ -572,7 +576,7 @@ const drop = (e) => {
|
|||||||
if (normalizedResources.node) {
|
if (normalizedResources.node) {
|
||||||
lf.setProperties(normalizedResources.node.id, normalizedResources.node.properties);
|
lf.setProperties(normalizedResources.node.id, normalizedResources.node.properties);
|
||||||
}
|
}
|
||||||
if (loopNode && !["start", "end", "loop", "selectArea"].includes(newNode.type)) {
|
if (loopNode && !["start", "end", "selectArea"].includes(newNode.type)) {
|
||||||
lf.addToGroup(loopNode.id, newNode.id, point.canvasOverlayPosition);
|
lf.addToGroup(loopNode.id, newNode.id, point.canvasOverlayPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -612,12 +616,39 @@ const handleNodeDoubleClick = ({ node }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查找 findLoopAtPoint 对应的数据或交互;作用范围仅限当前组件或模块。
|
* 查找指定位置最内层且可作为目标的循环。
|
||||||
* @param {*} point 画布坐标
|
*
|
||||||
|
* 实现思路:嵌套循环会同时命中多个容器,优先选择层级更深、面积更小的循环,确保节点
|
||||||
|
* 落入用户视觉上指向的循环体;excludedNodeIds 用于排除被拖动循环及其后代,防止成环。
|
||||||
|
*
|
||||||
|
* @param {{x: number, y: number}} point 画布绝对坐标
|
||||||
|
* @param {Set<string>} excludedNodeIds 不允许作为目标的循环 ID
|
||||||
|
* @returns {*} 命中的 Vue Flow 循环节点
|
||||||
*/
|
*/
|
||||||
const findLoopAtPoint = (point) => {
|
const findLoopAtPoint = (point, excludedNodeIds = new Set()) => {
|
||||||
return (vueFlow.getNodes.value || []).find((candidate) => {
|
const nodes = vueFlow.getNodes.value || [];
|
||||||
if (candidate.data?.flowType !== "loop") return false;
|
const nodeLookup = new Map(nodes.map((node) => [node.id, node]));
|
||||||
|
/**
|
||||||
|
* 计算候选循环的嵌套深度,供重叠命中时优先选择最内层容器。
|
||||||
|
*
|
||||||
|
* @param {*} node 候选 Vue Flow 节点
|
||||||
|
* @returns {number} 根节点为 0 的父子深度
|
||||||
|
*/
|
||||||
|
const depthOf = (node) => {
|
||||||
|
let depth = 0;
|
||||||
|
let parentId = node.parentNode;
|
||||||
|
const visited = new Set([node.id]);
|
||||||
|
while (parentId && !visited.has(parentId)) {
|
||||||
|
visited.add(parentId);
|
||||||
|
const parent = nodeLookup.get(parentId);
|
||||||
|
if (!parent) break;
|
||||||
|
depth += 1;
|
||||||
|
parentId = parent.parentNode;
|
||||||
|
}
|
||||||
|
return depth;
|
||||||
|
};
|
||||||
|
return nodes.filter((candidate) => {
|
||||||
|
if (candidate.data?.flowType !== "loop" || excludedNodeIds.has(candidate.id)) return false;
|
||||||
const groupPosition = candidate.computedPosition || candidate.position;
|
const groupPosition = candidate.computedPosition || candidate.position;
|
||||||
const groupWidth = Number(candidate.dimensions?.width || candidate.data?.size?.width || 0);
|
const groupWidth = Number(candidate.dimensions?.width || candidate.data?.size?.width || 0);
|
||||||
const groupHeight = Number(candidate.dimensions?.height || candidate.data?.size?.height || 0);
|
const groupHeight = Number(candidate.dimensions?.height || candidate.data?.size?.height || 0);
|
||||||
@ -625,18 +656,42 @@ const findLoopAtPoint = (point) => {
|
|||||||
&& point.x <= groupPosition.x + groupWidth - 24
|
&& point.x <= groupPosition.x + groupWidth - 24
|
||||||
&& point.y >= groupPosition.y + 112
|
&& point.y >= groupPosition.y + 112
|
||||||
&& point.y <= groupPosition.y + groupHeight - 24;
|
&& point.y <= groupPosition.y + groupHeight - 24;
|
||||||
});
|
}).sort((left, right) => {
|
||||||
|
const depthDifference = depthOf(right) - depthOf(left);
|
||||||
|
if (depthDifference) return depthDifference;
|
||||||
|
const leftArea = Number(left.dimensions?.width || left.data?.size?.width || 0)
|
||||||
|
* Number(left.dimensions?.height || left.data?.size?.height || 0);
|
||||||
|
const rightArea = Number(right.dimensions?.width || right.data?.size?.width || 0)
|
||||||
|
* Number(right.dimensions?.height || right.data?.size?.height || 0);
|
||||||
|
return leftArea - rightArea;
|
||||||
|
})[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查找 findContainingLoop 对应的数据或交互;作用范围仅限当前组件或模块。
|
* 查找节点中心所在的可用循环,同时排除自身和全部后代循环。
|
||||||
* @param {*} node 当前流程节点
|
*
|
||||||
|
* @param {*} node 当前 Vue Flow 节点
|
||||||
|
* @returns {*} 可接收该节点的最内层循环
|
||||||
*/
|
*/
|
||||||
const findContainingLoop = (node) => {
|
const findContainingLoop = (node) => {
|
||||||
|
const nodes = vueFlow.getNodes.value || [];
|
||||||
|
const excludedNodeIds = new Set([node.id]);
|
||||||
|
const pending = [node.id];
|
||||||
|
while (pending.length) {
|
||||||
|
const parentId = pending.pop();
|
||||||
|
nodes.forEach((candidate) => {
|
||||||
|
if (candidate.parentNode !== parentId || excludedNodeIds.has(candidate.id)) return;
|
||||||
|
excludedNodeIds.add(candidate.id);
|
||||||
|
pending.push(candidate.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
const position = node.computedPosition || node.position;
|
const position = node.computedPosition || node.position;
|
||||||
const width = Number(node.dimensions?.width || node.data?.size?.width || 0);
|
const width = Number(node.dimensions?.width || node.data?.size?.width || 0);
|
||||||
const height = Number(node.dimensions?.height || node.data?.size?.height || 0);
|
const height = Number(node.dimensions?.height || node.data?.size?.height || 0);
|
||||||
return findLoopAtPoint({ x: position.x + width / 2, y: position.y + height / 2 });
|
return findLoopAtPoint(
|
||||||
|
{ x: position.x + width / 2, y: position.y + height / 2 },
|
||||||
|
excludedNodeIds,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -750,8 +805,12 @@ const handleNodeDrag = (payload) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理 handleNodeDragStop 对应的数据或交互;作用范围仅限当前组件或模块。
|
* 完成画布节点拖动,并在未挂载节点进入循环体时建立嵌套关系。
|
||||||
* @param {*} payload 事件数据
|
*
|
||||||
|
* 实现思路:队列转换优先处理;其余顶层节点根据中心点寻找最内层循环。循环节点也参与
|
||||||
|
* 命中,但目标筛选会排除自身和后代,避免产生循环父子关系。
|
||||||
|
*
|
||||||
|
* @param {*} payload Vue Flow 节点拖动结束事件
|
||||||
*/
|
*/
|
||||||
const handleNodeDragStop = (payload) => {
|
const handleNodeDragStop = (payload) => {
|
||||||
const { node } = payload;
|
const { node } = payload;
|
||||||
@ -769,7 +828,7 @@ const handleNodeDragStop = (payload) => {
|
|||||||
if (
|
if (
|
||||||
vueNode
|
vueNode
|
||||||
&& !vueNode.parentNode
|
&& !vueNode.parentNode
|
||||||
&& !["start", "end", "loop", "selectArea"].includes(vueNode.data?.flowType)
|
&& !["start", "end", "selectArea"].includes(vueNode.data?.flowType)
|
||||||
) {
|
) {
|
||||||
const loopNode = findContainingLoop(vueNode);
|
const loopNode = findContainingLoop(vueNode);
|
||||||
if (loopNode) lf.addToGroup(loopNode.id, vueNode.id);
|
if (loopNode) lf.addToGroup(loopNode.id, vueNode.id);
|
||||||
|
|||||||
@ -53,6 +53,7 @@ import NodeState from "../../components/NodeState.vue";
|
|||||||
import LoopIterationResults from "../../components/LoopIterationResults.vue";
|
import LoopIterationResults from "../../components/LoopIterationResults.vue";
|
||||||
import loop from "../../icon/loop.svg";
|
import loop from "../../icon/loop.svg";
|
||||||
import { emitter } from "@/utils/eventBus";
|
import { emitter } from "@/utils/eventBus";
|
||||||
|
import { ensureNodeExecutionResources } from "@/utils/flowResources";
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import { useFlowEvents } from "../../composables/useFlowEvents";
|
import { useFlowEvents } from "../../composables/useFlowEvents";
|
||||||
import "vue3-json-viewer/dist/index.css";
|
import "vue3-json-viewer/dist/index.css";
|
||||||
@ -126,8 +127,12 @@ const handleDragleave = (e) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理 handleDrop 对应的数据或交互;作用范围仅限当前组件或模块。
|
* 将组件库节点直接创建到当前循环体。
|
||||||
* @param {*} e 触发操作的浏览器事件
|
*
|
||||||
|
* 实现思路:允许 loop 作为子循环,其余容器限制保持不变;节点创建后先按顶层拖放相同的规则
|
||||||
|
* 初始化执行角色与设备用途,再由 Vue Flow 适配层统一处理父子关系、坐标换算和环检测。
|
||||||
|
*
|
||||||
|
* @param {DragEvent} e 组件库拖放事件
|
||||||
*/
|
*/
|
||||||
const handleDrop = (e) => {
|
const handleDrop = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -137,7 +142,7 @@ const handleDrop = (e) => {
|
|||||||
let flowNode = e.dataTransfer.getData("flowNode");
|
let flowNode = e.dataTransfer.getData("flowNode");
|
||||||
if (!flowNode) return;
|
if (!flowNode) return;
|
||||||
flowNode = JSON.parse(flowNode);
|
flowNode = JSON.parse(flowNode);
|
||||||
if (["start", "end", "loop", "selectArea"].includes(flowNode.type)) return;
|
if (["start", "end", "selectArea"].includes(flowNode.type)) return;
|
||||||
const point = lf.getPointByClient(e.clientX, e.clientY);
|
const point = lf.getPointByClient(e.clientX, e.clientY);
|
||||||
const { type, ...other } = flowNode;
|
const { type, ...other } = flowNode;
|
||||||
const node = lf.addNode({
|
const node = lf.addNode({
|
||||||
@ -149,13 +154,25 @@ const handleDrop = (e) => {
|
|||||||
...other,
|
...other,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (
|
|
||||||
!["selectArea", "branch", "stopLoop", "subStart", "subEnd"].includes(node.type)
|
// 循环体直接接管了 drop 事件,不会经过页面级 drop。
|
||||||
) {
|
// 因此必须在这里同步初始化资源,否则设备节点缺少 executionTarget,保存时会被校验拦截。
|
||||||
emitter.emit("openParamsDrawer", node);
|
const normalizedResources = ensureNodeExecutionResources(lf.getGraphData(), node.id);
|
||||||
|
if (normalizedResources.start) {
|
||||||
|
lf.setProperties(normalizedResources.start.id, normalizedResources.start.properties);
|
||||||
|
}
|
||||||
|
if (normalizedResources.node) {
|
||||||
|
lf.setProperties(normalizedResources.node.id, normalizedResources.node.properties);
|
||||||
}
|
}
|
||||||
|
|
||||||
lf.addToGroup(props.model.id, node.id, point.canvasOverlayPosition);
|
lf.addToGroup(props.model.id, node.id, point.canvasOverlayPosition);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!["selectArea", "branch", "stopLoop", "subStart", "subEnd"].includes(node.type)
|
||||||
|
) {
|
||||||
|
const configuredNode = lf.getGraphData().nodes.find((item) => item.id === node.id) || node;
|
||||||
|
emitter.emit("openParamsDrawer", configuredNode);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -73,6 +73,78 @@ const edgeId = () => `edge_${uuid().replace(/-/g, "")}`;
|
|||||||
*/
|
*/
|
||||||
const nodeId = () => uuid().replace(/-/g, "");
|
const nodeId = () => uuid().replace(/-/g, "");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断一个节点是否可以挂到指定容器下。
|
||||||
|
*
|
||||||
|
* 实现思路:普通节点仍可进入 loop 或 selectArea;容器节点中只开放 loop 嵌套 loop,
|
||||||
|
* 避免顺带改变 selectArea 的编组语义。
|
||||||
|
*
|
||||||
|
* @param {string} parentType 父容器的流程节点类型
|
||||||
|
* @param {string} childType 待挂载节点的流程节点类型
|
||||||
|
* @returns {boolean} 是否允许建立该父子关系
|
||||||
|
*/
|
||||||
|
const canAttachToGroup = (parentType, childType) => {
|
||||||
|
if (!GROUP_TYPES.has(parentType)) return false;
|
||||||
|
if (!GROUP_TYPES.has(childType)) return true;
|
||||||
|
return parentType === "loop" && childType === "loop";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收集流程节点的全部后代,用于移动和删除嵌套循环时保持整棵子树一致。
|
||||||
|
*
|
||||||
|
* 实现思路:同时读取 children 与 properties.parentId,兼容新旧流程数据可能只保存
|
||||||
|
* 其中一种关系的情况;visited 可阻止异常历史数据形成死循环。
|
||||||
|
*
|
||||||
|
* @param {{ nodes?: Array }} graph 后端格式的流程图
|
||||||
|
* @param {string} rootId 根节点 ID
|
||||||
|
* @returns {Set<string>} 不包含根节点自身的全部后代 ID
|
||||||
|
*/
|
||||||
|
const collectGraphDescendantIds = (graph, rootId) => {
|
||||||
|
const childrenByParent = new Map();
|
||||||
|
(graph.nodes || []).forEach((node) => {
|
||||||
|
const childIds = new Set(node.children || []);
|
||||||
|
if (childIds.size) childrenByParent.set(node.id, childIds);
|
||||||
|
});
|
||||||
|
(graph.nodes || []).forEach((node) => {
|
||||||
|
const parentId = node.properties?.parentId;
|
||||||
|
if (!parentId) return;
|
||||||
|
const childIds = childrenByParent.get(parentId) || new Set();
|
||||||
|
childIds.add(node.id);
|
||||||
|
childrenByParent.set(parentId, childIds);
|
||||||
|
});
|
||||||
|
|
||||||
|
const descendants = new Set();
|
||||||
|
const pending = [...(childrenByParent.get(rootId) || [])];
|
||||||
|
while (pending.length) {
|
||||||
|
const childId = pending.pop();
|
||||||
|
if (!childId || childId === rootId || descendants.has(childId)) continue;
|
||||||
|
descendants.add(childId);
|
||||||
|
pending.push(...(childrenByParent.get(childId) || []));
|
||||||
|
}
|
||||||
|
return descendants;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算 Vue Flow 节点的父子深度,保证父节点始终排在后代之前挂载。
|
||||||
|
*
|
||||||
|
* @param {*} node Vue Flow 节点
|
||||||
|
* @param {Map<string, *>} nodeLookup 节点索引
|
||||||
|
* @returns {number} 根节点为 0 的层级深度
|
||||||
|
*/
|
||||||
|
const nodeHierarchyDepth = (node, nodeLookup) => {
|
||||||
|
let depth = 0;
|
||||||
|
let current = node;
|
||||||
|
const visited = new Set([node.id]);
|
||||||
|
while (current?.parentNode) {
|
||||||
|
if (visited.has(current.parentNode)) break;
|
||||||
|
visited.add(current.parentNode);
|
||||||
|
current = nodeLookup.get(current.parentNode);
|
||||||
|
if (!current) break;
|
||||||
|
depth += 1;
|
||||||
|
}
|
||||||
|
return depth;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理 toVueNode 对应的数据或交互;作用范围仅限画布适配层。
|
* 处理 toVueNode 对应的数据或交互;作用范围仅限画布适配层。
|
||||||
* @param {*} node 当前流程节点
|
* @param {*} node 当前流程节点
|
||||||
@ -160,14 +232,19 @@ const toVueEdge = (edge) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关联 attachChildrenToGroups 对应的数据或交互;作用范围仅限画布适配层。
|
* 将后端绝对坐标节点转换为 Vue Flow 的多层父子节点结构。
|
||||||
* @param {*} nodes 调用方传入的 nodes 参数
|
*
|
||||||
|
* 实现思路:先冻结所有节点的绝对坐标,再建立父子关系并换算相对坐标,避免父循环
|
||||||
|
* 已转成相对坐标后影响更深层节点。循环尺寸按“最深层优先”向外扩展,确保外层循环
|
||||||
|
* 能包含已经完成尺寸计算的内层循环。
|
||||||
|
*
|
||||||
|
* @param {Array} nodes Vue Flow 节点列表,初始 position 均为画布绝对坐标
|
||||||
|
* @returns {Array} 父节点排在子节点之前的节点列表
|
||||||
*/
|
*/
|
||||||
const attachChildrenToGroups = (nodes) => {
|
const attachChildrenToGroups = (nodes) => {
|
||||||
// 先建立节点索引和显式父子关系,再用旧数据中的 children 补齐缺失关系。
|
|
||||||
// 最后统一换算相对坐标,避免父节点尚未就绪时提前计算造成位置漂移。
|
|
||||||
const nodeLookup = new Map(nodes.map((node) => [node.id, node]));
|
const nodeLookup = new Map(nodes.map((node) => [node.id, node]));
|
||||||
const declaredParents = new Map();
|
const declaredParents = new Map();
|
||||||
|
const absolutePositions = new Map(nodes.map((node) => [node.id, { ...node.position }]));
|
||||||
|
|
||||||
nodes.filter((node) => GROUP_TYPES.has(node.data.flowType)).forEach((group) => {
|
nodes.filter((node) => GROUP_TYPES.has(node.data.flowType)).forEach((group) => {
|
||||||
(group.data.children || []).forEach((childId) => {
|
(group.data.children || []).forEach((childId) => {
|
||||||
@ -178,27 +255,44 @@ const attachChildrenToGroups = (nodes) => {
|
|||||||
nodes.forEach((child) => {
|
nodes.forEach((child) => {
|
||||||
const parentId = child.data.properties?.parentId || declaredParents.get(child.id);
|
const parentId = child.data.properties?.parentId || declaredParents.get(child.id);
|
||||||
const parent = nodeLookup.get(parentId);
|
const parent = nodeLookup.get(parentId);
|
||||||
if (!parent || !GROUP_TYPES.has(parent.data.flowType) || GROUP_TYPES.has(child.data.flowType)) return;
|
if (!parent || !canAttachToGroup(parent.data.flowType, child.data.flowType)) return;
|
||||||
|
|
||||||
|
// 历史数据可能包含错误的循环父子引用;沿父链检查可避免 Vue Flow 收到环形层级。
|
||||||
|
const visited = new Set([child.id]);
|
||||||
|
let ancestor = parent;
|
||||||
|
while (ancestor) {
|
||||||
|
if (visited.has(ancestor.id)) return;
|
||||||
|
visited.add(ancestor.id);
|
||||||
|
const ancestorParentId = ancestor.data.properties?.parentId || declaredParents.get(ancestor.id);
|
||||||
|
ancestor = nodeLookup.get(ancestorParentId);
|
||||||
|
}
|
||||||
|
|
||||||
const isLoop = parent.data.flowType === "loop";
|
const isLoop = parent.data.flowType === "loop";
|
||||||
|
const childPosition = absolutePositions.get(child.id) || child.position;
|
||||||
|
const parentPosition = absolutePositions.get(parentId) || parent.position;
|
||||||
const relativePosition = {
|
const relativePosition = {
|
||||||
x: isLoop
|
x: isLoop
|
||||||
? Math.max(child.position.x - parent.position.x, LOOP_BODY_INSET.left)
|
? Math.max(childPosition.x - parentPosition.x, LOOP_BODY_INSET.left)
|
||||||
: child.position.x - parent.position.x,
|
: childPosition.x - parentPosition.x,
|
||||||
y: isLoop
|
y: isLoop
|
||||||
? Math.max(child.position.y - parent.position.y, LOOP_BODY_INSET.top)
|
? Math.max(childPosition.y - parentPosition.y, LOOP_BODY_INSET.top)
|
||||||
: child.position.y - parent.position.y,
|
: childPosition.y - parentPosition.y,
|
||||||
};
|
};
|
||||||
child.parentNode = parentId;
|
child.parentNode = parentId;
|
||||||
child.extent = isLoop ? loopChildExtent() : groupChildExtent();
|
child.extent = isLoop ? loopChildExtent() : groupChildExtent();
|
||||||
child.expandParent = false;
|
child.expandParent = false;
|
||||||
child.position = relativePosition;
|
child.position = relativePosition;
|
||||||
child.zIndex = 5;
|
// 边统一位于 zIndex 4。嵌套循环若仍使用普通子节点的 zIndex 5,其白色容器背景会
|
||||||
|
// 遮住循环体内新建的边,看起来像“无法连线”;容器置于边下方,普通节点仍置于边上方。
|
||||||
|
child.zIndex = GROUP_TYPES.has(child.data.flowType) ? 1 : 5;
|
||||||
child.data.properties = { ...child.data.properties, parentId };
|
child.data.properties = { ...child.data.properties, parentId };
|
||||||
parent.data.children = Array.from(new Set([...(parent.data.children || []), child.id]));
|
parent.data.children = Array.from(new Set([...(parent.data.children || []), child.id]));
|
||||||
});
|
});
|
||||||
|
|
||||||
nodes.filter((node) => node.data.flowType === "loop").forEach((group) => {
|
nodes
|
||||||
|
.filter((node) => node.data.flowType === "loop")
|
||||||
|
.sort((left, right) => nodeHierarchyDepth(right, nodeLookup) - nodeHierarchyDepth(left, nodeLookup))
|
||||||
|
.forEach((group) => {
|
||||||
const children = nodes.filter((node) => node.parentNode === group.id);
|
const children = nodes.filter((node) => node.parentNode === group.id);
|
||||||
if (!children.length) return;
|
if (!children.length) return;
|
||||||
const requiredWidth = Math.max(...children.map((child) => (
|
const requiredWidth = Math.max(...children.map((child) => (
|
||||||
@ -215,7 +309,9 @@ const attachChildrenToGroups = (nodes) => {
|
|||||||
group.data.properties = { ...group.data.properties, width, height };
|
group.data.properties = { ...group.data.properties, width, height };
|
||||||
});
|
});
|
||||||
|
|
||||||
return nodes.sort((left, right) => Number(Boolean(left.parentNode)) - Number(Boolean(right.parentNode)));
|
return nodes.sort((left, right) => (
|
||||||
|
nodeHierarchyDepth(left, nodeLookup) - nodeHierarchyDepth(right, nodeLookup)
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toVueFlowData = (graph = {}, options = {}) => {
|
export const toVueFlowData = (graph = {}, options = {}) => {
|
||||||
@ -251,8 +347,39 @@ export const createFlowFacade = (store) => {
|
|||||||
const getEdges = () => store.getEdges.value || [];
|
const getEdges = () => store.getEdges.value || [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理 backendNode 对应的数据或交互;作用范围仅限画布适配层。
|
* 获取节点在整个画布中的绝对左上角坐标。
|
||||||
|
*
|
||||||
|
* 实现思路:Vue Flow 的子节点 position 相对直接父节点,嵌套循环需要沿完整父链累加;
|
||||||
|
* visited 用于防御异常数据中的父子环,避免保存或布局时无限递归。
|
||||||
|
*
|
||||||
|
* @param {*} node Vue Flow 节点
|
||||||
|
* @param {Set<string>} visited 已访问的节点 ID
|
||||||
|
* @returns {{x: number, y: number}} 画布绝对坐标
|
||||||
|
*/
|
||||||
|
const getAbsoluteNodePosition = (node, visited = new Set()) => {
|
||||||
|
const position = node?.position || node?.computedPosition || { x: 0, y: 0 };
|
||||||
|
if (!node?.parentNode || visited.has(node.id)) {
|
||||||
|
return { x: Number(position.x || 0), y: Number(position.y || 0) };
|
||||||
|
}
|
||||||
|
const nextVisited = new Set(visited);
|
||||||
|
nextVisited.add(node.id);
|
||||||
|
const parent = store.findNode(node.parentNode);
|
||||||
|
if (!parent) return { x: Number(position.x || 0), y: Number(position.y || 0) };
|
||||||
|
const parentPosition = getAbsoluteNodePosition(parent, nextVisited);
|
||||||
|
return {
|
||||||
|
x: parentPosition.x + Number(position.x || 0),
|
||||||
|
y: parentPosition.y + Number(position.y || 0),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Vue Flow 节点序列化为后端使用的绝对坐标节点。
|
||||||
|
*
|
||||||
|
* 实现思路:先沿完整父链取得绝对位置,再恢复旧流程协议的中心点坐标;这样普通节点、
|
||||||
|
* 内层循环及更深层后代在保存和重新加载后都不会发生位置漂移。
|
||||||
|
*
|
||||||
* @param {*} node 当前流程节点
|
* @param {*} node 当前流程节点
|
||||||
|
* @returns {*} 后端格式节点
|
||||||
*/
|
*/
|
||||||
const backendNode = (node) => {
|
const backendNode = (node) => {
|
||||||
const result = clone(node.data.backend);
|
const result = clone(node.data.backend);
|
||||||
@ -261,10 +388,7 @@ export const createFlowFacade = (store) => {
|
|||||||
height: Number(node.height || node.dimensions?.height || node.data.size?.height || 0),
|
height: Number(node.height || node.dimensions?.height || node.data.size?.height || 0),
|
||||||
};
|
};
|
||||||
const parent = node.parentNode ? store.findNode(node.parentNode) : null;
|
const parent = node.parentNode ? store.findNode(node.parentNode) : null;
|
||||||
const parentPosition = parent?.position || parent?.computedPosition || { x: 0, y: 0 };
|
const absolutePosition = getAbsoluteNodePosition(node);
|
||||||
const absolutePosition = parent
|
|
||||||
? { x: parentPosition.x + node.position.x, y: parentPosition.y + node.position.y }
|
|
||||||
: (node.position || node.computedPosition);
|
|
||||||
result.id = node.id;
|
result.id = node.id;
|
||||||
result.type = node.data.flowType;
|
result.type = node.data.flowType;
|
||||||
if (GROUP_TYPES.has(result.type)) {
|
if (GROUP_TYPES.has(result.type)) {
|
||||||
@ -340,15 +464,13 @@ export const createFlowFacade = (store) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 getNodeRect 对应的数据或交互;作用范围仅限画布适配层。
|
* 获取节点在画布绝对坐标系中的矩形。
|
||||||
|
*
|
||||||
* @param {*} node 当前流程节点
|
* @param {*} node 当前流程节点
|
||||||
|
* @returns {{x: number, y: number, width: number, height: number}} 绝对矩形
|
||||||
*/
|
*/
|
||||||
const getNodeRect = (node) => {
|
const getNodeRect = (node) => {
|
||||||
const parent = node.parentNode ? store.findNode(node.parentNode) : null;
|
const position = getAbsoluteNodePosition(node);
|
||||||
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 {
|
return {
|
||||||
x: Number(position?.x || 0),
|
x: Number(position?.x || 0),
|
||||||
y: Number(position?.y || 0),
|
y: Number(position?.y || 0),
|
||||||
@ -393,10 +515,15 @@ export const createFlowFacade = (store) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 布局 layoutLoop 对应的数据或交互;作用范围仅限画布适配层。
|
* 根据直接子节点的绝对矩形调整循环容器。
|
||||||
* @param {*} group 调用方传入的 group 参数
|
*
|
||||||
* @param {*} activeChildId 调用方传入的 activeChildId 参数
|
* 实现思路:布局计算统一使用画布绝对坐标;写回嵌套循环时,再减去父循环的绝对坐标
|
||||||
* @param {*} activePosition 调用方传入的 activePosition 参数
|
* 得到 Vue Flow 所需的相对 position,从而让同一算法同时适用于顶层和任意深度循环。
|
||||||
|
*
|
||||||
|
* @param {*} group 待调整的循环节点
|
||||||
|
* @param {string} activeChildId 正在拖动的直接子节点 ID
|
||||||
|
* @param {{x: number, y: number}} activePosition 正在拖动节点的绝对坐标
|
||||||
|
* @returns {boolean} 是否完成布局
|
||||||
*/
|
*/
|
||||||
const layoutLoop = (group, activeChildId, activePosition) => {
|
const layoutLoop = (group, activeChildId, activePosition) => {
|
||||||
const state = getLoopLayoutState(group);
|
const state = getLoopLayoutState(group);
|
||||||
@ -427,9 +554,15 @@ export const createFlowFacade = (store) => {
|
|||||||
const width = right - left;
|
const width = right - left;
|
||||||
const height = bottom - top;
|
const height = bottom - top;
|
||||||
const properties = { ...group.data.properties, width, height };
|
const properties = { ...group.data.properties, width, height };
|
||||||
|
const parent = group.parentNode ? store.findNode(group.parentNode) : null;
|
||||||
|
const parentPosition = parent ? getAbsoluteNodePosition(parent) : { x: 0, y: 0 };
|
||||||
|
const relativePosition = {
|
||||||
|
x: left - parentPosition.x,
|
||||||
|
y: top - parentPosition.y,
|
||||||
|
};
|
||||||
|
|
||||||
store.updateNode(group.id, {
|
store.updateNode(group.id, {
|
||||||
position: { x: left, y: top },
|
position: relativePosition,
|
||||||
computedPosition: { ...group.computedPosition, x: left, y: top },
|
computedPosition: { ...group.computedPosition, x: left, y: top },
|
||||||
dimensions: { width, height },
|
dimensions: { width, height },
|
||||||
width,
|
width,
|
||||||
@ -453,6 +586,26 @@ export const createFlowFacade = (store) => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从指定节点开始向上重新计算所有父循环尺寸。
|
||||||
|
*
|
||||||
|
* 实现思路:内层循环尺寸变化后,外层循环必须由内向外依次更新,否则外层可能裁剪
|
||||||
|
* 新增内容。visited 防止异常父子关系导致无限循环。
|
||||||
|
*
|
||||||
|
* @param {*} node 尺寸或位置已经变化的节点
|
||||||
|
*/
|
||||||
|
const layoutParentLoops = (node) => {
|
||||||
|
let parentId = node?.parentNode;
|
||||||
|
const visited = new Set();
|
||||||
|
while (parentId && !visited.has(parentId)) {
|
||||||
|
visited.add(parentId);
|
||||||
|
const parent = store.findNode(parentId);
|
||||||
|
if (!parent) break;
|
||||||
|
if (parent.data.flowType === "loop") layoutLoop(parent);
|
||||||
|
parentId = parent.parentNode;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 getAnchors 对应的数据或交互;作用范围仅限画布适配层。
|
* 获取 getAnchors 对应的数据或交互;作用范围仅限画布适配层。
|
||||||
* @param {*} node 当前流程节点
|
* @param {*} node 当前流程节点
|
||||||
@ -626,8 +779,12 @@ export const createFlowFacade = (store) => {
|
|||||||
return backendNode(vueNode);
|
return backendNode(vueNode);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 删除 deleteNode 对应的数据或交互;作用范围仅限画布适配层。
|
* 删除节点,并在删除循环时递归清理其完整循环体。
|
||||||
* @param {*} id 目标元素 ID
|
*
|
||||||
|
* 实现思路:嵌套循环可能包含多层后代,不能只删除 children 中的第一层;先从当前
|
||||||
|
* 图数据统一收集后代,再一次性删除节点与关联边,避免留下带失效 parentId 的孤儿节点。
|
||||||
|
*
|
||||||
|
* @param {string} id 目标元素 ID
|
||||||
*/
|
*/
|
||||||
deleteNode(id) {
|
deleteNode(id) {
|
||||||
const target = store.findNode(id);
|
const target = store.findNode(id);
|
||||||
@ -636,8 +793,11 @@ export const createFlowFacade = (store) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const parentId = target?.parentNode;
|
const parentId = target?.parentNode;
|
||||||
const childIds = target?.data?.children || [];
|
const descendantIds = target?.data?.flowType === "loop"
|
||||||
const removeChildren = target?.data?.flowType === "loop" && childIds.length > 0;
|
? collectGraphDescendantIds(facade.getGraphData(), id)
|
||||||
|
: new Set();
|
||||||
|
const childIds = [...descendantIds];
|
||||||
|
const removeChildren = childIds.length > 0;
|
||||||
const nodeIds = removeChildren ? [id, ...childIds] : id;
|
const nodeIds = removeChildren ? [id, ...childIds] : id;
|
||||||
const removedIds = new Set(Array.isArray(nodeIds) ? nodeIds : [nodeIds]);
|
const removedIds = new Set(Array.isArray(nodeIds) ? nodeIds : [nodeIds]);
|
||||||
store.removeNodes(nodeIds, true, removeChildren);
|
store.removeNodes(nodeIds, true, removeChildren);
|
||||||
@ -649,8 +809,10 @@ export const createFlowFacade = (store) => {
|
|||||||
}, { replace: true });
|
}, { replace: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
componentInstances.delete(id);
|
removedIds.forEach((nodeIdToDelete) => {
|
||||||
childIds.forEach((childId) => componentInstances.delete(childId));
|
componentInstances.delete(nodeIdToDelete);
|
||||||
|
loopLayoutStates.delete(nodeIdToDelete);
|
||||||
|
});
|
||||||
changed();
|
changed();
|
||||||
if (parentId) nextTick(() => facade.resizeGroupToChildren(parentId));
|
if (parentId) nextTick(() => facade.resizeGroupToChildren(parentId));
|
||||||
},
|
},
|
||||||
@ -782,20 +944,29 @@ export const createFlowFacade = (store) => {
|
|||||||
return { canvasOverlayPosition: point, domOverlayPosition: point };
|
return { canvasOverlayPosition: point, domOverlayPosition: point };
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 新增 addToGroup 对应的数据或交互;作用范围仅限画布适配层。
|
* 将节点或循环子树挂载到目标容器。
|
||||||
* @param {*} groupId 分组节点 ID
|
*
|
||||||
* @param {*} childId 子节点 ID
|
* 实现思路:普通节点保持原有编组行为;loop 只允许进入另一个 loop,并在修改前
|
||||||
* @param {*} dropCenter 拖放中心坐标
|
* 检查目标是否属于自身后代,防止产生环形父子关系。移动循环时整棵子树按相同位移
|
||||||
|
* 平移,同时只断开循环根节点跨作用域的连线,保留循环体内部连线。
|
||||||
|
*
|
||||||
|
* @param {string} groupId 目标容器节点 ID
|
||||||
|
* @param {string} childId 待挂载节点 ID
|
||||||
|
* @param {{x: number, y: number}} dropCenter 拖放中心绝对坐标
|
||||||
|
* @returns {boolean} 是否成功接受本次挂载
|
||||||
*/
|
*/
|
||||||
addToGroup(groupId, childId, dropCenter) {
|
addToGroup(groupId, childId, dropCenter) {
|
||||||
const group = store.findNode(groupId);
|
const group = store.findNode(groupId);
|
||||||
const child = store.findNode(childId);
|
const child = store.findNode(childId);
|
||||||
if (!group || !child || GROUP_TYPES.has(child.data.flowType)) return;
|
if (!group || !child || groupId === childId) return false;
|
||||||
const previousParentId = child.parentNode || child.data?.properties?.parentId;
|
if (!canAttachToGroup(group.data.flowType, child.data.flowType)) return false;
|
||||||
const graph = facade.getGraphData();
|
const graph = facade.getGraphData();
|
||||||
const groupData = graph.nodes.find((node) => node.id === groupId);
|
const groupData = graph.nodes.find((node) => node.id === groupId);
|
||||||
const childData = graph.nodes.find((node) => node.id === childId);
|
const childData = graph.nodes.find((node) => node.id === childId);
|
||||||
if (!groupData || !childData) return;
|
if (!groupData || !childData) return false;
|
||||||
|
const descendantIds = collectGraphDescendantIds(graph, childId);
|
||||||
|
if (descendantIds.has(groupId)) return false;
|
||||||
|
const previousParentId = child.parentNode || child.data?.properties?.parentId;
|
||||||
|
|
||||||
// 节点从循环体外进入循环体后,原连线的两端已处于不同执行作用域,必须全部断开。
|
// 节点从循环体外进入循环体后,原连线的两端已处于不同执行作用域,必须全部断开。
|
||||||
// 在重新渲染父子结构前直接过滤图数据,可保证入边、出边和层级变更在同一次更新中完成。
|
// 在重新渲染父子结构前直接过滤图数据,可保证入边、出边和层级变更在同一次更新中完成。
|
||||||
@ -806,11 +977,22 @@ export const createFlowFacade = (store) => {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const previousGroup = graph.nodes.find((node) => node.id === previousParentId);
|
||||||
|
if (previousGroup && previousGroup.id !== groupId) {
|
||||||
|
previousGroup.children = (previousGroup.children || []).filter((id) => id !== childId);
|
||||||
|
}
|
||||||
groupData.children = Array.from(new Set([...(groupData.children || []), childId]));
|
groupData.children = Array.from(new Set([...(groupData.children || []), childId]));
|
||||||
childData.properties = { ...(childData.properties || {}), parentId: groupId };
|
childData.properties = { ...(childData.properties || {}), parentId: groupId };
|
||||||
if (dropCenter) {
|
if (dropCenter) {
|
||||||
|
const dx = Number(dropCenter.x) - Number(childData.x || 0);
|
||||||
|
const dy = Number(dropCenter.y) - Number(childData.y || 0);
|
||||||
childData.x = dropCenter.x;
|
childData.x = dropCenter.x;
|
||||||
childData.y = dropCenter.y;
|
childData.y = dropCenter.y;
|
||||||
|
graph.nodes.forEach((node) => {
|
||||||
|
if (!descendantIds.has(node.id)) return;
|
||||||
|
node.x = Number(node.x || 0) + dx;
|
||||||
|
node.y = Number(node.y || 0) + dy;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
facade.render(graph, {
|
facade.render(graph, {
|
||||||
resetHistory: false,
|
resetHistory: false,
|
||||||
@ -822,6 +1004,7 @@ export const createFlowFacade = (store) => {
|
|||||||
store.updateNodeInternals([groupId, childId]);
|
store.updateNodeInternals([groupId, childId]);
|
||||||
changed();
|
changed();
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 编组 groupNodes 对应的数据或交互;作用范围仅限画布适配层。
|
* 编组 groupNodes 对应的数据或交互;作用范围仅限画布适配层。
|
||||||
@ -909,27 +1092,32 @@ export const createFlowFacade = (store) => {
|
|||||||
return { groupId: group.id, position: { x: rect.x, y: rect.y } };
|
return { groupId: group.id, position: { x: rect.x, y: rect.y } };
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 布局 layoutLoopDuringDrag 对应的数据或交互;作用范围仅限画布适配层。
|
* 在拖拽循环体节点时同步调整当前循环及所有外层循环。
|
||||||
* @param {*} groupId 分组节点 ID
|
*
|
||||||
* @param {*} childId 子节点 ID
|
* @param {string} groupId 当前直接父循环 ID
|
||||||
* @param {*} position 调用方传入的 position 参数
|
* @param {string} childId 正在拖动的节点 ID
|
||||||
|
* @param {{x: number, y: number}} position 节点绝对坐标
|
||||||
*/
|
*/
|
||||||
layoutLoopDuringDrag(groupId, childId, position) {
|
layoutLoopDuringDrag(groupId, childId, position) {
|
||||||
const group = store.findNode(groupId);
|
const group = store.findNode(groupId);
|
||||||
if (!group || group.data.flowType !== "loop" || !position) return;
|
if (!group || group.data.flowType !== "loop" || !position) return;
|
||||||
layoutLoop(group, childId, position);
|
if (layoutLoop(group, childId, position)) layoutParentLoops(group);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 调整 resizeGroupToChildren 对应的数据或交互;作用范围仅限画布适配层。
|
* 按循环体内容调整当前循环及其外层循环尺寸。
|
||||||
* @param {*} groupId 分组节点 ID
|
*
|
||||||
* @param {*} options 行为配置
|
* @param {string} groupId 待调整的循环 ID
|
||||||
|
* @param {{markChanged?: boolean}} options 是否记录一次流程变更
|
||||||
*/
|
*/
|
||||||
resizeGroupToChildren(groupId, options = {}) {
|
resizeGroupToChildren(groupId, options = {}) {
|
||||||
// 循环容器沿用统一的内边距和最小尺寸约束,并按配置决定是否记录变更。
|
// 循环容器沿用统一的内边距和最小尺寸约束,并按配置决定是否记录变更。
|
||||||
// 拖拽过程可关闭变更标记,避免高频移动产生多余历史记录。
|
// 拖拽过程可关闭变更标记,避免高频移动产生多余历史记录。
|
||||||
const group = store.findNode(groupId);
|
const group = store.findNode(groupId);
|
||||||
if (!group || group.data.flowType !== "loop") return;
|
if (!group || group.data.flowType !== "loop") return;
|
||||||
if (layoutLoop(group) && options.markChanged !== false) changed();
|
if (layoutLoop(group)) {
|
||||||
|
layoutParentLoops(group);
|
||||||
|
if (options.markChanged !== false) changed();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 移动 moveNodeBy 对应的数据或交互;作用范围仅限画布适配层。
|
* 移动 moveNodeBy 对应的数据或交互;作用范围仅限画布适配层。
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user