This commit is contained in:
wfz
2026-01-20 21:53:09 +08:00
parent 4a90340ab3
commit bfa4e3107f
23 changed files with 2154 additions and 592 deletions

View File

@@ -6,9 +6,12 @@
import express from 'express'
import cors from 'cors'
import { moveElement } from './services/templateService.js'
import { moveElement, insertElement, parseElementTree, parseComponentProps, updateComponentProps, deleteElement } from './services/templateService.js'
import { readFile, writeFile } from 'fs/promises'
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const app = express()
const PORT = 3001
@@ -20,6 +23,9 @@ app.use(express.json())
// Vue源码目录相对于服务根目录
const VUE_SOURCE_DIR = path.resolve('../draggable-panels/src/views')
// 设计组件模板目录
const DESIGN_COMPONENTS_DIR = path.resolve('../draggable-panels/src/fauto/designComponents')
/**
* API: 执行元素移动操作
*
@@ -127,8 +133,276 @@ app.get('/api/health', (req, res) => {
})
})
/**
* API: 插入设计组件
*
* POST /api/insert-component
* Body: {
* pagePath: string, // Vue文件路径
* componentId: string, // 设计组件ID
* targetPath: string, // 目标元素路径
* direction: string // 插入方向
* }
*/
app.post('/api/insert-component', async (req, res) => {
try {
const { pagePath, componentId, targetPath, direction } = req.body
console.log('[API] 收到插入请求:', {
pagePath,
componentId,
targetPath,
direction
})
// 验证参数
if (!pagePath || !componentId || !targetPath || !direction) {
return res.status(400).json({
success: false,
error: '缺少必要参数'
})
}
// 构建完整文件路径
let normalizedPath = pagePath
const viewsIndex = normalizedPath.indexOf('views/')
if (viewsIndex !== -1) {
normalizedPath = normalizedPath.substring(viewsIndex + 6)
}
const filePath = path.join(VUE_SOURCE_DIR, normalizedPath)
// 读取设计组件模板
const templatePath = path.join(DESIGN_COMPONENTS_DIR, componentId, 'template.html')
let componentTemplate
try {
componentTemplate = await readFile(templatePath, 'utf-8')
} catch (err) {
return res.status(404).json({
success: false,
error: `设计组件模板不存在: ${componentId}`
})
}
console.log('[API] 设计组件模板:', componentTemplate.substring(0, 100) + '...')
// 读取Vue文件
let vueContent
try {
vueContent = await readFile(filePath, 'utf-8')
} catch (err) {
return res.status(404).json({
success: false,
error: `文件不存在: ${pagePath}`
})
}
// 执行插入操作
const result = insertElement(vueContent, {
templateContent: componentTemplate.trim(),
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: `已将 ${componentId} 插入到 ${targetPath}${direction}方向`
})
} catch (error) {
console.error('[API] 错误:', error)
res.status(500).json({
success: false,
error: error.message
})
}
})
/**
* API: 获取页面元素结构树
*
* GET /api/element-tree?pagePath=xxx
*/
app.get('/api/element-tree', async (req, res) => {
try {
const { pagePath } = req.query
if (!pagePath) {
return res.status(400).json({
success: false,
error: '缺少pagePath参数'
})
}
// 构建完整文件路径
let normalizedPath = pagePath
const viewsIndex = normalizedPath.indexOf('views/')
if (viewsIndex !== -1) {
normalizedPath = normalizedPath.substring(viewsIndex + 6)
}
const filePath = path.join(VUE_SOURCE_DIR, 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 = parseElementTree(vueContent)
if (!result.success) {
return res.status(400).json({
success: false,
error: result.error
})
}
res.json({
success: true,
tree: result.tree
})
} catch (error) {
console.error('[API] 错误:', error)
res.status(500).json({
success: false,
error: error.message
})
}
})
// 启动服务
app.listen(PORT, () => {
console.log(`🚀 Vue模板服务启动: http://localhost:${PORT}`)
console.log(`📁 源码目录: ${VUE_SOURCE_DIR}`)
})
/**
* API: 获取组件属性
* GET /api/component-props?pagePath=xxx&elementPath=xxx
*/
app.get('/api/component-props', async (req, res) => {
try {
const { pagePath, elementPath } = req.query
if (!pagePath || !elementPath) {
return res.status(400).json({
success: false,
error: '缺少参数'
})
}
let normalizedPath = pagePath
const viewsIndex = normalizedPath.indexOf('views/')
if (viewsIndex !== -1) {
normalizedPath = normalizedPath.substring(viewsIndex + 6)
}
const filePath = path.join(VUE_SOURCE_DIR, normalizedPath)
const vueContent = await readFile(filePath, 'utf-8')
const result = parseComponentProps(vueContent, elementPath)
if (!result.success) {
return res.status(400).json(result)
}
res.json(result)
} catch (error) {
res.status(500).json({ success: false, error: error.message })
}
})
/**
* API: 更新组件属性
* POST /api/update-props
*/
app.post('/api/update-props', async (req, res) => {
try {
const { pagePath, elementPath, updates } = req.body
if (!pagePath || !elementPath || !updates) {
return res.status(400).json({
success: false,
error: '缺少参数'
})
}
let normalizedPath = pagePath
const viewsIndex = normalizedPath.indexOf('views/')
if (viewsIndex !== -1) {
normalizedPath = normalizedPath.substring(viewsIndex + 6)
}
const filePath = path.join(VUE_SOURCE_DIR, normalizedPath)
const vueContent = await readFile(filePath, 'utf-8')
const result = updateComponentProps(vueContent, elementPath, updates)
if (!result.success) {
return res.status(400).json(result)
}
await writeFile(filePath, result.content, 'utf-8')
res.json({ success: true, message: '属性更新成功' })
} catch (error) {
res.status(500).json({ success: false, error: error.message })
}
})
/**
* API: 删除元素
* POST /api/delete-element
*/
app.post('/api/delete-element', async (req, res) => {
try {
const { pagePath, elementPath } = req.body
if (!pagePath || !elementPath) {
return res.status(400).json({
success: false,
error: '缺少参数'
})
}
let normalizedPath = pagePath
const viewsIndex = normalizedPath.indexOf('views/')
if (viewsIndex !== -1) {
normalizedPath = normalizedPath.substring(viewsIndex + 6)
}
const filePath = path.join(VUE_SOURCE_DIR, normalizedPath)
const vueContent = await readFile(filePath, 'utf-8')
const result = deleteElement(vueContent, elementPath)
if (!result.success) {
return res.status(400).json(result)
}
await writeFile(filePath, result.content, 'utf-8')
console.log(`[API] 删除成功: ${elementPath}`)
res.json({ success: true, message: '元素删除成功' })
} catch (error) {
res.status(500).json({ success: false, error: error.message })
}
})

View File

@@ -375,3 +375,509 @@ export function getTemplateAST(vueContent) {
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 }
}
}