Files
fauto-design/项目设计文档.md
2026-01-20 20:25:03 +08:00

367 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
<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-col>
</el-row>
</template>
```
### 代码规范
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