From da6e86b039481e44323e5dd69e42c9d148a6789b Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Mon, 17 Mar 2025 18:11:24 -0700 Subject: [PATCH 01/10] fix: adapt multi-node-drag --- .../canvas/container/src/CanvasContainer.vue | 102 ++- .../container/src/components/CanvasAction.vue | 58 +- .../components/CanvasMultiDragIndicator.vue | 265 +++++++ .../container/src/composables/useMultiDrag.js | 666 ++++++++++++++++++ .../src/composables/useMultiSelect.ts | 8 +- packages/canvas/container/src/container.ts | 2 +- 6 files changed, 1070 insertions(+), 31 deletions(-) create mode 100644 packages/canvas/container/src/components/CanvasMultiDragIndicator.vue create mode 100644 packages/canvas/container/src/composables/useMultiDrag.js diff --git a/packages/canvas/container/src/CanvasContainer.vue b/packages/canvas/container/src/CanvasContainer.vue index e1f969b37b..7adace711d 100644 --- a/packages/canvas/container/src/CanvasContainer.vue +++ b/packages/canvas/container/src/CanvasContainer.vue @@ -8,10 +8,18 @@ :windowGetClickEventTarget="target" :resize="canvasState.type === 'absolute'" :multiStateLength="multiStateLength" + :isMultiDragging="isMultiDragging()" @select-slot="selectSlot" @setting="settingModel" > + @@ -62,7 +70,9 @@ import CanvasViewerSwitcher from './components/CanvasViewerSwitcher.vue' import CanvasResize from './components/CanvasResize.vue' import CanvasDivider from './components/CanvasDivider.vue' import CanvasResizeBorder from './components/CanvasResizeBorder.vue' +import CanvasMultiDragIndicator from './components/CanvasMultiDragIndicator.vue' import { useMultiSelect } from './composables/useMultiSelect' +import { useMultiDrag } from './composables/useMultiDrag' import { canvasState, onMouseUp, @@ -92,7 +102,8 @@ export default { CanvasDivider, CanvasResizeBorder, CanvasRouterJumper, - CanvasViewerSwitcher + CanvasViewerSwitcher, + CanvasMultiDragIndicator }, props: { controller: Object, @@ -113,9 +124,11 @@ export default { const containerPanel = ref(null) const insertContainer = ref(false) - const { multiSelectedStates } = useMultiSelect() + const { multiSelectedStates, isMouseDown } = useMultiSelect() const multiStateLength = computed(() => multiSelectedStates.value.length) + const { startMultiDrag, moveMultiDrag, endMultiDrag, isMultiDragging, getMultiDragPositionText, multiDragState } = + useMultiDrag() const computedSelectState = computed(() => { if (multiSelectedStates.value.length === 1) { @@ -129,6 +142,15 @@ export default { const { clientX, clientY } = event const element = getElement(event.target) closeMenu() + + if (!element) return + + // 优先处理右键菜单 + if (event.button === 2) { + openMenu(event) + return + } + let node = getCurrent().schema if (element) { @@ -144,15 +166,17 @@ export default { } } - if (event.button === 0 && element !== element.ownerDocument.body) { - const { x, y } = element.getBoundingClientRect() - - dragStart(node, element, { offsetX: clientX - x, offsetY: clientY - y }) + // 处理多选拖拽开始 - 确保在单节点拖拽之前处理 + if (startMultiDrag(event, element)) { + return } - // 如果是点击右键则打开右键菜单 - if (event.button === 2) { - openMenu(event) + // 处理单节点拖拽开始 + if (event.button === 0 && element !== element.ownerDocument.body) { + const { x, y } = element.getBoundingClientRect() + if (multiStateLength.value === 1) { + dragStart(node, element, { offsetX: clientX - x, offsetY: clientY - y }) + } } } } @@ -206,9 +230,10 @@ export default { const doc = iframe.value.contentDocument const win = iframe.value.contentWindow + // eslint-disable-next-line @typescript-eslint/no-unused-vars let isScrolling = false - // 以下是内部iframe监听的事件 + // 监听鼠标按下事件 win.addEventListener('mousedown', (event) => { handleCanvasEvent(() => { // html元素使用scroll和mouseup事件处理 @@ -222,6 +247,8 @@ export default { return } + isMouseDown.value = true + insertPosition.value = false insertContainer.value = false setCurrentNode(event) @@ -235,32 +262,49 @@ export default { isScrolling = true }) - win.addEventListener('mouseup', (event) => { - if (event.target !== doc.documentElement || isScrolling) { - return - } + // 监听鼠标移动事件 + win.addEventListener('mousemove', (ev) => { + handleCanvasEvent(() => { + // 优先处理多选拖拽移动 + if (!moveMultiDrag(ev)) { + // 如果不是多选拖拽,则处理普通拖拽 + dragMove(ev, true) + } + }) + }) + + // 监听拖拽结束事件 + win.addEventListener('mouseup', (ev) => { + handleCanvasEvent(() => { + if (ev.button === 0 && isMouseDown.value) { + isMouseDown.value = false + } - insertPosition.value = false - insertContainer.value = false - setCurrentNode(event) - target.value = event.target + // 优先处理多选拖拽结束 + if (!endMultiDrag()) { + // 如果不是多选拖拽,则处理普通拖拽结束 + onMouseUp(ev) + } + }) }) + // 监听拖拽过程事件 win.addEventListener('dragover', (ev) => { ev.dataTransfer.dropEffect = 'move' ev.preventDefault() - dragMove(ev) + // 优先处理多选拖拽移动 + if (!moveMultiDrag(ev)) { + dragMove(ev) + } }) + // 监听放置事件 win.addEventListener('drop', (ev) => { ev.preventDefault() - onMouseUp(ev) - }) - - win.addEventListener('mousemove', (ev) => { - handleCanvasEvent(() => { - dragMove(ev, true) - }) + // 优先处理多选拖拽结束 + if (!endMultiDrag()) { + onMouseUp(ev) + } }) // 阻止浏览器默认的右键菜单功能 @@ -327,6 +371,7 @@ export default { document.addEventListener('canvasReady', canvasReady) return { + isMouseDown, iframe, dragState, hoverState, @@ -347,7 +392,10 @@ export default { insertPosition, insertContainer, loading, - srcAttrName + srcAttrName, + isMultiDragging, + multiDragState, + getMultiDragPositionText } } } diff --git a/packages/canvas/container/src/components/CanvasAction.vue b/packages/canvas/container/src/components/CanvasAction.vue index 029a4375e0..3b16ecb492 100644 --- a/packages/canvas/container/src/components/CanvasAction.vue +++ b/packages/canvas/container/src/components/CanvasAction.vue @@ -1,7 +1,7 @@ + + + + diff --git a/packages/canvas/container/src/composables/useMultiDrag.js b/packages/canvas/container/src/composables/useMultiDrag.js new file mode 100644 index 0000000000..4328e8a10a --- /dev/null +++ b/packages/canvas/container/src/composables/useMultiDrag.js @@ -0,0 +1,666 @@ +import { reactive, computed, toRaw } from 'vue' +import { useMultiSelect } from './useMultiSelect' +import { useCanvas } from '@opentiny/tiny-engine-meta-register' +import { NODE_TAG, NODE_UID } from '../../../common' +import { + lineState, + querySelectById, + removeNode, + getController, + getElement, + getConfigure, + allowInsert, + POSITION, + insertNode, + syncNodeScroll, + dragState, + initialDragState, + isAncestor, + getDocument +} from '../container' + +const initialMultiDragState = { + keydown: false, + draging: false, + dragStarted: false, // 标记是否已经开始拖拽 + initialMousePos: null, // 初始鼠标位置 + nodes: [], // 存储被拖拽的多个节点信息 + offsets: new Map(), // 存储每个节点的偏移量 + mouse: null, // 鼠标位置 + position: null, // 放置位置 + targetNodeId: null // 当前点击的节点ID +} + +// 拖拽阈值,鼠标移动超过这个距离才会触发拖拽 +const DRAG_THRESHOLD = 5 + +export const useMultiDrag = () => { + const multiDragState = reactive({ ...initialMultiDragState }) + const { multiSelectedStates, multiStateLength } = useMultiSelect() + + // 准备拖拽 - 仅记录初始状态,不立即开始拖拽 + const startMultiDrag = (event, element) => { + if (multiStateLength.value <= 1) return false + + // 检查点击的元素是否是已选中的节点之一 + const clickedNodeId = element?.getAttribute(NODE_UID) + if (!clickedNodeId || !multiSelectedStates.value.some((state) => state.id === clickedNodeId)) { + return false + } + + const { clientX, clientY } = event + multiDragState.keydown = true + multiDragState.dragStarted = false + multiDragState.draging = false + multiDragState.initialMousePos = { x: clientX, y: clientY } + multiDragState.targetNodeId = clickedNodeId + multiDragState.nodes = toRaw(multiSelectedStates.value).map((state) => state.schema) + + // 计算每个节点相对于鼠标的偏移量 + multiSelectedStates.value.forEach((state) => { + const elem = querySelectById(state.id) + if (elem) { + const { x, y } = elem.getBoundingClientRect() + multiDragState.offsets.set(state.id, { + offsetX: clientX - x, + offsetY: clientY - y, + initialX: x, + initialY: y + }) + } + }) + + return true + } + + // 计算放置位置 + const calculateDropPosition = (event, rect, configure) => { + const { clientX: mouseX, clientY: mouseY } = event + // 参考单选节点的实现,使用更精确的计算方式 + const yAbs = Math.min(20, rect.height / 3) + const xAbs = Math.min(20, rect.width / 3) + + // 优先判断是否在边缘区域 + if (mouseY < rect.top + yAbs) { + return POSITION.TOP + } else if (mouseY > rect.bottom - yAbs) { + return POSITION.BOTTOM + } else if (mouseX < rect.left + xAbs) { + return POSITION.LEFT + } else if (mouseX > rect.right - xAbs) { + return POSITION.RIGHT + } else if (configure?.isContainer) { + // 如果是容器,且鼠标在中间区域,则放置到容器内 + return POSITION.IN + } + + // 默认放置到底部 + return POSITION.BOTTOM + } + + // 计算鼠标移动距离 + const calculateDistance = (pos1, pos2) => { + if (!pos1 || !pos2) return 0 + const dx = pos1.x - pos2.x + const dy = pos1.y - pos2.y + return Math.sqrt(dx * dx + dy * dy) + } + + // 检查是否允许放置 + const checkAllowInsert = (configure, nodes, targetId, position) => { + // 如果没有配置,不允许放置 + if (!configure) return false + + // 获取目标节点的信息 + const { parent: targetParent } = useCanvas().getNodeWithParentById(targetId) || {} + const targetParentId = targetParent?.id + + // 如果目标是body,特殊处理 + if (targetId === 'body') { + // 对于body,允许放置到内部、上方和下方 + if (position !== POSITION.IN && position !== POSITION.TOP && position !== POSITION.BOTTOM) { + // 强制将position设置为IN,因为body只能放置到内部、上方或下方 + lineState.position = POSITION.IN + } + + // 检查所有节点是否都允许放置到body内 + for (const node of nodes) { + if (!allowInsert({ isContainer: true }, node)) { + return false + } + } + return true + } + + // 如果目标节点的父节点是body,特殊处理 + if (targetParentId === 'body') { + // 允许在body的直接子节点前后放置 + if (position === POSITION.TOP || position === POSITION.BOTTOM) { + // 检查所有节点是否都允许放置到body内 + for (const node of nodes) { + if (!allowInsert({ isContainer: true }, node)) { + return false + } + } + return true + } + } + + // 检查所有节点是否都允许放置 + for (const node of nodes) { + // 如果是放置到容器内,检查节点是否是目标节点的祖先 + if (position === POSITION.IN && isAncestor(node.id, targetId)) { + return false + } + + // 如果是放置到节点前后,检查节点是否是目标节点的父节点 + if ( + (position === POSITION.TOP || + position === POSITION.BOTTOM || + position === POSITION.LEFT || + position === POSITION.RIGHT) && + node.id === targetParentId + ) { + return false + } + + // 检查节点是否允许放置到目标位置 + if (position === POSITION.IN) { + // 放置到容器内需要检查容器的配置 + if (!allowInsert(configure, node)) { + return false + } + } else { + // 放置到节点前后需要检查父节点的配置 + const parentConfigure = targetParent ? getConfigure(targetParent.componentName) : { isContainer: true } + if (!allowInsert(parentConfigure, node)) { + return false + } + } + } + + return true + } + + // 拖拽移动 + const moveMultiDrag = (event) => { + if (!multiDragState.keydown || multiStateLength.value <= 1) return false + + const { clientX, clientY } = event + const currentMousePos = { x: clientX, y: clientY } + + // 如果拖拽还未开始,检查是否超过阈值 + if (!multiDragState.dragStarted) { + const distance = calculateDistance(multiDragState.initialMousePos, currentMousePos) + + // 如果移动距离小于阈值,不触发拖拽 + if (distance < DRAG_THRESHOLD) { + return false + } + + // 超过阈值,标记拖拽已开始 + multiDragState.dragStarted = true + + // 清除单选拖动状态,防止单选拖动的虚影显示 + Object.assign(dragState, initialDragState) + } + + // 始终更新鼠标位置,确保拖拽预览能够跟随鼠标 + multiDragState.mouse = currentMousePos + + if (!multiDragState.draging && multiDragState.dragStarted) { + multiDragState.draging = true + } + + // 如果没有真正开始拖拽,不处理后续逻辑 + if (!multiDragState.draging) { + return false + } + + const targetElement = getElement(event.target) + + // 特殊处理:如果没有找到目标元素,检查是否是body元素或其直接子元素 + if (!targetElement) { + // 检查是否是body元素或其直接子元素 + const doc = getDocument() + const body = doc.body + + // 如果鼠标在body区域内,则视为拖拽到body + if (event.target === body || event.target.parentElement === body || event.target === doc.documentElement) { + // 获取body中的所有顶级节点 + const { getSchema } = useCanvas() + const bodySchema = getSchema() + const bodyChildren = bodySchema.children || [] + + // 如果body中没有子节点,直接放置到body内部 + if (bodyChildren.length === 0) { + const bodyRect = body.getBoundingClientRect() + Object.assign(lineState, { + id: 'body', + top: bodyRect.top, + left: bodyRect.left, + width: bodyRect.width, + height: bodyRect.height, + position: POSITION.IN, + forbidden: false, + configure: { isContainer: true } + }) + return true + } + + // 如果body中有子节点,需要判断放置位置 + const { clientY } = event + + // 遍历body的直接子节点,找到最接近鼠标位置的节点 + let closestNode = null + let closestDistance = Infinity + let position = POSITION.IN // 默认放置到body内部 + + for (const childSchema of bodyChildren) { + const childElement = querySelectById(childSchema.id) + if (!childElement) continue + + const childRect = childElement.getBoundingClientRect() + const childMiddle = childRect.top + childRect.height / 2 + + // 计算鼠标与节点中点的距离 + const distance = Math.abs(clientY - childMiddle) + + if (distance < closestDistance) { + closestDistance = distance + closestNode = childElement + + // 判断放置位置:在节点上方还是下方 + position = clientY < childMiddle ? POSITION.TOP : POSITION.BOTTOM + } + } + + // 如果找到了最近的节点 + if (closestNode) { + const nodeId = closestNode.getAttribute(NODE_UID) + const componentName = closestNode.getAttribute(NODE_TAG) + const configure = getConfigure(componentName) + const rect = closestNode.getBoundingClientRect() + + // 检查是否允许放置 + const isForbidden = !checkAllowInsert(configure, multiDragState.nodes, nodeId, position) + + // 更新lineState + Object.assign(lineState, { + id: nodeId, + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height, + position: position, + forbidden: isForbidden, + configure + }) + } else { + // 如果没有找到合适的节点,放置到body内部 + const bodyRect = body.getBoundingClientRect() + Object.assign(lineState, { + id: 'body', + top: bodyRect.top, + left: bodyRect.left, + width: bodyRect.width, + height: bodyRect.height, + position: POSITION.IN, + forbidden: false, + configure: { isContainer: true } + }) + } + + return true + } + + // 其他情况,设置为禁止放置 + lineState.position = '' + lineState.forbidden = true + return true + } + + // 更新放置位置指示器 + const componentName = targetElement.getAttribute(NODE_TAG) + const configure = getConfigure(componentName) + const rect = targetElement.getBoundingClientRect() + const targetId = targetElement.getAttribute(NODE_UID) || 'body' + + // 计算放置位置 + const position = calculateDropPosition(event, rect, configure) + + // 检查是否是拖拽自身节点 + const isDraggingSelf = multiDragState.nodes.some((node) => node.id === targetId) + + // 如果是拖拽到自身节点,需要特殊处理 + if (isDraggingSelf && position !== POSITION.IN) { + // 获取目标节点的父节点和兄弟节点 + const { getNodeWithParentById } = useCanvas() + const { parent } = getNodeWithParentById(targetId) || {} + + if (parent) { + // 根据放置位置调整目标节点 + const children = parent.children || [] + const targetIndex = children.findIndex((child) => child.id === targetId) + + // 如果是放置到节点下方,使用下一个兄弟节点作为目标 + if ((position === POSITION.BOTTOM || position === POSITION.RIGHT) && targetIndex < children.length - 1) { + const nextSibling = children[targetIndex + 1] + if (nextSibling && !multiDragState.nodes.some((node) => node.id === nextSibling.id)) { + // 使用下一个兄弟节点作为目标 + const nextElement = querySelectById(nextSibling.id) + if (nextElement) { + const nextRect = nextElement.getBoundingClientRect() + const nextComponentName = nextElement.getAttribute(NODE_TAG) + const nextConfigure = getConfigure(nextComponentName) + + // 更新lineState + Object.assign(lineState, { + id: nextSibling.id, + top: nextRect.top, + left: nextRect.left, + width: nextRect.width, + height: nextRect.height, + position: POSITION.TOP, // 放置到下一个节点的上方 + forbidden: !checkAllowInsert(nextConfigure, multiDragState.nodes, nextSibling.id, POSITION.TOP), + configure: nextConfigure + }) + + return true + } + } + } + + // 如果是放置到节点上方,或者是最后一个节点的下方 + if ( + position === POSITION.TOP || + position === POSITION.LEFT || + (position === POSITION.BOTTOM && targetIndex === children.length - 1) || + (position === POSITION.RIGHT && targetIndex === children.length - 1) + ) { + // 检查是否允许放置 + const isForbidden = !checkAllowInsert(configure, multiDragState.nodes, targetId, position) + + // 更新lineState + Object.assign(lineState, { + id: targetId, + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height, + position: position, + forbidden: isForbidden, + configure + }) + + return true + } + } + + // 默认情况下,禁止放置 + lineState.forbidden = true + return true + } + + // 检查是否允许放置 + const isForbidden = !checkAllowInsert(configure, multiDragState.nodes, targetId, position) + + // 特殊处理容器内放置 + if (position === POSITION.IN && configure?.isContainer) { + const { getNodeWithParentById, getSchema } = useCanvas() + const { node } = targetId === 'body' ? { node: getSchema() } : getNodeWithParentById(targetId) || {} + const children = node?.children || [] + + // 如果容器有子节点,考虑放置到最后一个子节点后面 + if (children.length > 0) { + const lastChild = children[children.length - 1] + // 如果最后一个子节点不是被拖拽的节点之一 + if (!multiDragState.nodes.some((node) => node.id === lastChild.id)) { + const childElement = querySelectById(lastChild.id) + if (childElement) { + const childRect = childElement.getBoundingClientRect() + + // 更新lineState,显示在最后一个子节点下方 + Object.assign(lineState, { + id: targetId, // 保持目标是容器 + top: childRect.top, + left: childRect.left, + width: childRect.width, + height: childRect.height, + position: POSITION.IN, // 仍然表示放置到容器内 + forbidden: isForbidden, + configure + }) + + return true + } + } + } + } + + // 更新lineState + Object.assign(lineState, { + id: targetId, + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height, + position, + forbidden: isForbidden, + configure + }) + + return true + } + + // 结束拖拽 + const endMultiDrag = () => { + // 只有真正开始拖拽后才处理放置逻辑 + if (!multiDragState.draging || !multiDragState.dragStarted || multiStateLength.value <= 1) { + // 重置状态 + Object.assign(multiDragState, initialMultiDragState) + return false + } + + const { position, forbidden, id: targetId } = lineState + + if (!forbidden && targetId) { + const { getNodeWithParentById, getSchema } = useCanvas() + const { node: targetNode, parent: targetParent } = getNodeWithParentById(targetId) || {} + const isBodyTarget = targetId === 'body' + + // 如果目标是body,使用页面schema作为目标节点 + const finalTargetNode = isBodyTarget ? getSchema() : targetNode + const finalTargetParent = isBodyTarget ? null : targetParent + + if (finalTargetNode) { + // 创建一个操作批次,以便能够一次性添加历史记录 + const operations = [] + + // 收集要移动的节点ID,用于后续检查 + const movingNodeIds = multiDragState.nodes.map((node) => node.id) + + // 按照拖拽顺序依次插入节点 + multiDragState.nodes.forEach((node) => { + const sourceId = node.id + const { node: sourceNode, parent: sourceParent } = getNodeWithParentById(sourceId) || {} + + // 跳过目标节点自身 + if (sourceId === targetId) { + return + } + + // 如果源节点的父节点是目标节点,且放置位置是IN,则跳过(避免循环引用) + if (position === POSITION.IN && sourceParent?.id === targetId) { + return + } + + // 如果目标节点的父节点是正在移动的节点之一,且不是放置到容器内,则跳过 + if (position !== POSITION.IN && finalTargetParent && movingNodeIds.includes(finalTargetParent.id)) { + return + } + + // 准备插入数据 + const insertData = { ...sourceNode } + const targetNodeData = { + parent: toRaw(finalTargetParent), + node: toRaw(finalTargetNode), + data: { ...insertData, children: insertData.children || [] } + } + + // 记录操作 + operations.push({ + sourceId, + targetNodeData, + position + }) + }) + + // 执行所有操作 + if (operations.length > 0) { + // 先移除所有源节点 + operations.forEach((op) => { + removeNode(op.sourceId) + }) + + // 然后插入所有节点到目标位置 + operations.forEach((op) => { + // 对于body特殊处理 + if (isBodyTarget) { + // 判断是否是放置到body内的特定位置(TOP或BOTTOM) + if (op.position === POSITION.TOP || op.position === POSITION.BOTTOM) { + // 这种情况下,targetId 实际上是 body 中的某个子节点的 ID + // 需要构建正确的目标节点数据 + const { getNodeWithParentById } = useCanvas() + const { node: targetChildNode, parent: targetChildParent } = getNodeWithParentById(targetId) || {} + + if (targetChildNode && targetChildParent) { + const targetNodeData = { + parent: toRaw(targetChildParent), + node: toRaw(targetChildNode), + data: op.targetNodeData.data + } + + // 使用正确的位置和目标节点插入 + insertNode(targetNodeData, op.position, false) + return + } + } + + // 如果没有特定位置或找不到目标子节点,则默认插入到body内部 + insertNode({ node: getSchema(), data: op.targetNodeData.data }, POSITION.IN, false) + } else { + insertNode(op.targetNodeData, op.position, false) + } + }) + + // 更新画布历史 + getController().addHistory() + + // 延迟执行,确保DOM已更新 + setTimeout(() => { + // 重建多选状态 + const newMultiSelection = [] + + // 收集所有操作后的节点ID + const newNodeIds = operations.map((op) => op.targetNodeData.data.id) + + // 构建新的多选状态 + newNodeIds.forEach((nodeId) => { + const element = querySelectById(nodeId) + if (element) { + const { node } = useCanvas().getNodeWithParentById(nodeId) || {} + if (!node) return + + const state = { + id: nodeId, + componentName: element.getAttribute(NODE_TAG), + schema: node + } + + const rect = element.getBoundingClientRect() + Object.assign(state, { + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height, + doc: getDocument() + }) + + newMultiSelection.push(state) + } + }) + + // 同步节点滚动位置 + syncNodeScroll() + }, 100) + } + } + } + + // 清理拖拽状态,但保留多选状态 + setTimeout(() => { + // 只清理拖拽相关状态,不清理多选状态 + Object.assign(multiDragState, { + ...initialMultiDragState, + nodes: [] // 确保清空nodes数组,避免影响后续操作 + }) + + // 清除单选拖动状态 + Object.assign(dragState, initialDragState) + }, 150) + + return true + } + + // 判断是否处于多选拖拽状态 + const isMultiDragging = () => { + return multiDragState.draging && multiDragState.dragStarted && multiStateLength.value > 1 + } + + // 获取多选拖拽的位置描述 + const getMultiDragPositionText = computed(() => { + if (!isMultiDragging()) return '' + + const { position, forbidden, id } = lineState + + // 获取目标节点的组件名称,用于更详细的提示 + let targetComponentName = '' + if (id && id !== 'body') { + const targetElement = querySelectById(id) + if (targetElement) { + targetComponentName = targetElement.getAttribute(NODE_TAG) || '' + } + } else if (id === 'body') { + targetComponentName = '页面' + } + + if (forbidden) { + return `当前位置不允许放置 (${targetComponentName || '目标节点'})` + } + + switch (position) { + case 'top': + return `放置到 ${targetComponentName || '目标节点'} 上方` + case 'bottom': + return `放置到 ${targetComponentName || '目标节点'} 下方` + case 'left': + return `放置到 ${targetComponentName || '目标节点'} 左侧` + case 'right': + return `放置到 ${targetComponentName || '目标节点'} 右侧` + case 'in': + return `放置到 ${targetComponentName || '容器'} 内部` + default: + return '' + } + }) + + return { + multiDragState, + getMultiDragPositionText, + startMultiDrag, + moveMultiDrag, + endMultiDrag, + isMultiDragging + } +} diff --git a/packages/canvas/container/src/composables/useMultiSelect.ts b/packages/canvas/container/src/composables/useMultiSelect.ts index 3d645806b5..a40470f575 100644 --- a/packages/canvas/container/src/composables/useMultiSelect.ts +++ b/packages/canvas/container/src/composables/useMultiSelect.ts @@ -18,6 +18,9 @@ export interface MultiSelectedState { const multiSelectedStates = ref([]) export const useMultiSelect = () => { + + const isMouseDown = ref(false) + /** * 添加state到多选列表 * @param selectState @@ -32,8 +35,8 @@ export const useMultiSelect = () => { // 多选 if (isMultiple) { const isExistNode = multiSelectedStates.value.some((state) => state.id === selectState.id) - // 如果多选列表已经存在选中的state,则将选中的state移出多选列表 - if (isExistNode) { + // 如果多选列表已经存在选中的state且鼠标抬起,则将选中的state移出多选列表 + if (isExistNode && !isMouseDown.value) { multiSelectedStates.value = multiSelectedStates.value.filter((state) => state.id !== selectState.id) } else { multiSelectedStates.value = multiSelectedStates.value.concat(selectState) @@ -71,6 +74,7 @@ export const useMultiSelect = () => { return { multiSelectedStates, + isMouseDown, toggleMultiSelection, refreshSelectionState, clearMultiSelection diff --git a/packages/canvas/container/src/container.ts b/packages/canvas/container/src/container.ts index b2611cff97..53eb018a03 100644 --- a/packages/canvas/container/src/container.ts +++ b/packages/canvas/container/src/container.ts @@ -47,7 +47,7 @@ export const POSITION = Object.freeze({ OUT: 'out' }) -const initialDragState = { +export const initialDragState = { keydown: false, draging: false, data: null as Node | null, From fff1549495ea9f94dd8fb1925b8b0b254c3610fd Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Thu, 17 Apr 2025 05:36:57 -0700 Subject: [PATCH 02/10] fix: enhancing typescript --- .../{useMultiDrag.js => useMultiDrag.ts} | 133 +++++++++++++----- packages/canvas/container/src/container.ts | 20 +-- 2 files changed, 112 insertions(+), 41 deletions(-) rename packages/canvas/container/src/composables/{useMultiDrag.js => useMultiDrag.ts} (86%) diff --git a/packages/canvas/container/src/composables/useMultiDrag.js b/packages/canvas/container/src/composables/useMultiDrag.ts similarity index 86% rename from packages/canvas/container/src/composables/useMultiDrag.js rename to packages/canvas/container/src/composables/useMultiDrag.ts index 4328e8a10a..52d26e73b0 100644 --- a/packages/canvas/container/src/composables/useMultiDrag.js +++ b/packages/canvas/container/src/composables/useMultiDrag.ts @@ -1,4 +1,6 @@ import { reactive, computed, toRaw } from 'vue' +import type { ComputedRef } from 'vue' +import type { PositionType } from '../container' import { useMultiSelect } from './useMultiSelect' import { useCanvas } from '@opentiny/tiny-engine-meta-register' import { NODE_TAG, NODE_UID } from '../../../common' @@ -19,13 +21,66 @@ import { getDocument } from '../container' -const initialMultiDragState = { +interface Position { + x: number + y: number +} + +interface Offset { + offsetX: number + offsetY: number + initialX: number + initialY: number +} + +interface NodeSchema { + id: string + componentName: string + children?: NodeSchema[] + [key: string]: any +} + +interface MultiDragState { + keydown: boolean + draging: boolean + dragStarted: boolean + initialMousePos: Position | null + nodes: NodeSchema[] + offsets: Map + mouse: Position | null + position: PositionType | null + targetNodeId: string | null +} + +interface SelectState { + id: string + componentName: string + schema: NodeSchema + top?: number + left?: number + width?: number + height?: number + doc?: Document + [key: string]: any +} + +interface InsertOperation { + sourceId: string + targetNodeData: { + parent: NodeSchema | null + node: NodeSchema + data: NodeSchema + } + position: PositionType +} + +const initialMultiDragState: MultiDragState = { keydown: false, draging: false, dragStarted: false, // 标记是否已经开始拖拽 initialMousePos: null, // 初始鼠标位置 nodes: [], // 存储被拖拽的多个节点信息 - offsets: new Map(), // 存储每个节点的偏移量 + offsets: new Map(), // 存储每个节点的偏移量 mouse: null, // 鼠标位置 position: null, // 放置位置 targetNodeId: null // 当前点击的节点ID @@ -35,16 +90,17 @@ const initialMultiDragState = { const DRAG_THRESHOLD = 5 export const useMultiDrag = () => { - const multiDragState = reactive({ ...initialMultiDragState }) - const { multiSelectedStates, multiStateLength } = useMultiSelect() + const multiDragState = reactive({ ...initialMultiDragState }) + const { multiSelectedStates } = useMultiSelect() + const multiStateLength = computed(() => (multiSelectedStates.value as SelectState[]).length) // 准备拖拽 - 仅记录初始状态,不立即开始拖拽 - const startMultiDrag = (event, element) => { + const startMultiDrag = (event: MouseEvent, element: HTMLElement): boolean => { if (multiStateLength.value <= 1) return false // 检查点击的元素是否是已选中的节点之一 const clickedNodeId = element?.getAttribute(NODE_UID) - if (!clickedNodeId || !multiSelectedStates.value.some((state) => state.id === clickedNodeId)) { + if (!clickedNodeId || !(multiSelectedStates.value as SelectState[]).some((state) => state.id === clickedNodeId)) { return false } @@ -54,10 +110,10 @@ export const useMultiDrag = () => { multiDragState.draging = false multiDragState.initialMousePos = { x: clientX, y: clientY } multiDragState.targetNodeId = clickedNodeId - multiDragState.nodes = toRaw(multiSelectedStates.value).map((state) => state.schema) + multiDragState.nodes = toRaw(multiSelectedStates.value as SelectState[]).map((state) => state.schema) // 计算每个节点相对于鼠标的偏移量 - multiSelectedStates.value.forEach((state) => { + ;(multiSelectedStates.value as SelectState[]).forEach((state) => { const elem = querySelectById(state.id) if (elem) { const { x, y } = elem.getBoundingClientRect() @@ -74,7 +130,11 @@ export const useMultiDrag = () => { } // 计算放置位置 - const calculateDropPosition = (event, rect, configure) => { + const calculateDropPosition = ( + event: MouseEvent, + rect: DOMRect, + configure: { isContainer?: boolean } | null + ): PositionType => { const { clientX: mouseX, clientY: mouseY } = event // 参考单选节点的实现,使用更精确的计算方式 const yAbs = Math.min(20, rect.height / 3) @@ -99,7 +159,7 @@ export const useMultiDrag = () => { } // 计算鼠标移动距离 - const calculateDistance = (pos1, pos2) => { + const calculateDistance = (pos1: Position | null, pos2: Position | null): number => { if (!pos1 || !pos2) return 0 const dx = pos1.x - pos2.x const dy = pos1.y - pos2.y @@ -107,7 +167,12 @@ export const useMultiDrag = () => { } // 检查是否允许放置 - const checkAllowInsert = (configure, nodes, targetId, position) => { + const checkAllowInsert = ( + configure: { isContainer?: boolean } | null, + nodes: NodeSchema[], + targetId: string, + position: PositionType + ): boolean => { // 如果没有配置,不允许放置 if (!configure) return false @@ -183,11 +248,11 @@ export const useMultiDrag = () => { } // 拖拽移动 - const moveMultiDrag = (event) => { + const moveMultiDrag = (event: MouseEvent): boolean => { if (!multiDragState.keydown || multiStateLength.value <= 1) return false const { clientX, clientY } = event - const currentMousePos = { x: clientX, y: clientY } + const currentMousePos: Position = { x: clientX, y: clientY } // 如果拖拽还未开始,检查是否超过阈值 if (!multiDragState.dragStarted) { @@ -217,7 +282,7 @@ export const useMultiDrag = () => { return false } - const targetElement = getElement(event.target) + const targetElement = getElement(event.target as HTMLElement) // 特殊处理:如果没有找到目标元素,检查是否是body元素或其直接子元素 if (!targetElement) { @@ -226,7 +291,11 @@ export const useMultiDrag = () => { const body = doc.body // 如果鼠标在body区域内,则视为拖拽到body - if (event.target === body || event.target.parentElement === body || event.target === doc.documentElement) { + if ( + event.target === body || + (event.target as HTMLElement).parentElement === body || + event.target === doc.documentElement + ) { // 获取body中的所有顶级节点 const { getSchema } = useCanvas() const bodySchema = getSchema() @@ -252,9 +321,9 @@ export const useMultiDrag = () => { const { clientY } = event // 遍历body的直接子节点,找到最接近鼠标位置的节点 - let closestNode = null + let closestNode: HTMLElement | null = null let closestDistance = Infinity - let position = POSITION.IN // 默认放置到body内部 + let position: PositionType = POSITION.IN // 默认放置到body内部 for (const childSchema of bodyChildren) { const childElement = querySelectById(childSchema.id) @@ -283,7 +352,7 @@ export const useMultiDrag = () => { const rect = closestNode.getBoundingClientRect() // 检查是否允许放置 - const isForbidden = !checkAllowInsert(configure, multiDragState.nodes, nodeId, position) + const isForbidden = !checkAllowInsert(configure, multiDragState.nodes, nodeId!, position) // 更新lineState Object.assign(lineState, { @@ -341,7 +410,7 @@ export const useMultiDrag = () => { if (parent) { // 根据放置位置调整目标节点 const children = parent.children || [] - const targetIndex = children.findIndex((child) => child.id === targetId) + const targetIndex = children.findIndex((child: NodeSchema) => child.id === targetId) // 如果是放置到节点下方,使用下一个兄弟节点作为目标 if ((position === POSITION.BOTTOM || position === POSITION.RIGHT) && targetIndex < children.length - 1) { @@ -454,7 +523,7 @@ export const useMultiDrag = () => { } // 结束拖拽 - const endMultiDrag = () => { + const endMultiDrag = (): boolean => { // 只有真正开始拖拽后才处理放置逻辑 if (!multiDragState.draging || !multiDragState.dragStarted || multiStateLength.value <= 1) { // 重置状态 @@ -475,7 +544,7 @@ export const useMultiDrag = () => { if (finalTargetNode) { // 创建一个操作批次,以便能够一次性添加历史记录 - const operations = [] + const operations: InsertOperation[] = [] // 收集要移动的节点ID,用于后续检查 const movingNodeIds = multiDragState.nodes.map((node) => node.id) @@ -512,7 +581,7 @@ export const useMultiDrag = () => { operations.push({ sourceId, targetNodeData, - position + position: position as PositionType }) }) @@ -560,7 +629,7 @@ export const useMultiDrag = () => { // 延迟执行,确保DOM已更新 setTimeout(() => { // 重建多选状态 - const newMultiSelection = [] + const newMultiSelection: SelectState[] = [] // 收集所有操作后的节点ID const newNodeIds = operations.map((op) => op.targetNodeData.data.id) @@ -572,9 +641,9 @@ export const useMultiDrag = () => { const { node } = useCanvas().getNodeWithParentById(nodeId) || {} if (!node) return - const state = { + const state: SelectState = { id: nodeId, - componentName: element.getAttribute(NODE_TAG), + componentName: element.getAttribute(NODE_TAG) || '', schema: node } @@ -614,12 +683,12 @@ export const useMultiDrag = () => { } // 判断是否处于多选拖拽状态 - const isMultiDragging = () => { + const isMultiDragging = (): boolean => { return multiDragState.draging && multiDragState.dragStarted && multiStateLength.value > 1 } // 获取多选拖拽的位置描述 - const getMultiDragPositionText = computed(() => { + const getMultiDragPositionText: ComputedRef = computed(() => { if (!isMultiDragging()) return '' const { position, forbidden, id } = lineState @@ -640,15 +709,15 @@ export const useMultiDrag = () => { } switch (position) { - case 'top': + case POSITION.TOP: return `放置到 ${targetComponentName || '目标节点'} 上方` - case 'bottom': + case POSITION.BOTTOM: return `放置到 ${targetComponentName || '目标节点'} 下方` - case 'left': + case POSITION.LEFT: return `放置到 ${targetComponentName || '目标节点'} 左侧` - case 'right': + case POSITION.RIGHT: return `放置到 ${targetComponentName || '目标节点'} 右侧` - case 'in': + case POSITION.IN: return `放置到 ${targetComponentName || '容器'} 内部` default: return '' diff --git a/packages/canvas/container/src/container.ts b/packages/canvas/container/src/container.ts index 53eb018a03..867fed71ef 100644 --- a/packages/canvas/container/src/container.ts +++ b/packages/canvas/container/src/container.ts @@ -38,14 +38,16 @@ export interface DragOffset { y: number } +export type PositionType = 'top' | 'bottom' | 'left' | 'right' | 'in' | 'out' + export const POSITION = Object.freeze({ - TOP: 'top', - BOTTOM: 'bottom', - LEFT: 'left', - RIGHT: 'right', - IN: 'in', - OUT: 'out' -}) + TOP: 'top' as PositionType, + BOTTOM: 'bottom' as PositionType, + LEFT: 'left' as PositionType, + RIGHT: 'right' as PositionType, + IN: 'in' as PositionType, + OUT: 'out' as PositionType +} as const) export const initialDragState = { keydown: false, @@ -506,7 +508,7 @@ export const allowInsert = (configure: any = hoverState.configure || {}, data: N return flag } -const isAncestor = (ancestor: string | Node, descendant: string | Node) => { +export const isAncestor = (ancestor: string | Node, descendant: string | Node) => { const ancestorId = typeof ancestor === 'string' ? ancestor : ancestor.id let descendantId = typeof descendant === 'string' ? descendant : descendant.id @@ -848,7 +850,7 @@ export const hoverNode = (id: string, data: Node) => { export const insertNode = ( node: { node: Node; parent: Node; data: Node }, - position: string = POSITION.IN, + position: PositionType = POSITION.IN, select = true ) => { if (!node.parent) { From 00e32f017cda31deb200d7aceb674bed6e81241e Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Mon, 21 Apr 2025 05:50:52 -0700 Subject: [PATCH 03/10] fix: review suggestion --- packages/canvas/container/src/CanvasContainer.vue | 4 ++-- packages/canvas/container/src/composables/useMultiDrag.ts | 6 +++--- packages/canvas/container/src/container.ts | 4 +--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/canvas/container/src/CanvasContainer.vue b/packages/canvas/container/src/CanvasContainer.vue index 7adace711d..4b636d2e74 100644 --- a/packages/canvas/container/src/CanvasContainer.vue +++ b/packages/canvas/container/src/CanvasContainer.vue @@ -8,7 +8,7 @@ :windowGetClickEventTarget="target" :resize="canvasState.type === 'absolute'" :multiStateLength="multiStateLength" - :isMultiDragging="isMultiDragging()" + :isMultiDragging="isMultiDragging" @select-slot="selectSlot" @setting="settingModel" > @@ -17,7 +17,7 @@ :lineState="lineState" :multiDragState="multiDragState" :multiStateLength="multiStateLength" - :isMultiDragging="isMultiDragging()" + :isMultiDragging="isMultiDragging" :getMultiDragPositionText="getMultiDragPositionText" > diff --git a/packages/canvas/container/src/composables/useMultiDrag.ts b/packages/canvas/container/src/composables/useMultiDrag.ts index 52d26e73b0..120b74db22 100644 --- a/packages/canvas/container/src/composables/useMultiDrag.ts +++ b/packages/canvas/container/src/composables/useMultiDrag.ts @@ -683,13 +683,13 @@ export const useMultiDrag = () => { } // 判断是否处于多选拖拽状态 - const isMultiDragging = (): boolean => { + const isMultiDragging = computed(() => { return multiDragState.draging && multiDragState.dragStarted && multiStateLength.value > 1 - } + }) // 获取多选拖拽的位置描述 const getMultiDragPositionText: ComputedRef = computed(() => { - if (!isMultiDragging()) return '' + if (!isMultiDragging.value) return '' const { position, forbidden, id } = lineState diff --git a/packages/canvas/container/src/container.ts b/packages/canvas/container/src/container.ts index 867fed71ef..f05215bf10 100644 --- a/packages/canvas/container/src/container.ts +++ b/packages/canvas/container/src/container.ts @@ -20,7 +20,7 @@ import { NODE_LOOP, NODE_INACTIVE_UID } from '../../common' -import { useCanvas, useLayout, useTranslate, useMaterial } from '@opentiny/tiny-engine-meta-register' +import { useCanvas, useTranslate, useMaterial } from '@opentiny/tiny-engine-meta-register' import { utils } from '@opentiny/tiny-engine-utils' import { isVsCodeEnv } from '@opentiny/tiny-engine-common/js/environments' import Builtin from '../../render/src/builtin/builtin.json' //TODO 画布内外应该分开 @@ -624,8 +624,6 @@ const setHoverRect = (element?: Element, data?: Node | null) => { forbidden: posLine.forbidden }) } - - useLayout().closePlugin() } // 设置元素hover状态 From f9da5267c2aa7594d2f067a92ced565c06acb855 Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Tue, 22 Apr 2025 05:59:57 -0700 Subject: [PATCH 04/10] fix: optimize multi-node-drag call time --- .../canvas/container/src/CanvasContainer.vue | 26 ++++++++++++++----- .../container/src/composables/useMultiDrag.ts | 20 +++++++++++--- packages/canvas/container/src/container.ts | 16 ++++++------ 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/packages/canvas/container/src/CanvasContainer.vue b/packages/canvas/container/src/CanvasContainer.vue index 4b636d2e74..d87e522a20 100644 --- a/packages/canvas/container/src/CanvasContainer.vue +++ b/packages/canvas/container/src/CanvasContainer.vue @@ -154,8 +154,14 @@ export default { let node = getCurrent().schema if (element) { - const currentElement = querySelectById(getCurrent().schema?.id) + // 首先尝试处理多选拖拽开始 + // 只有在满足多选条件的情况下才会返回true并阻止后续的选择操作 + if (startMultiDrag(event, element)) { + return + } + // 只有当不是多选拖拽的情况下,才进行选择操作 + const currentElement = querySelectById(getCurrent().schema?.id) if (!currentElement?.contains(element) || event.button === 0) { const isCtrlKey = event.ctrlKey || event.metaKey const loopId = element.getAttribute(NODE_LOOP) @@ -166,11 +172,6 @@ export default { } } - // 处理多选拖拽开始 - 确保在单节点拖拽之前处理 - if (startMultiDrag(event, element)) { - return - } - // 处理单节点拖拽开始 if (event.button === 0 && element !== element.ownerDocument.body) { const { x, y } = element.getBoundingClientRect() @@ -278,6 +279,19 @@ export default { handleCanvasEvent(() => { if (ev.button === 0 && isMouseDown.value) { isMouseDown.value = false + + // 判断是否需要切换到单选状态 + // 只有当点击多选节点但没有拖动时,才需要切换到单选状态 + if (multiDragState.keydown && !multiDragState.dragStarted && multiStateLength.value > 1) { + const element = getElement(ev.target) + if (element) { + const clickedNodeId = element?.getAttribute(NODE_UID) + // 只有点击的是多选节点中的一个时才切换到单选 + if (clickedNodeId && multiSelectedStates.value.some((state) => state.id === clickedNodeId)) { + selectNode(clickedNodeId) + } + } + } } // 优先处理多选拖拽结束 diff --git a/packages/canvas/container/src/composables/useMultiDrag.ts b/packages/canvas/container/src/composables/useMultiDrag.ts index 120b74db22..09529d0785 100644 --- a/packages/canvas/container/src/composables/useMultiDrag.ts +++ b/packages/canvas/container/src/composables/useMultiDrag.ts @@ -260,7 +260,7 @@ export const useMultiDrag = () => { // 如果移动距离小于阈值,不触发拖拽 if (distance < DRAG_THRESHOLD) { - return false + return true // 返回true表示已处理,但不启动拖拽 } // 超过阈值,标记拖拽已开始 @@ -279,7 +279,7 @@ export const useMultiDrag = () => { // 如果没有真正开始拖拽,不处理后续逻辑 if (!multiDragState.draging) { - return false + return true } const targetElement = getElement(event.target as HTMLElement) @@ -524,8 +524,22 @@ export const useMultiDrag = () => { // 结束拖拽 const endMultiDrag = (): boolean => { + // 检查是否处于多选状态 + if (multiStateLength.value <= 1) { + // 重置状态但不做其他处理 + Object.assign(multiDragState, initialMultiDragState) + return false + } + + // 检查是否按下了鼠标但没有拖拽 + if (!multiDragState.draging && !multiDragState.dragStarted && multiDragState.keydown) { + // 鼠标按下但没有拖拽,重置状态 + Object.assign(multiDragState, initialMultiDragState) + return true // 返回true表示已处理 + } + // 只有真正开始拖拽后才处理放置逻辑 - if (!multiDragState.draging || !multiDragState.dragStarted || multiStateLength.value <= 1) { + if (!multiDragState.draging || !multiDragState.dragStarted) { // 重置状态 Object.assign(multiDragState, initialMultiDragState) return false diff --git a/packages/canvas/container/src/container.ts b/packages/canvas/container/src/container.ts index f05215bf10..8662362a02 100644 --- a/packages/canvas/container/src/container.ts +++ b/packages/canvas/container/src/container.ts @@ -38,17 +38,17 @@ export interface DragOffset { y: number } -export type PositionType = 'top' | 'bottom' | 'left' | 'right' | 'in' | 'out' - export const POSITION = Object.freeze({ - TOP: 'top' as PositionType, - BOTTOM: 'bottom' as PositionType, - LEFT: 'left' as PositionType, - RIGHT: 'right' as PositionType, - IN: 'in' as PositionType, - OUT: 'out' as PositionType + TOP: 'top', + BOTTOM: 'bottom', + LEFT: 'left', + RIGHT: 'right', + IN: 'in', + OUT: 'out' } as const) +export type PositionType = typeof POSITION[keyof typeof POSITION] + export const initialDragState = { keydown: false, draging: false, From 28a03b4e62d2d8065c3898cb2dea80746e48bd53 Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Tue, 22 Apr 2025 18:33:01 -0700 Subject: [PATCH 05/10] fix: optimize insertion node order --- packages/canvas/container/src/composables/useMultiDrag.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/canvas/container/src/composables/useMultiDrag.ts b/packages/canvas/container/src/composables/useMultiDrag.ts index 09529d0785..f85362b382 100644 --- a/packages/canvas/container/src/composables/useMultiDrag.ts +++ b/packages/canvas/container/src/composables/useMultiDrag.ts @@ -606,8 +606,8 @@ export const useMultiDrag = () => { removeNode(op.sourceId) }) - // 然后插入所有节点到目标位置 - operations.forEach((op) => { + // 然后按照原始顺序插入所有节点到目标位置 + operations.reverse().forEach((op) => { // 对于body特殊处理 if (isBodyTarget) { // 判断是否是放置到body内的特定位置(TOP或BOTTOM) From 41434d47538148d1438957bd7d219c19f289e43f Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Tue, 22 Apr 2025 20:28:58 -0700 Subject: [PATCH 06/10] fix: refactor endMultiDrag method and fix insert node order --- .../container/src/composables/useMultiDrag.ts | 328 +++++++++++------- 1 file changed, 208 insertions(+), 120 deletions(-) diff --git a/packages/canvas/container/src/composables/useMultiDrag.ts b/packages/canvas/container/src/composables/useMultiDrag.ts index f85362b382..7e3d43e884 100644 --- a/packages/canvas/container/src/composables/useMultiDrag.ts +++ b/packages/canvas/container/src/composables/useMultiDrag.ts @@ -522,8 +522,8 @@ export const useMultiDrag = () => { return true } - // 结束拖拽 - const endMultiDrag = (): boolean => { + // 检查是否应该处理拖拽 + const shouldProcessDrag = (): boolean => { // 检查是否处于多选状态 if (multiStateLength.value <= 1) { // 重置状态但不做其他处理 @@ -545,142 +545,196 @@ export const useMultiDrag = () => { return false } - const { position, forbidden, id: targetId } = lineState + return true + } - if (!forbidden && targetId) { - const { getNodeWithParentById, getSchema } = useCanvas() - const { node: targetNode, parent: targetParent } = getNodeWithParentById(targetId) || {} - const isBodyTarget = targetId === 'body' + // 获取目标节点信息 + const getTargetNodeInfo = (targetId: string) => { + const { getNodeWithParentById, getSchema } = useCanvas() + const { node: targetNode, parent: targetParent } = getNodeWithParentById(targetId) || {} + const isBodyTarget = targetId === 'body' + + // 如果目标是body,使用页面schema作为目标节点 + const finalTargetNode = isBodyTarget ? getSchema() : targetNode + const finalTargetParent = isBodyTarget ? null : targetParent + + return { + targetNode, + targetParent, + isBodyTarget, + finalTargetNode, + finalTargetParent + } + } - // 如果目标是body,使用页面schema作为目标节点 - const finalTargetNode = isBodyTarget ? getSchema() : targetNode - const finalTargetParent = isBodyTarget ? null : targetParent + // 收集拖拽操作 + const collectDragOperations = (targetInfo: any, position: PositionType | null): InsertOperation[] => { + const { finalTargetNode, finalTargetParent } = targetInfo + const targetId = lineState.id as string + const operations: InsertOperation[] = [] - if (finalTargetNode) { - // 创建一个操作批次,以便能够一次性添加历史记录 - const operations: InsertOperation[] = [] + // 收集要移动的节点ID,用于后续检查 + const movingNodeIds = multiDragState.nodes.map((node) => node.id) - // 收集要移动的节点ID,用于后续检查 - const movingNodeIds = multiDragState.nodes.map((node) => node.id) + // 按照拖拽顺序依次插入节点 + multiDragState.nodes.forEach((node) => { + const sourceId = node.id + const { node: sourceNode, parent: sourceParent } = useCanvas().getNodeWithParentById(sourceId) || {} - // 按照拖拽顺序依次插入节点 - multiDragState.nodes.forEach((node) => { - const sourceId = node.id - const { node: sourceNode, parent: sourceParent } = getNodeWithParentById(sourceId) || {} + // 跳过目标节点自身 + if (sourceId === targetId) { + return + } - // 跳过目标节点自身 - if (sourceId === targetId) { - return - } + // 如果源节点的父节点是目标节点,且放置位置是IN,则跳过(避免循环引用) + if (position === POSITION.IN && sourceParent?.id === targetId) { + return + } - // 如果源节点的父节点是目标节点,且放置位置是IN,则跳过(避免循环引用) - if (position === POSITION.IN && sourceParent?.id === targetId) { - return - } + // 如果目标节点的父节点是正在移动的节点之一,且不是放置到容器内,则跳过 + if (position !== POSITION.IN && finalTargetParent && movingNodeIds.includes(finalTargetParent.id)) { + return + } - // 如果目标节点的父节点是正在移动的节点之一,且不是放置到容器内,则跳过 - if (position !== POSITION.IN && finalTargetParent && movingNodeIds.includes(finalTargetParent.id)) { - return - } + // 准备插入数据 + const insertData = { ...sourceNode } + const targetNodeData = { + parent: toRaw(finalTargetParent), + node: toRaw(finalTargetNode), + data: { ...insertData, children: insertData.children || [] } + } - // 准备插入数据 - const insertData = { ...sourceNode } - const targetNodeData = { - parent: toRaw(finalTargetParent), - node: toRaw(finalTargetNode), - data: { ...insertData, children: insertData.children || [] } - } + // 记录操作 + operations.push({ + sourceId, + targetNodeData, + position: position as PositionType + }) + }) - // 记录操作 - operations.push({ - sourceId, - targetNodeData, - position: position as PositionType - }) - }) + return operations + } - // 执行所有操作 - if (operations.length > 0) { - // 先移除所有源节点 - operations.forEach((op) => { - removeNode(op.sourceId) - }) + // 按DOM顺序排序操作 + const sortOperationsByDOMOrder = (operations: InsertOperation[]): InsertOperation[] => { + return [...operations].sort((a, b) => { + const elemA = querySelectById(a.sourceId) + const elemB = querySelectById(b.sourceId) - // 然后按照原始顺序插入所有节点到目标位置 - operations.reverse().forEach((op) => { - // 对于body特殊处理 - if (isBodyTarget) { - // 判断是否是放置到body内的特定位置(TOP或BOTTOM) - if (op.position === POSITION.TOP || op.position === POSITION.BOTTOM) { - // 这种情况下,targetId 实际上是 body 中的某个子节点的 ID - // 需要构建正确的目标节点数据 - const { getNodeWithParentById } = useCanvas() - const { node: targetChildNode, parent: targetChildParent } = getNodeWithParentById(targetId) || {} - - if (targetChildNode && targetChildParent) { - const targetNodeData = { - parent: toRaw(targetChildParent), - node: toRaw(targetChildNode), - data: op.targetNodeData.data - } - - // 使用正确的位置和目标节点插入 - insertNode(targetNodeData, op.position, false) - return - } - } - - // 如果没有特定位置或找不到目标子节点,则默认插入到body内部 - insertNode({ node: getSchema(), data: op.targetNodeData.data }, POSITION.IN, false) - } else { - insertNode(op.targetNodeData, op.position, false) - } - }) + if (!elemA || !elemB) return 0 - // 更新画布历史 - getController().addHistory() - - // 延迟执行,确保DOM已更新 - setTimeout(() => { - // 重建多选状态 - const newMultiSelection: SelectState[] = [] - - // 收集所有操作后的节点ID - const newNodeIds = operations.map((op) => op.targetNodeData.data.id) - - // 构建新的多选状态 - newNodeIds.forEach((nodeId) => { - const element = querySelectById(nodeId) - if (element) { - const { node } = useCanvas().getNodeWithParentById(nodeId) || {} - if (!node) return - - const state: SelectState = { - id: nodeId, - componentName: element.getAttribute(NODE_TAG) || '', - schema: node - } - - const rect = element.getBoundingClientRect() - Object.assign(state, { - top: rect.top, - left: rect.left, - width: rect.width, - height: rect.height, - doc: getDocument() - }) - - newMultiSelection.push(state) - } - }) + // compareDocumentPosition返回相对位置,按照节点在DOM中的顺序排序 + if (elemA.compareDocumentPosition) { + const position = elemA.compareDocumentPosition(elemB) + // Node.DOCUMENT_POSITION_FOLLOWING表示B在A之后 + if (position & Node.DOCUMENT_POSITION_FOLLOWING) { + return -1 + } + // Node.DOCUMENT_POSITION_PRECEDING表示B在A之前 + if (position & Node.DOCUMENT_POSITION_PRECEDING) { + return 1 + } + } + + return 0 + }) + } - // 同步节点滚动位置 - syncNodeScroll() - }, 100) + // 将节点插入到目标位置 + const insertNodeToTarget = (op: InsertOperation, isBodyTarget: boolean, targetId: string) => { + // 对于body特殊处理 + if (isBodyTarget) { + // 需要构建正确的目标节点数据 + const { getNodeWithParentById } = useCanvas() + const { node: targetChildNode, parent: targetChildParent } = getNodeWithParentById(targetId) || {} + + if (targetChildNode && targetChildParent) { + const targetNodeData = { + parent: toRaw(targetChildParent), + node: toRaw(targetChildNode), + data: op.targetNodeData.data } + + // 使用正确的位置和目标节点插入 + insertNode(targetNodeData, op.position, false) + return } + + // 如果没有特定位置或找不到目标子节点,则默认插入到body内部 + insertNode({ node: useCanvas().getSchema(), data: op.targetNodeData.data }, POSITION.IN, false) + } else { + insertNode(op.targetNodeData, op.position, false) } + } + // 更新多选状态 + const updateMultiSelectionAfterDrag = (operations: InsertOperation[]) => { + // 延迟执行,确保DOM已更新 + setTimeout(() => { + // 重建多选状态 + const newMultiSelection: SelectState[] = [] + + // 收集所有操作后的节点ID + const newNodeIds = operations.map((op) => op.targetNodeData.data.id) + + // 构建新的多选状态 + newNodeIds.forEach((nodeId) => { + const element = querySelectById(nodeId) + if (element) { + const { node } = useCanvas().getNodeWithParentById(nodeId) || {} + if (!node) return + + const state: SelectState = { + id: nodeId, + componentName: element.getAttribute(NODE_TAG) || '', + schema: node + } + + const rect = element.getBoundingClientRect() + Object.assign(state, { + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height, + doc: getDocument() + }) + + newMultiSelection.push(state) + } + }) + + // 同步节点滚动位置 + syncNodeScroll() + }, 100) + } + + // 执行拖拽操作 + const executeDragOperations = (operations: InsertOperation[], targetInfo: any) => { + const { isBodyTarget } = targetInfo + const targetId = lineState.id as string + + // 先移除所有源节点 + operations.forEach((op) => { + removeNode(op.sourceId) + }) + + // 按DOM顺序排序操作 + const sortedOperations = sortOperationsByDOMOrder(operations) + + // 然后按照原始相对顺序插入所有节点到目标位置 + sortedOperations.forEach((op) => { + insertNodeToTarget(op, isBodyTarget, targetId) + }) + + // 更新画布历史 + getController().addHistory() + + // 更新多选状态 + updateMultiSelectionAfterDrag(sortedOperations) + } + + // 清理拖拽状态 + const cleanupDragState = () => { // 清理拖拽状态,但保留多选状态 setTimeout(() => { // 只清理拖拽相关状态,不清理多选状态 @@ -692,6 +746,40 @@ export const useMultiDrag = () => { // 清除单选拖动状态 Object.assign(dragState, initialDragState) }, 150) + } + + // 结束拖拽 + const endMultiDrag = (): boolean => { + // 检查是否处于多选状态或是否真正开始拖拽 + if (!shouldProcessDrag()) { + return false + } + + const { position, forbidden, id: targetId } = lineState + + // 如果目标位置不允许放置或没有目标ID,直接返回 + if (forbidden || !targetId) { + cleanupDragState() + return true + } + + // 获取目标节点信息 + const targetInfo = getTargetNodeInfo(targetId) + if (!targetInfo.finalTargetNode) { + cleanupDragState() + return true + } + + // 收集拖拽操作 + const operations = collectDragOperations(targetInfo, position as PositionType) + + // 执行拖拽操作并更新选择状态 + if (operations.length > 0) { + executeDragOperations(operations, targetInfo) + } + + // 清理拖拽状态 + cleanupDragState() return true } From e47d0fc25268d231f272ca6a4ea1f607417dd2df Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Tue, 22 Apr 2025 22:38:31 -0700 Subject: [PATCH 07/10] fix: insert node order --- .../container/src/composables/useMultiDrag.ts | 100 +++++++++++++----- 1 file changed, 73 insertions(+), 27 deletions(-) diff --git a/packages/canvas/container/src/composables/useMultiDrag.ts b/packages/canvas/container/src/composables/useMultiDrag.ts index 7e3d43e884..198a42bf72 100644 --- a/packages/canvas/container/src/composables/useMultiDrag.ts +++ b/packages/canvas/container/src/composables/useMultiDrag.ts @@ -615,29 +615,55 @@ export const useMultiDrag = () => { return operations } - // 按DOM顺序排序操作 - const sortOperationsByDOMOrder = (operations: InsertOperation[]): InsertOperation[] => { - return [...operations].sort((a, b) => { - const elemA = querySelectById(a.sourceId) - const elemB = querySelectById(b.sourceId) - - if (!elemA || !elemB) return 0 - - // compareDocumentPosition返回相对位置,按照节点在DOM中的顺序排序 - if (elemA.compareDocumentPosition) { - const position = elemA.compareDocumentPosition(elemB) - // Node.DOCUMENT_POSITION_FOLLOWING表示B在A之后 - if (position & Node.DOCUMENT_POSITION_FOLLOWING) { - return -1 - } - // Node.DOCUMENT_POSITION_PRECEDING表示B在A之前 - if (position & Node.DOCUMENT_POSITION_PRECEDING) { - return 1 - } + // 计算节点之间的相对位置 + const calculateRelativePositions = (nodeIds: string[]): Map => { + const positions = new Map() + + // 获取所有节点的初始位置 + nodeIds.forEach((id) => { + const elem = querySelectById(id) + if (elem) { + const rect = elem.getBoundingClientRect() + positions.set(id, { + top: rect.top, + left: rect.left + }) + } + }) + + return positions + } + + // 按相对位置排序操作,并根据拖拽方向调整插入顺序 + const sortOperationsByPosition = ( + operations: InsertOperation[], + positions: Map, + position: PositionType + ): InsertOperation[] => { + // 根据节点的原始位置进行排序 + const sortedOperations = [...operations].sort((a, b) => { + const posA = positions.get(a.sourceId) + const posB = positions.get(b.sourceId) + + if (!posA || !posB) return 0 + + // 先按垂直位置排序 + if (Math.abs(posA.top - posB.top) > 5) { + return posA.top - posB.top } - return 0 + // 如果垂直位置接近,则按水平位置排序 + return posA.left - posB.left }) + + // 获取拖拽的目标位置,根据位置调整插入顺序 + if (position === POSITION.BOTTOM || position === POSITION.RIGHT) { + return sortedOperations.reverse() + } else if (position === POSITION.IN) { + return sortedOperations + } else { + return sortedOperations + } } // 将节点插入到目标位置 @@ -712,19 +738,39 @@ export const useMultiDrag = () => { const executeDragOperations = (operations: InsertOperation[], targetInfo: any) => { const { isBodyTarget } = targetInfo const targetId = lineState.id as string + const position = lineState.position as PositionType + + const nodeIds = operations.map((op) => op.sourceId) + + // 计算节点的初始相对位置 + const positions = calculateRelativePositions(nodeIds) + + // 按照原始相对位置排序操作,并根据拖拽方向调整顺序 + const sortedOperations = sortOperationsByPosition(operations, positions, position) // 先移除所有源节点 operations.forEach((op) => { removeNode(op.sourceId) }) - // 按DOM顺序排序操作 - const sortedOperations = sortOperationsByDOMOrder(operations) - - // 然后按照原始相对顺序插入所有节点到目标位置 - sortedOperations.forEach((op) => { - insertNodeToTarget(op, isBodyTarget, targetId) - }) + // 处理页面底部和容器的情况 + if (isBodyTarget && position === POSITION.BOTTOM) { + // 放置到页面底部,始终保持从上到下的顺序 + const reorderedOperations = [...sortedOperations].reverse() + reorderedOperations.forEach((op) => { + insertNodeToTarget(op, isBodyTarget, targetId) + }) + } else if (position === POSITION.IN) { + // 放置到容器内部,应该保持原始从上到下的顺序 + sortedOperations.forEach((op) => { + insertNodeToTarget(op, isBodyTarget, targetId) + }) + } else { + // 其他情况按照排序后的顺序插入 + sortedOperations.forEach((op) => { + insertNodeToTarget(op, isBodyTarget, targetId) + }) + } // 更新画布历史 getController().addHistory() From 4eee8d922739b9387b8c707a12b8421fbc478b84 Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Tue, 22 Apr 2025 23:24:41 -0700 Subject: [PATCH 08/10] fix: refactor moveMultiDrag method --- .../container/src/composables/useMultiDrag.ts | 443 ++++++++++-------- 1 file changed, 242 insertions(+), 201 deletions(-) diff --git a/packages/canvas/container/src/composables/useMultiDrag.ts b/packages/canvas/container/src/composables/useMultiDrag.ts index 198a42bf72..ca034d9844 100644 --- a/packages/canvas/container/src/composables/useMultiDrag.ts +++ b/packages/canvas/container/src/composables/useMultiDrag.ts @@ -247,20 +247,14 @@ export const useMultiDrag = () => { return true } - // 拖拽移动 - const moveMultiDrag = (event: MouseEvent): boolean => { - if (!multiDragState.keydown || multiStateLength.value <= 1) return false - - const { clientX, clientY } = event - const currentMousePos: Position = { x: clientX, y: clientY } - - // 如果拖拽还未开始,检查是否超过阈值 + // 初始化拖拽状态 + const initDragState = (currentMousePos: Position): boolean => { if (!multiDragState.dragStarted) { const distance = calculateDistance(multiDragState.initialMousePos, currentMousePos) // 如果移动距离小于阈值,不触发拖拽 if (distance < DRAG_THRESHOLD) { - return true // 返回true表示已处理,但不启动拖拽 + return false // 不启动拖拽 } // 超过阈值,标记拖拽已开始 @@ -270,194 +264,197 @@ export const useMultiDrag = () => { Object.assign(dragState, initialDragState) } - // 始终更新鼠标位置,确保拖拽预览能够跟随鼠标 - multiDragState.mouse = currentMousePos - if (!multiDragState.draging && multiDragState.dragStarted) { multiDragState.draging = true } - // 如果没有真正开始拖拽,不处理后续逻辑 - if (!multiDragState.draging) { + return multiDragState.draging + } + + // 处理 body 元素的放置逻辑 + const handleBodyPlacement = (event: MouseEvent, body: HTMLElement): boolean => { + // 获取body中的所有顶级节点 + const { getSchema } = useCanvas() + const bodySchema = getSchema() + const bodyChildren = bodySchema.children || [] + + // 如果body中没有子节点,直接放置到body内部 + if (bodyChildren.length === 0) { + const bodyRect = body.getBoundingClientRect() + Object.assign(lineState, { + id: 'body', + top: bodyRect.top, + left: bodyRect.left, + width: bodyRect.width, + height: bodyRect.height, + position: POSITION.IN, + forbidden: false, + configure: { isContainer: true } + }) return true } - const targetElement = getElement(event.target as HTMLElement) + // 如果body中有子节点,需要判断放置位置 + const { clientY } = event + let closestNode: HTMLElement | null = null + let closestDistance = Infinity + let position: PositionType = POSITION.IN // 默认放置到body内部 - // 特殊处理:如果没有找到目标元素,检查是否是body元素或其直接子元素 - if (!targetElement) { - // 检查是否是body元素或其直接子元素 - const doc = getDocument() - const body = doc.body + // 遍历body的直接子节点,找到最接近鼠标位置的节点 + for (const childSchema of bodyChildren) { + const childElement = querySelectById(childSchema.id) + if (!childElement) continue - // 如果鼠标在body区域内,则视为拖拽到body - if ( - event.target === body || - (event.target as HTMLElement).parentElement === body || - event.target === doc.documentElement - ) { - // 获取body中的所有顶级节点 - const { getSchema } = useCanvas() - const bodySchema = getSchema() - const bodyChildren = bodySchema.children || [] - - // 如果body中没有子节点,直接放置到body内部 - if (bodyChildren.length === 0) { - const bodyRect = body.getBoundingClientRect() - Object.assign(lineState, { - id: 'body', - top: bodyRect.top, - left: bodyRect.left, - width: bodyRect.width, - height: bodyRect.height, - position: POSITION.IN, - forbidden: false, - configure: { isContainer: true } - }) - return true - } - - // 如果body中有子节点,需要判断放置位置 - const { clientY } = event + const childRect = childElement.getBoundingClientRect() + const childMiddle = childRect.top + childRect.height / 2 - // 遍历body的直接子节点,找到最接近鼠标位置的节点 - let closestNode: HTMLElement | null = null - let closestDistance = Infinity - let position: PositionType = POSITION.IN // 默认放置到body内部 + // 计算鼠标与节点中点的距离 + const distance = Math.abs(clientY - childMiddle) - for (const childSchema of bodyChildren) { - const childElement = querySelectById(childSchema.id) - if (!childElement) continue + if (distance < closestDistance) { + closestDistance = distance + closestNode = childElement - const childRect = childElement.getBoundingClientRect() - const childMiddle = childRect.top + childRect.height / 2 + // 判断放置位置:在节点上方还是下方 + position = clientY < childMiddle ? POSITION.TOP : POSITION.BOTTOM + } + } - // 计算鼠标与节点中点的距离 - const distance = Math.abs(clientY - childMiddle) + // 如果找到了最近的节点 + if (closestNode) { + const nodeId = closestNode.getAttribute(NODE_UID) + const componentName = closestNode.getAttribute(NODE_TAG) + const configure = getConfigure(componentName) + const rect = closestNode.getBoundingClientRect() + + // 检查是否允许放置 + const isForbidden = !checkAllowInsert(configure, multiDragState.nodes, nodeId!, position) + + // 更新lineState + Object.assign(lineState, { + id: nodeId, + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height, + position: position, + forbidden: isForbidden, + configure + }) + } else { + // 如果没有找到合适的节点,放置到body内部 + const bodyRect = body.getBoundingClientRect() + Object.assign(lineState, { + id: 'body', + top: bodyRect.top, + left: bodyRect.left, + width: bodyRect.width, + height: bodyRect.height, + position: POSITION.IN, + forbidden: false, + configure: { isContainer: true } + }) + } - if (distance < closestDistance) { - closestDistance = distance - closestNode = childElement + return true + } - // 判断放置位置:在节点上方还是下方 - position = clientY < childMiddle ? POSITION.TOP : POSITION.BOTTOM - } - } + // 处理自身节点的拖拽 + const handleSelfNodeDrag = (targetId: string, rect: DOMRect, configure: any, position: PositionType): boolean => { + // 获取目标节点的父节点和兄弟节点 + const { getNodeWithParentById } = useCanvas() + const { parent } = getNodeWithParentById(targetId) || {} - // 如果找到了最近的节点 - if (closestNode) { - const nodeId = closestNode.getAttribute(NODE_UID) - const componentName = closestNode.getAttribute(NODE_TAG) - const configure = getConfigure(componentName) - const rect = closestNode.getBoundingClientRect() + if (!parent) { + lineState.forbidden = true + return true + } - // 检查是否允许放置 - const isForbidden = !checkAllowInsert(configure, multiDragState.nodes, nodeId!, position) + // 根据放置位置调整目标节点 + const children = parent.children || [] + const targetIndex = children.findIndex((child: NodeSchema) => child.id === targetId) + + // 如果是放置到节点下方,使用下一个兄弟节点作为目标 + if ((position === POSITION.BOTTOM || position === POSITION.RIGHT) && targetIndex < children.length - 1) { + const nextSibling = children[targetIndex + 1] + if (nextSibling && !multiDragState.nodes.some((node) => node.id === nextSibling.id)) { + // 使用下一个兄弟节点作为目标 + const nextElement = querySelectById(nextSibling.id) + if (nextElement) { + const nextRect = nextElement.getBoundingClientRect() + const nextComponentName = nextElement.getAttribute(NODE_TAG) + const nextConfigure = getConfigure(nextComponentName) // 更新lineState Object.assign(lineState, { - id: nodeId, - top: rect.top, - left: rect.left, - width: rect.width, - height: rect.height, - position: position, - forbidden: isForbidden, - configure - }) - } else { - // 如果没有找到合适的节点,放置到body内部 - const bodyRect = body.getBoundingClientRect() - Object.assign(lineState, { - id: 'body', - top: bodyRect.top, - left: bodyRect.left, - width: bodyRect.width, - height: bodyRect.height, - position: POSITION.IN, - forbidden: false, - configure: { isContainer: true } + id: nextSibling.id, + top: nextRect.top, + left: nextRect.left, + width: nextRect.width, + height: nextRect.height, + position: POSITION.TOP, // 放置到下一个节点的上方 + forbidden: !checkAllowInsert(nextConfigure, multiDragState.nodes, nextSibling.id, POSITION.TOP), + configure: nextConfigure }) - } - return true + return true + } } - - // 其他情况,设置为禁止放置 - lineState.position = '' - lineState.forbidden = true - return true } - // 更新放置位置指示器 - const componentName = targetElement.getAttribute(NODE_TAG) - const configure = getConfigure(componentName) - const rect = targetElement.getBoundingClientRect() - const targetId = targetElement.getAttribute(NODE_UID) || 'body' - - // 计算放置位置 - const position = calculateDropPosition(event, rect, configure) + // 如果是放置到节点上方,或者是最后一个节点的下方 + if ( + position === POSITION.TOP || + position === POSITION.LEFT || + (position === POSITION.BOTTOM && targetIndex === children.length - 1) || + (position === POSITION.RIGHT && targetIndex === children.length - 1) + ) { + // 检查是否允许放置 + const isForbidden = !checkAllowInsert(configure, multiDragState.nodes, targetId, position) + + // 更新lineState + Object.assign(lineState, { + id: targetId, + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height, + position: position, + forbidden: isForbidden, + configure + }) - // 检查是否是拖拽自身节点 - const isDraggingSelf = multiDragState.nodes.some((node) => node.id === targetId) + return true + } - // 如果是拖拽到自身节点,需要特殊处理 - if (isDraggingSelf && position !== POSITION.IN) { - // 获取目标节点的父节点和兄弟节点 - const { getNodeWithParentById } = useCanvas() - const { parent } = getNodeWithParentById(targetId) || {} - - if (parent) { - // 根据放置位置调整目标节点 - const children = parent.children || [] - const targetIndex = children.findIndex((child: NodeSchema) => child.id === targetId) - - // 如果是放置到节点下方,使用下一个兄弟节点作为目标 - if ((position === POSITION.BOTTOM || position === POSITION.RIGHT) && targetIndex < children.length - 1) { - const nextSibling = children[targetIndex + 1] - if (nextSibling && !multiDragState.nodes.some((node) => node.id === nextSibling.id)) { - // 使用下一个兄弟节点作为目标 - const nextElement = querySelectById(nextSibling.id) - if (nextElement) { - const nextRect = nextElement.getBoundingClientRect() - const nextComponentName = nextElement.getAttribute(NODE_TAG) - const nextConfigure = getConfigure(nextComponentName) - - // 更新lineState - Object.assign(lineState, { - id: nextSibling.id, - top: nextRect.top, - left: nextRect.left, - width: nextRect.width, - height: nextRect.height, - position: POSITION.TOP, // 放置到下一个节点的上方 - forbidden: !checkAllowInsert(nextConfigure, multiDragState.nodes, nextSibling.id, POSITION.TOP), - configure: nextConfigure - }) - - return true - } - } - } + // 默认情况下,禁止放置 + lineState.forbidden = true + return true + } - // 如果是放置到节点上方,或者是最后一个节点的下方 - if ( - position === POSITION.TOP || - position === POSITION.LEFT || - (position === POSITION.BOTTOM && targetIndex === children.length - 1) || - (position === POSITION.RIGHT && targetIndex === children.length - 1) - ) { - // 检查是否允许放置 - const isForbidden = !checkAllowInsert(configure, multiDragState.nodes, targetId, position) + // 处理容器内放置 + const handleContainerPlacement = (targetId: string, rect: DOMRect, configure: any, isForbidden: boolean): boolean => { + const { getNodeWithParentById, getSchema } = useCanvas() + const { node } = targetId === 'body' ? { node: getSchema() } : getNodeWithParentById(targetId) || {} + const children = node?.children || [] + + // 如果容器有子节点,考虑放置到最后一个子节点后面 + if (children.length > 0) { + const lastChild = children[children.length - 1] + // 如果最后一个子节点不是被拖拽的节点之一 + if (!multiDragState.nodes.some((node) => node.id === lastChild.id)) { + const childElement = querySelectById(lastChild.id) + if (childElement) { + const childRect = childElement.getBoundingClientRect() - // 更新lineState + // 更新lineState,显示在最后一个子节点下方 Object.assign(lineState, { - id: targetId, - top: rect.top, - left: rect.left, - width: rect.width, - height: rect.height, - position: position, + id: targetId, // 保持目标是容器 + top: childRect.top, + left: childRect.left, + width: childRect.width, + height: childRect.height, + position: POSITION.IN, // 仍然表示放置到容器内 forbidden: isForbidden, configure }) @@ -465,46 +462,32 @@ export const useMultiDrag = () => { return true } } - - // 默认情况下,禁止放置 - lineState.forbidden = true - return true } + // 更新lineState + Object.assign(lineState, { + id: targetId, + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height, + position: POSITION.IN, + forbidden: isForbidden, + configure + }) + + return true + } + + // 更新线框提示状态 + const updateLineFeedback = (targetId: string, rect: DOMRect, configure: any, position: PositionType): void => { // 检查是否允许放置 const isForbidden = !checkAllowInsert(configure, multiDragState.nodes, targetId, position) // 特殊处理容器内放置 if (position === POSITION.IN && configure?.isContainer) { - const { getNodeWithParentById, getSchema } = useCanvas() - const { node } = targetId === 'body' ? { node: getSchema() } : getNodeWithParentById(targetId) || {} - const children = node?.children || [] - - // 如果容器有子节点,考虑放置到最后一个子节点后面 - if (children.length > 0) { - const lastChild = children[children.length - 1] - // 如果最后一个子节点不是被拖拽的节点之一 - if (!multiDragState.nodes.some((node) => node.id === lastChild.id)) { - const childElement = querySelectById(lastChild.id) - if (childElement) { - const childRect = childElement.getBoundingClientRect() - - // 更新lineState,显示在最后一个子节点下方 - Object.assign(lineState, { - id: targetId, // 保持目标是容器 - top: childRect.top, - left: childRect.left, - width: childRect.width, - height: childRect.height, - position: POSITION.IN, // 仍然表示放置到容器内 - forbidden: isForbidden, - configure - }) - - return true - } - } - } + handleContainerPlacement(targetId, rect, configure, isForbidden) + return } // 更新lineState @@ -518,6 +501,64 @@ export const useMultiDrag = () => { forbidden: isForbidden, configure }) + } + + // 拖拽移动 + const moveMultiDrag = (event: MouseEvent): boolean => { + if (!multiDragState.keydown || multiStateLength.value <= 1) return false + + const { clientX, clientY } = event + const currentMousePos: Position = { x: clientX, y: clientY } + + // 更新鼠标位置 + multiDragState.mouse = currentMousePos + + // 初始化拖拽状态,检查是否应该开始拖拽 + if (!initDragState(currentMousePos)) { + return true // 返回true表示已处理,但不启动拖拽 + } + + const targetElement = getElement(event.target as HTMLElement) + + // 特殊处理:如果没有找到目标元素,检查是否是body元素或其直接子元素 + if (!targetElement) { + const doc = getDocument() + const body = doc.body + + // 如果鼠标在body区域内,则视为拖拽到body + if ( + event.target === body || + (event.target as HTMLElement).parentElement === body || + event.target === doc.documentElement + ) { + return handleBodyPlacement(event, body) + } + + // 其他情况,设置为禁止放置 + lineState.position = '' + lineState.forbidden = true + return true + } + + // 获取目标元素信息 + const componentName = targetElement.getAttribute(NODE_TAG) + const configure = getConfigure(componentName) + const rect = targetElement.getBoundingClientRect() + const targetId = targetElement.getAttribute(NODE_UID) || 'body' + + // 计算放置位置 + const position = calculateDropPosition(event, rect, configure) + + // 检查是否是拖拽自身节点 + const isDraggingSelf = multiDragState.nodes.some((node) => node.id === targetId) + + // 如果是拖拽到自身节点,需要特殊处理 + if (isDraggingSelf && position !== POSITION.IN) { + return handleSelfNodeDrag(targetId, rect, configure, position) + } + + // 更新线框提示状态 + updateLineFeedback(targetId, rect, configure, position) return true } @@ -791,7 +832,7 @@ export const useMultiDrag = () => { // 清除单选拖动状态 Object.assign(dragState, initialDragState) - }, 150) + }, 50) } // 结束拖拽 From 6bc0afdd4e80239cdcc3ebf2702d95d321c9b956 Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Thu, 24 Apr 2025 02:52:52 -0700 Subject: [PATCH 09/10] fix: review suggestion --- .../canvas/container/src/CanvasContainer.vue | 101 +++++++++++++++--- .../container/src/composables/useMultiDrag.ts | 15 +-- 2 files changed, 89 insertions(+), 27 deletions(-) diff --git a/packages/canvas/container/src/CanvasContainer.vue b/packages/canvas/container/src/CanvasContainer.vue index d87e522a20..a8150cbc97 100644 --- a/packages/canvas/container/src/CanvasContainer.vue +++ b/packages/canvas/container/src/CanvasContainer.vue @@ -124,11 +124,30 @@ export default { const containerPanel = ref(null) const insertContainer = ref(false) + const DRAG_TYPE = { + // 无拖拽 + NONE: 'none', + // 单选拖拽 + SINGLE: 'single', + // 多选拖拽 + MULTI: 'multi' + } + + // 当前拖拽类型状态 + const currentDragType = ref(DRAG_TYPE.NONE) + const { multiSelectedStates, isMouseDown } = useMultiSelect() const multiStateLength = computed(() => multiSelectedStates.value.length) - const { startMultiDrag, moveMultiDrag, endMultiDrag, isMultiDragging, getMultiDragPositionText, multiDragState } = - useMultiDrag() + const { + startMultiDrag, + moveMultiDrag, + endMultiDrag, + isMultiDragging, + getMultiDragPositionText, + multiDragState, + cleanupDragState + } = useMultiDrag() const computedSelectState = computed(() => { if (multiSelectedStates.value.length === 1) { @@ -138,6 +157,13 @@ export default { return initialRectState }) + // 强制清除所有拖拽指示状态 + const clearAllDragStates = () => { + clearLineState() + cleanupDragState() + currentDragType.value = DRAG_TYPE.NONE + } + const setCurrentNode = async (event) => { const { clientX, clientY } = event const element = getElement(event.target) @@ -155,8 +181,9 @@ export default { if (element) { // 首先尝试处理多选拖拽开始 - // 只有在满足多选条件的情况下才会返回true并阻止后续的选择操作 if (startMultiDrag(event, element)) { + // 设置为多选拖拽状态 + currentDragType.value = DRAG_TYPE.MULTI return } @@ -177,6 +204,8 @@ export default { const { x, y } = element.getBoundingClientRect() if (multiStateLength.value === 1) { dragStart(node, element, { offsetX: clientX - x, offsetY: clientY - y }) + // 设置为单选拖拽状态 + currentDragType.value = DRAG_TYPE.SINGLE } } } @@ -249,6 +278,8 @@ export default { } isMouseDown.value = true + // 重置拖拽状态 + currentDragType.value = DRAG_TYPE.NONE insertPosition.value = false insertContainer.value = false @@ -266,10 +297,26 @@ export default { // 监听鼠标移动事件 win.addEventListener('mousemove', (ev) => { handleCanvasEvent(() => { - // 优先处理多选拖拽移动 - if (!moveMultiDrag(ev)) { - // 如果不是多选拖拽,则处理普通拖拽 - dragMove(ev, true) + // 根据当前拖拽类型执行相应操作 + switch (currentDragType.value) { + case DRAG_TYPE.MULTI: + moveMultiDrag(ev) + break + case DRAG_TYPE.SINGLE: + dragMove(ev, true) + break + case DRAG_TYPE.NONE: + // 如果尚未确定拖拽类型,尝试确定 + if (isMouseDown.value) { + if (multiDragState.keydown) { + currentDragType.value = DRAG_TYPE.MULTI + moveMultiDrag(ev) + } else if (dragState.element) { + currentDragType.value = DRAG_TYPE.SINGLE + dragMove(ev, true) + } + } + break } }) }) @@ -294,11 +341,17 @@ export default { } } - // 优先处理多选拖拽结束 - if (!endMultiDrag()) { - // 如果不是多选拖拽,则处理普通拖拽结束 - onMouseUp(ev) + // 根据当前拖拽类型执行相应的结束操作 + switch (currentDragType.value) { + case DRAG_TYPE.MULTI: + endMultiDrag() + break + case DRAG_TYPE.SINGLE: + onMouseUp(ev) + break } + + clearAllDragStates() }) }) @@ -306,19 +359,33 @@ export default { win.addEventListener('dragover', (ev) => { ev.dataTransfer.dropEffect = 'move' ev.preventDefault() - // 优先处理多选拖拽移动 - if (!moveMultiDrag(ev)) { - dragMove(ev) + + // 根据当前拖拽类型执行相应操作 + switch (currentDragType.value) { + case DRAG_TYPE.MULTI: + moveMultiDrag(ev) + break + case DRAG_TYPE.SINGLE: + dragMove(ev) + break } }) // 监听放置事件 win.addEventListener('drop', (ev) => { ev.preventDefault() - // 优先处理多选拖拽结束 - if (!endMultiDrag()) { - onMouseUp(ev) + + // 根据当前拖拽类型执行相应的结束操作 + switch (currentDragType.value) { + case DRAG_TYPE.MULTI: + endMultiDrag() + break + case DRAG_TYPE.SINGLE: + onMouseUp(ev) + break } + + clearAllDragStates() }) // 阻止浏览器默认的右键菜单功能 diff --git a/packages/canvas/container/src/composables/useMultiDrag.ts b/packages/canvas/container/src/composables/useMultiDrag.ts index ca034d9844..b8383a655f 100644 --- a/packages/canvas/container/src/composables/useMultiDrag.ts +++ b/packages/canvas/container/src/composables/useMultiDrag.ts @@ -823,16 +823,10 @@ export const useMultiDrag = () => { // 清理拖拽状态 const cleanupDragState = () => { // 清理拖拽状态,但保留多选状态 - setTimeout(() => { - // 只清理拖拽相关状态,不清理多选状态 - Object.assign(multiDragState, { - ...initialMultiDragState, - nodes: [] // 确保清空nodes数组,避免影响后续操作 - }) - - // 清除单选拖动状态 - Object.assign(dragState, initialDragState) - }, 50) + Object.assign(multiDragState, { + ...initialMultiDragState, + nodes: [] + }) } // 结束拖拽 @@ -919,6 +913,7 @@ export const useMultiDrag = () => { startMultiDrag, moveMultiDrag, endMultiDrag, + cleanupDragState, isMultiDragging } } From 1100764b33d5ad949f69f4aaf00bf0c5cdae6528 Mon Sep 17 00:00:00 2001 From: SonyLeo <746591437@qq.com> Date: Thu, 24 Apr 2025 04:44:59 -0700 Subject: [PATCH 10/10] fix: review suggestion --- .../canvas/container/src/CanvasContainer.vue | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/packages/canvas/container/src/CanvasContainer.vue b/packages/canvas/container/src/CanvasContainer.vue index 559e979730..3b4a1d0360 100644 --- a/packages/canvas/container/src/CanvasContainer.vue +++ b/packages/canvas/container/src/CanvasContainer.vue @@ -368,13 +368,10 @@ export default { ev.preventDefault() // 根据当前拖拽类型执行相应操作 - switch (currentDragType.value) { - case DRAG_TYPE.MULTI: - moveMultiDrag(ev) - break - case DRAG_TYPE.SINGLE: - dragMove(ev) - break + if (currentDragType.value === DRAG_TYPE.MULTI) { + moveMultiDrag(ev) + } else { + dragMove(ev) } }) @@ -383,13 +380,10 @@ export default { ev.preventDefault() // 根据当前拖拽类型执行相应的结束操作 - switch (currentDragType.value) { - case DRAG_TYPE.MULTI: - endMultiDrag() - break - case DRAG_TYPE.SINGLE: - onMouseUp(ev) - break + if (currentDragType.value === DRAG_TYPE.MULTI) { + endMultiDrag() + } else { + onMouseUp(ev) } clearAllDragStates()