31
This commit is contained in:
@@ -147,5 +147,5 @@
|
|||||||
"activeTabId": "mxfx11j"
|
"activeTabId": "mxfx11j"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lastUpdated": "2025-12-22T15:54:13.388Z"
|
"lastUpdated": "2026-01-20T11:33:21.385Z"
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue'
|
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue'
|
||||||
import { useInteractionStore, useDragStore, generateElementPath } from '../../plugins'
|
import { useInteractionStore, useDragStore, generateElementPath } from '../../plugins'
|
||||||
import type { ElementType, InteractionTarget } from '../../plugins'
|
import type { ElementType, InteractionTarget } from '../../plugins'
|
||||||
|
import { useVueFileStore } from '../../stores/vueFileStore'
|
||||||
import DropZone from './DropZone.vue'
|
import DropZone from './DropZone.vue'
|
||||||
import DragPreview from './DragPreview.vue'
|
import DragPreview from './DragPreview.vue'
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const interactionStore = useInteractionStore()
|
const interactionStore = useInteractionStore()
|
||||||
const dragStore = useDragStore()
|
const dragStore = useDragStore()
|
||||||
|
const vueFileStore = useVueFileStore()
|
||||||
const containerRef = ref<HTMLElement | null>(null)
|
const containerRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
// 存储所有绑定的事件清理函数
|
// 存储所有绑定的事件清理函数
|
||||||
@@ -222,13 +224,25 @@ const clearDragHighlight = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局鼠标移动处理(拖拽时更新方向)
|
||||||
|
*/
|
||||||
|
const handleGlobalMouseMove = (e: MouseEvent) => {
|
||||||
|
if (dragStore.isDragging && dragStore.selectedNode) {
|
||||||
|
dragStore.updateDirectionFromMouse(e.clientX, e.clientY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全局鼠标松开处理
|
* 全局鼠标松开处理
|
||||||
*/
|
*/
|
||||||
const handleGlobalMouseUp = () => {
|
const handleGlobalMouseUp = async () => {
|
||||||
if (dragStore.isDragging && dragStore.selectedNode && dragStore.hoverDirection) {
|
if (dragStore.isDragging && dragStore.selectedNode && dragStore.hoverDirection) {
|
||||||
// 确认拖放
|
// 获取当前页面路径(从vueFileStore)
|
||||||
dragStore.confirmDrop()
|
const pagePath = vueFileStore.selectedFilePath
|
||||||
|
|
||||||
|
// 确认拖放,传入页面路径
|
||||||
|
await dragStore.confirmDrop(pagePath || undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
dragStore.endDrag()
|
dragStore.endDrag()
|
||||||
@@ -247,6 +261,8 @@ onMounted(() => {
|
|||||||
|
|
||||||
// 添加键盘事件监听
|
// 添加键盘事件监听
|
||||||
document.addEventListener('keydown', handleKeyDown)
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
// 添加全局鼠标移动事件(用于更新拖放方向)
|
||||||
|
document.addEventListener('mousemove', handleGlobalMouseMove)
|
||||||
// 添加全局鼠标松开事件
|
// 添加全局鼠标松开事件
|
||||||
document.addEventListener('mouseup', handleGlobalMouseUp)
|
document.addEventListener('mouseup', handleGlobalMouseUp)
|
||||||
})
|
})
|
||||||
@@ -263,6 +279,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
// 移除键盘事件监听
|
// 移除键盘事件监听
|
||||||
document.removeEventListener('keydown', handleKeyDown)
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
document.removeEventListener('mousemove', handleGlobalMouseMove)
|
||||||
document.removeEventListener('mouseup', handleGlobalMouseUp)
|
document.removeEventListener('mouseup', handleGlobalMouseUp)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { ref, computed } from 'vue'
|
|||||||
import type { InteractionTarget, ElementType } from './interactionStore'
|
import type { InteractionTarget, ElementType } from './interactionStore'
|
||||||
import { parsePath } from './pathUtils'
|
import { parsePath } from './pathUtils'
|
||||||
|
|
||||||
|
// 后端服务地址
|
||||||
|
const TEMPLATE_SERVICE_URL = 'http://localhost:3001'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 拖放方向
|
* 拖放方向
|
||||||
*/
|
*/
|
||||||
@@ -177,16 +180,17 @@ export const useDragStore = defineStore('drag', () => {
|
|||||||
// 3. 找到最接近的深度
|
// 3. 找到最接近的深度
|
||||||
if (oldSelectedDepth !== undefined && nodes.length > 0) {
|
if (oldSelectedDepth !== undefined && nodes.length > 0) {
|
||||||
let closestIndex = 0
|
let closestIndex = 0
|
||||||
let minDiff = Math.abs(nodes[0].depth - oldSelectedDepth)
|
let minDiff = Math.abs((nodes[0]?.depth ?? 0) - oldSelectedDepth)
|
||||||
for (let i = 1; i < nodes.length; i++) {
|
for (let i = 1; i < nodes.length; i++) {
|
||||||
const diff = Math.abs(nodes[i].depth - oldSelectedDepth)
|
const nodeDepth = nodes[i]?.depth ?? 0
|
||||||
|
const diff = Math.abs(nodeDepth - oldSelectedDepth)
|
||||||
if (diff < minDiff) {
|
if (diff < minDiff) {
|
||||||
minDiff = diff
|
minDiff = diff
|
||||||
closestIndex = i
|
closestIndex = i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
selectedHierarchyIndex.value = closestIndex
|
selectedHierarchyIndex.value = closestIndex
|
||||||
console.log('[DragStore] 选择最接近深度:', nodes[closestIndex].path)
|
console.log('[DragStore] 选择最接近深度:', nodes[closestIndex]?.path)
|
||||||
} else {
|
} else {
|
||||||
// 4. 默认选中最深层级
|
// 4. 默认选中最深层级
|
||||||
selectedHierarchyIndex.value = 0
|
selectedHierarchyIndex.value = 0
|
||||||
@@ -242,9 +246,32 @@ export const useDragStore = defineStore('drag', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 确认拖放
|
* 根据鼠标位置自动计算拖放方向
|
||||||
|
* @param mouseX 鼠标X坐标
|
||||||
|
* @param mouseY 鼠标Y坐标
|
||||||
*/
|
*/
|
||||||
const confirmDrop = () => {
|
const updateDirectionFromMouse = (mouseX: number, mouseY: number) => {
|
||||||
|
if (!selectedNode.value?.element) return
|
||||||
|
|
||||||
|
const rect = selectedNode.value.element.getBoundingClientRect()
|
||||||
|
const isRow = selectedNode.value.type === 'er'
|
||||||
|
|
||||||
|
if (isRow) {
|
||||||
|
// Row: 上下方向
|
||||||
|
const centerY = rect.top + rect.height / 2
|
||||||
|
hoverDirection.value = mouseY < centerY ? 'top' : 'bottom'
|
||||||
|
} else {
|
||||||
|
// Col: 左右方向
|
||||||
|
const centerX = rect.left + rect.width / 2
|
||||||
|
hoverDirection.value = mouseX < centerX ? 'left' : 'right'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认拖放
|
||||||
|
* @param pagePath 当前页面路径(相对于views目录)
|
||||||
|
*/
|
||||||
|
const confirmDrop = async (pagePath?: string): Promise<DropRecord | null> => {
|
||||||
if (!isDragging.value || !dragSource.value || !selectedNode.value || !hoverDirection.value) {
|
if (!isDragging.value || !dragSource.value || !selectedNode.value || !hoverDirection.value) {
|
||||||
console.log('[DragStore] 拖放条件不满足')
|
console.log('[DragStore] 拖放条件不满足')
|
||||||
return null
|
return null
|
||||||
@@ -263,9 +290,52 @@ export const useDragStore = defineStore('drag', () => {
|
|||||||
|
|
||||||
console.log('[DragStore] 确认拖放:', record)
|
console.log('[DragStore] 确认拖放:', record)
|
||||||
|
|
||||||
|
// 如果提供了页面路径,发送API请求到后端
|
||||||
|
if (pagePath && record.source.type === 'canvas-element') {
|
||||||
|
await sendMoveRequest(pagePath, record)
|
||||||
|
}
|
||||||
|
|
||||||
return record
|
return record
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送移动请求到后端服务
|
||||||
|
*/
|
||||||
|
const sendMoveRequest = async (pagePath: string, record: DropRecord) => {
|
||||||
|
try {
|
||||||
|
console.log('[DragStore] 发送移动请求:', { pagePath, record })
|
||||||
|
|
||||||
|
const response = await fetch(`${TEMPLATE_SERVICE_URL}/api/move-element`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
pagePath,
|
||||||
|
source: record.source,
|
||||||
|
targetPath: record.targetPath,
|
||||||
|
targetType: record.targetType,
|
||||||
|
direction: record.direction
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log('[DragStore] 移动成功:', result.message)
|
||||||
|
// 触发页面刷新事件
|
||||||
|
window.dispatchEvent(new CustomEvent('vue-template-updated', { detail: { pagePath } }))
|
||||||
|
} else {
|
||||||
|
console.error('[DragStore] 移动失败:', result.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DragStore] API请求失败:', error)
|
||||||
|
return { success: false, error: (error as Error).message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取消拖拽
|
* 取消拖拽
|
||||||
*/
|
*/
|
||||||
@@ -342,6 +412,7 @@ export const useDragStore = defineStore('drag', () => {
|
|||||||
selectParentLevel,
|
selectParentLevel,
|
||||||
selectChildLevel,
|
selectChildLevel,
|
||||||
setHoverDirection,
|
setHoverDirection,
|
||||||
|
updateDirectionFromMouse,
|
||||||
confirmDrop,
|
confirmDrop,
|
||||||
cancelDrag,
|
cancelDrag,
|
||||||
endDrag,
|
endDrag,
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "../../../.."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
20
vue-template-service/.gitignore
vendored
Normal file
20
vue-template-service/.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
Thumbs.db
|
||||||
1067
vue-template-service/package-lock.json
generated
Normal file
1067
vue-template-service/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
vue-template-service/package.json
Normal file
19
vue-template-service/package.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "vue-template-service",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Vue模板源码修改服务",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node src/index.js",
|
||||||
|
"dev": "node --watch src/index.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"@vue/compiler-sfc": "^3.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.10.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
134
vue-template-service/src/index.js
Normal file
134
vue-template-service/src/index.js
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
/**
|
||||||
|
* Vue模板源码修改服务
|
||||||
|
*
|
||||||
|
* 接收前端拖放请求,修改Vue文件中的template结构
|
||||||
|
*/
|
||||||
|
|
||||||
|
import express from 'express'
|
||||||
|
import cors from 'cors'
|
||||||
|
import { moveElement } from './services/templateService.js'
|
||||||
|
import { readFile, writeFile } from 'fs/promises'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
const PORT = 3001
|
||||||
|
|
||||||
|
// 中间件
|
||||||
|
app.use(cors())
|
||||||
|
app.use(express.json())
|
||||||
|
|
||||||
|
// Vue源码目录(相对于服务根目录)
|
||||||
|
const VUE_SOURCE_DIR = path.resolve('../draggable-panels/src/views')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API: 执行元素移动操作
|
||||||
|
*
|
||||||
|
* POST /api/move-element
|
||||||
|
* Body: {
|
||||||
|
* pagePath: string, // Vue文件路径(相对于views目录)
|
||||||
|
* source: { type, path, elementType },
|
||||||
|
* targetPath: string,
|
||||||
|
* targetType: string,
|
||||||
|
* direction: 'top' | 'bottom' | 'left' | 'right'
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
app.post('/api/move-element', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { pagePath, source, targetPath, targetType, direction } = req.body
|
||||||
|
|
||||||
|
console.log('[API] 收到移动请求:', {
|
||||||
|
pagePath,
|
||||||
|
source: source.path,
|
||||||
|
target: targetPath,
|
||||||
|
direction
|
||||||
|
})
|
||||||
|
|
||||||
|
// 验证参数
|
||||||
|
if (!pagePath || !source?.path || !targetPath || !direction) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: '缺少必要参数'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建完整文件路径
|
||||||
|
// 前端传来的路径可能是多种格式:
|
||||||
|
// - "../../../views/TestPage1.vue"
|
||||||
|
// - "../views/xxx.vue"
|
||||||
|
// - "./views/xxx.vue"
|
||||||
|
// - "TestPage1.vue"
|
||||||
|
// 需要提取 views/ 后面的部分
|
||||||
|
let normalizedPath = pagePath
|
||||||
|
|
||||||
|
// 查找 views/ 的位置,取其后面的内容
|
||||||
|
const viewsIndex = normalizedPath.indexOf('views/')
|
||||||
|
if (viewsIndex !== -1) {
|
||||||
|
normalizedPath = normalizedPath.substring(viewsIndex + 6) // 6 = 'views/'.length
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.join(VUE_SOURCE_DIR, normalizedPath)
|
||||||
|
|
||||||
|
console.log('[API] 原始路径:', pagePath)
|
||||||
|
console.log('[API] 规范化路径:', normalizedPath)
|
||||||
|
console.log('[API] 完整路径:', filePath)
|
||||||
|
|
||||||
|
// 读取Vue文件
|
||||||
|
let vueContent
|
||||||
|
try {
|
||||||
|
vueContent = await readFile(filePath, 'utf-8')
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: `文件不存在: ${pagePath}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行移动操作
|
||||||
|
const result = moveElement(vueContent, {
|
||||||
|
sourcePath: source.path,
|
||||||
|
targetPath,
|
||||||
|
direction
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: result.error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写回文件
|
||||||
|
await writeFile(filePath, result.content, 'utf-8')
|
||||||
|
|
||||||
|
console.log('[API] 移动成功')
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: `已将 ${source.path} 移动到 ${targetPath} 的${direction}方向`
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[API] 错误:', error)
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API: 获取服务状态
|
||||||
|
*/
|
||||||
|
app.get('/api/health', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
service: 'vue-template-service',
|
||||||
|
sourceDir: VUE_SOURCE_DIR
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 启动服务
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`🚀 Vue模板服务启动: http://localhost:${PORT}`)
|
||||||
|
console.log(`📁 源码目录: ${VUE_SOURCE_DIR}`)
|
||||||
|
})
|
||||||
305
vue-template-service/src/services/templateService.js
Normal file
305
vue-template-service/src/services/templateService.js
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
/**
|
||||||
|
* Vue模板解析和修改服务
|
||||||
|
*
|
||||||
|
* 使用 @vue/compiler-sfc 解析Vue文件
|
||||||
|
* 使用 @vue/compiler-dom 解析template获取AST
|
||||||
|
* 通过AST的位置信息定位元素,使用字符串操作移动元素(保持原始格式)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { parse as parseSFC } from '@vue/compiler-sfc'
|
||||||
|
import { parse as parseTemplate } from '@vue/compiler-dom'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析路径字符串为路径节点数组
|
||||||
|
* 例如: "r1c2r1" => [{type: 'r', index: 1}, {type: 'c', index: 2}, {type: 'r', index: 1}]
|
||||||
|
*/
|
||||||
|
function parsePath(pathStr) {
|
||||||
|
const nodes = []
|
||||||
|
const regex = /([rc])(\d+)/g
|
||||||
|
let match
|
||||||
|
|
||||||
|
while ((match = regex.exec(pathStr)) !== null) {
|
||||||
|
nodes.push({
|
||||||
|
type: match[1], // 'r' 或 'c'
|
||||||
|
index: parseInt(match[2], 10)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断节点是否为el-row
|
||||||
|
*/
|
||||||
|
function isElRow(node) {
|
||||||
|
return node.type === 1 && node.tag === 'el-row'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断节点是否为el-col
|
||||||
|
*/
|
||||||
|
function isElCol(node) {
|
||||||
|
return node.type === 1 && node.tag === 'el-col'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据路径在AST中查找元素
|
||||||
|
* @param {Object} ast - Vue template AST
|
||||||
|
* @param {string} pathStr - 路径字符串,如 "r1c2"
|
||||||
|
* @returns {Object|null} - 找到的AST节点
|
||||||
|
*/
|
||||||
|
function findElementByPath(ast, pathStr) {
|
||||||
|
const pathNodes = parsePath(pathStr)
|
||||||
|
|
||||||
|
if (pathNodes.length === 0) return null
|
||||||
|
|
||||||
|
// 从template的子节点开始查找
|
||||||
|
let currentChildren = ast.children
|
||||||
|
let currentNode = null
|
||||||
|
|
||||||
|
for (const pathNode of pathNodes) {
|
||||||
|
const targetType = pathNode.type === 'r' ? 'el-row' : 'el-col'
|
||||||
|
const targetIndex = pathNode.index
|
||||||
|
|
||||||
|
// 在当前层级查找第N个目标类型的元素
|
||||||
|
let count = 0
|
||||||
|
let found = false
|
||||||
|
|
||||||
|
for (const child of currentChildren) {
|
||||||
|
if (child.type !== 1) continue // 跳过非元素节点(文本、注释等)
|
||||||
|
|
||||||
|
if (child.tag === targetType) {
|
||||||
|
count++
|
||||||
|
if (count === targetIndex) {
|
||||||
|
currentNode = child
|
||||||
|
currentChildren = child.children || []
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
console.log(`[findElementByPath] 未找到: ${targetType} #${targetIndex}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentNode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取节点在源码中的完整文本
|
||||||
|
*/
|
||||||
|
function getNodeSourceText(source, node) {
|
||||||
|
if (!node.loc) return null
|
||||||
|
|
||||||
|
const start = node.loc.start.offset
|
||||||
|
const end = node.loc.end.offset
|
||||||
|
|
||||||
|
return source.substring(start, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移动元素
|
||||||
|
*
|
||||||
|
* @param {string} vueContent - 完整的Vue文件内容
|
||||||
|
* @param {Object} options - 移动选项
|
||||||
|
* @param {string} options.sourcePath - 源元素路径,如 "r1c1"
|
||||||
|
* @param {string} options.targetPath - 目标元素路径,如 "r1c2"
|
||||||
|
* @param {string} options.direction - 移动方向: 'top' | 'bottom' | 'left' | 'right'
|
||||||
|
* @returns {Object} - { success: boolean, content?: string, error?: string }
|
||||||
|
*/
|
||||||
|
export function moveElement(vueContent, options) {
|
||||||
|
const { sourcePath, targetPath, direction } = options
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 解析Vue SFC文件
|
||||||
|
const sfcResult = parseSFC(vueContent)
|
||||||
|
|
||||||
|
if (sfcResult.errors.length > 0) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Vue文件解析错误: ${sfcResult.errors[0].message}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateBlock = sfcResult.descriptor.template
|
||||||
|
if (!templateBlock) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '未找到template块'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateContent = templateBlock.content
|
||||||
|
|
||||||
|
// 找到 <template> 内容在原文件中的实际位置
|
||||||
|
// templateBlock.loc 指向整个 <template>...</template> 块
|
||||||
|
// 我们需要找到内容的起始和结束位置
|
||||||
|
const templateTagStart = vueContent.indexOf('<template')
|
||||||
|
const templateTagEnd = vueContent.indexOf('>', templateTagStart) + 1
|
||||||
|
const templateCloseStart = vueContent.indexOf('</template>', templateTagEnd)
|
||||||
|
|
||||||
|
// template内容在原文件中的位置
|
||||||
|
const contentStart = templateTagEnd
|
||||||
|
const contentEnd = templateCloseStart
|
||||||
|
|
||||||
|
// 2. 解析template获取AST
|
||||||
|
const templateAST = parseTemplate(templateContent, {
|
||||||
|
// 保留注释
|
||||||
|
comments: true,
|
||||||
|
// 保留空白(用于保持格式)
|
||||||
|
whitespace: 'preserve'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3. 查找源元素和目标元素
|
||||||
|
const sourceNode = findElementByPath(templateAST, sourcePath)
|
||||||
|
const targetNode = findElementByPath(templateAST, targetPath)
|
||||||
|
|
||||||
|
if (!sourceNode) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `未找到源元素: ${sourcePath}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetNode) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `未找到目标元素: ${targetPath}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 获取元素在template中的位置
|
||||||
|
const sourceStart = sourceNode.loc.start.offset
|
||||||
|
const sourceEnd = sourceNode.loc.end.offset
|
||||||
|
const sourceText = templateContent.substring(sourceStart, sourceEnd)
|
||||||
|
|
||||||
|
const targetStart = targetNode.loc.start.offset
|
||||||
|
const targetEnd = targetNode.loc.end.offset
|
||||||
|
|
||||||
|
console.log(`[moveElement] 源: ${sourcePath} [${sourceStart}-${sourceEnd}]`)
|
||||||
|
console.log(`[moveElement] 目标: ${targetPath} [${targetStart}-${targetEnd}]`)
|
||||||
|
console.log(`[moveElement] 方向: ${direction}`)
|
||||||
|
|
||||||
|
// 5. 计算插入位置和新内容
|
||||||
|
let newTemplateContent
|
||||||
|
|
||||||
|
// 检测源元素前面的空白(用于保持缩进)
|
||||||
|
const sourceLineStart = templateContent.lastIndexOf('\n', sourceStart - 1) + 1
|
||||||
|
const sourceIndent = templateContent.substring(sourceLineStart, sourceStart)
|
||||||
|
|
||||||
|
// 检测目标元素的缩进
|
||||||
|
const targetLineStart = templateContent.lastIndexOf('\n', targetStart - 1) + 1
|
||||||
|
const targetIndent = templateContent.substring(targetLineStart, targetStart)
|
||||||
|
|
||||||
|
// 判断是否需要先删除源,再插入
|
||||||
|
// 关键:需要考虑源和目标的相对位置
|
||||||
|
if (sourceStart < targetStart) {
|
||||||
|
// 源在目标前面:先处理目标位置,再删除源
|
||||||
|
const insertPosition = (direction === 'bottom' || direction === 'right')
|
||||||
|
? targetEnd
|
||||||
|
: targetStart
|
||||||
|
|
||||||
|
// 构建新内容
|
||||||
|
let parts = []
|
||||||
|
|
||||||
|
// 删除源元素(包括前后的空白)
|
||||||
|
const beforeSource = templateContent.substring(0, sourceLineStart)
|
||||||
|
const afterSource = templateContent.substring(sourceEnd)
|
||||||
|
|
||||||
|
// 合并时跳过源元素后的换行
|
||||||
|
let afterSourceTrimmed = afterSource
|
||||||
|
if (afterSource.startsWith('\n')) {
|
||||||
|
afterSourceTrimmed = afterSource.substring(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const withoutSource = beforeSource + afterSourceTrimmed
|
||||||
|
|
||||||
|
// 计算新的目标位置(因为删除了源,位置会变化)
|
||||||
|
const removedLength = templateContent.length - withoutSource.length
|
||||||
|
const newInsertPosition = insertPosition - removedLength
|
||||||
|
|
||||||
|
// 在新位置插入源元素
|
||||||
|
const insertText = (direction === 'bottom' || direction === 'right')
|
||||||
|
? '\n' + targetIndent + sourceText
|
||||||
|
: sourceText + '\n' + targetIndent
|
||||||
|
|
||||||
|
newTemplateContent =
|
||||||
|
withoutSource.substring(0, newInsertPosition) +
|
||||||
|
insertText +
|
||||||
|
withoutSource.substring(newInsertPosition)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// 源在目标后面:先插入,再删除
|
||||||
|
const insertPosition = (direction === 'bottom' || direction === 'right')
|
||||||
|
? targetEnd
|
||||||
|
: targetStart
|
||||||
|
|
||||||
|
// 先在目标位置插入
|
||||||
|
const insertText = (direction === 'bottom' || direction === 'right')
|
||||||
|
? '\n' + targetIndent + sourceText
|
||||||
|
: sourceText + '\n' + targetIndent
|
||||||
|
|
||||||
|
const withInsert =
|
||||||
|
templateContent.substring(0, insertPosition) +
|
||||||
|
insertText +
|
||||||
|
templateContent.substring(insertPosition)
|
||||||
|
|
||||||
|
// 计算源元素新位置(因为插入了内容,位置会变化)
|
||||||
|
const insertedLength = insertText.length
|
||||||
|
const newSourceStart = sourceLineStart + insertedLength
|
||||||
|
const newSourceEnd = sourceEnd + insertedLength
|
||||||
|
|
||||||
|
// 删除源元素
|
||||||
|
const beforeNewSource = withInsert.substring(0, newSourceStart)
|
||||||
|
const afterNewSource = withInsert.substring(newSourceEnd)
|
||||||
|
|
||||||
|
// 跳过换行
|
||||||
|
let afterTrimmed = afterNewSource
|
||||||
|
if (afterTrimmed.startsWith('\n')) {
|
||||||
|
afterTrimmed = afterTrimmed.substring(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
newTemplateContent = beforeNewSource + afterTrimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 重建Vue文件:保留 <template> 标签,只替换内容
|
||||||
|
const beforeContent = vueContent.substring(0, contentStart)
|
||||||
|
const afterContent = vueContent.substring(contentEnd)
|
||||||
|
|
||||||
|
const newVueContent = beforeContent + newTemplateContent + afterContent
|
||||||
|
|
||||||
|
console.log('[moveElement] 文件更新成功')
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
content: newVueContent
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[moveElement] 错误:', error)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取template的AST(用于调试)
|
||||||
|
*/
|
||||||
|
export function getTemplateAST(vueContent) {
|
||||||
|
const sfcResult = parseSFC(vueContent)
|
||||||
|
|
||||||
|
if (!sfcResult.descriptor.template) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateContent = sfcResult.descriptor.template.content
|
||||||
|
return parseTemplate(templateContent, {
|
||||||
|
comments: true,
|
||||||
|
whitespace: 'preserve'
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user