# Vue页面可视化设计器 - 项目设计文档 ## 项目概述 本项目是一个基于 **Vite + Vue3 + TypeScript + Element Plus** 的可视化页面设计器,通过拖拽操作直接编辑真实的Vue源文件,实现低代码页面快速构建。 ## 技术架构 ### 技术栈 | 类别 | 技术 | 版本 | |------|------|------| | 构建工具 | Vite | 7.3.0 | | 前端框架 | Vue 3 (Composition API) | 3.5.24 | | UI组件库 | Element Plus | - | | 状态管理 | Pinia | 3.0.4 | | 类型系统 | TypeScript | - | | 后端运行时 | Node.js | 22.x | | 后端框架 | Express | - | | 模板解析 | @vue/compiler-sfc, @vue/compiler-dom | - | ### 系统架构 ``` ┌─────────────────────────────────────────────────────────────┐ │ 前端 (Vue3 + Vite) │ ├─────────────────────────────────────────────────────────────┤ │ Designer.vue │ │ ├── Header (顶部菜单) │ │ ├── MainLayout (三栏布局) │ │ │ ├── 左侧面板 (PageManager, DesignComponentList) │ │ │ ├── 中间面板 (DesignCenter + InteractiveWrapper) │ │ │ └── 右侧面板 (属性编辑器 - 待实现) │ │ └── Footer (状态栏 - 显示拖放记录) │ ├─────────────────────────────────────────────────────────────┤ │ 状态管理层 │ │ ├── dragStore.ts (拖拽状态、两阶段逻辑、层级选择) │ │ ├── panelStore.ts (面板布局状态) │ │ ├── designStore.ts (设计组件元数据) │ │ └── vueFileStore.ts (Vue文件选择状态) │ └─────────────────────────────────────────────────────────────┘ │ │ HTTP API ▼ ┌─────────────────────────────────────────────────────────────┐ │ 后端 (Node.js + Express) │ ├─────────────────────────────────────────────────────────────┤ │ API: POST /api/move-element │ │ ├── 接收: { pagePath, source, target, direction } │ │ └── 处理: templateService.moveElement() │ ├─────────────────────────────────────────────────────────────┤ │ templateService.js │ │ ├── parseSFC() - 解析Vue单文件组件 │ │ ├── parseTemplate() - 解析template获取AST │ │ ├── findElementByPath() - 根据路径定位元素 │ │ ├── moveElement() - 移动元素(同级前后) │ │ └── moveElementInside() - 移动元素(放入内部) │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Vue源文件 (src/views/) │ └─────────────────────────────────────────────────────────────┘ ``` --- ## 核心功能模块 ### 1. 拖拽状态管理 (dragStore.ts) #### 核心状态 ```typescript // 拖拽阶段 dragPhase: 'source' | 'target' // 拖拽源信息 dragSource: { type: 'design-component' | 'canvas-element' componentId?: string componentName?: string path?: string elementType?: 'er' | 'ec' } // 已确定的源元素(进入目标阶段时保存) confirmedSource: { path: string type: 'er' | 'ec' element: HTMLElement } // 层级节点列表(从深到浅排序) hierarchyNodes: HierarchyNode[] // 当前选中的层级索引 selectedHierarchyIndex: number // 拖放方向 hoverDirection: 'top' | 'bottom' | 'left' | 'right' | 'inside' | null ``` #### 两阶段拖拽流程 ``` 源选择阶段 (source) ├── 触发: startDragFromCanvas(path, type, element) ├── 功能: │ ├── 构建当前元素的层级列表 │ └── ↑↓键切换要拖拽的源层级 ├── 特点: │ ├── 不显示DropZone │ └── 松开鼠标取消拖拽 └── 退出: 移动到其他元素 → enterTargetPhase() 目标选择阶段 (target) ├── 触发: enterTargetPhase(targetElement) ├── 功能: │ ├── 保存确定的源元素到 confirmedSource │ ├── 构建目标元素的层级列表 │ └── ↑↓键切换目标层级 ├── 特点: │ ├── 显示DropZone │ └── 根据源/目标类型显示方向 └── 退出: 松开鼠标 → confirmDrop() ``` #### 跨类型拖放逻辑 ```typescript const isCrossTypeDrop = computed(() => { if (dragPhase.value !== 'target') return false if (!confirmedSource.value || !selectedNode.value) return false // 源类型与目标类型不同 return confirmedSource.value.type !== selectedNode.value.type }) // 跨类型时显示 'inside' 方向 if (isCrossTypeDrop.value) { hoverDirection.value = 'inside' } ``` ### 2. 交互注入器 (InteractiveWrapper.vue) #### 核心职责 1. **动态渲染Vue页面** - 使用 `defineAsyncComponent` 加载选中的页面 2. **注入交互事件** - 为所有 el-row/el-col 绑定事件 3. **监听DOM变化** - 使用 MutationObserver 检测新元素 4. **键盘事件处理** - ↑↓切换层级,Esc取消 #### 事件绑定 ```typescript const bindElementEvents = (element, type, path) => { // 标记已绑定 element.setAttribute('data-fauto-bindend', 'true') element.setAttribute('data-path', path) element.classList.add('fauto-interactive') // 悬停事件 - 进入目标阶段 element.addEventListener('mouseenter', (e) => { if (dragStore.isDragging) { const sourcePath = dragStore.confirmedSource?.path || dragStore.dragSource?.path if (sourcePath && path !== sourcePath && !path.startsWith(sourcePath)) { dragStore.enterTargetPhase(element) } } }) // 按下事件 - 开始拖拽 element.addEventListener('mousedown', (e) => { if (!dragStore.isDragging) { dragStore.startDragFromCanvas(path, type, element) } }) } ``` #### 全局事件处理 ```typescript // 全局鼠标移动 - 更新拖放方向 const handleGlobalMouseMove = (e) => { if (dragStore.isDragging && dragStore.dragPhase === 'target') { dragStore.updateDirectionFromMouse(e.clientX, e.clientY) } } // 全局鼠标松开 - 确认拖放 const handleGlobalMouseUp = async () => { if (dragStore.isDragging && dragStore.dragPhase === 'target') { await dragStore.confirmDrop(vueFileStore.selectedFilePath) } dragStore.endDrag() } ``` ### 3. 后端模板服务 (templateService.js) #### 路径解析 ```javascript // 解析路径: "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], index: parseInt(match[2], 10) }) } return nodes } ``` #### 元素定位 ```javascript function findElementByPath(ast, pathStr) { const pathNodes = parsePath(pathStr) let currentChildren = ast.children let currentNode = null for (const pathNode of pathNodes) { const targetType = pathNode.type === 'r' ? 'el-row' : 'el-col' let count = 0 for (const child of currentChildren) { if (child.type === 1 && child.tag === targetType) { count++ if (count === pathNode.index) { currentNode = child currentChildren = child.children || [] break } } } } return currentNode } ``` #### 缩进调整算法 ```javascript function adjustIndentation(sourceText, targetIndent) { const lines = sourceText.split('\n') // 获取第一行的原始缩进 const firstLineIndent = getIndentLevel(lines[0]) // 计算缩进差值 const indentDiff = targetIndent.length - firstLineIndent // 对每一行应用缩进差值(保持相对缩进) return lines.map(line => { if (!line.trim()) return '' const currentIndent = getIndentLevel(line) const newIndent = Math.max(0, currentIndent + indentDiff) return ' '.repeat(newIndent) + line.trimStart() }).join('\n') } ``` #### 元素移动(从后向前处理) ```javascript // 关键:从后向前处理,避免偏移量错误 if (deleteStart > insertPosition) { // 删除位置在后:先删除,再插入 result = content.substring(0, deleteStart) + content.substring(deleteEnd) result = result.substring(0, insertPosition) + insertText + result.substring(insertPosition) } else { // 删除位置在前:先删除,调整插入位置 const deletedLength = deleteEnd - deleteStart const adjustedInsertPos = insertPosition - deletedLength result = content.substring(0, deleteStart) + content.substring(deleteEnd) result = result.substring(0, adjustedInsertPos) + insertText + result.substring(adjustedInsertPos) } ``` --- ## 开发规范 ### Vue页面结构规范 ```vue ``` ### 代码规范 1. 使用 TypeScript 严格模式 2. 组件使用 Composition API 3. Props 必须明确定义类型 4. 样式使用 scoped 避免污染 ### 插件开发规范 所有与页面交互相关的代码放在 `src/fauto/plugins/` 目录: - 状态管理类使用 Pinia defineStore - 工具函数单独文件导出 - 统一通过 `index.ts` 导出 --- ## API 接口 ### 移动元素 ``` POST /api/move-element Request: { "pagePath": "views/TestPage1.vue", // 相对于 src 的路径 "source": "r1c1", // 源元素路径 "target": "r1c2", // 目标元素路径 "direction": "right" // top|bottom|left|right|inside } Response: { "success": true, "message": "元素移动成功" } ``` --- ## 待实现功能 1. **属性编辑器** - 展示和编辑选中组件的属性 2. **撤销/重做** - 操作历史记录 3. **设计组件拖入** - 从组件列表拖入新组件 4. **预览模式** - 隐藏交互层预览最终效果 5. **导出功能** - 导出设计结果 --- ## 已知问题 1. 拖放操作有概率生成错误HTML结构(已基本修复) 2. 复杂嵌套结构下缩进可能不完美 --- **最后更新**:2026-01-20