31
This commit is contained in:
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