diff --git a/draggable-panels/src/fauto/materials/DesignCenter/DropZone.vue b/draggable-panels/src/fauto/materials/DesignCenter/DropZone.vue index 835274b..4b969a2 100644 --- a/draggable-panels/src/fauto/materials/DesignCenter/DropZone.vue +++ b/draggable-panels/src/fauto/materials/DesignCenter/DropZone.vue @@ -38,13 +38,17 @@ const isRowLayout = computed(() => { return dragStore.selectedNode?.type === 'er' }) +// 是否是跨类型拖放 +const isCrossType = computed(() => dragStore.isCrossTypeDrop) + // 获取方向文本 const getDirectionText = (direction: DropDirection): string => { const texts: Record = { 'top': '移动至上方', 'bottom': '移动至下方', 'left': '移动至左侧', - 'right': '移动至右侧' + 'right': '移动至右侧', + 'inside': '放入其中' } return texts[direction] } @@ -55,7 +59,8 @@ const getDirectionIcon = (direction: DropDirection): string => { 'top': '⬆', 'bottom': '⬇', 'left': '⬅', - 'right': '➡' + 'right': '➡', + 'inside': '⬇️⤵️' // 放入图标 } return icons[direction] } @@ -66,7 +71,11 @@ const getDirectionIcon = (direction: DropDirection): string => {
{ border-left: 2px solid rgba(64, 158, 255, 0.8); } +/* 放入内部区域 */ +.drop-zone-container.is-inside { + flex-direction: column; +} + +.zone-inside { + background: rgba(103, 194, 58, 0.15); + border: 3px dashed rgba(103, 194, 58, 0.6); + border-radius: 8px; +} + +.zone-inside.is-active { + background: rgba(103, 194, 58, 0.3); + border-color: rgba(103, 194, 58, 0.9); +} + +.zone-inside .zone-icon, +.zone-inside .zone-text { + color: #67c23a; +} + +.zone-inside.is-active .zone-icon, +.zone-inside.is-active .zone-text { + color: #85ce61; +} + .drop-zone:hover, .drop-zone.is-active { background: rgba(64, 158, 255, 0.25); diff --git a/draggable-panels/src/fauto/plugins/dragStore.ts b/draggable-panels/src/fauto/plugins/dragStore.ts index 06d066f..3883b9b 100644 --- a/draggable-panels/src/fauto/plugins/dragStore.ts +++ b/draggable-panels/src/fauto/plugins/dragStore.ts @@ -8,8 +8,11 @@ const TEMPLATE_SERVICE_URL = 'http://localhost:3001' /** * 拖放方向 + * - top/bottom: el-row 的上下方 + * - left/right: el-col 的左右方 + * - inside: 放入内部(跨类型拖放时) */ -export type DropDirection = 'top' | 'bottom' | 'left' | 'right' +export type DropDirection = 'top' | 'bottom' | 'left' | 'right' | 'inside' /** * 拖拽源信息 @@ -245,6 +248,22 @@ export const useDragStore = defineStore('drag', () => { hoverDirection.value = direction } + /** + * 是否为跨类型拖放(源和目标类型不同) + */ + const isCrossTypeDrop = computed(() => { + if (!dragSource.value || !selectedNode.value) return false + + // 只有画布内元素拖放才考虑跨类型 + if (dragSource.value.type !== 'canvas-element') return false + + const sourceType = dragSource.value.elementType + const targetType = selectedNode.value.type + + // er 拖到 ec 或 ec 拖到 er 都是跨类型 + return sourceType !== targetType + }) + /** * 根据鼠标位置自动计算拖放方向 * @param mouseX 鼠标X坐标 @@ -253,6 +272,12 @@ export const useDragStore = defineStore('drag', () => { const updateDirectionFromMouse = (mouseX: number, mouseY: number) => { if (!selectedNode.value?.element) return + // 跨类型拖放时,始终显示 "inside" + if (isCrossTypeDrop.value) { + hoverDirection.value = 'inside' + return + } + const rect = selectedNode.value.element.getBoundingClientRect() const isRow = selectedNode.value.type === 'er' @@ -365,7 +390,8 @@ export const useDragStore = defineStore('drag', () => { 'top': '上方', 'bottom': '下方', 'left': '左侧', - 'right': '右侧' + 'right': '右侧', + 'inside': '内部' } let sourceName = '' @@ -384,6 +410,11 @@ export const useDragStore = defineStore('drag', () => { const getDropDirections = computed((): DropDirection[] => { if (!selectedNode.value) return [] + // 跨类型拖放:显示"放入内部" + if (isCrossTypeDrop.value) { + return ['inside'] + } + // el-row 显示上下 if (selectedNode.value.type === 'er') { return ['top', 'bottom'] @@ -403,6 +434,7 @@ export const useDragStore = defineStore('drag', () => { lastDropRecord, dropRecords, getDropDirections, + isCrossTypeDrop, // 方法 startDragFromComponentList, diff --git a/draggable-panels/src/views/TestPage1.vue b/draggable-panels/src/views/TestPage1.vue index f89281f..cc395e4 100644 --- a/draggable-panels/src/views/TestPage1.vue +++ b/draggable-panels/src/views/TestPage1.vue @@ -10,13 +10,14 @@ - -
右侧列1
-
+
右侧列2
+ +
右侧列1
+
diff --git a/vue-template-service/src/services/templateService.js b/vue-template-service/src/services/templateService.js index b4bf467..dbe2202 100644 --- a/vue-template-service/src/services/templateService.js +++ b/vue-template-service/src/services/templateService.js @@ -100,6 +100,93 @@ function getNodeSourceText(source, node) { return source.substring(start, end) } +/** + * 将元素移动到目标元素内部 + * 如果目标已有子元素,则放到最后 + */ +function moveElementInside(templateContent, sourceNode, targetNode, sourcePath, targetPath) { + const sourceStart = sourceNode.loc.start.offset + const sourceEnd = sourceNode.loc.end.offset + const sourceText = templateContent.substring(sourceStart, sourceEnd) + + // 找到源元素行的开始位置(用于删除整行) + const sourceLineStart = templateContent.lastIndexOf('\n', sourceStart - 1) + 1 + + // 获取目标元素的缩进(子元素需要比父元素多一层缩进) + const targetLineStart = templateContent.lastIndexOf('\n', targetNode.loc.start.offset - 1) + 1 + const targetIndent = templateContent.substring(targetLineStart, targetNode.loc.start.offset) + const childIndent = targetIndent + ' ' // 子元素缩进 + + // 找到目标元素的结束标签位置 + // 目标元素内容的结束位置(结束标签前) + const targetText = templateContent.substring(targetNode.loc.start.offset, targetNode.loc.end.offset) + const targetTag = targetNode.tag // 'el-row' 或 'el-col' + const closeTagPattern = `` + const closeTagIndex = targetText.lastIndexOf(closeTagPattern) + + if (closeTagIndex === -1) { + console.error('[moveElementInside] 未找到结束标签:', targetTag) + return templateContent + } + + // 计算插入位置(在结束标签之前) + const insertPositionInTarget = closeTagIndex + const insertPositionInTemplate = targetNode.loc.start.offset + insertPositionInTarget + + // 调整源元素的缩进 + const sourceLines = sourceText.split('\n') + const adjustedSourceLines = sourceLines.map((line, index) => { + if (index === 0) { + return childIndent + line.trimStart() + } + // 保持相对缩进 + return childIndent + line.trimStart() + }) + const adjustedSourceText = adjustedSourceLines.join('\n') + + // 根据源和目标的位置关系进行操作 + if (sourceStart < targetNode.loc.start.offset) { + // 源在目标前面:先删除源,再插入 + + // 删除源元素 + const beforeSource = templateContent.substring(0, sourceLineStart) + const afterSource = templateContent.substring(sourceEnd) + let afterSourceTrimmed = afterSource.startsWith('\n') ? afterSource.substring(1) : afterSource + const withoutSource = beforeSource + afterSourceTrimmed + + // 计算新的插入位置 + const removedLength = templateContent.length - withoutSource.length + const newInsertPosition = insertPositionInTemplate - removedLength + + // 插入到目标内部 + const insertText = adjustedSourceText + '\n' + targetIndent + + return withoutSource.substring(0, newInsertPosition) + + insertText + + withoutSource.substring(newInsertPosition) + } else { + // 源在目标后面:先插入,再删除 + + // 插入到目标内部 + const insertText = adjustedSourceText + '\n' + targetIndent + const withInsert = templateContent.substring(0, insertPositionInTemplate) + + insertText + + templateContent.substring(insertPositionInTemplate) + + // 计算源元素新位置 + const insertedLength = insertText.length + const newSourceLineStart = sourceLineStart + insertedLength + const newSourceEnd = sourceEnd + insertedLength + + // 删除源元素 + const beforeNewSource = withInsert.substring(0, newSourceLineStart) + const afterNewSource = withInsert.substring(newSourceEnd) + let afterTrimmed = afterNewSource.startsWith('\n') ? afterNewSource.substring(1) : afterNewSource + + return beforeNewSource + afterTrimmed + } +} + /** * 移动元素 * @@ -107,7 +194,7 @@ function getNodeSourceText(source, node) { * @param {Object} options - 移动选项 * @param {string} options.sourcePath - 源元素路径,如 "r1c1" * @param {string} options.targetPath - 目标元素路径,如 "r1c2" - * @param {string} options.direction - 移动方向: 'top' | 'bottom' | 'left' | 'right' + * @param {string} options.direction - 移动方向: 'top' | 'bottom' | 'left' | 'right' | 'inside' * @returns {Object} - { success: boolean, content?: string, error?: string } */ export function moveElement(vueContent, options) { @@ -194,9 +281,16 @@ export function moveElement(vueContent, options) { const targetLineStart = templateContent.lastIndexOf('\n', targetStart - 1) + 1 const targetIndent = templateContent.substring(targetLineStart, targetStart) - // 判断是否需要先删除源,再插入 - // 关键:需要考虑源和目标的相对位置 - if (sourceStart < targetStart) { + // 处理 'inside' 方向(放入目标元素内部) + if (direction === 'inside') { + newTemplateContent = moveElementInside( + templateContent, + sourceNode, + targetNode, + sourcePath, + targetPath + ) + } else if (sourceStart < targetStart) { // 源在目标前面:先处理目标位置,再删除源 const insertPosition = (direction === 'bottom' || direction === 'right') ? targetEnd