Files
fauto-design/vue-template-service/src/services/templateService.js
2026-01-20 21:53:09 +08:00

884 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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)
}
/**
* 获取字符串的缩进级别(空格数)
*/
function getIndentLevel(str) {
const match = str.match(/^(\s*)/)
return match ? match[1].length : 0
}
/**
* 调整源代码的缩进,保持相对缩进关系
* @param {string} sourceText - 源元素文本
* @param {string} targetIndent - 目标位置的缩进
* @returns {string} - 调整后的文本
*/
function adjustIndentation(sourceText, targetIndent) {
const lines = sourceText.split('\n')
if (lines.length === 0) return sourceText
// 获取第一行的原始缩进
const firstLineIndent = getIndentLevel(lines[0])
// 计算缩进差值
const targetIndentLevel = targetIndent.length
const indentDiff = targetIndentLevel - firstLineIndent
// 对每一行应用缩进差值
const adjustedLines = lines.map((line, index) => {
if (!line.trim()) return '' // 空行保持空
const currentIndent = getIndentLevel(line)
const newIndent = Math.max(0, currentIndent + indentDiff)
return ' '.repeat(newIndent) + line.trimStart()
})
return adjustedLines.join('\n')
}
/**
* 将元素移动到目标元素内部
* 如果目标已有子元素,则放到最后
*/
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)
// 1. 计算删除范围
let deleteStart = sourceStart
let deleteEnd = sourceEnd
// 向前查找这一行的开始
const lineStart = templateContent.lastIndexOf('\n', sourceStart - 1) + 1
const beforeElement = templateContent.substring(lineStart, sourceStart)
// 如果元素前面只有空白,则从行开始删除
if (/^\s*$/.test(beforeElement)) {
deleteStart = lineStart
}
// 向后查找是否有换行
if (templateContent[sourceEnd] === '\n') {
deleteEnd = sourceEnd + 1
}
// 2. 获取目标元素的缩进(子元素需要比父元素多一层缩进)
const targetLineStart = templateContent.lastIndexOf('\n', targetNode.loc.start.offset - 1) + 1
const targetIndent = templateContent.substring(targetLineStart, targetNode.loc.start.offset)
const childIndent = targetIndent + ' '
// 3. 找到目标元素的结束标签位置
const targetText = templateContent.substring(targetNode.loc.start.offset, targetNode.loc.end.offset)
const closeTagPattern = `</${targetNode.tag}>`
const closeTagIndex = targetText.lastIndexOf(closeTagPattern)
if (closeTagIndex === -1) {
console.error('[moveElementInside] 未找到结束标签:', targetNode.tag)
return templateContent
}
// 插入位置(在结束标签之前)
const insertPosition = targetNode.loc.start.offset + closeTagIndex
// 4. 调整源元素的缩进(保持相对缩进)
const adjustedSourceText = adjustIndentation(sourceText, childIndent)
// 构建插入文本
const insertText = adjustedSourceText + '\n' + targetIndent
// 5. 执行操作(从后向前处理)
if (deleteStart > insertPosition) {
// 删除位置在插入位置后面:先删除,再插入
let result = templateContent.substring(0, deleteStart) + templateContent.substring(deleteEnd)
result = result.substring(0, insertPosition) + insertText + result.substring(insertPosition)
return result
} else {
// 删除位置在插入位置前面:先插入,再删除
const deletedLength = deleteEnd - deleteStart
const adjustedInsertPos = insertPosition - deletedLength
let result = templateContent.substring(0, deleteStart) + templateContent.substring(deleteEnd)
result = result.substring(0, adjustedInsertPos) + insertText + result.substring(adjustedInsertPos)
return result
}
}
/**
* 移动元素
*
* @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' | 'inside'
* @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> 内容在原文件中的实际位置
const templateTagStart = vueContent.indexOf('<template')
const templateTagEnd = vueContent.indexOf('>', templateTagStart) + 1
const templateCloseStart = vueContent.indexOf('</template>', templateTagEnd)
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 deleteStart = sourceStart
let deleteEnd = sourceEnd
// 向前查找这一行的开始(换行符后的第一个字符)
const lineStart = templateContent.lastIndexOf('\n', sourceStart - 1) + 1
const beforeElement = templateContent.substring(lineStart, sourceStart)
// 如果元素前面只有空白,则从行开始删除
if (/^\s*$/.test(beforeElement)) {
deleteStart = lineStart
}
// 向后查找是否有换行
if (templateContent[sourceEnd] === '\n') {
deleteEnd = sourceEnd + 1
}
// 获取目标元素的缩进
const targetLineStart = templateContent.lastIndexOf('\n', targetStart - 1) + 1
const targetIndent = templateContent.substring(targetLineStart, targetStart)
let newTemplateContent
// 6. 处理 'inside' 方向
if (direction === 'inside') {
newTemplateContent = moveElementInside(
templateContent,
sourceNode,
targetNode,
sourcePath,
targetPath
)
} else {
// 7. 计算插入位置
const insertAfterTarget = (direction === 'bottom' || direction === 'right')
const insertPosition = insertAfterTarget ? targetEnd : targetStart
// 构建插入文本(使用目标缩进,保持相对缩进)
const adjustedSource = adjustIndentation(sourceText, targetIndent)
const insertText = insertAfterTarget
? '\n' + adjustedSource
: adjustedSource + '\n'
// 8. 执行操作(从后向前处理,避免偏移量问题)
if (deleteStart > insertPosition) {
// 删除位置在插入位置后面:先删除,再插入
newTemplateContent = templateContent.substring(0, deleteStart) +
templateContent.substring(deleteEnd)
newTemplateContent = newTemplateContent.substring(0, insertPosition) +
insertText +
newTemplateContent.substring(insertPosition)
} else {
// 删除位置在插入位置前面:先插入,再删除(需要调整偏移)
const deletedLength = deleteEnd - deleteStart
const adjustedInsertPos = insertPosition - deletedLength
newTemplateContent = templateContent.substring(0, deleteStart) +
templateContent.substring(deleteEnd)
newTemplateContent = newTemplateContent.substring(0, adjustedInsertPos) +
insertText +
newTemplateContent.substring(adjustedInsertPos)
}
}
// 9. 重建Vue文件
const newVueContent = vueContent.substring(0, contentStart) +
newTemplateContent +
vueContent.substring(contentEnd)
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'
})
}
/**
* 插入新元素到指定位置
*
* @param {string} vueContent - 完整的Vue文件内容
* @param {Object} options - 插入选项
* @param {string} options.templateContent - 要插入的模板内容
* @param {string} options.targetPath - 目标元素路径,如 "r1c2"
* @param {string} options.direction - 插入方向: 'top' | 'bottom' | 'left' | 'right' | 'inside'
* @returns {Object} - { success: boolean, content?: string, error?: string }
*/
/**
* 解析Vue文件的el-row/el-col结构树
* @param {string} vueContent - Vue文件内容
* @returns {Object} - { success: boolean, tree?: Array, error?: string }
*/
export function parseElementTree(vueContent) {
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块'
}
}
// 2. 解析template获取AST
const templateAST = parseTemplate(templateBlock.content, {
comments: true,
whitespace: 'preserve'
})
// 3. 递归构建el-row/el-col树
const buildTree = (children, pathPrefix = '') => {
const result = []
let rowIndex = 0
let colIndex = 0
for (const child of children) {
if (child.type !== 1) continue // 跳过非元素节点
if (child.tag === 'el-row') {
rowIndex++
const path = pathPrefix + 'r' + rowIndex
const node = {
type: 'row',
path,
label: 'el-row',
children: buildTree(child.children || [], path)
}
result.push(node)
} else if (child.tag === 'el-col') {
colIndex++
const path = pathPrefix + 'c' + colIndex
// 检查el-col内部的子组件
const componentName = getInnerComponentName(child.children || [])
const spanAttr = child.props?.find(p => p.name === 'span' || (p.name === 'bind' && p.arg?.content === 'span'))
let span = 24
if (spanAttr) {
if (spanAttr.name === 'span' && spanAttr.value) {
span = parseInt(spanAttr.value.content) || 24
} else if (spanAttr.exp) {
span = parseInt(spanAttr.exp.content) || 24
}
}
const node = {
type: 'col',
path,
label: `el-col :span="${span}"`,
componentName,
children: buildTree(child.children || [], path)
}
result.push(node)
}
}
return result
}
// 获取el-col内部的组件名称优先读data-component属性
const getInnerComponentName = (children) => {
for (const child of children) {
if (child.type !== 1) continue
if (child.tag === 'el-row' || child.tag === 'el-col') continue
// 优先检查 data-component 属性
if (child.props) {
const dataComponentAttr = child.props.find(p => p.name === 'data-component')
if (dataComponentAttr?.value?.content) {
return dataComponentAttr.value.content
}
}
// 其他元素返回"其他"
return '其他'
}
return null
}
const tree = buildTree(templateAST.children)
return { success: true, tree }
} catch (error) {
console.error('[parseElementTree] 错误:', error)
return { success: false, error: error.message }
}
}
export function insertElement(vueContent, options) {
const { templateContent: insertContent, 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> 内容在原文件中的实际位置
const templateTagStart = vueContent.indexOf('<template')
const templateTagEnd = vueContent.indexOf('>', templateTagStart) + 1
const templateCloseStart = vueContent.indexOf('</template>', templateTagEnd)
const contentStart = templateTagEnd
const contentEnd = templateCloseStart
// 2. 解析template获取AST
const templateAST = parseTemplate(templateContent, {
comments: true,
whitespace: 'preserve'
})
// 3. 查找目标元素
const targetNode = findElementByPath(templateAST, targetPath)
if (!targetNode) {
return { success: false, error: `未找到目标元素: ${targetPath}` }
}
// 4. 获取目标元素的位置和缩进
const targetStart = targetNode.loc.start.offset
const targetEnd = targetNode.loc.end.offset
const targetLineStart = templateContent.lastIndexOf('\n', targetStart - 1) + 1
const targetIndent = templateContent.substring(targetLineStart, targetStart)
console.log(`[insertElement] 目标: ${targetPath} [${targetStart}-${targetEnd}]`)
console.log(`[insertElement] 方向: ${direction}`)
let newTemplateContent
// 5. 根据方向插入
if (direction === 'inside') {
// 放入目标元素内部
const targetText = templateContent.substring(targetStart, targetEnd)
const closeTagPattern = `</${targetNode.tag}>`
const closeTagIndex = targetText.lastIndexOf(closeTagPattern)
if (closeTagIndex === -1) {
return { success: false, error: `未找到目标元素的结束标签` }
}
const insertPosition = targetStart + closeTagIndex
const childIndent = targetIndent + ' '
const adjustedContent = adjustIndentation(insertContent, childIndent)
const insertText = adjustedContent + '\n' + targetIndent
newTemplateContent = templateContent.substring(0, insertPosition) +
insertText +
templateContent.substring(insertPosition)
} else {
// 放在目标元素前面或后面
const insertAfterTarget = (direction === 'bottom' || direction === 'right')
const insertPosition = insertAfterTarget ? targetEnd : targetStart
const adjustedContent = adjustIndentation(insertContent, targetIndent)
const insertText = insertAfterTarget
? '\n' + adjustedContent
: adjustedContent + '\n'
newTemplateContent = templateContent.substring(0, insertPosition) +
insertText +
templateContent.substring(insertPosition)
}
// 6. 重建Vue文件
const newVueContent = vueContent.substring(0, contentStart) +
newTemplateContent +
vueContent.substring(contentEnd)
console.log('[insertElement] 插入成功')
return { success: true, content: newVueContent }
} catch (error) {
console.error('[insertElement] 错误:', error)
return { success: false, error: error.message }
}
}
/**
* 解析组件属性值
* @param {string} vueContent - Vue文件内容
* @param {string} elementPath - 元素路径
* @returns {Object} - { success: boolean, props?: Object, componentId?: string, error?: string }
*/
export function parseComponentProps(vueContent, elementPath) {
try {
const sfcResult = parseSFC(vueContent)
if (sfcResult.errors.length > 0) {
return { success: false, error: `Vue文件解析错误` }
}
const templateBlock = sfcResult.descriptor.template
if (!templateBlock) {
return { success: false, error: '未找到template块' }
}
const templateAST = parseTemplate(templateBlock.content, {
comments: true,
whitespace: 'preserve'
})
const targetNode = findElementByPath(templateAST, elementPath)
if (!targetNode) {
return { success: false, error: `未找到元素: ${elementPath}` }
}
// 提取属性
const props = {}
// 提取span属性从el-col
if (targetNode.tag === 'el-col') {
const spanProp = targetNode.props?.find(p =>
p.name === 'span' || (p.name === 'bind' && p.arg?.content === 'span')
)
if (spanProp) {
if (spanProp.name === 'span' && spanProp.value) {
props.span = parseInt(spanProp.value.content) || 24
} else if (spanProp.exp) {
props.span = parseInt(spanProp.exp.content) || 24
}
} else {
props.span = 24
}
}
// 查找内部组件并提取属性
let componentId = null
const extractChildProps = (children) => {
for (const child of children || []) {
if (child.type !== 1) continue
if (child.tag === 'el-row' || child.tag === 'el-col') continue
// 检查 data-component 属性
const dataCompAttr = child.props?.find(p => p.name === 'data-component')
if (dataCompAttr?.value?.content) {
componentId = dataCompAttr.value.content
}
// 提取子元素属性
extractElementProps(child, props)
// 递归提取孩子节点
if (child.children) {
extractChildProps(child.children)
}
}
}
// 提取元素属性
const extractElementProps = (node, result) => {
if (!node.props) return
for (const prop of node.props) {
if (prop.type === 6) { // 普通属性
const key = `${node.tag}:${prop.name}`
result[key] = prop.value?.content || ''
} else if (prop.type === 7 && prop.name === 'bind') { // v-bind 属性
const key = `${node.tag}::${prop.arg?.content}`
result[key] = prop.exp?.content || ''
}
}
}
extractChildProps(targetNode.children)
console.log('[parseComponentProps] 解析属性:', { elementPath, componentId, props })
return { success: true, props, componentId }
} catch (error) {
console.error('[parseComponentProps] 错误:', error)
return { success: false, error: error.message }
}
}
/**
* 更新组件属性值
* @param {string} vueContent - Vue文件内容
* @param {string} elementPath - 元素路径
* @param {Object} updates - 要更新的属性 { key: value }
* @returns {Object} - { success: boolean, content?: string, error?: string }
*/
export function updateComponentProps(vueContent, elementPath, updates) {
try {
const sfcResult = parseSFC(vueContent)
if (sfcResult.errors.length > 0) {
return { success: false, error: `Vue文件解析错误` }
}
const templateBlock = sfcResult.descriptor.template
if (!templateBlock) {
return { success: false, error: '未找到template块' }
}
const templateContent = templateBlock.content
const templateTagStart = vueContent.indexOf('<template')
const templateTagEnd = vueContent.indexOf('>', templateTagStart) + 1
const templateCloseStart = vueContent.indexOf('</template>', templateTagEnd)
const templateAST = parseTemplate(templateContent, {
comments: true,
whitespace: 'preserve'
})
const targetNode = findElementByPath(templateAST, elementPath)
if (!targetNode) {
return { success: false, error: `未找到元素: ${elementPath}` }
}
let newTemplateContent = templateContent
// 处理span更新el-col的:span属性
if (updates.span !== undefined && targetNode.tag === 'el-col') {
const nodeText = templateContent.substring(targetNode.loc.start.offset, targetNode.loc.end.offset)
const spanMatch = nodeText.match(/:span="(\d+)"/)
if (spanMatch) {
const newNodeText = nodeText.replace(/:span="\d+"/, `:span="${updates.span}"`)
newTemplateContent = newTemplateContent.substring(0, targetNode.loc.start.offset) +
newNodeText +
newTemplateContent.substring(targetNode.loc.end.offset)
}
delete updates.span
}
// 处理其他属性更新
for (const [key, value] of Object.entries(updates)) {
const [tagName, attrName] = key.split(':')
if (!tagName || !attrName) continue
// 查找目标元素
const findTargetElement = (node) => {
if (node.type === 1 && node.tag === tagName) return node
if (node.children) {
for (const child of node.children) {
const found = findTargetElement(child)
if (found) return found
}
}
return null
}
const targetEl = findTargetElement(targetNode)
if (!targetEl) continue
const elStart = targetEl.loc.start.offset
const elEnd = targetEl.loc.end.offset
let elText = newTemplateContent.substring(elStart, elEnd)
// 更新属性
const isBinding = attrName.startsWith(':')
const realAttrName = isBinding ? attrName.substring(1) : attrName
const attrRegex = new RegExp(`(${isBinding ? ':' : ''}${realAttrName})="[^"]*"`)
if (value === '' || value === null || value === undefined) {
// 删除属性
elText = elText.replace(attrRegex, '')
elText = elText.replace(/\s+>/g, '>').replace(/\s{2,}/g, ' ')
} else if (attrRegex.test(elText)) {
// 更新属性
elText = elText.replace(attrRegex, `${isBinding ? ':' : ''}${realAttrName}="${value}"`)
} else {
// 添加属性
const tagEndMatch = elText.match(/<[\w-]+/)
if (tagEndMatch) {
const insertPos = tagEndMatch[0].length
elText = elText.substring(0, insertPos) +
` ${isBinding ? ':' : ''}${realAttrName}="${value}"` +
elText.substring(insertPos)
}
}
newTemplateContent = newTemplateContent.substring(0, elStart) +
elText +
newTemplateContent.substring(elEnd)
}
const newVueContent = vueContent.substring(0, templateTagEnd) +
newTemplateContent +
vueContent.substring(templateCloseStart)
console.log('[updateComponentProps] 更新成功')
return { success: true, content: newVueContent }
} catch (error) {
console.error('[updateComponentProps] 错误:', error)
return { success: false, error: error.message }
}
}
/**
* 删除元素
* @param {string} vueContent - Vue文件内容
* @param {string} elementPath - 元素路径
* @returns {Object} - { success: boolean, content?: string, error?: string }
*/
export function deleteElement(vueContent, elementPath) {
try {
const sfcResult = parseSFC(vueContent)
if (sfcResult.errors.length > 0) {
return { success: false, error: `Vue文件解析错误` }
}
const templateBlock = sfcResult.descriptor.template
if (!templateBlock) {
return { success: false, error: '未找到template块' }
}
const templateContent = templateBlock.content
const templateTagStart = vueContent.indexOf('<template')
const templateTagEnd = vueContent.indexOf('>', templateTagStart) + 1
const templateCloseStart = vueContent.indexOf('</template>', templateTagEnd)
const templateAST = parseTemplate(templateContent, {
comments: true,
whitespace: 'preserve'
})
const targetNode = findElementByPath(templateAST, elementPath)
if (!targetNode) {
return { success: false, error: `未找到元素: ${elementPath}` }
}
// 计算删除范围
let deleteStart = targetNode.loc.start.offset
let deleteEnd = targetNode.loc.end.offset
// 向前查找这一行的开始
const lineStart = templateContent.lastIndexOf('\n', deleteStart - 1) + 1
const beforeElement = templateContent.substring(lineStart, deleteStart)
// 如果元素前面只有空白,则从行开始删除
if (/^\s*$/.test(beforeElement)) {
deleteStart = lineStart
}
// 向后查找是否有换行
if (templateContent[deleteEnd] === '\n') {
deleteEnd = deleteEnd + 1
}
const newTemplateContent = templateContent.substring(0, deleteStart) +
templateContent.substring(deleteEnd)
const newVueContent = vueContent.substring(0, templateTagEnd) +
newTemplateContent +
vueContent.substring(templateCloseStart)
console.log(`[deleteElement] 删除成功: ${elementPath}`)
return { success: true, content: newVueContent }
} catch (error) {
console.error('[deleteElement] 错误:', error)
return { success: false, error: error.message }
}
}