/** * 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 = `` 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 // 找到 ', 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 // 找到 ', 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 = `` 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('', templateTagStart) + 1 const templateCloseStart = vueContent.indexOf('', 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('', templateTagStart) + 1 const templateCloseStart = vueContent.indexOf('', 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 } } }