diff --git a/ui/src/workflow/common/shortcut.ts b/ui/src/workflow/common/shortcut.ts index ec65d504967..69b69e11fe3 100644 --- a/ui/src/workflow/common/shortcut.ts +++ b/ui/src/workflow/common/shortcut.ts @@ -5,9 +5,50 @@ import { MsgSuccess, MsgError, MsgConfirm } from '@/utils/message' import { WorkflowType } from '@/enums/application' import { t } from '@/locales' import { copyClick } from '@/utils/clipboard' +import { randomId } from '@/utils/common' import { getMenuNodes, workflowModelDict } from './data' +let activeCanvasId: string | null = null +type Point = { x: number; y: number } +const lastMouse = { + x: 0, + y: 0, + hasValue: false, +} let selected: any | null = null +const bindMousePosition = (lf: any) => { + const updateMouse = (e: MouseEvent) => { + lastMouse.x = e.clientX + lastMouse.y = e.clientY + lastMouse.hasValue = true + } + + // 推荐直接监听容器,这样鼠标在节点上移动也能拿到 + lf.container.addEventListener('mousemove', updateMouse) + + return () => { + lf.container.removeEventListener('mousemove', updateMouse) + } +} +const bindCanvasActive = (lf: any) => { + const container = lf.container as HTMLElement + if (!container) return + // 让容器可聚焦 + container.tabIndex = 0 + + const activate = () => { + activeCanvasId = lf.graphModel.flowId + container.focus() + } + + container.addEventListener('mousedown', activate) + container.addEventListener('focus', activate) + + return () => { + container.removeEventListener('mousedown', activate) + container.removeEventListener('focus', activate) + } +} function translationNodeData(nodeData: any, distance: any) { nodeData.x += distance nodeData.y += distance @@ -44,6 +85,8 @@ const TRANSLATION_DISTANCE = 40 let CHILDREN_TRANSLATION_DISTANCE = 40 export function initDefaultShortcut(lf: LogicFlow, graph: GraphModel) { + bindMousePosition(lf) + bindCanvasActive(lf) const { keyboard } = lf const { options: { keyboard: keyboardOptions }, @@ -72,18 +115,124 @@ export function initDefaultShortcut(lf: LogicFlow, graph: GraphModel) { copyClick(JSON.stringify(selected)) return false } + // 3. 求节点包围盒 + const getBounds = (nodes: any[]) => { + if (!nodes.length) { + return { minX: 0, maxX: 0, minY: 0, maxY: 0 } + } + + let minX = nodes[0].x + let maxX = nodes[0].x + let minY = nodes[0].y + let maxY = nodes[0].y + + for (const node of nodes) { + if (node.x < minX) minX = node.x + if (node.x > maxX) maxX = node.x + if (node.y < minY) minY = node.y + if (node.y > maxY) maxY = node.y + } + + return { minX, maxX, minY, maxY } + } + + // 4. 整体平移 + const moveData = (data: any, dx: number, dy: number) => { + for (const node of data.nodes ?? []) { + node.x += dx + node.y += dy + } + + for (const edge of data.edges ?? []) { + if (edge.startPoint) { + edge.startPoint.x += dx + edge.startPoint.y += dy + } + if (edge.endPoint) { + edge.endPoint.x += dx + edge.endPoint.y += dy + } + if (edge.text && typeof edge.text.x === 'number' && typeof edge.text.y === 'number') { + edge.text.x += dx + edge.text.y += dy + } + if (Array.isArray(edge.pointsList)) { + edge.pointsList = edge.pointsList.map((p: Point) => ({ + ...p, + x: p.x + dx, + y: p.y + dy, + })) + } + } + } + const resetData = (data: any) => { + const idMap = new Map() + + const getOrCreateId = (oldId: string) => { + let newId = idMap.get(oldId) + if (!newId) { + newId = randomId() + idMap.set(oldId, newId) + } + return newId + } + + for (const node of data.nodes) { + node.id = getOrCreateId(node.id) + } + + for (const edge of data.edges) { + const oldEdgeId = edge.id + const oldSourceNodeId = edge.sourceNodeId + const oldTargetNodeId = edge.targetNodeId + + edge.id = getOrCreateId(oldEdgeId) + edge.sourceNodeId = getOrCreateId(oldSourceNodeId) + edge.targetNodeId = getOrCreateId(oldTargetNodeId) + + if (typeof edge.sourceAnchorId === 'string') { + edge.sourceAnchorId = edge.sourceAnchorId.replace(oldSourceNodeId, edge.sourceNodeId) + } + + if (typeof edge.targetAnchorId === 'string') { + edge.targetAnchorId = edge.targetAnchorId.replace(oldTargetNodeId, edge.targetNodeId) + } + } + + return data + } const paste_node = async (e: ClipboardEvent) => { + if (lf.graphModel.flowId !== activeCanvasId) { + return true + } if (!keyboardOptions?.enabled) return true if (graph.textEditElement) return true const text = e.clipboardData?.getData('text/plain') || '' const data = parseAndValidate(text) - selected = data + selected = resetData(data) const workflowMode = lf.graphModel.get_provide(null, null).workflowMode const menus = getMenuNodes(workflowMode) const nodes = menus?.flatMap((m: any) => m.list).map((n) => n.type) if (selected && (selected.nodes || selected.edges)) { + if (!lastMouse.hasValue) { + moveData(data, 40, 40) + } else { + // LogicFlow 文档里 getPointByClient 会把页面坐标转成画布坐标 + const point = lf.graphModel.getPointByClient({ + x: lastMouse.x, + y: lastMouse.y, + }) + const mouseCanvasX = point.canvasOverlayPosition.x + const mouseCanvasY = point.canvasOverlayPosition.y + + const { minX, maxX, minY, maxY } = getBounds(selected.nodes) + const centerX = (minX + maxX) / 2 + const centerY = (minY + maxY) / 2 + moveData(data, mouseCanvasX - centerX, mouseCanvasY - centerY) + } + selected.nodes = selected.nodes.filter( (n: any) => nodes?.includes(n.type) || workflowModelDict[workflowMode](n), )