feat(flow): 添加节点选择和复制粘贴功能
- 实现了 selectElementsByIds 方法用于批量选择元素 - 更新了选择和拖拽行为配置以支持禁用表单时的操作 - 引入 uuid 库用于生成唯一标识符 - 重构复制粘贴逻辑以支持节点和边的完整复制 - 实现了节点引用映射和锚点ID重映射功能 - 添加了子节点递归选择和引用更新机制 - 优化了粘贴后节点的渲染和选择逻辑 - 更新了拖拽相关的CSS样式以改善用户体验
This commit is contained in:
parent
424df50112
commit
916e6f5756
@ -110,8 +110,8 @@
|
||||
:elevate-nodes-on-select="false"
|
||||
:delete-key-code="null"
|
||||
:multi-selection-key-code="['Control', 'Meta']"
|
||||
:selection-key-code="groupSelectionPending"
|
||||
:pan-on-drag="!groupSelectionPending"
|
||||
:selection-key-code="flowStore.disableForm ? null : true"
|
||||
:pan-on-drag="flowStore.disableForm ? true : [1, 2]"
|
||||
:zoom-on-double-click="false"
|
||||
:default-edge-options="defaultEdgeOptions"
|
||||
@connect="handleConnect"
|
||||
@ -251,6 +251,7 @@
|
||||
<script setup>
|
||||
import { computed, reactive, ref, onMounted, onUnmounted, nextTick } from "vue";
|
||||
import { MarkerType, VueFlow, useVueFlow } from "@vue-flow/core";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { Background } from "@vue-flow/background";
|
||||
import { Controls } from "@vue-flow/controls";
|
||||
import "@vue-flow/core/dist/style.css";
|
||||
@ -1076,7 +1077,7 @@ const clearRun = () => {
|
||||
|
||||
const NODE_PASTE_OFFSET = 40;
|
||||
const NON_COPYABLE_NODE_TYPES = new Set(["start", "end", "selectArea"]);
|
||||
let copiedNodes = [];
|
||||
let copiedGraph = { nodes: [], edges: [] };
|
||||
let pasteCount = 0;
|
||||
|
||||
const EDITABLE_SELECTOR = [
|
||||
@ -1105,51 +1106,134 @@ const isTextEditing = (event) => {
|
||||
};
|
||||
|
||||
const copySelectedNodes = (event) => {
|
||||
const nodes = lf.getSelectElements().nodes.filter(
|
||||
(node) => !NON_COPYABLE_NODE_TYPES.has(node.type),
|
||||
const selectedNodes = lf.getSelectElements().nodes;
|
||||
if (!selectedNodes.length) return;
|
||||
|
||||
const graph = lf.getGraphData();
|
||||
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
||||
const selectedIds = new Set(selectedNodes.map((node) => node.id));
|
||||
const addChildren = (nodeId) => {
|
||||
const node = nodeById.get(nodeId);
|
||||
(node?.children || []).forEach((childId) => {
|
||||
if (selectedIds.has(childId)) return;
|
||||
selectedIds.add(childId);
|
||||
addChildren(childId);
|
||||
});
|
||||
};
|
||||
[...selectedIds].forEach(addChildren);
|
||||
|
||||
const copyableIds = new Set(
|
||||
[...selectedIds].filter((id) => {
|
||||
const node = nodeById.get(id);
|
||||
return node && !NON_COPYABLE_NODE_TYPES.has(node.type);
|
||||
}),
|
||||
);
|
||||
const nodes = graph.nodes.filter((node) => copyableIds.has(node.id));
|
||||
if (!nodes.length) return;
|
||||
const edges = graph.edges.filter((edge) => (
|
||||
copyableIds.has(edge.sourceNodeId) && copyableIds.has(edge.targetNodeId)
|
||||
));
|
||||
|
||||
event.preventDefault();
|
||||
copiedNodes = JSON.parse(JSON.stringify(nodes));
|
||||
copiedGraph = JSON.parse(JSON.stringify({ nodes, edges }));
|
||||
pasteCount = 0;
|
||||
};
|
||||
|
||||
const remapNodeAnchorId = (anchorId, oldNodeId, newNodeId) => {
|
||||
if (!anchorId || !String(anchorId).startsWith(`${oldNodeId}_`)) return anchorId;
|
||||
return `${newNodeId}${String(anchorId).slice(oldNodeId.length)}`;
|
||||
};
|
||||
|
||||
const remapCopiedNodeReferences = (value, idMap, parentKey = "") => {
|
||||
if (Array.isArray(value)) {
|
||||
const result = value.map((item) => remapCopiedNodeReferences(item, idMap));
|
||||
if (parentKey === "quote" && result.length && idMap.has(result[0])) {
|
||||
result[0] = idMap.get(result[0]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (!value || typeof value !== "object") return value;
|
||||
return Object.fromEntries(Object.entries(value).map(([key, item]) => (
|
||||
[key, remapCopiedNodeReferences(item, idMap, key)]
|
||||
)));
|
||||
};
|
||||
|
||||
const pasteCopiedNodes = (event) => {
|
||||
if (!copiedNodes.length || flowStore.disableForm) return;
|
||||
if (!copiedGraph.nodes.length || flowStore.disableForm) return;
|
||||
|
||||
event.preventDefault();
|
||||
pasteCount += 1;
|
||||
const offset = NODE_PASTE_OFFSET * pasteCount;
|
||||
const pastedNodes = copiedNodes.map((sourceNode) => {
|
||||
const properties = JSON.parse(JSON.stringify(sourceNode.properties || {}));
|
||||
const parentId = properties.parentId;
|
||||
delete properties.parentId;
|
||||
const graph = lf.getGraphData();
|
||||
const existingNodeIds = new Set(graph.nodes.map((node) => node.id));
|
||||
const idMap = new Map(copiedGraph.nodes.map((node) => (
|
||||
[node.id, uuid().replace(/-/g, "")]
|
||||
)));
|
||||
|
||||
const newNode = lf.addNode({
|
||||
type: sourceNode.type,
|
||||
const pastedNodes = copiedGraph.nodes.map((sourceNode) => {
|
||||
const newId = idMap.get(sourceNode.id);
|
||||
const properties = remapCopiedNodeReferences(
|
||||
JSON.parse(JSON.stringify(sourceNode.properties || {})),
|
||||
idMap,
|
||||
);
|
||||
const parentId = properties.parentId;
|
||||
if (idMap.has(parentId)) properties.parentId = idMap.get(parentId);
|
||||
else if (!existingNodeIds.has(parentId)) delete properties.parentId;
|
||||
|
||||
if (sourceNode.type === "branch" && Array.isArray(properties.anchor)) {
|
||||
properties.anchor = properties.anchor.map((anchor) => ({
|
||||
...anchor,
|
||||
id: remapNodeAnchorId(anchor.id, sourceNode.id, newId),
|
||||
}));
|
||||
}
|
||||
|
||||
const children = (sourceNode.children || [])
|
||||
.filter((childId) => idMap.has(childId))
|
||||
.map((childId) => idMap.get(childId));
|
||||
return {
|
||||
...JSON.parse(JSON.stringify(sourceNode)),
|
||||
id: newId,
|
||||
x: Number(sourceNode.x || 0) + offset,
|
||||
y: Number(sourceNode.y || 0) + offset,
|
||||
properties,
|
||||
children,
|
||||
};
|
||||
});
|
||||
|
||||
if (sourceNode.type === "branch" && Array.isArray(properties.anchor)) {
|
||||
const oldElseId = `${sourceNode.id}_else`;
|
||||
const newElseId = `${newNode.id}_else`;
|
||||
const anchors = properties.anchor.map((anchor) => (
|
||||
anchor.id === oldElseId ? { ...anchor, id: newElseId } : anchor
|
||||
));
|
||||
lf.setProperties(newNode.id, { ...properties, anchor: anchors });
|
||||
}
|
||||
|
||||
if (parentId && lf.getNodeModelById(parentId)) {
|
||||
lf.addToGroup(parentId, newNode.id, { x: newNode.x, y: newNode.y });
|
||||
}
|
||||
return newNode;
|
||||
const pastedEdges = copiedGraph.edges.map((sourceEdge) => {
|
||||
const sourceNodeId = idMap.get(sourceEdge.sourceNodeId);
|
||||
const targetNodeId = idMap.get(sourceEdge.targetNodeId);
|
||||
return {
|
||||
...JSON.parse(JSON.stringify(sourceEdge)),
|
||||
id: `edge_${uuid().replace(/-/g, "")}`,
|
||||
sourceNodeId,
|
||||
targetNodeId,
|
||||
sourceAnchorId: remapNodeAnchorId(
|
||||
sourceEdge.sourceAnchorId,
|
||||
sourceEdge.sourceNodeId,
|
||||
sourceNodeId,
|
||||
),
|
||||
targetAnchorId: remapNodeAnchorId(
|
||||
sourceEdge.targetAnchorId,
|
||||
sourceEdge.targetNodeId,
|
||||
targetNodeId,
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
lf.clearSelectElements();
|
||||
if (pastedNodes.length === 1) lf.selectElementById(pastedNodes[0].id);
|
||||
lf.render({
|
||||
nodes: [...graph.nodes, ...pastedNodes],
|
||||
edges: [...graph.edges, ...pastedEdges],
|
||||
}, {
|
||||
resetHistory: false,
|
||||
replaceNodes: true,
|
||||
preserveNodeSizes: true,
|
||||
preserveNodePositions: true,
|
||||
});
|
||||
nextTick(() => {
|
||||
lf.selectElementsByIds(pastedNodes.map((node) => node.id));
|
||||
lf.markChanged();
|
||||
});
|
||||
};
|
||||
|
||||
const handleKeyboardShortcut = (event) => {
|
||||
@ -1478,9 +1562,11 @@ onUnmounted(() => {
|
||||
:deep(.vue-flow__node) {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
:deep(.vue-flow__node.dragging) { cursor: grabbing; }
|
||||
|
||||
:deep(.vue-flow__node .flow-node-shell),
|
||||
:deep(.vue-flow__node .flow-node-content),
|
||||
:deep(.vue-flow__node .node__container),
|
||||
@ -1501,7 +1587,18 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
:deep(.vue-flow__pane.dragging) {
|
||||
cursor: pointer;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
:deep(.vue-flow__selection) {
|
||||
border: 1px solid #2f80ed;
|
||||
background: rgba(47, 128, 237, 0.1);
|
||||
box-shadow: 0 0 0 1px rgba(47, 128, 237, 0.08);
|
||||
}
|
||||
|
||||
:deep(.vue-flow__nodesselection-rect) {
|
||||
border: 1px dashed rgba(47, 128, 237, 0.72);
|
||||
background: rgba(47, 128, 237, 0.04);
|
||||
}
|
||||
|
||||
:deep(.vue-flow__handle) {
|
||||
|
||||
@ -558,6 +558,11 @@ export const createFlowFacade = (store) => {
|
||||
const edge = store.findEdge(id);
|
||||
if (edge) store.addSelectedEdges([edge]);
|
||||
},
|
||||
selectElementsByIds(ids = []) {
|
||||
store.removeSelectedElements();
|
||||
const nodes = ids.map((id) => store.findNode(id)).filter(Boolean);
|
||||
if (nodes.length) store.addSelectedNodes(nodes);
|
||||
},
|
||||
focusOn({ id }) {
|
||||
const node = store.findNode(id);
|
||||
if (!node) return;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user