mirror of
https://github.com/Powerful-517/yys-editor.git
synced 2026-03-05 15:05:27 +00:00
fix(flow): support dynamic-group palette and restore framework clipboard
This commit is contained in:
@@ -77,11 +77,10 @@ import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import LogicFlow, { EventType } 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, MiniMap, Control } from '@logicflow/extension';
|
||||
import { Menu, Label, Snapshot, SelectionSelect, MiniMap, Control, DynamicGroup } 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 PropertySelectNode from './nodes/yys/PropertySelectNode.vue';
|
||||
@@ -104,7 +103,6 @@ type DistributeType = 'horizontal' | 'vertical';
|
||||
|
||||
const MOVE_STEP = 2;
|
||||
const MOVE_STEP_LARGE = 10;
|
||||
const COPY_TRANSLATION = 40;
|
||||
const RIGHT_MOUSE_BUTTON = 2;
|
||||
const RIGHT_DRAG_THRESHOLD = 2;
|
||||
const RIGHT_DRAG_CONTEXTMENU_SUPPRESS_MS = 300;
|
||||
@@ -113,7 +111,7 @@ const props = withDefaults(defineProps<{
|
||||
height?: string;
|
||||
enableLabel?: boolean;
|
||||
}>(), {
|
||||
enableLabel: true
|
||||
enableLabel: false
|
||||
});
|
||||
|
||||
const flowHostRef = ref<HTMLElement | null>(null);
|
||||
@@ -137,10 +135,8 @@ const { showMessage } = useGlobalMessage();
|
||||
|
||||
// 当前选中节点
|
||||
const selectedNode = ref<any>(null);
|
||||
const copyBuffer = ref<GraphData | null>(null);
|
||||
const groupRuleWarnings = ref<GroupRuleWarning[]>([]);
|
||||
const flowControlsCollapsed = ref(true);
|
||||
let nextPasteDistance = COPY_TRANSLATION;
|
||||
let containerResizeObserver: ResizeObserver | null = null;
|
||||
let groupRuleValidationTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let unsubscribeSharedGroupRules: (() => void) | null = null;
|
||||
@@ -151,6 +147,22 @@ let rightDragLastY = 0;
|
||||
let rightDragDistance = 0;
|
||||
let suppressContextMenuUntil = 0;
|
||||
|
||||
function logClipboardDebug(stage: string, payload: Record<string, unknown> = {}) {
|
||||
if (!import.meta.env.DEV) return;
|
||||
const lfInstance = lf.value as any;
|
||||
const graphModel = lfInstance?.graphModel;
|
||||
const selectNodeIds: string[] = graphModel?.selectNodes?.map((node: BaseNodeModel) => node.id) ?? [];
|
||||
const selectElementIds: string[] = graphModel?.selectElements
|
||||
? Array.from(graphModel.selectElements.keys())
|
||||
: [];
|
||||
console.info('[FlowClipboardDebug]', stage, {
|
||||
selectedCount: selectedCount.value,
|
||||
selectNodeIds,
|
||||
selectElementIds,
|
||||
...payload
|
||||
});
|
||||
}
|
||||
|
||||
const resolveResizeHost = () => {
|
||||
const container = containerRef.value;
|
||||
if (!container) return null;
|
||||
@@ -347,6 +359,48 @@ function normalizeAllNodes() {
|
||||
});
|
||||
}
|
||||
|
||||
function sanitizeLabelInProperties(properties: Record<string, any> | undefined) {
|
||||
if (!properties || !Object.prototype.hasOwnProperty.call(properties, '_label')) {
|
||||
return properties;
|
||||
}
|
||||
const currentLabel = properties._label;
|
||||
if (!Array.isArray(currentLabel)) {
|
||||
return properties;
|
||||
}
|
||||
const cleaned = currentLabel.filter((item) => item && typeof item === 'object');
|
||||
if (cleaned.length === currentLabel.length) {
|
||||
return properties;
|
||||
}
|
||||
if (!cleaned.length) {
|
||||
const { _label, ...rest } = properties;
|
||||
return rest;
|
||||
}
|
||||
return {
|
||||
...properties,
|
||||
_label: cleaned
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeGraphLabels() {
|
||||
const graphModel = lf.value?.graphModel as any;
|
||||
if (!graphModel) return;
|
||||
|
||||
const sanitizeModel = (model: any) => {
|
||||
const props = model?.getProperties?.() ?? model?.properties;
|
||||
if (!props) return;
|
||||
const next = sanitizeLabelInProperties(props);
|
||||
if (!next || next === props) return;
|
||||
if (typeof model.setProperties === 'function') {
|
||||
model.setProperties(next);
|
||||
return;
|
||||
}
|
||||
model.properties = next;
|
||||
};
|
||||
|
||||
(graphModel.nodes ?? []).forEach((model: any) => sanitizeModel(model));
|
||||
(graphModel.edges ?? []).forEach((model: any) => sanitizeModel(model));
|
||||
}
|
||||
|
||||
function updateNodeMeta(model: BaseNodeModel, updater: (meta: Record<string, any>) => Record<string, any>) {
|
||||
const lfInstance = lf.value;
|
||||
if (!lfInstance) return;
|
||||
@@ -610,59 +664,6 @@ function handleArrowMove(direction: 'left' | 'right' | 'up' | 'down', event?: Ke
|
||||
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;
|
||||
@@ -856,6 +857,7 @@ onMounted(() => {
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
DynamicGroup,
|
||||
Menu,
|
||||
...(props.enableLabel ? [Label] : []),
|
||||
Snapshot,
|
||||
@@ -884,8 +886,6 @@ 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) => {
|
||||
@@ -897,8 +897,6 @@ onMounted(() => {
|
||||
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);
|
||||
@@ -933,21 +931,6 @@ onMounted(() => {
|
||||
{
|
||||
text: '---' // 分隔线
|
||||
},
|
||||
{
|
||||
text: '复制 (Ctrl+C)',
|
||||
callback() {
|
||||
handleCopy();
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '粘贴 (Ctrl+V)',
|
||||
callback() {
|
||||
handlePaste();
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '---' // 分隔线
|
||||
},
|
||||
{
|
||||
text: '组合 (Ctrl+G)',
|
||||
callback() {
|
||||
@@ -1005,11 +988,8 @@ onMounted(() => {
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '粘贴 (Ctrl+V)',
|
||||
callback(data: Position) {
|
||||
handlePaste();
|
||||
}
|
||||
}
|
||||
text: '提示:使用 Ctrl+V 粘贴',
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
@@ -1017,21 +997,6 @@ onMounted(() => {
|
||||
lfInstance.extension.menu.setMenuByType({
|
||||
type: 'lf:defaultSelectionMenu',
|
||||
menu: [
|
||||
{
|
||||
text: '复制 (Ctrl+C)',
|
||||
callback() {
|
||||
handleCopy();
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '粘贴 (Ctrl+V)',
|
||||
callback() {
|
||||
handlePaste();
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '---' // 分隔线
|
||||
},
|
||||
{
|
||||
text: '组合 (Ctrl+G)',
|
||||
callback() {
|
||||
@@ -1079,6 +1044,12 @@ onMounted(() => {
|
||||
|
||||
// 监听所有可能的节点添加事件
|
||||
lfInstance.on(EventType.NODE_ADD, ({ data }) => {
|
||||
if (!data?.id) {
|
||||
logClipboardDebug('node:add-invalid-payload', {
|
||||
payload: data ?? null
|
||||
});
|
||||
return;
|
||||
}
|
||||
const model = lfInstance.getNodeModelById(data.id);
|
||||
if (model) {
|
||||
normalizeNodeModel(model);
|
||||
@@ -1092,6 +1063,7 @@ onMounted(() => {
|
||||
|
||||
// 监听 DND 添加节点事件
|
||||
lfInstance.on('node:dnd-add', ({ data }) => {
|
||||
if (!data?.id) return;
|
||||
const model = lfInstance.getNodeModelById(data.id);
|
||||
if (model) {
|
||||
// 设置新节点的 zIndex 为 1000
|
||||
@@ -1103,6 +1075,7 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
lfInstance.on(EventType.GRAPH_RENDERED, () => {
|
||||
sanitizeGraphLabels();
|
||||
applySelectionSelect(selectionEnabled.value);
|
||||
normalizeAllNodes();
|
||||
scheduleGroupRuleValidation(0);
|
||||
@@ -1145,8 +1118,16 @@ onMounted(() => {
|
||||
scheduleGroupRuleValidation();
|
||||
});
|
||||
|
||||
lfInstance.on('selection:selected', () => updateSelectedCount());
|
||||
lfInstance.on('selection:drop', () => updateSelectedCount());
|
||||
lfInstance.on('selection:selected', () => {
|
||||
sanitizeGraphLabels();
|
||||
updateSelectedCount();
|
||||
logClipboardDebug('selection:selected');
|
||||
});
|
||||
lfInstance.on('selection:drop', () => {
|
||||
sanitizeGraphLabels();
|
||||
updateSelectedCount();
|
||||
logClipboardDebug('selection:drop');
|
||||
});
|
||||
|
||||
nextTick(() => {
|
||||
queueCanvasResize();
|
||||
|
||||
Reference in New Issue
Block a user