This commit is contained in:
wfz
2026-01-20 22:07:05 +08:00
parent bfa4e3107f
commit 842a132ec6
5 changed files with 1301 additions and 535 deletions

View File

@@ -1,366 +1,423 @@
# Vue页面可视化设计器 - 项目设计文档
## 项目概述
## 1. 项目概述
本项目是一个基于 **Vite + Vue3 + TypeScript + Element Plus** 的可视化页面设计器通过拖拽操作直接编辑真实的Vue源文件实现低代码页面快速构建。
本项目是一个基于 **Vue3 + TypeScript + Element Plus** 的可视化页面设计器通过拖拽操作直接编辑真实的Vue源文件实现低代码页面快速构建。
## 技术架构
### 1.1 核心特性
### 技术栈
| 特性 | 描述 |
|------|------|
| 直接解析Vue文件 | 动态渲染真实的.vue页面 |
| 源码级修改 | 拖拽操作直接修改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 | - |
### 系统架构
### 1.2 技术架构
```
┌─────────────────────────────────────────────────────────────┐
前端 (Vue3 + Vite)
─────────────────────────────────────────────────────────────┤
Designer.vue
├── Header (顶部菜单)
── MainLayout (三栏布局)
├── 左侧面板 (PageManager, DesignComponentList)
│ ├── 中间面板 (DesignCenter + InteractiveWrapper)
│ │ └── 右侧面板 (属性编辑器 - 待实现)
└── Footer (状态栏 - 显示拖放记录)
─────────────────────────────────────────────────────────────┤
│ 状态管理层 │
│ ├── dragStore.ts (拖拽状态、两阶段逻辑、层级选择) │
│ ├── panelStore.ts (面板布局状态) │
│ ├── designStore.ts (设计组件元数据) │
│ └── vueFileStore.ts (Vue文件选择状态) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────
前端应用 (Vue3 + Vite) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ 页面管理 │ │设计组件列│ │ 设计中心 │ │ 元数据 │
│ │表 │ │ 编辑器 │
──────────┘ └──────────┘ └──────────┘ └──────────┘
┌──────────────────────────────────────────────────────────┐
│ │ Pinia 状态管理 + 插件系统
│ dragStore │ interactionStore │ designStore │ vueFileStore│
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│ 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() - 移动元素(放入内部) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────
│ 后端服务 (Node.js + Express)
│ ┌──────────────────────────────────────────────────────────┐ │
│ templateService - Vue模板AST解析与修改
│ @vue/compiler-sfc + @vue/compiler-dom │
│ └──────────────────────────────────────────────────────────┘
└─────────────────────────────────────────────────────────────────
文件操作
┌─────────────────────────────────────────────────────────────┐
│ Vue源文件 (src/views/)
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────
│ Vue源文件 (src/views/*.vue) │
└─────────────────────────────────────────────────────────────────
```
---
## 核心功能模块
## 2. 核心模块设计
### 1. 拖拽状态管理 (dragStore.ts)
### 2.1 前端模块
#### 核心状态
#### 2.1.1 设计中心 (DesignCenter)
**职责**动态渲染选中的Vue页面注入交互事件
**核心组件**
- `index.vue` - 主容器动态加载Vue页面组件
- `InteractiveWrapper.vue` - 交互包装器,注入事件监听
- `DropZone.vue` - 拖放区域指示器
- `DragPreview.vue` - 拖拽跟随预览
**交互事件注入流程**
```typescript
// 拖拽阶段
dragPhase: 'source' | 'target'
// 1. 动态渲染页面组件
<component :is="selectedPageComponent" />
// 拖拽源信息
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)
}
// 2. 挂载后扫描DOM注入事件
const injectInteractionEvents = () => {
document.querySelectorAll('.el-row, .el-col').forEach(el => {
const path = generateElementPath(el) // 生成结构化路径
bindElementEvents(el, type, path) // 绑定交互事件
})
}
// 3. MutationObserver监听DOM变化动态注入
observer.observe(container, { childList: true, subtree: true })
```
#### 全局事件处理
#### 2.1.2 结构树 (TreeViewer)
**职责**展示页面的el-row/el-col结构支持选中和删除
**功能**
- 树形展示页面结构
- 点击节点选中对应元素
- 拖拽节点调整顺序
- 右键菜单/删除图标/Del键删除
#### 2.1.3 元数据编辑器 (DataTable)
**职责**:展示和编辑选中组件的属性
**支持的属性类型**
- `number` - 数字输入如span宽度
- `text` - 文本输入如placeholder
- `select` - 下拉选择如size尺寸
- `boolean` - 开关切换如stripe斑马纹
- `color` - 颜色选择器
#### 2.1.4 插件系统 (plugins/)
**dragStore** - 拖拽状态管理
```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
interface DragStore {
// 状态
isDragging: boolean
dragPhase: 'source' | 'target' // 两阶段拖拽
dragSource: DragSource | null
hierarchyNodes: HierarchyNode[] // 层级节点列表
selectedHierarchyIndex: number // 当前选中层级
hoverDirection: Direction | 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
// 方法
startDragFromCanvas(path, type, element) // 开始画布内拖拽
startDragFromComponentList(id, name, template) // 开始组件列表拖拽
enterTargetPhase(element) // 进入目标选择阶段
selectParentLevel() // 选择父级(↑键)
selectChildLevel() // 选择子级(↓键)
confirmDrop(pagePath) // 确认拖放
}
```
**interactionStore** - 交互事件钩子
```typescript
interface InteractionStore {
hoverTarget: InteractionTarget | null
selectedTarget: InteractionTarget | null
onHover(target)
onClick(target)
onLeave()
}
```
### 2.2 后端模块
#### 2.2.1 templateService
**职责**解析Vue文件执行结构修改
**核心函数**
| 函数 | 功能 |
|------|------|
| `moveElement(vueContent, options)` | 移动元素到新位置 |
| `insertElement(vueContent, options)` | 插入新元素 |
| `deleteElement(vueContent, elementPath)` | 删除元素 |
| `parseElementTree(vueContent)` | 解析页面结构树 |
| `parseComponentProps(vueContent, path)` | 获取组件属性 |
| `updateComponentProps(vueContent, path, updates)` | 更新组件属性 |
**技术实现**
```javascript
// 使用 @vue/compiler-sfc 解析Vue文件
import { parse as parseSFC } from '@vue/compiler-sfc'
// 使用 @vue/compiler-dom 解析template获取AST
import { parse as parseTemplate } from '@vue/compiler-dom'
// 通过AST定位元素使用字符串操作修改保持原始格式
```
---
## 3. 数据结构设计
### 3.1 结构化路径
**格式**`r{n}c{m}r{x}c{y}...`
| 前缀 | 含义 |
|------|------|
| r | el-row |
| c | el-col |
| 数字 | 同级元素中的索引从1开始 |
**示例**`r1c2r1` = 第1个row → 第2个col → 第1个row
### 3.2 设计组件配置
```json
{
"id": "TextInput",
"name": "输入框",
"icon": "✏️",
"description": "用于输入文本内容的表单组件",
"defaultSpan": 12,
"metadata": {
"span": {
"label": "宽度",
"type": "number",
"min": 1,
"max": 24,
"target": "el-col",
"attr": ":span"
},
"placeholder": {
"label": "占位符",
"type": "text",
"target": "el-input",
"attr": "placeholder"
}
}
}
```
### 3.3 设计组件模板
```html
<el-col :span="12">
<div class="design-component design-text-input" data-component="输入框">
<el-form-item label="文本输入">
<el-input v-model="inputValue" placeholder="请输入内容"></el-input>
</el-form-item>
</div>
</el-col>
```
**关键点**
- 外层必须是 `el-col`
- 内层 `div` 必须有 `data-component` 属性标识组件类型
- 使用 `class="design-component"` 标识设计组件
---
## 4. API设计
### 4.1 元素操作API
#### 移动元素
```
POST /api/move-element
Body: {
"pagePath": "D:/workspace/.../TestPage1.vue",
"source": {
"type": "canvas-element",
"path": "r1c1",
"elementType": "ec"
},
"targetPath": "r1c2",
"targetType": "ec",
"direction": "right" // top|bottom|left|right|inside
}
```
#### 插入元素
```
POST /api/insert-element
Body: {
"pagePath": "...",
"templateContent": "<el-col :span=\"12\">...</el-col>",
"targetPath": "r1c1",
"direction": "right"
}
```
#### 删除元素
```
POST /api/delete-element
Body: {
"pagePath": "...",
"elementPath": "r1c2"
}
```
### 4.2 属性操作API
#### 获取组件属性
```
GET /api/component-props?pagePath=...&elementPath=r1c1
Response: {
"success": true,
"componentId": "输入框",
"props": {
"span": 12,
"el-input:placeholder": "请输入"
}
}
```
#### 更新组件属性
```
POST /api/update-props
Body: {
"pagePath": "...",
"elementPath": "r1c1",
"updates": {
"span": 8,
"el-input:placeholder": "新占位符"
}
}
```
### 4.3 结构查询API
#### 获取页面结构树
```
GET /api/element-tree?pagePath=...
Response: {
"success": true,
"tree": [
{
"type": "row",
"path": "r1",
"label": "el-row",
"children": [
{
"type": "col",
"path": "r1c1",
"componentName": "输入框",
"children": []
}
}
]
}
}
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)
]
}
```
---
## 开发规范
## 5. 交互设计
### Vue页面结构规范
### 5.1 拖拽流程
```
1. 用户开始拖拽(组件列表/画布元素)
2. 进入源选择阶段,收集层级节点
3. 移动到目标位置,进入目标选择阶段
4. 显示DropZone上/下/左/右/放入)
5. 用户点击方向确认拖放
6. 调用后端API修改源文件
7. 触发vue-template-updated事件
8. 前端刷新页面和结构树
```
### 5.2 层级选择
当鼠标悬停在嵌套元素上时(如`r1c1r1c1`
- **默认选中**最深层级r1c1r1c1
- **↑键**切换到父级r1c1r1 → r1c1 → r1
- **↓键**:切换到子级
- **Esc**:取消拖拽
### 5.3 多视图联动
- 点击**设计中心**元素 → 结构树高亮 + 元数据面板更新
- 点击**结构树**节点 → 设计中心高亮 + 元数据面板更新
- **元数据面板**修改 → 源文件更新 → 设计中心刷新
---
## 6. 扩展指南
### 6.1 添加设计组件
1.`src/fauto/designComponents/` 创建目录
2. 添加 `index.json`(配置+元数据schema
3. 添加 `template.html`(组件模板)
4. 自动在组件列表显示
### 6.2 添加物料组件
1.`src/fauto/materials/` 创建目录
2. 添加 `index.vue`(组件实现)
3. 添加 `index.json`(配置信息)
4.`materials/index.ts` 注册
### 6.3 扩展元数据类型
`designStore.ts``MetadataField` 接口添加新类型,并在 `DataTable/index.vue` 添加对应的编辑控件。
---
## 7. 技术规范
### 7.1 Vue页面规范
```vue
<template>
<!-- 第一层级必须且只能有一个 el-row -->
<el-row class="page-container" :gutter="20">
<!-- el-row 内只能包含 el-col -->
<el-col :span="12">
<!-- el-col 内可以放设计组件或嵌套 el-row -->
<div class="design-component">内容</div>
</el-col>
<el-col :span="12">
<el-row :gutter="10">
<el-col :span="24">嵌套内容</el-col>
</el-row>
<!-- 第一层级必须且只能有一个el-row -->
<el-row :gutter="20">
<el-col :span="24">
<!-- 设计组件必须有data-component属性 -->
<div class="design-component" data-component="组件名">
<!-- 组件内容 -->
</div>
</el-col>
</el-row>
</template>
```
### 代码规范
### 7.2 代码规范
1. 使用 TypeScript 严格模式
2. 组件使用 Composition API
3. Props 必须明确定义类型
4. 样式使用 scoped 避免污染
### 插件开发规范
所有与页面交互相关的代码放在 `src/fauto/plugins/` 目录:
- 状态管理类使用 Pinia defineStore
- 工具函数单独文件导出
- 统一通过 `index.ts` 导出
- TypeScript 严格模式
- Composition API 组织代码
- Props 必须定义类型
- 样式使用 scoped
- 插件代码放 `fauto/plugins/`
---
## API 接口
## 8. 版本信息
### 移动元素
```
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": "元素移动成功"
}
```
- **前端框架**: Vue 3.5.24
- **构建工具**: Vite 7.3.0
- **状态管理**: Pinia 3.0.4
- **UI框架**: Element Plus
- **后端运行时**: Node.js
- **模板解析**: @vue/compiler-sfc, @vue/compiler-dom
---
## 待实现功能
1. **属性编辑器** - 展示和编辑选中组件的属性
2. **撤销/重做** - 操作历史记录
3. **设计组件拖入** - 从组件列表拖入新组件
4. **预览模式** - 隐藏交互层预览最终效果
5. **导出功能** - 导出设计结果
---
## 已知问题
1. 拖放操作有概率生成错误HTML结构已基本修复
2. 复杂嵌套结构下缩进可能不完美
---
**最后更新**2026-01-20
**文档更新时间**2026-01-20