31
This commit is contained in:
@@ -38,13 +38,17 @@ const isRowLayout = computed(() => {
|
|||||||
return dragStore.selectedNode?.type === 'er'
|
return dragStore.selectedNode?.type === 'er'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 是否是跨类型拖放
|
||||||
|
const isCrossType = computed(() => dragStore.isCrossTypeDrop)
|
||||||
|
|
||||||
// 获取方向文本
|
// 获取方向文本
|
||||||
const getDirectionText = (direction: DropDirection): string => {
|
const getDirectionText = (direction: DropDirection): string => {
|
||||||
const texts: Record<DropDirection, string> = {
|
const texts: Record<DropDirection, string> = {
|
||||||
'top': '移动至上方',
|
'top': '移动至上方',
|
||||||
'bottom': '移动至下方',
|
'bottom': '移动至下方',
|
||||||
'left': '移动至左侧',
|
'left': '移动至左侧',
|
||||||
'right': '移动至右侧'
|
'right': '移动至右侧',
|
||||||
|
'inside': '放入其中'
|
||||||
}
|
}
|
||||||
return texts[direction]
|
return texts[direction]
|
||||||
}
|
}
|
||||||
@@ -55,7 +59,8 @@ const getDirectionIcon = (direction: DropDirection): string => {
|
|||||||
'top': '⬆',
|
'top': '⬆',
|
||||||
'bottom': '⬇',
|
'bottom': '⬇',
|
||||||
'left': '⬅',
|
'left': '⬅',
|
||||||
'right': '➡'
|
'right': '➡',
|
||||||
|
'inside': '⬇️⤵️' // 放入图标
|
||||||
}
|
}
|
||||||
return icons[direction]
|
return icons[direction]
|
||||||
}
|
}
|
||||||
@@ -66,7 +71,11 @@ const getDirectionIcon = (direction: DropDirection): string => {
|
|||||||
<div
|
<div
|
||||||
v-if="dragStore.isDragging && dragStore.selectedNode"
|
v-if="dragStore.isDragging && dragStore.selectedNode"
|
||||||
class="drop-zone-container"
|
class="drop-zone-container"
|
||||||
:class="{ 'is-row': isRowLayout, 'is-col': !isRowLayout }"
|
:class="{
|
||||||
|
'is-row': isRowLayout && !isCrossType,
|
||||||
|
'is-col': !isRowLayout && !isCrossType,
|
||||||
|
'is-inside': isCrossType
|
||||||
|
}"
|
||||||
:style="zoneStyle"
|
:style="zoneStyle"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -137,6 +146,32 @@ const getDirectionIcon = (direction: DropDirection): string => {
|
|||||||
border-left: 2px solid rgba(64, 158, 255, 0.8);
|
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:hover,
|
||||||
.drop-zone.is-active {
|
.drop-zone.is-active {
|
||||||
background: rgba(64, 158, 255, 0.25);
|
background: rgba(64, 158, 255, 0.25);
|
||||||
|
|||||||
@@ -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
|
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坐标
|
* @param mouseX 鼠标X坐标
|
||||||
@@ -253,6 +272,12 @@ export const useDragStore = defineStore('drag', () => {
|
|||||||
const updateDirectionFromMouse = (mouseX: number, mouseY: number) => {
|
const updateDirectionFromMouse = (mouseX: number, mouseY: number) => {
|
||||||
if (!selectedNode.value?.element) return
|
if (!selectedNode.value?.element) return
|
||||||
|
|
||||||
|
// 跨类型拖放时,始终显示 "inside"
|
||||||
|
if (isCrossTypeDrop.value) {
|
||||||
|
hoverDirection.value = 'inside'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const rect = selectedNode.value.element.getBoundingClientRect()
|
const rect = selectedNode.value.element.getBoundingClientRect()
|
||||||
const isRow = selectedNode.value.type === 'er'
|
const isRow = selectedNode.value.type === 'er'
|
||||||
|
|
||||||
@@ -365,7 +390,8 @@ export const useDragStore = defineStore('drag', () => {
|
|||||||
'top': '上方',
|
'top': '上方',
|
||||||
'bottom': '下方',
|
'bottom': '下方',
|
||||||
'left': '左侧',
|
'left': '左侧',
|
||||||
'right': '右侧'
|
'right': '右侧',
|
||||||
|
'inside': '内部'
|
||||||
}
|
}
|
||||||
|
|
||||||
let sourceName = ''
|
let sourceName = ''
|
||||||
@@ -384,6 +410,11 @@ export const useDragStore = defineStore('drag', () => {
|
|||||||
const getDropDirections = computed((): DropDirection[] => {
|
const getDropDirections = computed((): DropDirection[] => {
|
||||||
if (!selectedNode.value) return []
|
if (!selectedNode.value) return []
|
||||||
|
|
||||||
|
// 跨类型拖放:显示"放入内部"
|
||||||
|
if (isCrossTypeDrop.value) {
|
||||||
|
return ['inside']
|
||||||
|
}
|
||||||
|
|
||||||
// el-row 显示上下
|
// el-row 显示上下
|
||||||
if (selectedNode.value.type === 'er') {
|
if (selectedNode.value.type === 'er') {
|
||||||
return ['top', 'bottom']
|
return ['top', 'bottom']
|
||||||
@@ -403,6 +434,7 @@ export const useDragStore = defineStore('drag', () => {
|
|||||||
lastDropRecord,
|
lastDropRecord,
|
||||||
dropRecords,
|
dropRecords,
|
||||||
getDropDirections,
|
getDropDirections,
|
||||||
|
isCrossTypeDrop,
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
startDragFromComponentList,
|
startDragFromComponentList,
|
||||||
|
|||||||
@@ -10,13 +10,14 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row :gutter="10">
|
<el-row :gutter="10">
|
||||||
<el-col :span="12">
|
|
||||||
<div class="design-component">右侧列1</div>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<div class="design-component">右侧列2</div>
|
<div class="design-component">右侧列2</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="design-component">右侧列1</div>
|
||||||
|
</el-col>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -100,6 +100,93 @@ function getNodeSourceText(source, node) {
|
|||||||
return source.substring(start, end)
|
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 = `</${targetTag}>`
|
||||||
|
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 {Object} options - 移动选项
|
||||||
* @param {string} options.sourcePath - 源元素路径,如 "r1c1"
|
* @param {string} options.sourcePath - 源元素路径,如 "r1c1"
|
||||||
* @param {string} options.targetPath - 目标元素路径,如 "r1c2"
|
* @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 }
|
* @returns {Object} - { success: boolean, content?: string, error?: string }
|
||||||
*/
|
*/
|
||||||
export function moveElement(vueContent, options) {
|
export function moveElement(vueContent, options) {
|
||||||
@@ -194,9 +281,16 @@ export function moveElement(vueContent, options) {
|
|||||||
const targetLineStart = templateContent.lastIndexOf('\n', targetStart - 1) + 1
|
const targetLineStart = templateContent.lastIndexOf('\n', targetStart - 1) + 1
|
||||||
const targetIndent = templateContent.substring(targetLineStart, targetStart)
|
const targetIndent = templateContent.substring(targetLineStart, targetStart)
|
||||||
|
|
||||||
// 判断是否需要先删除源,再插入
|
// 处理 'inside' 方向(放入目标元素内部)
|
||||||
// 关键:需要考虑源和目标的相对位置
|
if (direction === 'inside') {
|
||||||
if (sourceStart < targetStart) {
|
newTemplateContent = moveElementInside(
|
||||||
|
templateContent,
|
||||||
|
sourceNode,
|
||||||
|
targetNode,
|
||||||
|
sourcePath,
|
||||||
|
targetPath
|
||||||
|
)
|
||||||
|
} else if (sourceStart < targetStart) {
|
||||||
// 源在目标前面:先处理目标位置,再删除源
|
// 源在目标前面:先处理目标位置,再删除源
|
||||||
const insertPosition = (direction === 'bottom' || direction === 'right')
|
const insertPosition = (direction === 'bottom' || direction === 'right')
|
||||||
? targetEnd
|
? targetEnd
|
||||||
|
|||||||
Reference in New Issue
Block a user