22 KiB
22 KiB
🎯 可视化拖拽设计器 - 整体实施计划
目标:将当前的设计器升级为可直接解析和编辑真实Vue文件的可视化拖拽编辑器
核心技术:Vue编译时AST转换 + Element Plus布局 + 实时文件修改
参考实现:plugins示例项目的拖拽机制
📋 项目总体架构设计
整体技术栈升级
现有架构 → 目标架构
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
设计组件 (虚拟对象) → 真实Vue文件解析
DesignCenter (模拟预览) → 实时渲染的Vue页面
属性编辑 (内存状态) → 直接修改源文件
拖拽排序 (组件实例) → 拖拽修改el-row/el-col布局
核心功能模块划分
┌─────────────────────────────────────────────────────────┐
│ 可视化拖拽设计器 │
└─────────────────────────────────────────────────────────┘
│
├─ 模块1: Vue文件解析引擎
│ ├─ AST解析与遍历
│ ├─ 组件树结构提取
│ └─ 元数据生成 (_material路径ID)
│
├─ 模块2: 编译时增强插件系统
│ ├─ Vite插件配置
│ ├─ Vue模板AST转换
│ ├─ 拖拽指令自动注入
│ └─ 结构化ID生成
│
├─ 模块3: 运行时拖拽交互系统
│ ├─ 自定义拖拽指令 (v-auto-drag)
│ ├─ 拖拽事件处理 (dragstart/drop/over)
│ ├─ 元素选中与高亮
│ └─ 滚轮切换父子元素
│
├─ 模块4: 可视化画布系统
│ ├─ 实时渲染Vue页面
│ ├─ el-row/el-col布局展示
│ ├─ 拖拽预览与占位符
│ └─ 组件边界高亮
│
├─ 模块5: 源文件修改引擎
│ ├─ 模板字符串操作
│ ├─ 元素位置计算与插入
│ ├─ 代码格式化与清理
│ └─ 文件写入API (后端支持)
│
└─ 模块6: 设计器UI增强
├─ 左侧: 组件树查看器
├─ 中间: 可视化画布
├─ 右侧: 属性编辑器
└─ 顶部: 文件选择与工具栏
🗂️ 项目文件结构设计
draggable-panels/
├── src/
│ ├── fauto/
│ │ ├── Designer.vue # 设计器主入口
│ │ │
│ │ ├── components/
│ │ │ ├── Header.vue # ✨新增: 文件选择工具栏
│ │ │ ├── MainLayout.vue # (保持原有)
│ │ │ └── Panel.vue # (保持原有)
│ │ │
│ │ ├── materials/
│ │ │ ├── VueFileExplorer/ # ✨新增: Vue文件浏览器
│ │ │ │ ├── index.vue
│ │ │ │ └── index.json
│ │ │ │
│ │ │ ├── ComponentTreeViewer/ # ✨新增: 组件结构树
│ │ │ │ ├── index.vue
│ │ │ │ └── index.json
│ │ │ │
│ │ │ ├── VisualCanvas/ # ✨新增: 可视化画布
│ │ │ │ ├── index.vue # (实时渲染目标Vue文件)
│ │ │ │ └── index.json
│ │ │ │
│ │ │ ├── PropertyEditor/ # ✨新增: 属性编辑器
│ │ │ │ ├── index.vue
│ │ │ │ └── index.json
│ │ │ │
│ │ │ └── (保留原有物料组件...)
│ │ │
│ │ ├── designComponents/ # (保留,但不再是主要编辑对象)
│ │ │
│ │ ├── core/ # ✨新增: 核心功能模块
│ │ │ ├── parser/ # Vue文件解析器
│ │ │ │ ├── vueParser.ts # SFC解析入口
│ │ │ │ ├── templateParser.ts # template AST解析
│ │ │ │ ├── componentTreeBuilder.ts # 组件树构建
│ │ │ │ └── pathGenerator.ts # 结构化路径生成
│ │ │ │
│ │ │ ├── modifier/ # 源文件修改器
│ │ │ │ ├── templateModifier.ts # 模板修改逻辑
│ │ │ │ ├── elementExtractor.ts # 元素提取与定位
│ │ │ │ ├── codeFormatter.ts # 代码格式化
│ │ │ │ └── fileWriter.ts # 文件写入API封装
│ │ │ │
│ │ │ └── directives/ # 自定义指令
│ │ │ ├── autoDrag.ts # 拖拽指令实现
│ │ │ └── elementSelect.ts # 元素选中指令
│ │ │
│ │ ├── stores/
│ │ │ ├── panelStore.ts # (保留)
│ │ │ ├── designStore.ts # (保留,但角色调整)
│ │ │ │
│ │ │ ├── vueFileStore.ts # ✨新增: Vue文件状态管理
│ │ │ │ # - 当前打开的Vue文件路径
│ │ │ │ # - 解析后的组件树结构
│ │ │ │ # - 当前选中的元素
│ │ │ │ # - 文件缓存与同步
│ │ │ │
│ │ │ └── canvasStore.ts # ✨新增: 画布交互状态
│ │ │ # - 拖拽状态管理
│ │ │ # - 选中元素路径
│ │ │ # - 悬停目标信息
│ │ │
│ │ └── types/
│ │ ├── index.ts # (保留原有)
│ │ ├── vueFile.ts # ✨新增: Vue文件相关类型
│ │ └── canvas.ts # ✨新增: 画布相关类型
│ │
│ └── plugins/ # ✨新增: Vite/Vue插件
│ ├── index.ts # 插件总入口
│ ├── vite/
│ │ └── vueFilePlugin.ts # Vite插件 (文件写入API)
│ └── vue/
│ ├── dragPlugin.ts # Vue编译时插件
│ ├── autoDrag.ts # 拖拽指令实现
│ └── dragStyles.css # 拖拽样式
│
├── vite.config.ts # ✨修改: 集成新插件
├── package.json # ✨修改: 新增依赖
└── IMPLEMENTATION_PLAN.md # 本文档
📦 阶段性实施计划
阶段1: 基础设施搭建 (第1-2天)
任务1.1: 安装依赖与环境准备
- 安装Element Plus (
npm install element-plus) - 安装@vue/compiler-sfc (用于AST解析)
- 配置vite.config.ts引入插件系统
- 创建测试用的TestPage.vue文件
任务1.2: 创建插件系统框架
- 创建
src/plugins/目录结构 - 实现
vite/vueFilePlugin.ts- 文件写入API中间件 - 实现
vue/dragPlugin.ts- Vue编译时节点转换器 - 配置vite.config.ts加载插件
验收标准:
- ✅ Vite能正常启动,插件加载无报错
- ✅ 访问
/api/write-file能收到响应 - ✅ TestPage.vue编译时能看到插件日志
阶段2: Vue文件解析引擎 (第3-4天)
任务2.1: 实现SFC解析器
- 创建
core/parser/vueParser.ts- 解析Vue单文件组件
- 提取template/script/style
- 缓存原始代码
任务2.2: 实现template AST解析
- 创建
core/parser/templateParser.ts- 使用@vue/compiler-dom解析template
- 遍历AST节点
- 识别el-row和el-col元素
任务2.3: 实现组件树构建器
- 创建
core/parser/componentTreeBuilder.ts- 递归构建树形结构
- 生成结构化路径ID (r1c2r1c1...)
- 提取组件属性信息
任务2.4: 创建vueFileStore
- 创建
stores/vueFileStore.ts- 管理当前打开的Vue文件
- 存储解析后的组件树
- 提供文件加载/保存接口
验收标准:
- ✅ 能解析TestPage.vue并生成组件树
- ✅ 每个el-row/el-col都有唯一的路径ID
- ✅ vueFileStore能正确缓存解析结果
阶段3: 编译时增强系统 (第5-6天)
任务3.1: 实现路径ID生成算法
- 创建
core/parser/pathGenerator.ts- 实现plugins示例中的路径生成逻辑
- 递归计算父子关系
- 生成r1c2格式的ID
任务3.2: 实现Vue编译时转换器
- 完善
plugins/vue/dragPlugin.ts- 在编译时遍历AST
- 自动为el-row/el-col注入v-auto-drag指令
- 注入_material属性
任务3.3: 实现拖拽指令
- 创建
plugins/vue/autoDrag.ts- 实现dragstart/dragover/drop事件处理
- 实现元素选中逻辑
- 实现滚轮切换父子元素
- 添加拖拽样式
验收标准:
- ✅ 编译后的Vue组件自动包含拖拽功能
- ✅ 每个el-col都能被拖拽
- ✅ 点击元素能高亮选中
- ✅ 滚轮能切换选中父子元素
阶段4: 可视化画布系统 (第7-8天)
任务4.1: 创建VisualCanvas物料组件
- 创建
materials/VisualCanvas/index.vue- 动态加载目标Vue文件
- 使用iframe或动态组件渲染
- 应用拖拽样式
任务4.2: 实现画布交互状态管理
- 创建
stores/canvasStore.ts- 管理拖拽状态(拖拽元素ID、目标位置)
- 管理选中状态(当前选中元素路径)
- 提供拖拽预览数据
任务4.3: 实现拖拽视觉反馈
- 创建
plugins/vue/dragStyles.css- 拖拽中的半透明效果
- 拖拽进入的高亮边框
- 元素选中的蓝色描边
- 拖拽占位符样式
验收标准:
- ✅ 画布能正确渲染TestPage.vue
- ✅ 拖拽时有清晰的视觉反馈
- ✅ 选中元素有蓝色描边
- ✅ 悬停目标有红色高亮
阶段5: 源文件修改引擎 (第9-10天)
任务5.1: 实现元素提取与定位
- 创建
core/modifier/elementExtractor.ts- 根据_material ID定位元素
- 提取完整的元素字符串(含子元素)
- 计算元素在模板中的位置
任务5.2: 实现模板修改逻辑
- 创建
core/modifier/templateModifier.ts- 移除源元素
- 根据方向插入到目标位置
- 处理嵌套结构的移动
任务5.3: 实现代码格式化与清理
- 创建
core/modifier/codeFormatter.ts- 清除运行时注入的_material属性
- 保持原有代码缩进
- 移除多余的空行
任务5.4: 实现文件写入封装
- 创建
core/modifier/fileWriter.ts- 调用/api/write-file接口
- 错误处理与重试
- 写入成功后刷新页面
验收标准:
- ✅ 拖拽后能正确修改Vue源文件
- ✅ 修改后的代码格式正确
- ✅ 不会破坏原有代码结构
- ✅ 页面自动刷新显示修改结果
阶段6: 设计器UI增强 (第11-12天)
任务6.1: 创建VueFileExplorer
- 创建
materials/VueFileExplorer/index.vue- 显示项目中的所有.vue文件
- 点击文件加载到画布
- 显示当前打开的文件路径
任务6.2: 创建ComponentTreeViewer
- 创建
materials/ComponentTreeViewer/index.vue- 展示当前Vue文件的组件树
- 显示el-row/el-col的层级结构
- 点击树节点选中画布中的元素
- 支持拖拽调整顺序(可选)
任务6.3: 创建PropertyEditor
- 创建
materials/PropertyEditor/index.vue- 显示选中元素的属性(span、gutter等)
- 支持修改属性值
- 实时更新到源文件
任务6.4: 升级Header工具栏
- 修改
components/Header.vue- 添加"打开文件"按钮
- 显示当前文件名
- 添加"保存"、"撤销"按钮(可选)
验收标准:
- ✅ 能浏览并选择Vue文件
- ✅ 组件树能实时同步画布状态
- ✅ 属性编辑器能修改元素属性
- ✅ 工具栏显示当前操作状态
阶段7: 功能完善与优化 (第13-14天)
任务7.1: 实现撤销/重做功能
- 创建历史记录栈
- 实现Ctrl+Z撤销
- 实现Ctrl+Shift+Z重做
任务7.2: 实现拖拽位置预览
- 拖拽时显示插入位置占位符
- 左右方向箭头指示
- 不合法拖拽的禁止光标
任务7.3: 实现快捷键支持
- Delete键删除选中元素
- Ctrl+C/V 复制粘贴(可选)
- 方向键微调位置(可选)
任务7.4: 性能优化
- 大文件解析性能优化
- 拖拽事件节流
- 组件树虚拟滚动(如果元素很多)
验收标准:
- ✅ 撤销/重做功能正常
- ✅ 拖拽预览清晰直观
- ✅ 快捷键响应流畅
- ✅ 大文件(100+元素)操作流畅
阶段8: 测试与文档 (第15天)
任务8.1: 编写单元测试
- 测试Vue文件解析器
- 测试路径ID生成算法
- 测试模板修改逻辑
任务8.2: 编写集成测试
- 端到端拖拽测试
- 文件加载与保存测试
- 多文件切换测试
任务8.3: 更新项目文档
- 更新PROJECT_CONTEXT.md
- 编写用户使用指南
- 编写开发者文档
🔧 技术实现关键点
1. Vue编译时AST转换
// plugins/vue/dragPlugin.ts
export function dragNodeTransform(node, context) {
// 1. 只处理el-row和el-col元素
if (node.type === 1 && (node.tag === 'el-row' || node.tag === 'el-col')) {
// 2. 生成结构化路径ID
const materialId = generateStructuredPath(node, context)
// 3. 注入v-auto-drag指令
node.props.push({
type: 7, // 指令类型
name: 'auto-drag',
exp: undefined,
modifiers: []
})
// 4. 注入_material属性
node.props.push({
type: 6, // 属性类型
name: '_material',
value: { type: 2, content: materialId }
})
}
}
2. 结构化路径ID生成算法
// core/parser/pathGenerator.ts
/**
* 生成结构化路径ID
* 例如: r1c2r1 表示 第1个row -> 第2个col -> 第1个row
*/
function generateStructuredPath(node, context) {
// 1. 递归查找父节点路径
const parent = findParentNode(node, context.root)
let parentPath = parent ? generateStructuredPath(parent) : ''
// 2. 计算当前节点在同级中的索引
if (node.tag === 'el-row') {
const rowIndex = calculateSiblingIndex(node, parent, 'el-row')
return `${parentPath}r${rowIndex}`
} else if (node.tag === 'el-col') {
const colIndex = calculateSiblingIndex(node, parent, 'el-col')
return `${parentPath}c${colIndex}`
}
return parentPath
}
3. 拖拽事件处理流程
// plugins/vue/autoDrag.ts
// 拖拽开始
function onDragStart(e, elementId) {
__dragElement.value = elementId
e.target.classList.add('drag-overlay')
}
// 拖拽悬停
function onDragOver(e, targetId) {
const rect = e.target.getBoundingClientRect()
const direction = e.clientX - rect.left < rect.width / 2 ? 'left' : 'right'
__dropInfo.value = { targetId, direction }
e.target.classList.add('drag-enter')
}
// 拖拽放置
function onDrop(e, targetId) {
// 调用模板修改器
modifyVueTemplate(__dragElement.value, targetId, __dropInfo.value.direction)
// 清理状态
cleanupDragState()
}
4. 模板字符串修改逻辑
// core/modifier/templateModifier.ts
function modifyTemplate(sourceId, targetId, direction) {
// 1. 提取源元素
const sourceElement = extractElementById(template, sourceId)
// 2. 从模板中移除源元素
template = template.replace(sourceElement, '')
// 3. 查找目标元素位置
const targetPosition = findElementPosition(template, targetId)
// 4. 根据方向插入源元素
if (direction === 'left') {
template = insertBefore(template, targetPosition, sourceElement)
} else {
template = insertAfter(template, targetPosition, sourceElement)
}
// 5. 清理_material属性
template = template.replace(/\s+_material="[^"]*"/g, '')
return template
}
5. 文件写入API
// plugins/vite/vueFilePlugin.ts
configureServer(server) {
server.middlewares.use('/api/write-file', async (req, res) => {
const { filePath, content } = await parseBody(req)
const fullPath = path.resolve(process.cwd(), filePath)
fs.writeFileSync(fullPath, content, 'utf8')
res.end(JSON.stringify({ success: true }))
})
}
🎯 核心数据流
1. 用户打开Vue文件
↓
2. vueFileStore.loadFile(path)
↓
3. vueParser解析SFC → 提取template
↓
4. templateParser解析AST → 构建组件树
↓
5. 组件树显示在ComponentTreeViewer
↓
6. VisualCanvas渲染Vue文件(编译时已注入拖拽指令)
↓
7. 用户在画布中拖拽el-col
↓
8. dragStart → 记录源ID
↓
9. dragOver → 计算目标ID和方向
↓
10. drop → 调用templateModifier
↓
11. 修改template字符串
↓
12. 调用/api/write-file写入文件
↓
13. Vite热更新 → 页面自动刷新
↓
14. 显示修改后的布局 ✅
📊 关键类型定义
// types/vueFile.ts
export interface VueFileInfo {
path: string // 文件路径
name: string // 文件名
content: string // 原始内容
template: string // 模板部分
componentTree: ComponentNode[] // 组件树
lastModified: number // 最后修改时间
}
export interface ComponentNode {
id: string // 结构化路径ID (r1c2)
tag: 'el-row' | 'el-col' // 标签类型
attrs: Record<string, any> // 属性 (span, gutter等)
children: ComponentNode[] // 子节点
parent: ComponentNode | null // 父节点引用
depth: number // 深度(用于树形展示)
}
// types/canvas.ts
export interface DragState {
dragElementId: string | null // 正在拖拽的元素ID
targetElementId: string | null // 目标元素ID
direction: 'left' | 'right' | null // 插入方向
isDragging: boolean // 是否正在拖拽
}
export interface SelectState {
selectedElementId: string | null // 选中的元素ID
selectedElementPath: ComponentNode[] // 选中元素的父级路径
hoverElementId: string | null // 鼠标悬停的元素ID
}
⚠️ 技术难点与解决方案
难点1: AST节点父级查找
问题:Vue编译时AST节点没有parent引用
解决:递归遍历根节点,构建节点关系映射表
难点2: 字符串操作的准确性
问题:正则匹配可能误删嵌套元素
解决:使用深度计数器,精确匹配开始/结束标签
难点3: 热更新与拖拽状态冲突
问题:文件修改后页面刷新,拖拽状态丢失
解决:将拖拽状态保存到sessionStorage,刷新后恢复
难点4: 大文件性能问题
问题:100+元素的Vue文件解析缓慢
解决:使用Web Worker后台解析,分片渲染组件树
难点5: 跨iframe通信
问题:如果画布使用iframe,事件监听复杂
解决:使用动态组件代替iframe,或通过postMessage通信
🚀 启动指南(实施后)
# 1. 安装依赖
npm install
# 2. 启动开发服务器
npm run dev
# 3. 访问设计器
http://localhost:5173/draggable
# 4. 操作流程
- 点击"窗口" → 打开"Vue文件浏览器"(左侧)
- 点击"窗口" → 打开"可视化画布"(中间)
- 点击"窗口" → 打开"组件树查看器"(左侧)
- 在文件浏览器中选择TestPage.vue
- 在画布中拖拽el-col调整布局
- 查看源文件自动更新
📝 后续可扩展功能
功能增强
-
组件库集成
- 支持从Element Plus组件库拖拽添加组件
- 不仅限于el-row/el-col,支持所有组件
-
多文件项目管理
- 项目文件树浏览
- 多文件同时编辑(Tab切换)
- 文件依赖关系分析
-
智能布局
- 自动计算span总和
- 响应式布局预览(xs/sm/md/lg)
- 对齐辅助线
-
团队协作
- Git集成(提交/拉取)
- 实时协作编辑(WebSocket)
- 版本历史对比
开发体验
-
代码生成
- 自动生成配套的script代码
- 自动导入组件
- 自动生成数据绑定
-
预设模板
- 常用布局模板库
- 一键应用模板
- 自定义模板保存
✅ 验收清单(最终)
- 能浏览并选择项目中的.vue文件
- 能在画布中实时渲染Vue文件
- 能通过拖拽调整el-row/el-col布局
- 拖拽操作能正确修改源文件
- 文件修改后页面自动刷新
- 点击元素能高亮选中
- 滚轮能切换父子元素选中
- 组件树能实时同步画布状态
- 属性编辑器能修改元素属性
- 支持撤销/重做操作
- 所有操作流畅无明显卡顿
- 代码格式保持规范
🎉 总结
本计划将分8个阶段、15天完成,核心思路是:
- 编译时增强:通过Vue编译器插件自动注入拖拽能力
- 运行时交互:通过自定义指令实现拖拽逻辑
- 字符串操作:精确修改模板字符串
- 文件同步:通过Vite中间件实现文件写入
相比于传统的虚拟组件设计器,这种方案的优势是:
- ✅ 所见即所得:直接编辑真实的Vue文件
- ✅ 零学习成本:开发者无需学习DSL或JSON配置
- ✅ 完全可控:生成的代码就是手写的代码
- ✅ 灵活扩展:可以支持任意Vue组件
让我们开始实施吧! 🚀