This commit is contained in:
2025-12-26 22:33:30 +08:00
parent 869201d08a
commit 93a8eb9ffb
10 changed files with 581 additions and 2 deletions

View File

@@ -56,12 +56,13 @@
<script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount } from 'vue';
import LogicFlow, { EventType } from '@logicflow/core';
import type { Position, NodeData, EdgeData, BaseNodeModel, GraphModel } from '@logicflow/core';
import type { Position, NodeData, EdgeData, BaseNodeModel, GraphModel, GraphData } from '@logicflow/core';
import '@logicflow/core/lib/style/index.css';
import { Menu, Label, Snapshot, SelectionSelect } from '@logicflow/extension';
import '@logicflow/extension/lib/style/index.css';
import '@logicflow/core/es/index.css';
import '@logicflow/extension/es/index.css';
import { translateEdgeData, translateNodeData } from '@logicflow/core/es/keyboard/shortcut';
import { register } from '@logicflow/vue-node-registry';
import ShikigamiSelectNode from './nodes/yys/ShikigamiSelectNode.vue';
@@ -76,6 +77,10 @@ import { setLogicFlowInstance, destroyLogicFlowInstance } from '@/ts/useLogicFlo
type AlignType = 'left' | 'right' | 'top' | 'bottom' | 'hcenter' | 'vcenter';
type DistributeType = 'horizontal' | 'vertical';
const MOVE_STEP = 2;
const MOVE_STEP_LARGE = 10;
const COPY_TRANSLATION = 40;
const props = defineProps<{
height?: string;
}>();
@@ -101,6 +106,280 @@ const { showMessage } = useGlobalMessage();
// 当前选中节点
const selectedNode = ref<any>(null);
const copyBuffer = ref<GraphData | null>(null);
let nextPasteDistance = COPY_TRANSLATION;
function isInputLike(event?: KeyboardEvent) {
const target = event?.target as HTMLElement | null;
if (!target) return false;
const tag = target.tagName?.toLowerCase();
return ['input', 'textarea', 'select', 'option'].includes(tag) || target.isContentEditable;
}
function shouldSkipShortcut(event?: KeyboardEvent) {
const lfInstance = lf.value as any;
if (!lfInstance) return true;
if (lfInstance.keyboard?.disabled) return true;
if (lfInstance.graphModel?.textEditElement) return true;
if (isInputLike(event)) return true;
return false;
}
function ensureMeta(meta?: Record<string, any>) {
const next: Record<string, any> = meta ? { ...meta } : {};
if (next.visible == null) next.visible = true;
if (next.locked == null) next.locked = false;
return next;
}
function applyMetaToModel(model: BaseNodeModel, metaInput?: Record<string, any>) {
const lfInstance = lf.value;
const meta = ensureMeta(metaInput ?? (model.getProperties?.() as any)?.meta ?? (model as any)?.properties?.meta);
model.visible = meta.visible !== false;
model.draggable = !meta.locked;
model.setHittable?.(!meta.locked);
model.setHitable?.(!meta.locked);
model.setIsShowAnchor?.(!meta.locked);
model.setRotatable?.(!meta.locked);
model.setResizable?.(!meta.locked);
if (lfInstance) {
const connectedEdges = lfInstance.getNodeEdges(model.id);
connectedEdges.forEach((edgeModel) => {
edgeModel.visible = meta.visible !== false;
});
}
}
function normalizeNodeModel(model: BaseNodeModel) {
const lfInstance = lf.value;
if (!lfInstance) return;
const props = (model.getProperties?.() as any) ?? (model as any)?.properties ?? {};
const incomingMeta = ensureMeta(props.meta);
const currentMeta = ensureMeta((model as any)?.properties?.meta);
const needPersist =
currentMeta.visible !== incomingMeta.visible ||
currentMeta.locked !== incomingMeta.locked ||
currentMeta.groupId !== incomingMeta.groupId;
if (needPersist) {
lfInstance.setProperties(model.id, { ...props, meta: incomingMeta });
}
applyMetaToModel(model, incomingMeta);
}
function normalizeAllNodes() {
const lfInstance = lf.value;
if (!lfInstance) return;
lfInstance.graphModel?.nodes.forEach((model: BaseNodeModel) => normalizeNodeModel(model));
}
function updateNodeMeta(model: BaseNodeModel, updater: (meta: Record<string, any>) => Record<string, any>) {
const lfInstance = lf.value;
if (!lfInstance) return;
const props = (model.getProperties?.() as any) ?? (model as any)?.properties ?? {};
const nextMeta = updater(ensureMeta(props.meta));
lfInstance.setProperties(model.id, { ...props, meta: nextMeta });
applyMetaToModel(model, nextMeta);
}
function getSelectedNodeModels() {
const graphModel = lf.value?.graphModel;
if (!graphModel) return [];
return [...graphModel.selectNodes];
}
function collectGroupNodeIds(models: BaseNodeModel[]) {
const graphModel = lf.value?.graphModel;
if (!graphModel) return [];
const ids = new Set<string>();
models.forEach((model) => {
const meta = ensureMeta((model.getProperties?.() as any)?.meta ?? (model as any)?.properties?.meta);
if (meta.locked) return;
if (meta.groupId) {
graphModel.nodes.forEach((node) => {
const peerMeta = ensureMeta((node.getProperties?.() as any)?.meta ?? (node as any)?.properties?.meta);
if (peerMeta.groupId === meta.groupId && !peerMeta.locked) {
ids.add(node.id);
}
});
} else {
ids.add(model.id);
}
});
return Array.from(ids);
}
function moveSelectedNodes(deltaX: number, deltaY: number) {
const graphModel = lf.value?.graphModel;
if (!graphModel) return;
const targets = collectGroupNodeIds(getSelectedNodeModels());
if (!targets.length) return;
graphModel.moveNodes(targets, deltaX, deltaY);
}
function deleteSelectedElements(event?: KeyboardEvent) {
if (shouldSkipShortcut(event)) return true;
const lfInstance = lf.value;
if (!lfInstance) return true;
const { nodes, edges } = lfInstance.getSelectElements(true);
const lockedNodes = nodes.filter((node) => ensureMeta((node as any).properties?.meta).locked);
edges.forEach((edge) => edge.id && lfInstance.deleteEdge(edge.id));
nodes
.filter((node) => {
const meta = ensureMeta((node as any).properties?.meta);
return !meta.locked && meta.visible !== false;
})
.forEach((node) => node.id && lfInstance.deleteNode(node.id));
if (lockedNodes.length) {
showMessage('warning', '部分节点已锁定,未删除');
}
updateSelectedCount();
selectedNode.value = null;
return false;
}
function toggleLockSelected(event?: KeyboardEvent) {
if (shouldSkipShortcut(event)) return true;
const models = getSelectedNodeModels();
if (!models.length) {
showMessage('info', '请选择节点后再执行锁定/解锁');
return true;
}
const hasUnlocked = models.some((model) => !ensureMeta((model.getProperties?.() as any)?.meta).locked);
models.forEach((model) => {
updateNodeMeta(model, (meta) => ({ ...meta, locked: hasUnlocked ? true : false }));
});
return false;
}
function toggleVisibilitySelected(event?: KeyboardEvent) {
if (shouldSkipShortcut(event)) return true;
const models = getSelectedNodeModels();
if (!models.length) {
showMessage('info', '请选择节点后再执行显示/隐藏');
return true;
}
const hasVisible = models.some((model) => ensureMeta((model.getProperties?.() as any)?.meta).visible !== false);
models.forEach((model) => {
updateNodeMeta(model, (meta) => ({ ...meta, visible: !hasVisible ? true : false }));
});
if (hasVisible) {
selectedNode.value = null;
}
updateSelectedCount();
return false;
}
function groupSelectedNodes(event?: KeyboardEvent) {
if (shouldSkipShortcut(event)) return true;
const models = getSelectedNodeModels().filter((model) => !ensureMeta((model.getProperties?.() as any)?.meta).locked);
if (models.length < 2) {
showMessage('warning', '请选择至少两个未锁定的节点进行分组');
return true;
}
const groupId = `group_${Date.now().toString(36)}`;
models.forEach((model) => {
updateNodeMeta(model, (meta) => ({ ...meta, groupId }));
});
return false;
}
function ungroupSelectedNodes(event?: KeyboardEvent) {
if (shouldSkipShortcut(event)) return true;
const models = getSelectedNodeModels();
if (!models.length) {
showMessage('info', '请选择节点后再执行解组');
return true;
}
models.forEach((model) => {
updateNodeMeta(model, (meta) => {
const nextMeta = { ...meta };
delete nextMeta.groupId;
return nextMeta;
});
});
return false;
}
function handleArrowMove(direction: 'left' | 'right' | 'up' | 'down', event?: KeyboardEvent) {
if (shouldSkipShortcut(event)) return true;
const step = (event?.shiftKey ? MOVE_STEP_LARGE : MOVE_STEP) * (direction === 'left' || direction === 'up' ? -1 : 1);
if (direction === 'left' || direction === 'right') {
moveSelectedNodes(step, 0);
} else {
moveSelectedNodes(0, step);
}
return false;
}
function remapGroupIds(nodes: GraphData['nodes']) {
const map = new Map<string, string>();
const seed = Date.now().toString(36);
nodes.forEach((node, index) => {
const meta = ensureMeta((node as any).properties?.meta);
if (meta.groupId) {
if (!map.has(meta.groupId)) {
map.set(meta.groupId, `group_${seed}_${index}`);
}
meta.groupId = map.get(meta.groupId);
}
(node as any).properties = { ...(node as any).properties, meta };
});
}
function handleCopy(event?: KeyboardEvent) {
if (shouldSkipShortcut(event)) return true;
const lfInstance = lf.value;
if (!lfInstance) return true;
const elements = lfInstance.getSelectElements(false);
if (!elements.nodes.length && !elements.edges.length) {
copyBuffer.value = null;
return true;
}
const nodes = elements.nodes.map((node) => translateNodeData(JSON.parse(JSON.stringify(node)), COPY_TRANSLATION));
const edges = elements.edges.map((edge) => translateEdgeData(JSON.parse(JSON.stringify(edge)), COPY_TRANSLATION));
remapGroupIds(nodes);
copyBuffer.value = { nodes, edges };
nextPasteDistance = COPY_TRANSLATION;
return false;
}
function handlePaste(event?: KeyboardEvent) {
if (shouldSkipShortcut(event)) return true;
const lfInstance = lf.value;
if (!lfInstance || !copyBuffer.value) return true;
lfInstance.clearSelectElements();
const added = lfInstance.addElements(copyBuffer.value, nextPasteDistance);
if (added) {
added.nodes.forEach((model) => {
normalizeNodeModel(model);
lfInstance.selectElementById(model.id, true);
});
added.edges.forEach((edge) => lfInstance.selectElementById(edge.id, true));
copyBuffer.value.nodes.forEach((node) => translateNodeData(node, COPY_TRANSLATION));
copyBuffer.value.edges.forEach((edge) => translateEdgeData(edge, COPY_TRANSLATION));
nextPasteDistance += COPY_TRANSLATION;
updateSelectedCount(lfInstance.graphModel);
}
return false;
}
function handleNodeDrag(args: { data: NodeData; deltaX: number; deltaY: number }) {
const { data, deltaX, deltaY } = args;
if (!deltaX && !deltaY) return;
const graphModel = lf.value?.graphModel;
if (!graphModel) return;
const model = graphModel.getNodeModelById(data.id);
if (!model) return;
const targets = collectGroupNodeIds([model]).filter((id) => id !== model.id);
if (!targets.length) return;
graphModel.moveNodes(targets, deltaX, deltaY);
}
function updateSelectedCount(model?: GraphModel) {
const lfInstance = lf.value;
@@ -127,7 +406,11 @@ function applySnapGrid(enabled: boolean) {
function getSelectedRects() {
const lfInstance = lf.value;
if (!lfInstance) return [];
return lfInstance.graphModel.selectNodes.map((model: BaseNodeModel) => {
const unlocked = lfInstance.graphModel.selectNodes.filter((model: BaseNodeModel) => {
const meta = ensureMeta((model.getProperties?.() as any)?.meta ?? (model as any)?.properties?.meta);
return !meta.locked && meta.visible !== false;
});
return unlocked.map((model: BaseNodeModel) => {
const bounds = model.getBounds();
const width = bounds.maxX - bounds.minX;
const height = bounds.maxY - bounds.minY;
@@ -242,6 +525,9 @@ onMounted(() => {
allowRotate: true,
overlapMode: -1,
snapline: true,
keyboard: {
enabled: true
},
plugins: [Menu, Label, Snapshot, SelectionSelect],
pluginsOptions: {
label: {
@@ -257,6 +543,26 @@ onMounted(() => {
const lfInstance = lf.value;
if (!lfInstance) return;
lfInstance.keyboard.off(['cmd + c', 'ctrl + c']);
lfInstance.keyboard.off(['cmd + v', 'ctrl + v']);
lfInstance.keyboard.off(['backspace']);
const bindShortcut = (keys: string | string[], handler: (event?: KeyboardEvent) => boolean | void) => {
lfInstance.keyboard.on(keys, (event: KeyboardEvent) => handler(event));
};
bindShortcut(['del', 'backspace'], deleteSelectedElements);
bindShortcut(['left'], (event) => handleArrowMove('left', event));
bindShortcut(['right'], (event) => handleArrowMove('right', event));
bindShortcut(['up'], (event) => handleArrowMove('up', event));
bindShortcut(['down'], (event) => handleArrowMove('down', event));
bindShortcut(['cmd + c', 'ctrl + c'], handleCopy);
bindShortcut(['cmd + v', 'ctrl + v'], handlePaste);
bindShortcut(['cmd + g', 'ctrl + g'], groupSelectedNodes);
bindShortcut(['cmd + u', 'ctrl + u'], ungroupSelectedNodes);
bindShortcut(['cmd + l', 'ctrl + l'], toggleLockSelected);
bindShortcut(['cmd + shift + h', 'ctrl + shift + h'], toggleVisibilitySelected);
lfInstance.extension.menu.addMenuConfig({
nodeMenu: [
{
@@ -307,6 +613,7 @@ onMounted(() => {
lfInstance.render({
// 渲染的数据
});
normalizeAllNodes();
lfInstance.updateEditConfig({
multipleSelectKey: 'shift',
snapGrid: snapGridEnabled.value
@@ -320,6 +627,15 @@ onMounted(() => {
updateSelectedCount();
});
lfInstance.on(EventType.NODE_DRAG, (args) => handleNodeDrag(args as any));
lfInstance.on(EventType.NODE_ADD, ({ data }) => {
const model = lfInstance.getNodeModelById(data.id);
if (model) normalizeNodeModel(model);
});
lfInstance.on(EventType.GRAPH_RENDERED, () => normalizeAllNodes());
// 监听空白点击事件,取消选中
lfInstance.on(EventType.BLANK_CLICK, () => {
selectedNode.value = null;
@@ -337,6 +653,10 @@ onMounted(() => {
};
}
}
const model = lfInstance.getNodeModelById(nodeId);
if (model && data.properties?.meta) {
applyMetaToModel(model, data.properties.meta);
}
});
lfInstance.on('selection:selected', () => updateSelectedCount());