884 lines
29 KiB
JavaScript
884 lines
29 KiB
JavaScript
/**
|
||
* 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 }
|
||
}
|
||
}
|