初步完成
This commit is contained in:
@@ -1,702 +0,0 @@
|
|||||||
# 🎯 可视化拖拽设计器 - 整体实施计划
|
|
||||||
|
|
||||||
> **目标**:将当前的设计器升级为可直接解析和编辑真实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转换**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 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生成算法**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 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. 拖拽事件处理流程**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 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. 模板字符串修改逻辑**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 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**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 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. 显示修改后的布局 ✅
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 关键类型定义
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 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通信
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 启动指南(实施后)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. 安装依赖
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# 2. 启动开发服务器
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# 3. 访问设计器
|
|
||||||
http://localhost:5173/draggable
|
|
||||||
|
|
||||||
# 4. 操作流程
|
|
||||||
- 点击"窗口" → 打开"Vue文件浏览器"(左侧)
|
|
||||||
- 点击"窗口" → 打开"可视化画布"(中间)
|
|
||||||
- 点击"窗口" → 打开"组件树查看器"(左侧)
|
|
||||||
- 在文件浏览器中选择TestPage.vue
|
|
||||||
- 在画布中拖拽el-col调整布局
|
|
||||||
- 查看源文件自动更新
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 后续可扩展功能
|
|
||||||
|
|
||||||
### **功能增强**
|
|
||||||
1. **组件库集成**
|
|
||||||
- 支持从Element Plus组件库拖拽添加组件
|
|
||||||
- 不仅限于el-row/el-col,支持所有组件
|
|
||||||
|
|
||||||
2. **多文件项目管理**
|
|
||||||
- 项目文件树浏览
|
|
||||||
- 多文件同时编辑(Tab切换)
|
|
||||||
- 文件依赖关系分析
|
|
||||||
|
|
||||||
3. **智能布局**
|
|
||||||
- 自动计算span总和
|
|
||||||
- 响应式布局预览(xs/sm/md/lg)
|
|
||||||
- 对齐辅助线
|
|
||||||
|
|
||||||
4. **团队协作**
|
|
||||||
- Git集成(提交/拉取)
|
|
||||||
- 实时协作编辑(WebSocket)
|
|
||||||
- 版本历史对比
|
|
||||||
|
|
||||||
### **开发体验**
|
|
||||||
1. **代码生成**
|
|
||||||
- 自动生成配套的script代码
|
|
||||||
- 自动导入组件
|
|
||||||
- 自动生成数据绑定
|
|
||||||
|
|
||||||
2. **预设模板**
|
|
||||||
- 常用布局模板库
|
|
||||||
- 一键应用模板
|
|
||||||
- 自定义模板保存
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ 验收清单(最终)
|
|
||||||
|
|
||||||
- [ ] 能浏览并选择项目中的.vue文件
|
|
||||||
- [ ] 能在画布中实时渲染Vue文件
|
|
||||||
- [ ] 能通过拖拽调整el-row/el-col布局
|
|
||||||
- [ ] 拖拽操作能正确修改源文件
|
|
||||||
- [ ] 文件修改后页面自动刷新
|
|
||||||
- [ ] 点击元素能高亮选中
|
|
||||||
- [ ] 滚轮能切换父子元素选中
|
|
||||||
- [ ] 组件树能实时同步画布状态
|
|
||||||
- [ ] 属性编辑器能修改元素属性
|
|
||||||
- [ ] 支持撤销/重做操作
|
|
||||||
- [ ] 所有操作流畅无明显卡顿
|
|
||||||
- [ ] 代码格式保持规范
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 总结
|
|
||||||
|
|
||||||
本计划将分**8个阶段、15天**完成,核心思路是:
|
|
||||||
|
|
||||||
1. **编译时增强**:通过Vue编译器插件自动注入拖拽能力
|
|
||||||
2. **运行时交互**:通过自定义指令实现拖拽逻辑
|
|
||||||
3. **字符串操作**:精确修改模板字符串
|
|
||||||
4. **文件同步**:通过Vite中间件实现文件写入
|
|
||||||
|
|
||||||
相比于传统的虚拟组件设计器,这种方案的优势是:
|
|
||||||
- ✅ **所见即所得**:直接编辑真实的Vue文件
|
|
||||||
- ✅ **零学习成本**:开发者无需学习DSL或JSON配置
|
|
||||||
- ✅ **完全可控**:生成的代码就是手写的代码
|
|
||||||
- ✅ **灵活扩展**:可以支持任意Vue组件
|
|
||||||
|
|
||||||
**让我们开始实施吧!** 🚀
|
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
# Vue页面可视化设计器 - 项目上下文
|
|
||||||
|
|
||||||
> **用途**:用于在新环境快速恢复 AI 协作上下文
|
|
||||||
> **更新时间**:2025-12-22
|
|
||||||
> **项目路径**:`d:/workspace/fauto-design/draggable-panels`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 项目概述
|
|
||||||
|
|
||||||
这是一个基于 **Vue3 + TypeScript + Vite + Element Plus** 的**可视化页面设计器**,通过拖拽操作直接编辑真实的Vue页面文件。
|
|
||||||
|
|
||||||
### 核心特性
|
|
||||||
|
|
||||||
1. **直接解析Vue文件** - 动态扫描并渲染`src/views`下的页面
|
|
||||||
2. **拖拽式设计** - 将设计组件拖拽到页面的el-row/el-col上
|
|
||||||
3. **非侵入式交互** - 通过注入方式绑定事件,不修改页面代码
|
|
||||||
4. **智能层级选择** - 键盘方向键切换嵌套元素层级
|
|
||||||
5. **实时视觉反馈** - 拖拽预览 + 拖放区域显示(上/下/左/右)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏗️ 项目结构
|
|
||||||
|
|
||||||
```
|
|
||||||
draggable-panels/
|
|
||||||
├── src/
|
|
||||||
│ ├── fauto/ # 🔥 设计器核心代码
|
|
||||||
│ │ ├── Designer.vue # 设计器主入口
|
|
||||||
│ │ │
|
|
||||||
│ │ ├── components/ # 基础UI组件
|
|
||||||
│ │ │ ├── Header.vue # 顶部菜单栏
|
|
||||||
│ │ │ ├── Footer.vue # 底部状态栏(显示拖拽状态)
|
|
||||||
│ │ │ ├── MainLayout.vue # 三栏布局容器
|
|
||||||
│ │ │ ├── Panel.vue # 面板容器
|
|
||||||
│ │ │ └── Resizer.vue # 面板分隔器
|
|
||||||
│ │ │
|
|
||||||
│ │ ├── materials/ # 🎁 物料组件系统
|
|
||||||
│ │ │ ├── PageManagement/ # 页面管理(树形选择Vue文件)
|
|
||||||
│ │ │ ├── DesignComponentList/ # 设计组件列表
|
|
||||||
│ │ │ └── DesignCenter/ # 设计中心(动态渲染页面)
|
|
||||||
│ │ │ ├── index.vue # 主组件
|
|
||||||
│ │ │ ├── InteractiveWrapper.vue # 交互包装器(注入事件)
|
|
||||||
│ │ │ ├── DropZone.vue # 拖放区域指示器
|
|
||||||
│ │ │ └── DragPreview.vue # 拖拽预览
|
|
||||||
│ │ │
|
|
||||||
│ │ ├── designComponents/ # 🎨 设计组件库
|
|
||||||
│ │ │ ├── TextInput/ # 文本输入框
|
|
||||||
│ │ │ ├── RadioSelect/ # 单选器
|
|
||||||
│ │ │ └── GridTable/ # 表格
|
|
||||||
│ │ │
|
|
||||||
│ │ ├── plugins/ # 🔌 插件系统(核心功能)
|
|
||||||
│ │ │ ├── index.ts # 统一导出
|
|
||||||
│ │ │ ├── interactionStore.ts # 全局交互事件钩子
|
|
||||||
│ │ │ ├── dragStore.ts # 拖拽状态管理(层级选择)
|
|
||||||
│ │ │ └── pathUtils.ts # 结构化路径工具
|
|
||||||
│ │ │
|
|
||||||
│ │ ├── stores/ # 🗄️ Pinia状态管理
|
|
||||||
│ │ │ ├── panelStore.ts # 面板布局状态
|
|
||||||
│ │ │ ├── designStore.ts # 设计组件元数据
|
|
||||||
│ │ │ └── vueFileStore.ts # Vue文件选择状态
|
|
||||||
│ │ │
|
|
||||||
│ │ └── types/ # 📝 类型定义
|
|
||||||
│ │
|
|
||||||
│ ├── views/ # 📄 示例页面
|
|
||||||
│ │ ├── TestPage1.vue # 测试页面1
|
|
||||||
│ │ ├── TestPage2.vue # 测试页面2
|
|
||||||
│ │ ├── user/Profile.vue # 用户资料页
|
|
||||||
│ │ └── dashboard/Overview.vue # 仪表板
|
|
||||||
│ │
|
|
||||||
│ ├── router.ts # 路由配置
|
|
||||||
│ ├── main.ts # 应用入口
|
|
||||||
│ ├── App.vue # 根组件
|
|
||||||
│ └── style.css # 全局样式
|
|
||||||
│
|
|
||||||
└── vite.config.ts # Vite配置
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔑 核心技术要点
|
|
||||||
|
|
||||||
### 1. **Vue页面规范** ⭐
|
|
||||||
|
|
||||||
**强制规则**:template的第一层级**有且仅有一个el-row**
|
|
||||||
|
|
||||||
**示例**:
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<el-row :gutter="20">
|
|
||||||
<el-col :span="12">
|
|
||||||
<div class="design-component">左侧内容</div>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-row :gutter="10">
|
|
||||||
<el-col :span="24">
|
|
||||||
<div class="design-component">标题</div>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
**目的**:保证布局可解析性,方便自动生成结构化路径ID
|
|
||||||
|
|
||||||
### 2. **结构化路径ID** ⭐
|
|
||||||
|
|
||||||
**格式**:`r{n}c{m}r{x}c{y}...`
|
|
||||||
- `r`: el-row
|
|
||||||
- `c`: el-col
|
|
||||||
- 数字:同级索引(从1开始)
|
|
||||||
|
|
||||||
**示例**:`r1c2r1c3` 表示:
|
|
||||||
- 第1个el-row → 第2个el-col → 第1个el-row → 第3个el-col
|
|
||||||
|
|
||||||
**实现**:`fauto/plugins/pathUtils.ts`
|
|
||||||
|
|
||||||
### 3. **交互事件注入** ⭐
|
|
||||||
|
|
||||||
**InteractiveWrapper.vue** 核心流程:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 1. 动态渲染组件
|
|
||||||
<component :is="selectedPageComponent" />
|
|
||||||
|
|
||||||
// 2. 挂载后扫描DOM
|
|
||||||
const injectInteractionEvents = () => {
|
|
||||||
const rows = containerRef.value.querySelectorAll('.el-row')
|
|
||||||
const cols = containerRef.value.querySelectorAll('.el-col')
|
|
||||||
|
|
||||||
// 3. 为每个元素注入事件
|
|
||||||
rows.forEach((row) => {
|
|
||||||
const path = generateElementPath(row) // 生成路径ID
|
|
||||||
bindElementEvents(row, 'er', path) // 绑定事件
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 使用MutationObserver监听DOM变化
|
|
||||||
observer.observe(containerRef.value, { childList: true, subtree: true })
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. **层级选择机制** ⭐
|
|
||||||
|
|
||||||
**核心思想**:当鼠标悬停在嵌套元素上时(如`r1c1r1`),会同时触发`r1c1r1`、`r1c1`、`r1`三个层级。
|
|
||||||
|
|
||||||
**解决方案**(`dragStore.ts`):
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 1. 收集所有层级节点
|
|
||||||
const updateHierarchy = (element: HTMLElement) => {
|
|
||||||
const nodes = []
|
|
||||||
let current = element
|
|
||||||
|
|
||||||
while (current) {
|
|
||||||
if (current.classList.contains('el-row') || current.classList.contains('el-col')) {
|
|
||||||
nodes.push({ path, type, element, depth })
|
|
||||||
}
|
|
||||||
current = current.parentElement
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按深度排序,最深的在前
|
|
||||||
nodes.sort((a, b) => b.depth - a.depth)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 键盘切换层级
|
|
||||||
↑ 键 → selectParentLevel() // 切换到父级
|
|
||||||
↓ 键 → selectChildLevel() // 切换到子级
|
|
||||||
Esc → cancelDrag() // 取消拖拽
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. **拖拽交互优化** ⭐
|
|
||||||
|
|
||||||
#### 禁用文本选择
|
|
||||||
```typescript
|
|
||||||
// DragPreview.vue
|
|
||||||
watch(() => dragStore.isDragging, (isDragging) => {
|
|
||||||
if (isDragging) {
|
|
||||||
document.body.classList.add('is-dragging')
|
|
||||||
} else {
|
|
||||||
document.body.classList.remove('is-dragging')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
```css
|
|
||||||
/* style.css */
|
|
||||||
body.is-dragging {
|
|
||||||
user-select: none !important;
|
|
||||||
cursor: grabbing !important;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 拖拽预览效果
|
|
||||||
- 跟随鼠标的半透明卡片
|
|
||||||
- 显示组件图标 + 名称
|
|
||||||
- Teleport到body避免z-index问题
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 数据流图
|
|
||||||
|
|
||||||
```
|
|
||||||
1. 页面管理
|
|
||||||
用户点击Vue文件
|
|
||||||
↓
|
|
||||||
vueFileStore.selectFile(path)
|
|
||||||
↓
|
|
||||||
DesignCenter动态渲染该页面
|
|
||||||
↓
|
|
||||||
InteractiveWrapper注入交互事件
|
|
||||||
|
|
||||||
2. 拖拽交互
|
|
||||||
用户拖拽设计组件
|
|
||||||
↓
|
|
||||||
dragStore.startDragFromComponentList(id, name)
|
|
||||||
↓
|
|
||||||
鼠标移动到页面元素上
|
|
||||||
↓
|
|
||||||
dragStore.updateHierarchy(element) // 识别层级
|
|
||||||
↓
|
|
||||||
用户按↑↓键切换层级
|
|
||||||
↓
|
|
||||||
DropZone显示可放置区域(上/下/左/右)
|
|
||||||
↓
|
|
||||||
用户点击区域确认
|
|
||||||
↓
|
|
||||||
dragStore.confirmDrop()
|
|
||||||
↓
|
|
||||||
Footer显示拖放记录:TextInput → r1c1 左侧
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ 快速开始
|
|
||||||
|
|
||||||
### 访问设计器
|
|
||||||
```
|
|
||||||
http://localhost:5173/draggable
|
|
||||||
```
|
|
||||||
|
|
||||||
### 使用流程
|
|
||||||
1. 点击左侧"页面管理"物料
|
|
||||||
2. 在树形列表中选择一个Vue页面
|
|
||||||
3. 页面在"设计中心"中渲染
|
|
||||||
4. 从"设计组件列表"拖拽组件
|
|
||||||
5. 移动到页面的el-row或el-col上
|
|
||||||
6. 使用↑↓键切换层级
|
|
||||||
7. 点击放置方向(上/下/左/右)
|
|
||||||
8. 查看Footer中的拖放记录
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 开发规范
|
|
||||||
|
|
||||||
### 创建符合规范的Vue页面
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<!-- 第一层级必须且只能有一个el-row -->
|
|
||||||
<el-row :gutter="20">
|
|
||||||
<el-col :span="24">
|
|
||||||
<!-- 内容 -->
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 添加设计组件
|
|
||||||
1. 在`src/fauto/designComponents/`下创建目录
|
|
||||||
2. 添加`index.vue`和`index.json`
|
|
||||||
3. 自动在列表中显示
|
|
||||||
|
|
||||||
### 插件开发
|
|
||||||
所有与页面解析、交互相关的代码放在`src/fauto/plugins/`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 下一步计划
|
|
||||||
|
|
||||||
1. **拖放后修改源文件** - 通过API将操作写入.vue文件
|
|
||||||
2. **属性编辑器** - 展示和编辑组件属性
|
|
||||||
3. **热更新同步** - 修改后自动刷新页面
|
|
||||||
4. **撤销/重做** - 操作历史记录
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 相关文档
|
|
||||||
|
|
||||||
- **详细设计文档**:`项目设计文档.md`
|
|
||||||
- **技术选型**:Vue3 + TypeScript + Element Plus + Pinia
|
|
||||||
- **构建工具**:Vite 7.3.0
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💡 核心创新点
|
|
||||||
|
|
||||||
1. **非侵入式设计** - 无需修改Vue页面代码即可实现拖拽设计
|
|
||||||
2. **智能层级选择** - 自动识别嵌套结构,精准定位目标位置
|
|
||||||
3. **实时视觉反馈** - 完善的拖拽交互体验
|
|
||||||
4. **结构化路径** - 人类可读的元素定位方式
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**最后更新**:2025-12-22
|
|
||||||
**AI协作建议**:优先阅读本文档的"核心技术要点"部分,理解项目的设计理念和实现方式
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
# 第一阶段完成说明
|
|
||||||
|
|
||||||
## ✅ 已完成的功能
|
|
||||||
|
|
||||||
### 1. 创建了5个测试页面
|
|
||||||
|
|
||||||
#### 根目录页面:
|
|
||||||
- **TestPage1.vue** - 基础布局测试页面(左右两列 + 三列布局)
|
|
||||||
- **TestPage2.vue** - 表单布局测试页面(标题 + 表单字段 + 提交按钮)
|
|
||||||
|
|
||||||
#### 文件夹分类页面:
|
|
||||||
- **dashboard/Overview.vue** - 仪表板概览页面(统计卡片 + 图表区域)
|
|
||||||
- **user/Profile.vue** - 用户资料页面(头像 + 个人信息)
|
|
||||||
|
|
||||||
所有测试页面都使用了 **el-row** 和 **el-col** 进行布局。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. 创建了"页面管理"物料组件
|
|
||||||
|
|
||||||
**位置**: `src/fauto/materials/PageManager/`
|
|
||||||
|
|
||||||
**功能**:
|
|
||||||
- ✅ 自动扫描 `src/views/` 目录下的所有Vue文件
|
|
||||||
- ✅ 以树形结构展示文件夹和Vue文件
|
|
||||||
- ✅ 点击Vue文件后,会选中该文件并通知设计中心
|
|
||||||
- ✅ 显示当前选中的文件名
|
|
||||||
- ✅ 文件夹默认展开
|
|
||||||
- ✅ 使用Element Plus的el-tree组件
|
|
||||||
|
|
||||||
**特点**:
|
|
||||||
- 📁 文件夹图标
|
|
||||||
- 📄 Vue文件图标
|
|
||||||
- 🔵 选中文件高亮显示
|
|
||||||
- 自动分类显示(文件夹在前,文件在后)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. 升级了"设计中心"物料组件
|
|
||||||
|
|
||||||
**位置**: `src/fauto/materials/DesignCenter/`
|
|
||||||
|
|
||||||
**新增功能**:
|
|
||||||
- ✅ **动态渲染选中的Vue页面**:当从"页面管理"选择文件后,自动加载并渲染该页面
|
|
||||||
- ✅ 显示当前渲染的文件名
|
|
||||||
- ✅ 保留了原有的设计组件实例预览功能
|
|
||||||
- ✅ 智能切换显示模式:
|
|
||||||
- 如果选中了Vue页面 → 显示页面预览
|
|
||||||
- 如果没有选中页面但有设计组件 → 显示设计组件列表
|
|
||||||
- 如果都没有 → 显示提示信息
|
|
||||||
|
|
||||||
**渲染方式**:
|
|
||||||
- 使用 `defineAsyncComponent` 动态加载Vue组件
|
|
||||||
- 使用 `import.meta.glob` 扫描所有views下的文件
|
|
||||||
- 实时响应文件选择变化
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. 创建了vueFileStore状态管理
|
|
||||||
|
|
||||||
**位置**: `src/fauto/stores/vueFileStore.ts`
|
|
||||||
|
|
||||||
**管理的状态**:
|
|
||||||
- `selectedFilePath` - 当前选中的文件路径
|
|
||||||
- `selectedFileName` - 当前选中的文件名
|
|
||||||
- `fileTree` - 文件树结构
|
|
||||||
|
|
||||||
**提供的方法**:
|
|
||||||
- `selectFile(path, name)` - 选中文件
|
|
||||||
- `clearSelection()` - 清除选中
|
|
||||||
- `buildFileTree(modules)` - 从import.meta.glob结果构建树形结构
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. 集成了Element Plus
|
|
||||||
|
|
||||||
**安装的依赖**:
|
|
||||||
- `element-plus@^2.8.8`
|
|
||||||
|
|
||||||
**配置位置**:
|
|
||||||
- `src/main.ts` - 全局注册Element Plus
|
|
||||||
- 自动导入Element Plus样式
|
|
||||||
|
|
||||||
**使用的组件**:
|
|
||||||
- `el-tree` - 树形组件(页面管理)
|
|
||||||
- `el-row` / `el-col` - 布局组件(测试页面)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 使用方法
|
|
||||||
|
|
||||||
### 启动项目
|
|
||||||
```bash
|
|
||||||
cd draggable-panels
|
|
||||||
npm install # 已完成
|
|
||||||
npm run dev # 已启动,访问 http://localhost:5173
|
|
||||||
```
|
|
||||||
|
|
||||||
### 操作步骤
|
|
||||||
|
|
||||||
1. **访问设计器**
|
|
||||||
```
|
|
||||||
http://localhost:5173/draggable
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **打开页面管理**
|
|
||||||
- 点击顶部"窗口"菜单
|
|
||||||
- 选择"页面管理"
|
|
||||||
- 面板会在左侧或中间打开
|
|
||||||
|
|
||||||
3. **打开设计中心**
|
|
||||||
- 点击顶部"窗口"菜单
|
|
||||||
- 选择"设计中心"
|
|
||||||
- 建议拖到中间面板
|
|
||||||
|
|
||||||
4. **选择并预览页面**
|
|
||||||
- 在"页面管理"中点击任意Vue文件(如 TestPage1.vue)
|
|
||||||
- "设计中心"会立即显示该页面的实时渲染效果
|
|
||||||
- 可以在树中切换不同的页面查看效果
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📂 项目文件结构(新增部分)
|
|
||||||
|
|
||||||
```
|
|
||||||
draggable-panels/
|
|
||||||
├── src/
|
|
||||||
│ ├── views/ # ✨测试页面目录
|
|
||||||
│ │ ├── HelloWorld.vue # (原有)
|
|
||||||
│ │ ├── TestPage1.vue # ✨新增:基础布局
|
|
||||||
│ │ ├── TestPage2.vue # ✨新增:表单布局
|
|
||||||
│ │ ├── dashboard/ # ✨新增文件夹
|
|
||||||
│ │ │ └── Overview.vue # 仪表板概览
|
|
||||||
│ │ └── user/ # ✨新增文件夹
|
|
||||||
│ │ └── Profile.vue # 用户资料
|
|
||||||
│ │
|
|
||||||
│ ├── fauto/
|
|
||||||
│ │ ├── materials/
|
|
||||||
│ │ │ ├── PageManager/ # ✨新增:页面管理物料
|
|
||||||
│ │ │ │ ├── index.vue
|
|
||||||
│ │ │ │ └── index.json
|
|
||||||
│ │ │ └── DesignCenter/ # ✨修改:升级为动态渲染
|
|
||||||
│ │ │ ├── index.vue # (已修改)
|
|
||||||
│ │ │ └── index.json # (已修改)
|
|
||||||
│ │ │
|
|
||||||
│ │ └── stores/
|
|
||||||
│ │ └── vueFileStore.ts # ✨新增:Vue文件状态管理
|
|
||||||
│ │
|
|
||||||
│ └── main.ts # ✨修改:集成Element Plus
|
|
||||||
│
|
|
||||||
└── package.json # ✨修改:添加element-plus依赖
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎨 界面预览
|
|
||||||
|
|
||||||
### 页面管理物料组件
|
|
||||||
```
|
|
||||||
┌─────────────────────────────┐
|
|
||||||
│ 页面管理 5 项 │
|
|
||||||
├─────────────────────────────┤
|
|
||||||
│ 当前文件: TestPage1.vue │
|
|
||||||
├─────────────────────────────┤
|
|
||||||
│ 📁 dashboard │
|
|
||||||
│ 📄 Overview.vue │
|
|
||||||
│ 📄 HelloWorld.vue │
|
|
||||||
│ 📄 TestPage1.vue ←选中 │
|
|
||||||
│ 📄 TestPage2.vue │
|
|
||||||
│ 📁 user │
|
|
||||||
│ 📄 Profile.vue │
|
|
||||||
└─────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### 设计中心(渲染Vue页面)
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────┐
|
|
||||||
│ 设计中心 📄 TestPage1.vue │
|
|
||||||
├─────────────────────────────────────┤
|
|
||||||
│ ┌───────────────────────────────┐ │
|
|
||||||
│ │ 测试页面 1 │ │
|
|
||||||
│ │ ┌────────┐ ┌────────┐ │ │
|
|
||||||
│ │ │左侧内容│ │右侧内容│ │ │
|
|
||||||
│ │ └────────┘ └────────┘ │ │
|
|
||||||
│ │ ┌──┐ ┌──┐ ┌──┐ │ │
|
|
||||||
│ │ │列1│ │列2│ │列3│ │ │
|
|
||||||
│ │ └──┘ └──┘ └──┘ │ │
|
|
||||||
│ └───────────────────────────────┘ │
|
|
||||||
└─────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✨ 技术亮点
|
|
||||||
|
|
||||||
1. **自动扫描机制**
|
|
||||||
- 使用 `import.meta.glob` 自动扫描views目录
|
|
||||||
- 无需手动注册新页面
|
|
||||||
- 支持嵌套文件夹结构
|
|
||||||
|
|
||||||
2. **动态组件加载**
|
|
||||||
- 使用 `defineAsyncComponent` 按需加载
|
|
||||||
- 节省初始加载时间
|
|
||||||
- 支持代码分割
|
|
||||||
|
|
||||||
3. **状态同步**
|
|
||||||
- vueFileStore 和 DesignCenter 通过 Pinia 实时同步
|
|
||||||
- 选择文件后立即响应
|
|
||||||
- 使用 `watch` 监听状态变化
|
|
||||||
|
|
||||||
4. **智能UI切换**
|
|
||||||
- 根据不同状态显示不同内容
|
|
||||||
- 优先显示Vue页面预览
|
|
||||||
- 保留原有设计组件功能
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 验证功能是否正常
|
|
||||||
|
|
||||||
### 检查清单:
|
|
||||||
- [ ] 访问 http://localhost:5173/draggable 能看到设计器界面
|
|
||||||
- [ ] 点击"窗口"菜单能看到"页面管理"选项
|
|
||||||
- [ ] 打开"页面管理"能看到5个Vue文件
|
|
||||||
- [ ] 文件按文件夹分类显示(dashboard、user)
|
|
||||||
- [ ] 点击TestPage1.vue后,设计中心显示该页面
|
|
||||||
- [ ] 页面中能看到蓝色和绿色的布局区块
|
|
||||||
- [ ] 切换不同文件,设计中心实时更新
|
|
||||||
- [ ] 当前选中的文件在树中高亮显示
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 后续步骤
|
|
||||||
|
|
||||||
下一阶段我们将:
|
|
||||||
1. 创建Vite插件系统,实现编译时AST转换
|
|
||||||
2. 为el-row和el-col自动注入拖拽指令
|
|
||||||
3. 实现拖拽交互逻辑
|
|
||||||
4. 实现文件修改引擎
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 第一阶段完成!
|
|
||||||
|
|
||||||
现在你可以:
|
|
||||||
- ✅ 浏览views目录下的所有Vue文件
|
|
||||||
- ✅ 点击文件后在设计中心实时预览
|
|
||||||
- ✅ 查看包含el-row/el-col布局的测试页面
|
|
||||||
|
|
||||||
**测试建议**:
|
|
||||||
1. 先打开"页面管理"(左侧面板)
|
|
||||||
2. 再打开"设计中心"(中间面板)
|
|
||||||
3. 依次点击不同的Vue文件查看效果
|
|
||||||
4. 观察不同页面的布局样式
|
|
||||||
@@ -57,6 +57,14 @@ const bindElementEvents = (element: HTMLElement, type: ElementType, path: string
|
|||||||
|
|
||||||
if (!dragStore.isDragging) {
|
if (!dragStore.isDragging) {
|
||||||
element.classList.add('fauto-hover')
|
element.classList.add('fauto-hover')
|
||||||
|
} else {
|
||||||
|
// 拖拽时移动到不同元素,进入目标选择阶段
|
||||||
|
// 检查是否是不同的元素(不是源元素或其父级)
|
||||||
|
const sourcePath = dragStore.confirmedSource?.path || dragStore.dragSource?.path
|
||||||
|
if (sourcePath && path !== sourcePath && !path.startsWith(sourcePath)) {
|
||||||
|
dragStore.enterTargetPhase(element)
|
||||||
|
updateSelectedHighlight()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,8 +101,8 @@ const bindElementEvents = (element: HTMLElement, type: ElementType, path: string
|
|||||||
// 如果已经有拖拽源,不处理
|
// 如果已经有拖拽源,不处理
|
||||||
if (dragStore.isDragging) return
|
if (dragStore.isDragging) return
|
||||||
|
|
||||||
// 开始画布内元素拖拽
|
// 开始画布内元素拖拽,传入元素引用以立即构建层级列表
|
||||||
dragStore.startDragFromCanvas(path, type)
|
dragStore.startDragFromCanvas(path, type, element)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绑定事件
|
// 绑定事件
|
||||||
@@ -228,7 +236,8 @@ const clearDragHighlight = () => {
|
|||||||
* 全局鼠标移动处理(拖拽时更新方向)
|
* 全局鼠标移动处理(拖拽时更新方向)
|
||||||
*/
|
*/
|
||||||
const handleGlobalMouseMove = (e: MouseEvent) => {
|
const handleGlobalMouseMove = (e: MouseEvent) => {
|
||||||
if (dragStore.isDragging && dragStore.selectedNode) {
|
// 只在目标选择阶段才更新方向
|
||||||
|
if (dragStore.isDragging && dragStore.dragPhase === 'target' && dragStore.selectedNode) {
|
||||||
dragStore.updateDirectionFromMouse(e.clientX, e.clientY)
|
dragStore.updateDirectionFromMouse(e.clientX, e.clientY)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,7 +246,8 @@ const handleGlobalMouseMove = (e: MouseEvent) => {
|
|||||||
* 全局鼠标松开处理
|
* 全局鼠标松开处理
|
||||||
*/
|
*/
|
||||||
const handleGlobalMouseUp = async () => {
|
const handleGlobalMouseUp = async () => {
|
||||||
if (dragStore.isDragging && dragStore.selectedNode && dragStore.hoverDirection) {
|
// 只在目标选择阶段才执行拖放
|
||||||
|
if (dragStore.isDragging && dragStore.dragPhase === 'target' && dragStore.selectedNode && dragStore.hoverDirection) {
|
||||||
// 获取当前页面路径(从vueFileStore)
|
// 获取当前页面路径(从vueFileStore)
|
||||||
const pagePath = vueFileStore.selectedFilePath
|
const pagePath = vueFileStore.selectedFilePath
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,16 @@ export const useDragStore = defineStore('drag', () => {
|
|||||||
// 拖拽源
|
// 拖拽源
|
||||||
const dragSource = ref<DragSource | null>(null)
|
const dragSource = ref<DragSource | null>(null)
|
||||||
|
|
||||||
|
// 拖拽阶段: 'source' = 源选择阶段, 'target' = 目标选择阶段
|
||||||
|
const dragPhase = ref<'source' | 'target'>('source')
|
||||||
|
|
||||||
|
// 已确定的源元素信息(当进入目标选择阶段时保存)
|
||||||
|
const confirmedSource = ref<{
|
||||||
|
path: string
|
||||||
|
type: ElementType
|
||||||
|
element: HTMLElement
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
// ========== 层级选择状态 ==========
|
// ========== 层级选择状态 ==========
|
||||||
|
|
||||||
// 当前悬停位置的所有层级节点(从深到浅排序)
|
// 当前悬停位置的所有层级节点(从深到浅排序)
|
||||||
@@ -103,15 +113,57 @@ export const useDragStore = defineStore('drag', () => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 开始拖拽(从画布内元素)
|
* 开始拖拽(从画布内元素)
|
||||||
|
* @param path 元素路径
|
||||||
|
* @param elementType 元素类型
|
||||||
|
* @param element 元素DOM引用(用于立即构建层级列表)
|
||||||
*/
|
*/
|
||||||
const startDragFromCanvas = (path: string, elementType: ElementType) => {
|
const startDragFromCanvas = (path: string, elementType: ElementType, element?: HTMLElement) => {
|
||||||
isDragging.value = true
|
isDragging.value = true
|
||||||
|
dragPhase.value = 'source' // 进入源选择阶段
|
||||||
|
confirmedSource.value = null // 清除之前的确定源
|
||||||
dragSource.value = {
|
dragSource.value = {
|
||||||
type: 'canvas-element',
|
type: 'canvas-element',
|
||||||
path,
|
path,
|
||||||
elementType
|
elementType
|
||||||
}
|
}
|
||||||
console.log('[DragStore] 开始拖拽画布元素:', path)
|
|
||||||
|
// 立即构建层级列表,让用户可以在原地通过上下键切换源元素层级
|
||||||
|
if (element) {
|
||||||
|
updateHierarchy(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DragStore] 开始拖拽画布元素 (源选择阶段):', path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进入目标选择阶段
|
||||||
|
* 当鼠标移动到不同元素时调用
|
||||||
|
*/
|
||||||
|
const enterTargetPhase = (targetElement: HTMLElement) => {
|
||||||
|
if (dragPhase.value === 'source' && selectedNode.value) {
|
||||||
|
// 保存当前选中的源元素
|
||||||
|
confirmedSource.value = {
|
||||||
|
path: selectedNode.value.path,
|
||||||
|
type: selectedNode.value.type,
|
||||||
|
element: selectedNode.value.element
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新 dragSource 中的路径和类型
|
||||||
|
if (dragSource.value) {
|
||||||
|
dragSource.value.path = selectedNode.value.path
|
||||||
|
dragSource.value.elementType = selectedNode.value.type
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DragStore] 确定源元素:', confirmedSource.value.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进入目标选择阶段
|
||||||
|
dragPhase.value = 'target'
|
||||||
|
|
||||||
|
// 更新层级列表为目标元素的层级
|
||||||
|
updateHierarchy(targetElement)
|
||||||
|
|
||||||
|
console.log('[DragStore] 进入目标选择阶段')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -215,6 +267,8 @@ export const useDragStore = defineStore('drag', () => {
|
|||||||
hierarchyNodes.value = []
|
hierarchyNodes.value = []
|
||||||
selectedHierarchyIndex.value = 0
|
selectedHierarchyIndex.value = 0
|
||||||
hoverDirection.value = null
|
hoverDirection.value = null
|
||||||
|
dragPhase.value = 'source'
|
||||||
|
confirmedSource.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -252,12 +306,11 @@ export const useDragStore = defineStore('drag', () => {
|
|||||||
* 是否为跨类型拖放(源和目标类型不同)
|
* 是否为跨类型拖放(源和目标类型不同)
|
||||||
*/
|
*/
|
||||||
const isCrossTypeDrop = computed(() => {
|
const isCrossTypeDrop = computed(() => {
|
||||||
if (!dragSource.value || !selectedNode.value) return false
|
// 只有在目标选择阶段才考虑跨类型
|
||||||
|
if (dragPhase.value !== 'target') return false
|
||||||
|
if (!confirmedSource.value || !selectedNode.value) return false
|
||||||
|
|
||||||
// 只有画布内元素拖放才考虑跨类型
|
const sourceType = confirmedSource.value.type
|
||||||
if (dragSource.value.type !== 'canvas-element') return false
|
|
||||||
|
|
||||||
const sourceType = dragSource.value.elementType
|
|
||||||
const targetType = selectedNode.value.type
|
const targetType = selectedNode.value.type
|
||||||
|
|
||||||
// er 拖到 ec 或 ec 拖到 er 都是跨类型
|
// er 拖到 ec 或 ec 拖到 er 都是跨类型
|
||||||
@@ -427,6 +480,8 @@ export const useDragStore = defineStore('drag', () => {
|
|||||||
// 状态
|
// 状态
|
||||||
isDragging,
|
isDragging,
|
||||||
dragSource,
|
dragSource,
|
||||||
|
dragPhase,
|
||||||
|
confirmedSource,
|
||||||
hierarchyNodes,
|
hierarchyNodes,
|
||||||
selectedHierarchyIndex,
|
selectedHierarchyIndex,
|
||||||
selectedNode,
|
selectedNode,
|
||||||
@@ -439,6 +494,7 @@ export const useDragStore = defineStore('drag', () => {
|
|||||||
// 方法
|
// 方法
|
||||||
startDragFromComponentList,
|
startDragFromComponentList,
|
||||||
startDragFromCanvas,
|
startDragFromCanvas,
|
||||||
|
enterTargetPhase,
|
||||||
updateHierarchy,
|
updateHierarchy,
|
||||||
clearHierarchy,
|
clearHierarchy,
|
||||||
selectParentLevel,
|
selectParentLevel,
|
||||||
|
|||||||
@@ -9,16 +9,18 @@
|
|||||||
<div class="design-component">右侧标题</div>
|
<div class="design-component">右侧标题</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row :gutter="10">
|
|
||||||
|
|
||||||
<el-col :span="12">
|
|
||||||
<div class="design-component">右侧列2</div>
|
<el-row :gutter="10">
|
||||||
</el-col>
|
<el-col :span="12">
|
||||||
</el-row>
|
<div class="design-component">右侧列2</div>
|
||||||
<el-col :span="12">
|
</el-col>
|
||||||
<div class="design-component">右侧列1</div>
|
|
||||||
</el-col>
|
</el-row>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="design-component">右侧列1</div>
|
||||||
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,280 +0,0 @@
|
|||||||
# Vue页面可视化设计器项目文档
|
|
||||||
|
|
||||||
## 项目概述
|
|
||||||
|
|
||||||
本项目是一个基于 Vite + Vue3 + TypeScript 的**可视化页面设计器**,通过拖拽操作直接编辑真实的Vue页面文件,实现低代码页面快速构建。
|
|
||||||
|
|
||||||
## 核心特性
|
|
||||||
|
|
||||||
1. **直接解析Vue文件** - 动态渲染真实的.vue页面
|
|
||||||
2. **拖拽式设计** - 将设计组件拖拽到页面的el-row/el-col上
|
|
||||||
3. **层级选择** - 通过键盘方向键切换嵌套元素的选中层级
|
|
||||||
4. **实时预览** - 拖拽时显示放置方向(上/下/左/右)
|
|
||||||
5. **结构化ID** - 自动生成路径ID(如r1c2r1c1)
|
|
||||||
|
|
||||||
## 技术架构
|
|
||||||
|
|
||||||
### 核心技术栈
|
|
||||||
- **构建工具**: Vite 7.3.0
|
|
||||||
- **前端框架**: Vue 3.5.24 (Composition API)
|
|
||||||
- **状态管理**: Pinia 3.0.4
|
|
||||||
- **类型系统**: TypeScript
|
|
||||||
- **拖拽库**: vuedraggable 4.1.0
|
|
||||||
- **样式**: CSS Modules + Scoped Styles
|
|
||||||
|
|
||||||
### 项目结构
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── fauto/ # 核心功能模块
|
|
||||||
│ ├── components/ # UI组件
|
|
||||||
│ │ ├── Header.vue # 顶部菜单栏
|
|
||||||
│ │ ├── Footer.vue # 底部状态栏(显示拖拽状态)
|
|
||||||
│ │ ├── MainLayout.vue # 主布局容器
|
|
||||||
│ │ ├── Panel.vue # 面板容器
|
|
||||||
│ │ └── Resizer.vue # 面板分隔器
|
|
||||||
│ ├── materials/ # 物料组件系统
|
|
||||||
│ │ ├── PageManagement/ # 页面管理(树形选择Vue文件)
|
|
||||||
│ │ ├── DesignComponentList/ # 设计组件列表
|
|
||||||
│ │ └── DesignCenter/ # 设计中心(动态渲染页面)
|
|
||||||
│ ├── designComponents/ # 设计组件库
|
|
||||||
│ │ ├── TextInput/ # 文本输入框
|
|
||||||
│ │ ├── RadioSelect/ # 单选器
|
|
||||||
│ │ └── GridTable/ # 表格组件
|
|
||||||
│ ├── plugins/ # 插件系统(交互事件、拖拽管理)
|
|
||||||
│ │ ├── interactionStore.ts # 全局交互钩子
|
|
||||||
│ │ ├── dragStore.ts # 拖拽状态管理
|
|
||||||
│ │ └── pathUtils.ts # 结构化路径工具
|
|
||||||
│ ├── stores/ # 状态管理
|
|
||||||
│ │ ├── panelStore.ts # 面板布局状态
|
|
||||||
│ │ ├── designStore.ts # 设计组件元数据
|
|
||||||
│ │ └── vueFileStore.ts # Vue文件选择状态
|
|
||||||
│ └── Designer.vue # 设计器主入口
|
|
||||||
├── views/ # 示例页面(测试用)
|
|
||||||
└── App.vue # 应用入口
|
|
||||||
```
|
|
||||||
|
|
||||||
## 核心功能模块
|
|
||||||
|
|
||||||
### 1. 页面管理系统
|
|
||||||
|
|
||||||
#### 功能说明
|
|
||||||
- 动态扫描`src/views`目录下的所有Vue文件
|
|
||||||
- 以树形结构展示文件夹和页面
|
|
||||||
- 点击选中后在设计中心动态渲染
|
|
||||||
|
|
||||||
#### 技术实现
|
|
||||||
```typescript
|
|
||||||
// 使用import.meta.glob动态扫描
|
|
||||||
const viewModules = import.meta.glob('../../../views/**/*.vue')
|
|
||||||
|
|
||||||
// 异步加载选中的组件
|
|
||||||
const loader = viewModules[selectedFilePath]
|
|
||||||
const component = defineAsyncComponent(loader)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 设计中心系统
|
|
||||||
|
|
||||||
#### InteractiveWrapper(交互包装器)
|
|
||||||
**作用**:为动态渲染的Vue页面注入交互事件
|
|
||||||
|
|
||||||
**核心功能**:
|
|
||||||
1. 自动扫描页面中的el-row和el-col
|
|
||||||
2. 为每个元素注入结构化路径ID(如`r1c2`)
|
|
||||||
3. 绑定交互事件(悬停、点击、拖拽)
|
|
||||||
4. 使用MutationObserver监听DOM变化
|
|
||||||
|
|
||||||
#### DropZone(拖放区域指示器)
|
|
||||||
**作用**:拖拽时显示可放置区域
|
|
||||||
|
|
||||||
**显示规则**:
|
|
||||||
- el-row:显示 **上方** 和 **下方** 两个区域
|
|
||||||
- el-col:显示 **左侧** 和 **右侧** 两个区域
|
|
||||||
|
|
||||||
#### DragPreview(拖拽预览)
|
|
||||||
跟随鼠标显示正在拖拽的内容,提供视觉反馈。
|
|
||||||
|
|
||||||
### 3. 插件系统 (fauto/plugins)
|
|
||||||
|
|
||||||
#### interactionStore(交互状态管理)
|
|
||||||
管理所有交互事件的全局状态:
|
|
||||||
- `hoverTarget`: 当前悬停的元素
|
|
||||||
- `selectedTarget`: 当前选中的元素
|
|
||||||
- 提供全局事件钩子(hover, click, longpress, drag, release)
|
|
||||||
|
|
||||||
#### dragStore(拖拽状态管理)
|
|
||||||
管理拖拽操作的复杂状态:
|
|
||||||
|
|
||||||
**核心状态**:
|
|
||||||
- `dragSource`: 拖拽源(设计组件/画布元素)
|
|
||||||
- `hierarchyNodes`: 当前悬停位置的所有层级节点
|
|
||||||
- `selectedHierarchyIndex`: 当前选中的层级索引
|
|
||||||
- `hoverDirection`: 当前悬停的拖放方向
|
|
||||||
- `lastDropRecord`: 最后一次拖放记录
|
|
||||||
|
|
||||||
**层级选择机制**:
|
|
||||||
- 鼠标悬停在嵌套元素上时,自动识别所有父级节点
|
|
||||||
- 默认选中最深层级
|
|
||||||
- **↑键**:切换到父级
|
|
||||||
- **↓键**:切换到子级
|
|
||||||
- **Esc键**:取消拖拽
|
|
||||||
|
|
||||||
#### pathUtils(路径工具)
|
|
||||||
生成和解析结构化路径ID:
|
|
||||||
|
|
||||||
**路径格式**:`r{n}c{m}r{x}c{y}...`
|
|
||||||
- `r`: el-row
|
|
||||||
- `c`: el-col
|
|
||||||
- 数字:在同级元素中的索引(从1开始)
|
|
||||||
|
|
||||||
**示例**:`r1c2r1c3` 表示:
|
|
||||||
- 第1个el-row
|
|
||||||
- 的第2个el-col
|
|
||||||
- 内的第1个el-row
|
|
||||||
- 内的第3个el-col
|
|
||||||
|
|
||||||
## 状态管理设计
|
|
||||||
|
|
||||||
### PanelStore(面板状态)
|
|
||||||
管理整个应用的布局和物料组件状态:
|
|
||||||
- `layout`: 三区域面板配置
|
|
||||||
- `materialStates`: 物料组件状态独立存储
|
|
||||||
- 提供 Tab 操作 API(添加、关闭、移动、激活)
|
|
||||||
|
|
||||||
### DesignStore(设计组件元数据)
|
|
||||||
管理设计组件的元数据:
|
|
||||||
- `componentMetas`: 设计组件元数据列表
|
|
||||||
- `loadComponentMetas()`: 动态扫描设计组件目录
|
|
||||||
- `getComponentMeta()`: 获取指定组件的元数据
|
|
||||||
|
|
||||||
### VueFileStore(Vue文件状态)
|
|
||||||
管理页面管理的选中状态:
|
|
||||||
- `selectedFilePath`: 当前选中的文件路径
|
|
||||||
- `selectedFileName`: 当前选中的文件名
|
|
||||||
- `selectFile()`: 选中指定文件
|
|
||||||
|
|
||||||
### InteractionStore / DragStore
|
|
||||||
见**插件系统**部分
|
|
||||||
|
|
||||||
## 实现细节
|
|
||||||
|
|
||||||
### 1. Vue页面规范
|
|
||||||
**强制规则**:template的第一层级**有且仅有一个el-row**
|
|
||||||
|
|
||||||
**目的**:保证布局可解析性,方便自动生成结构化路径
|
|
||||||
|
|
||||||
示例:
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<el-row :gutter="20">
|
|
||||||
<el-col :span="12">
|
|
||||||
<div class="design-component">左侧内容</div>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-row :gutter="10">
|
|
||||||
<el-col :span="24">
|
|
||||||
<div class="design-component">标题</div>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 拖拽交互优化
|
|
||||||
|
|
||||||
#### 禁用文本选择
|
|
||||||
拖拽时给body添加`is-dragging`类,设置`user-select: none`
|
|
||||||
|
|
||||||
#### 拖拽预览效果
|
|
||||||
DragPreview组件跟随鼠标显示:
|
|
||||||
- 组件图标 + 名称
|
|
||||||
- 动画效果(缩放 + 淡入)
|
|
||||||
- Teleport到body,避免z-index问题
|
|
||||||
|
|
||||||
### 3. 层级选择实现
|
|
||||||
```typescript
|
|
||||||
// 递归遍历DOM生成层级节点
|
|
||||||
const updateHierarchy = (element: HTMLElement) => {
|
|
||||||
const nodes: HierarchyNode[] = []
|
|
||||||
let current: HTMLElement | null = element
|
|
||||||
|
|
||||||
while (current) {
|
|
||||||
if (current.classList.contains('el-row') ||
|
|
||||||
current.classList.contains('el-col')) {
|
|
||||||
nodes.push({ path, type, element, depth })
|
|
||||||
}
|
|
||||||
current = current.parentElement
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按深度排序,最深的在前
|
|
||||||
nodes.sort((a, b) => b.depth - a.depth)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 性能优化
|
|
||||||
- 使用 `defineAsyncComponent` 异步加载页面
|
|
||||||
- 使用 `MutationObserver` 监听DOM变化
|
|
||||||
- 防止重复绑定(`data-fauto-bindend`属性)
|
|
||||||
- 使用 `computed` 缓存派生数据
|
|
||||||
|
|
||||||
## API 设计
|
|
||||||
|
|
||||||
### 配置管理 API
|
|
||||||
```
|
|
||||||
GET /api/config # 获取布局配置
|
|
||||||
POST /api/config # 保存布局配置
|
|
||||||
GET /api/material-states # 获取物料组件状态
|
|
||||||
POST /api/material-states # 保存物料组件状态
|
|
||||||
```
|
|
||||||
|
|
||||||
### 设计系统 API
|
|
||||||
```
|
|
||||||
GET /api/design-components # 获取设计组件元数据
|
|
||||||
GET /api/design-state # 获取设计中心状态
|
|
||||||
POST /api/design-state # 保存设计中心状态
|
|
||||||
```
|
|
||||||
|
|
||||||
## 开发规范
|
|
||||||
|
|
||||||
### Vue页面结构规范
|
|
||||||
1. template第一层级有且仅有一个el-row
|
|
||||||
2. el-row内只能包含el-col或div(设计组件)
|
|
||||||
3. div使用class="design-component"标识
|
|
||||||
|
|
||||||
### 代码规范
|
|
||||||
1. 使用 TypeScript 严格模式
|
|
||||||
2. 组件 Props 必须明确定义类型
|
|
||||||
3. 使用 Composition API 组织代码逻辑
|
|
||||||
4. 样式使用 scoped 避免全局污染
|
|
||||||
|
|
||||||
### 组件开发规范
|
|
||||||
1. 遵循物料组件标准化规范
|
|
||||||
2. 每个组件包含`index.vue`和`index.json`
|
|
||||||
3. 插件相关代码放在`fauto/plugins`目录
|
|
||||||
|
|
||||||
### 状态管理规范
|
|
||||||
1. 使用 Pinia 进行全局状态管理
|
|
||||||
2. 状态变更必须通过 Store 的方法
|
|
||||||
3. 复杂状态逻辑封装在 Store 内部
|
|
||||||
|
|
||||||
## 扩展性设计
|
|
||||||
|
|
||||||
### 新增设计组件
|
|
||||||
1. 在 `designComponents/` 目录下创建组件文件夹
|
|
||||||
2. 实现 `index.vue` 和 `index.json`
|
|
||||||
3. 自动在 DesignComponentList 中显示
|
|
||||||
|
|
||||||
### 功能扩展
|
|
||||||
1. **拖放后修改源文件** - 通过API将拖放操作写入.vue文件
|
|
||||||
2. **属性编辑器** - 展示和编辑设计组件属性
|
|
||||||
3. **热更新同步** - 修改后自动刷新页面
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
|
|
||||||
本项目实现了一个完整的**Vue页面可视化设计器**,通过直接解析和编辑真实的Vue文件,实现了低代码的页面构建方式。
|
|
||||||
|
|
||||||
**核心亮点**:
|
|
||||||
1. **非侵入式设计** - 通过注入式交互,无需修改Vue页面代码
|
|
||||||
2. **智能层级选择** - 自动识别嵌套结构,键盘切换层级
|
|
||||||
3. **实时视觉反馈** - 拖拽预览 + 拖放区域指示
|
|
||||||
4. **结构化路径** - 自动生成可读的元素ID
|
|
||||||
@@ -100,6 +100,43 @@ function getNodeSourceText(source, node) {
|
|||||||
return source.substring(start, end)
|
return source.substring(start, end)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取字符串的缩进级别(空格数)
|
||||||
|
*/
|
||||||
|
function getIndentLevel(str) {
|
||||||
|
const match = str.match(/^(\s*)/)
|
||||||
|
return match ? match[1].length : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调整源代码的缩进,保持相对缩进关系
|
||||||
|
* @param {string} sourceText - 源元素文本
|
||||||
|
* @param {string} targetIndent - 目标位置的缩进
|
||||||
|
* @returns {string} - 调整后的文本
|
||||||
|
*/
|
||||||
|
function adjustIndentation(sourceText, targetIndent) {
|
||||||
|
const lines = sourceText.split('\n')
|
||||||
|
if (lines.length === 0) return sourceText
|
||||||
|
|
||||||
|
// 获取第一行的原始缩进
|
||||||
|
const firstLineIndent = getIndentLevel(lines[0])
|
||||||
|
|
||||||
|
// 计算缩进差值
|
||||||
|
const targetIndentLevel = targetIndent.length
|
||||||
|
const indentDiff = targetIndentLevel - firstLineIndent
|
||||||
|
|
||||||
|
// 对每一行应用缩进差值
|
||||||
|
const adjustedLines = lines.map((line, index) => {
|
||||||
|
if (!line.trim()) return '' // 空行保持空
|
||||||
|
|
||||||
|
const currentIndent = getIndentLevel(line)
|
||||||
|
const newIndent = Math.max(0, currentIndent + indentDiff)
|
||||||
|
return ' '.repeat(newIndent) + line.trimStart()
|
||||||
|
})
|
||||||
|
|
||||||
|
return adjustedLines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将元素移动到目标元素内部
|
* 将元素移动到目标元素内部
|
||||||
* 如果目标已有子元素,则放到最后
|
* 如果目标已有子元素,则放到最后
|
||||||
@@ -109,81 +146,62 @@ function moveElementInside(templateContent, sourceNode, targetNode, sourcePath,
|
|||||||
const sourceEnd = sourceNode.loc.end.offset
|
const sourceEnd = sourceNode.loc.end.offset
|
||||||
const sourceText = templateContent.substring(sourceStart, sourceEnd)
|
const sourceText = templateContent.substring(sourceStart, sourceEnd)
|
||||||
|
|
||||||
// 找到源元素行的开始位置(用于删除整行)
|
// 1. 计算删除范围
|
||||||
const sourceLineStart = templateContent.lastIndexOf('\n', sourceStart - 1) + 1
|
let deleteStart = sourceStart
|
||||||
|
let deleteEnd = sourceEnd
|
||||||
|
|
||||||
// 获取目标元素的缩进(子元素需要比父元素多一层缩进)
|
// 向前查找这一行的开始
|
||||||
|
const lineStart = templateContent.lastIndexOf('\n', sourceStart - 1) + 1
|
||||||
|
const beforeElement = templateContent.substring(lineStart, sourceStart)
|
||||||
|
|
||||||
|
// 如果元素前面只有空白,则从行开始删除
|
||||||
|
if (/^\s*$/.test(beforeElement)) {
|
||||||
|
deleteStart = lineStart
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向后查找是否有换行
|
||||||
|
if (templateContent[sourceEnd] === '\n') {
|
||||||
|
deleteEnd = sourceEnd + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取目标元素的缩进(子元素需要比父元素多一层缩进)
|
||||||
const targetLineStart = templateContent.lastIndexOf('\n', targetNode.loc.start.offset - 1) + 1
|
const targetLineStart = templateContent.lastIndexOf('\n', targetNode.loc.start.offset - 1) + 1
|
||||||
const targetIndent = templateContent.substring(targetLineStart, targetNode.loc.start.offset)
|
const targetIndent = templateContent.substring(targetLineStart, targetNode.loc.start.offset)
|
||||||
const childIndent = targetIndent + ' ' // 子元素缩进
|
const childIndent = targetIndent + ' '
|
||||||
|
|
||||||
// 找到目标元素的结束标签位置
|
// 3. 找到目标元素的结束标签位置
|
||||||
// 目标元素内容的结束位置(结束标签前)
|
|
||||||
const targetText = templateContent.substring(targetNode.loc.start.offset, targetNode.loc.end.offset)
|
const targetText = templateContent.substring(targetNode.loc.start.offset, targetNode.loc.end.offset)
|
||||||
const targetTag = targetNode.tag // 'el-row' 或 'el-col'
|
const closeTagPattern = `</${targetNode.tag}>`
|
||||||
const closeTagPattern = `</${targetTag}>`
|
|
||||||
const closeTagIndex = targetText.lastIndexOf(closeTagPattern)
|
const closeTagIndex = targetText.lastIndexOf(closeTagPattern)
|
||||||
|
|
||||||
if (closeTagIndex === -1) {
|
if (closeTagIndex === -1) {
|
||||||
console.error('[moveElementInside] 未找到结束标签:', targetTag)
|
console.error('[moveElementInside] 未找到结束标签:', targetNode.tag)
|
||||||
return templateContent
|
return templateContent
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算插入位置(在结束标签之前)
|
// 插入位置(在结束标签之前)
|
||||||
const insertPositionInTarget = closeTagIndex
|
const insertPosition = targetNode.loc.start.offset + closeTagIndex
|
||||||
const insertPositionInTemplate = targetNode.loc.start.offset + insertPositionInTarget
|
|
||||||
|
|
||||||
// 调整源元素的缩进
|
// 4. 调整源元素的缩进(保持相对缩进)
|
||||||
const sourceLines = sourceText.split('\n')
|
const adjustedSourceText = adjustIndentation(sourceText, childIndent)
|
||||||
const adjustedSourceLines = sourceLines.map((line, index) => {
|
|
||||||
if (index === 0) {
|
|
||||||
return childIndent + line.trimStart()
|
|
||||||
}
|
|
||||||
// 保持相对缩进
|
|
||||||
return childIndent + line.trimStart()
|
|
||||||
})
|
|
||||||
const adjustedSourceText = adjustedSourceLines.join('\n')
|
|
||||||
|
|
||||||
// 根据源和目标的位置关系进行操作
|
// 构建插入文本
|
||||||
if (sourceStart < targetNode.loc.start.offset) {
|
const insertText = adjustedSourceText + '\n' + targetIndent
|
||||||
// 源在目标前面:先删除源,再插入
|
|
||||||
|
// 5. 执行操作(从后向前处理)
|
||||||
// 删除源元素
|
if (deleteStart > insertPosition) {
|
||||||
const beforeSource = templateContent.substring(0, sourceLineStart)
|
// 删除位置在插入位置后面:先删除,再插入
|
||||||
const afterSource = templateContent.substring(sourceEnd)
|
let result = templateContent.substring(0, deleteStart) + templateContent.substring(deleteEnd)
|
||||||
let afterSourceTrimmed = afterSource.startsWith('\n') ? afterSource.substring(1) : afterSource
|
result = result.substring(0, insertPosition) + insertText + result.substring(insertPosition)
|
||||||
const withoutSource = beforeSource + afterSourceTrimmed
|
return result
|
||||||
|
|
||||||
// 计算新的插入位置
|
|
||||||
const removedLength = templateContent.length - withoutSource.length
|
|
||||||
const newInsertPosition = insertPositionInTemplate - removedLength
|
|
||||||
|
|
||||||
// 插入到目标内部
|
|
||||||
const insertText = adjustedSourceText + '\n' + targetIndent
|
|
||||||
|
|
||||||
return withoutSource.substring(0, newInsertPosition) +
|
|
||||||
insertText +
|
|
||||||
withoutSource.substring(newInsertPosition)
|
|
||||||
} else {
|
} else {
|
||||||
// 源在目标后面:先插入,再删除
|
// 删除位置在插入位置前面:先插入,再删除
|
||||||
|
const deletedLength = deleteEnd - deleteStart
|
||||||
|
const adjustedInsertPos = insertPosition - deletedLength
|
||||||
|
|
||||||
// 插入到目标内部
|
let result = templateContent.substring(0, deleteStart) + templateContent.substring(deleteEnd)
|
||||||
const insertText = adjustedSourceText + '\n' + targetIndent
|
result = result.substring(0, adjustedInsertPos) + insertText + result.substring(adjustedInsertPos)
|
||||||
const withInsert = templateContent.substring(0, insertPositionInTemplate) +
|
return result
|
||||||
insertText +
|
|
||||||
templateContent.substring(insertPositionInTemplate)
|
|
||||||
|
|
||||||
// 计算源元素新位置
|
|
||||||
const insertedLength = insertText.length
|
|
||||||
const newSourceLineStart = sourceLineStart + insertedLength
|
|
||||||
const newSourceEnd = sourceEnd + insertedLength
|
|
||||||
|
|
||||||
// 删除源元素
|
|
||||||
const beforeNewSource = withInsert.substring(0, newSourceLineStart)
|
|
||||||
const afterNewSource = withInsert.substring(newSourceEnd)
|
|
||||||
let afterTrimmed = afterNewSource.startsWith('\n') ? afterNewSource.substring(1) : afterNewSource
|
|
||||||
|
|
||||||
return beforeNewSource + afterTrimmed
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,21 +240,16 @@ export function moveElement(vueContent, options) {
|
|||||||
const templateContent = templateBlock.content
|
const templateContent = templateBlock.content
|
||||||
|
|
||||||
// 找到 <template> 内容在原文件中的实际位置
|
// 找到 <template> 内容在原文件中的实际位置
|
||||||
// templateBlock.loc 指向整个 <template>...</template> 块
|
|
||||||
// 我们需要找到内容的起始和结束位置
|
|
||||||
const templateTagStart = vueContent.indexOf('<template')
|
const templateTagStart = vueContent.indexOf('<template')
|
||||||
const templateTagEnd = vueContent.indexOf('>', templateTagStart) + 1
|
const templateTagEnd = vueContent.indexOf('>', templateTagStart) + 1
|
||||||
const templateCloseStart = vueContent.indexOf('</template>', templateTagEnd)
|
const templateCloseStart = vueContent.indexOf('</template>', templateTagEnd)
|
||||||
|
|
||||||
// template内容在原文件中的位置
|
|
||||||
const contentStart = templateTagEnd
|
const contentStart = templateTagEnd
|
||||||
const contentEnd = templateCloseStart
|
const contentEnd = templateCloseStart
|
||||||
|
|
||||||
// 2. 解析template获取AST
|
// 2. 解析template获取AST
|
||||||
const templateAST = parseTemplate(templateContent, {
|
const templateAST = parseTemplate(templateContent, {
|
||||||
// 保留注释
|
|
||||||
comments: true,
|
comments: true,
|
||||||
// 保留空白(用于保持格式)
|
|
||||||
whitespace: 'preserve'
|
whitespace: 'preserve'
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -245,20 +258,14 @@ export function moveElement(vueContent, options) {
|
|||||||
const targetNode = findElementByPath(templateAST, targetPath)
|
const targetNode = findElementByPath(templateAST, targetPath)
|
||||||
|
|
||||||
if (!sourceNode) {
|
if (!sourceNode) {
|
||||||
return {
|
return { success: false, error: `未找到源元素: ${sourcePath}` }
|
||||||
success: false,
|
|
||||||
error: `未找到源元素: ${sourcePath}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!targetNode) {
|
if (!targetNode) {
|
||||||
return {
|
return { success: false, error: `未找到目标元素: ${targetPath}` }
|
||||||
success: false,
|
|
||||||
error: `未找到目标元素: ${targetPath}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 获取元素在template中的位置
|
// 4. 获取元素在template中的精确位置
|
||||||
const sourceStart = sourceNode.loc.start.offset
|
const sourceStart = sourceNode.loc.start.offset
|
||||||
const sourceEnd = sourceNode.loc.end.offset
|
const sourceEnd = sourceNode.loc.end.offset
|
||||||
const sourceText = templateContent.substring(sourceStart, sourceEnd)
|
const sourceText = templateContent.substring(sourceStart, sourceEnd)
|
||||||
@@ -270,18 +277,32 @@ export function moveElement(vueContent, options) {
|
|||||||
console.log(`[moveElement] 目标: ${targetPath} [${targetStart}-${targetEnd}]`)
|
console.log(`[moveElement] 目标: ${targetPath} [${targetStart}-${targetEnd}]`)
|
||||||
console.log(`[moveElement] 方向: ${direction}`)
|
console.log(`[moveElement] 方向: ${direction}`)
|
||||||
|
|
||||||
// 5. 计算插入位置和新内容
|
// 5. 计算删除范围(包括前面的缩进和后面的换行)
|
||||||
let newTemplateContent
|
// 找到源元素所在行的开始
|
||||||
|
let deleteStart = sourceStart
|
||||||
|
let deleteEnd = sourceEnd
|
||||||
|
|
||||||
// 检测源元素前面的空白(用于保持缩进)
|
// 向前查找这一行的开始(换行符后的第一个字符)
|
||||||
const sourceLineStart = templateContent.lastIndexOf('\n', sourceStart - 1) + 1
|
const lineStart = templateContent.lastIndexOf('\n', sourceStart - 1) + 1
|
||||||
const sourceIndent = templateContent.substring(sourceLineStart, sourceStart)
|
const beforeElement = templateContent.substring(lineStart, sourceStart)
|
||||||
|
|
||||||
// 检测目标元素的缩进
|
// 如果元素前面只有空白,则从行开始删除
|
||||||
|
if (/^\s*$/.test(beforeElement)) {
|
||||||
|
deleteStart = lineStart
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向后查找是否有换行
|
||||||
|
if (templateContent[sourceEnd] === '\n') {
|
||||||
|
deleteEnd = sourceEnd + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取目标元素的缩进
|
||||||
const targetLineStart = templateContent.lastIndexOf('\n', targetStart - 1) + 1
|
const targetLineStart = templateContent.lastIndexOf('\n', targetStart - 1) + 1
|
||||||
const targetIndent = templateContent.substring(targetLineStart, targetStart)
|
const targetIndent = templateContent.substring(targetLineStart, targetStart)
|
||||||
|
|
||||||
// 处理 'inside' 方向(放入目标元素内部)
|
let newTemplateContent
|
||||||
|
|
||||||
|
// 6. 处理 'inside' 方向
|
||||||
if (direction === 'inside') {
|
if (direction === 'inside') {
|
||||||
newTemplateContent = moveElementInside(
|
newTemplateContent = moveElementInside(
|
||||||
templateContent,
|
templateContent,
|
||||||
@@ -290,94 +311,51 @@ export function moveElement(vueContent, options) {
|
|||||||
sourcePath,
|
sourcePath,
|
||||||
targetPath
|
targetPath
|
||||||
)
|
)
|
||||||
} else if (sourceStart < targetStart) {
|
|
||||||
// 源在目标前面:先处理目标位置,再删除源
|
|
||||||
const insertPosition = (direction === 'bottom' || direction === 'right')
|
|
||||||
? targetEnd
|
|
||||||
: targetStart
|
|
||||||
|
|
||||||
// 构建新内容
|
|
||||||
let parts = []
|
|
||||||
|
|
||||||
// 删除源元素(包括前后的空白)
|
|
||||||
const beforeSource = templateContent.substring(0, sourceLineStart)
|
|
||||||
const afterSource = templateContent.substring(sourceEnd)
|
|
||||||
|
|
||||||
// 合并时跳过源元素后的换行
|
|
||||||
let afterSourceTrimmed = afterSource
|
|
||||||
if (afterSource.startsWith('\n')) {
|
|
||||||
afterSourceTrimmed = afterSource.substring(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const withoutSource = beforeSource + afterSourceTrimmed
|
|
||||||
|
|
||||||
// 计算新的目标位置(因为删除了源,位置会变化)
|
|
||||||
const removedLength = templateContent.length - withoutSource.length
|
|
||||||
const newInsertPosition = insertPosition - removedLength
|
|
||||||
|
|
||||||
// 在新位置插入源元素
|
|
||||||
const insertText = (direction === 'bottom' || direction === 'right')
|
|
||||||
? '\n' + targetIndent + sourceText
|
|
||||||
: sourceText + '\n' + targetIndent
|
|
||||||
|
|
||||||
newTemplateContent =
|
|
||||||
withoutSource.substring(0, newInsertPosition) +
|
|
||||||
insertText +
|
|
||||||
withoutSource.substring(newInsertPosition)
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// 源在目标后面:先插入,再删除
|
// 7. 计算插入位置
|
||||||
const insertPosition = (direction === 'bottom' || direction === 'right')
|
const insertAfterTarget = (direction === 'bottom' || direction === 'right')
|
||||||
? targetEnd
|
const insertPosition = insertAfterTarget ? targetEnd : targetStart
|
||||||
: targetStart
|
|
||||||
|
|
||||||
// 先在目标位置插入
|
// 构建插入文本(使用目标缩进,保持相对缩进)
|
||||||
const insertText = (direction === 'bottom' || direction === 'right')
|
const adjustedSource = adjustIndentation(sourceText, targetIndent)
|
||||||
? '\n' + targetIndent + sourceText
|
|
||||||
: sourceText + '\n' + targetIndent
|
|
||||||
|
|
||||||
const withInsert =
|
const insertText = insertAfterTarget
|
||||||
templateContent.substring(0, insertPosition) +
|
? '\n' + adjustedSource
|
||||||
insertText +
|
: adjustedSource + '\n'
|
||||||
templateContent.substring(insertPosition)
|
|
||||||
|
|
||||||
// 计算源元素新位置(因为插入了内容,位置会变化)
|
// 8. 执行操作(从后向前处理,避免偏移量问题)
|
||||||
const insertedLength = insertText.length
|
if (deleteStart > insertPosition) {
|
||||||
const newSourceStart = sourceLineStart + insertedLength
|
// 删除位置在插入位置后面:先删除,再插入
|
||||||
const newSourceEnd = sourceEnd + insertedLength
|
newTemplateContent = templateContent.substring(0, deleteStart) +
|
||||||
|
templateContent.substring(deleteEnd)
|
||||||
// 删除源元素
|
newTemplateContent = newTemplateContent.substring(0, insertPosition) +
|
||||||
const beforeNewSource = withInsert.substring(0, newSourceStart)
|
insertText +
|
||||||
const afterNewSource = withInsert.substring(newSourceEnd)
|
newTemplateContent.substring(insertPosition)
|
||||||
|
} else {
|
||||||
// 跳过换行
|
// 删除位置在插入位置前面:先插入,再删除(需要调整偏移)
|
||||||
let afterTrimmed = afterNewSource
|
const deletedLength = deleteEnd - deleteStart
|
||||||
if (afterTrimmed.startsWith('\n')) {
|
const adjustedInsertPos = insertPosition - deletedLength
|
||||||
afterTrimmed = afterTrimmed.substring(1)
|
|
||||||
|
newTemplateContent = templateContent.substring(0, deleteStart) +
|
||||||
|
templateContent.substring(deleteEnd)
|
||||||
|
newTemplateContent = newTemplateContent.substring(0, adjustedInsertPos) +
|
||||||
|
insertText +
|
||||||
|
newTemplateContent.substring(adjustedInsertPos)
|
||||||
}
|
}
|
||||||
|
|
||||||
newTemplateContent = beforeNewSource + afterTrimmed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 重建Vue文件:保留 <template> 标签,只替换内容
|
// 9. 重建Vue文件
|
||||||
const beforeContent = vueContent.substring(0, contentStart)
|
const newVueContent = vueContent.substring(0, contentStart) +
|
||||||
const afterContent = vueContent.substring(contentEnd)
|
newTemplateContent +
|
||||||
|
vueContent.substring(contentEnd)
|
||||||
const newVueContent = beforeContent + newTemplateContent + afterContent
|
|
||||||
|
|
||||||
console.log('[moveElement] 文件更新成功')
|
console.log('[moveElement] 文件更新成功')
|
||||||
|
|
||||||
return {
|
return { success: true, content: newVueContent }
|
||||||
success: true,
|
|
||||||
content: newVueContent
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[moveElement] 错误:', error)
|
console.error('[moveElement] 错误:', error)
|
||||||
return {
|
return { success: false, error: error.message }
|
||||||
success: false,
|
|
||||||
error: error.message
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
176
项目上下文.md
Normal file
176
项目上下文.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# Vue页面可视化设计器 - 项目上下文
|
||||||
|
|
||||||
|
> **用途**:AI 协作快速恢复上下文
|
||||||
|
> **更新时间**:2026-01-20
|
||||||
|
> **项目路径**:`d:/workspace/fauto-design`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 项目概述
|
||||||
|
|
||||||
|
基于 **Vue3 + TypeScript + Vite + Element Plus** 的**可视化页面设计器**,通过拖拽操作直接编辑真实的Vue源文件。
|
||||||
|
|
||||||
|
### 核心特性
|
||||||
|
|
||||||
|
1. **直接解析Vue文件** - 动态扫描并渲染 `src/views` 下的页面
|
||||||
|
2. **拖拽式设计** - 将组件拖拽到页面的 el-row/el-col 上
|
||||||
|
3. **两阶段拖拽** - 源选择阶段 → 目标选择阶段
|
||||||
|
4. **智能层级选择** - 键盘 ↑↓ 切换嵌套元素层级
|
||||||
|
5. **源码实时修改** - 后端服务解析AST并修改Vue文件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
fauto-design/
|
||||||
|
├── draggable-panels/ # 前端项目
|
||||||
|
│ └── src/
|
||||||
|
│ ├── fauto/ # 🔥 设计器核心
|
||||||
|
│ │ ├── Designer.vue # 设计器主入口
|
||||||
|
│ │ ├── components/ # 基础UI组件
|
||||||
|
│ │ ├── materials/ # 物料组件系统
|
||||||
|
│ │ │ ├── DesignCenter/ # 设计中心
|
||||||
|
│ │ │ │ ├── InteractiveWrapper.vue # 交互注入器
|
||||||
|
│ │ │ │ └── DropZone.vue # 拖放指示器
|
||||||
|
│ │ │ ├── DesignComponentList/ # 设计组件列表
|
||||||
|
│ │ │ └── PageManager/ # 页面管理
|
||||||
|
│ │ ├── designComponents/ # 设计组件库
|
||||||
|
│ │ ├── plugins/ # 🔌 核心插件
|
||||||
|
│ │ │ ├── dragStore.ts # 拖拽状态管理
|
||||||
|
│ │ │ ├── interactionStore.ts
|
||||||
|
│ │ │ └── pathUtils.ts # 路径工具
|
||||||
|
│ │ └── stores/ # Pinia状态
|
||||||
|
│ └── views/ # 示例页面
|
||||||
|
│
|
||||||
|
└── vue-template-service/ # 后端服务(Node.js)
|
||||||
|
└── src/
|
||||||
|
├── index.js # Express API
|
||||||
|
└── services/
|
||||||
|
└── templateService.js # Vue模板解析修改
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 核心技术要点
|
||||||
|
|
||||||
|
### 1. Vue页面规范 ⭐
|
||||||
|
|
||||||
|
**强制规则**:template 第一层级**有且仅有一个 el-row**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">内容</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-row> <!-- 支持嵌套 -->
|
||||||
|
<el-col :span="24">嵌套内容</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 结构化路径ID ⭐
|
||||||
|
|
||||||
|
**格式**:`r{n}c{m}...`(r=row, c=col, 数字=索引从1开始)
|
||||||
|
|
||||||
|
**示例**:`r1c2r1c3` = 第1个row → 第2个col → 第1个row → 第3个col
|
||||||
|
|
||||||
|
### 3. 两阶段拖拽 ⭐⭐
|
||||||
|
|
||||||
|
| 阶段 | 触发 | ↑↓键功能 | 拖放方向 |
|
||||||
|
|------|------|----------|---------|
|
||||||
|
| **源选择** | 点击元素 | 切换源层级 | 不显示 |
|
||||||
|
| **目标选择** | 移到其他元素 | 切换目标层级 | 显示 |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// dragStore.ts 核心状态
|
||||||
|
dragPhase: 'source' | 'target'
|
||||||
|
confirmedSource: { path, type, element }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 跨类型拖放 ⭐
|
||||||
|
|
||||||
|
- **同类型**:el-row→el-row 显示上/下,el-col→el-col 显示左/右
|
||||||
|
- **跨类型**:el-row→el-col 或 el-col→el-row 显示"放入内部"(inside)
|
||||||
|
|
||||||
|
### 5. 后端模板服务 ⭐⭐
|
||||||
|
|
||||||
|
**API**:`POST http://localhost:3001/api/move-element`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pagePath": "views/TestPage1.vue",
|
||||||
|
"source": "r1c1",
|
||||||
|
"target": "r1c2",
|
||||||
|
"direction": "right" // top|bottom|left|right|inside
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**技术栈**:
|
||||||
|
- `@vue/compiler-sfc` - 解析Vue SFC
|
||||||
|
- `@vue/compiler-dom` - 解析template AST
|
||||||
|
- 字符串操作移动元素(保持原始格式)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 数据流
|
||||||
|
|
||||||
|
```
|
||||||
|
用户点击元素开始拖拽
|
||||||
|
↓
|
||||||
|
dragStore.startDragFromCanvas() → 源选择阶段
|
||||||
|
↓
|
||||||
|
按↑↓键切换源层级
|
||||||
|
↓
|
||||||
|
移动到其他元素 → dragStore.enterTargetPhase() → 目标选择阶段
|
||||||
|
↓
|
||||||
|
按↑↓键切换目标层级,显示DropZone
|
||||||
|
↓
|
||||||
|
松开鼠标 → dragStore.confirmDrop()
|
||||||
|
↓
|
||||||
|
调用后端API → templateService.moveElement()
|
||||||
|
↓
|
||||||
|
修改Vue源文件 → 页面热更新
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 启动前端(端口5173)
|
||||||
|
cd draggable-panels
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 启动后端(端口3001)
|
||||||
|
cd vue-template-service
|
||||||
|
node src/index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
**访问**:http://localhost:5173/draggable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 关键文件
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| `plugins/dragStore.ts` | 拖拽状态、两阶段逻辑、层级选择 |
|
||||||
|
| `materials/DesignCenter/InteractiveWrapper.vue` | 交互注入、键盘监听、拖放确认 |
|
||||||
|
| `materials/DesignCenter/DropZone.vue` | 拖放方向指示器 |
|
||||||
|
| `vue-template-service/src/services/templateService.js` | AST解析、元素移动 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 注意事项
|
||||||
|
|
||||||
|
1. 拖拽时会禁用文本选择(`user-select: none`)
|
||||||
|
2. 使用全局鼠标事件获取悬停位置(避免 pointer-events 问题)
|
||||||
|
3. 移动元素时保持相对缩进关系
|
||||||
|
4. 前后端都需要运行才能完成拖放后的源码修改
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**最后更新**:2026-01-20
|
||||||
366
项目设计文档.md
Normal file
366
项目设计文档.md
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
# 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
|
||||||
Reference in New Issue
Block a user