From 4a90340ab36c6856ca8fc32e95afd1835db3cafb Mon Sep 17 00:00:00 2001 From: wfz <1040079213@qq.com> Date: Tue, 20 Jan 2026 20:25:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- draggable-panels/IMPLEMENTATION_PLAN.md | 702 ------------------ draggable-panels/PROJECT_CONTEXT.md | 305 -------- draggable-panels/STAGE1_COMPLETED.md | 253 ------- .../DesignCenter/InteractiveWrapper.vue | 18 +- .../src/fauto/plugins/dragStore.ts | 70 +- draggable-panels/src/views/TestPage1.vue | 18 +- draggable-panels/项目设计文档.md | 280 ------- .../src/services/templateService.js | 290 ++++---- 项目上下文.md | 176 +++++ 项目设计文档.md | 366 +++++++++ 10 files changed, 763 insertions(+), 1715 deletions(-) delete mode 100644 draggable-panels/IMPLEMENTATION_PLAN.md delete mode 100644 draggable-panels/PROJECT_CONTEXT.md delete mode 100644 draggable-panels/STAGE1_COMPLETED.md delete mode 100644 draggable-panels/项目设计文档.md create mode 100644 项目上下文.md create mode 100644 项目设计文档.md diff --git a/draggable-panels/IMPLEMENTATION_PLAN.md b/draggable-panels/IMPLEMENTATION_PLAN.md deleted file mode 100644 index dd00606..0000000 --- a/draggable-panels/IMPLEMENTATION_PLAN.md +++ /dev/null @@ -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 // 属性 (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组件 - -**让我们开始实施吧!** 🚀 diff --git a/draggable-panels/PROJECT_CONTEXT.md b/draggable-panels/PROJECT_CONTEXT.md deleted file mode 100644 index 949179c..0000000 --- a/draggable-panels/PROJECT_CONTEXT.md +++ /dev/null @@ -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 - -``` - -**目的**:保证布局可解析性,方便自动生成结构化路径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. 动态渲染组件 - - -// 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 - -``` - -### 添加设计组件 -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协作建议**:优先阅读本文档的"核心技术要点"部分,理解项目的设计理念和实现方式 diff --git a/draggable-panels/STAGE1_COMPLETED.md b/draggable-panels/STAGE1_COMPLETED.md deleted file mode 100644 index d7a11a7..0000000 --- a/draggable-panels/STAGE1_COMPLETED.md +++ /dev/null @@ -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. 观察不同页面的布局样式 diff --git a/draggable-panels/src/fauto/materials/DesignCenter/InteractiveWrapper.vue b/draggable-panels/src/fauto/materials/DesignCenter/InteractiveWrapper.vue index 09a3515..723c484 100644 --- a/draggable-panels/src/fauto/materials/DesignCenter/InteractiveWrapper.vue +++ b/draggable-panels/src/fauto/materials/DesignCenter/InteractiveWrapper.vue @@ -57,6 +57,14 @@ const bindElementEvents = (element: HTMLElement, type: ElementType, path: string if (!dragStore.isDragging) { 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 - // 开始画布内元素拖拽 - dragStore.startDragFromCanvas(path, type) + // 开始画布内元素拖拽,传入元素引用以立即构建层级列表 + dragStore.startDragFromCanvas(path, type, element) } // 绑定事件 @@ -228,7 +236,8 @@ const clearDragHighlight = () => { * 全局鼠标移动处理(拖拽时更新方向) */ const handleGlobalMouseMove = (e: MouseEvent) => { - if (dragStore.isDragging && dragStore.selectedNode) { + // 只在目标选择阶段才更新方向 + if (dragStore.isDragging && dragStore.dragPhase === 'target' && dragStore.selectedNode) { dragStore.updateDirectionFromMouse(e.clientX, e.clientY) } } @@ -237,7 +246,8 @@ const handleGlobalMouseMove = (e: MouseEvent) => { * 全局鼠标松开处理 */ const handleGlobalMouseUp = async () => { - if (dragStore.isDragging && dragStore.selectedNode && dragStore.hoverDirection) { + // 只在目标选择阶段才执行拖放 + if (dragStore.isDragging && dragStore.dragPhase === 'target' && dragStore.selectedNode && dragStore.hoverDirection) { // 获取当前页面路径(从vueFileStore) const pagePath = vueFileStore.selectedFilePath diff --git a/draggable-panels/src/fauto/plugins/dragStore.ts b/draggable-panels/src/fauto/plugins/dragStore.ts index 3883b9b..973f137 100644 --- a/draggable-panels/src/fauto/plugins/dragStore.ts +++ b/draggable-panels/src/fauto/plugins/dragStore.ts @@ -58,6 +58,16 @@ export const useDragStore = defineStore('drag', () => { // 拖拽源 const dragSource = ref(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 + dragPhase.value = 'source' // 进入源选择阶段 + confirmedSource.value = null // 清除之前的确定源 dragSource.value = { type: 'canvas-element', path, 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 = [] selectedHierarchyIndex.value = 0 hoverDirection.value = null + dragPhase.value = 'source' + confirmedSource.value = null } /** @@ -252,12 +306,11 @@ export const useDragStore = defineStore('drag', () => { * 是否为跨类型拖放(源和目标类型不同) */ const isCrossTypeDrop = computed(() => { - if (!dragSource.value || !selectedNode.value) return false + // 只有在目标选择阶段才考虑跨类型 + if (dragPhase.value !== 'target') return false + if (!confirmedSource.value || !selectedNode.value) return false - // 只有画布内元素拖放才考虑跨类型 - if (dragSource.value.type !== 'canvas-element') return false - - const sourceType = dragSource.value.elementType + const sourceType = confirmedSource.value.type const targetType = selectedNode.value.type // er 拖到 ec 或 ec 拖到 er 都是跨类型 @@ -427,6 +480,8 @@ export const useDragStore = defineStore('drag', () => { // 状态 isDragging, dragSource, + dragPhase, + confirmedSource, hierarchyNodes, selectedHierarchyIndex, selectedNode, @@ -439,6 +494,7 @@ export const useDragStore = defineStore('drag', () => { // 方法 startDragFromComponentList, startDragFromCanvas, + enterTargetPhase, updateHierarchy, clearHierarchy, selectParentLevel, diff --git a/draggable-panels/src/views/TestPage1.vue b/draggable-panels/src/views/TestPage1.vue index cc395e4..7406d0c 100644 --- a/draggable-panels/src/views/TestPage1.vue +++ b/draggable-panels/src/views/TestPage1.vue @@ -9,16 +9,18 @@
右侧标题
- - -
右侧列2
-
-
- -
右侧列1
-
+ + + +
右侧列2
+
+ +
+ +
右侧列1
+
diff --git a/draggable-panels/项目设计文档.md b/draggable-panels/项目设计文档.md deleted file mode 100644 index 66beb44..0000000 --- a/draggable-panels/项目设计文档.md +++ /dev/null @@ -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 - -``` - -### 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 \ No newline at end of file diff --git a/vue-template-service/src/services/templateService.js b/vue-template-service/src/services/templateService.js index dbe2202..c3dc833 100644 --- a/vue-template-service/src/services/templateService.js +++ b/vue-template-service/src/services/templateService.js @@ -100,6 +100,43 @@ function getNodeSourceText(source, node) { 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 sourceText = templateContent.substring(sourceStart, sourceEnd) - // 找到源元素行的开始位置(用于删除整行) - const sourceLineStart = templateContent.lastIndexOf('\n', sourceStart - 1) + 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 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 targetTag = targetNode.tag // 'el-row' 或 'el-col' - const closeTagPattern = `` + const closeTagPattern = `` const closeTagIndex = targetText.lastIndexOf(closeTagPattern) if (closeTagIndex === -1) { - console.error('[moveElementInside] 未找到结束标签:', targetTag) + console.error('[moveElementInside] 未找到结束标签:', targetNode.tag) return templateContent } - // 计算插入位置(在结束标签之前) - const insertPositionInTarget = closeTagIndex - const insertPositionInTemplate = targetNode.loc.start.offset + insertPositionInTarget + // 插入位置(在结束标签之前) + const insertPosition = targetNode.loc.start.offset + closeTagIndex - // 调整源元素的缩进 - const sourceLines = sourceText.split('\n') - const adjustedSourceLines = sourceLines.map((line, index) => { - if (index === 0) { - return childIndent + line.trimStart() - } - // 保持相对缩进 - return childIndent + line.trimStart() - }) - const adjustedSourceText = adjustedSourceLines.join('\n') + // 4. 调整源元素的缩进(保持相对缩进) + const adjustedSourceText = adjustIndentation(sourceText, childIndent) - // 根据源和目标的位置关系进行操作 - if (sourceStart < targetNode.loc.start.offset) { - // 源在目标前面:先删除源,再插入 - - // 删除源元素 - const beforeSource = templateContent.substring(0, sourceLineStart) - const afterSource = templateContent.substring(sourceEnd) - let afterSourceTrimmed = afterSource.startsWith('\n') ? afterSource.substring(1) : afterSource - const withoutSource = beforeSource + afterSourceTrimmed - - // 计算新的插入位置 - const removedLength = templateContent.length - withoutSource.length - const newInsertPosition = insertPositionInTemplate - removedLength - - // 插入到目标内部 - const insertText = adjustedSourceText + '\n' + targetIndent - - return withoutSource.substring(0, newInsertPosition) + - insertText + - withoutSource.substring(newInsertPosition) + // 构建插入文本 + const insertText = adjustedSourceText + '\n' + targetIndent + + // 5. 执行操作(从后向前处理) + if (deleteStart > insertPosition) { + // 删除位置在插入位置后面:先删除,再插入 + let result = templateContent.substring(0, deleteStart) + templateContent.substring(deleteEnd) + result = result.substring(0, insertPosition) + insertText + result.substring(insertPosition) + return result } else { - // 源在目标后面:先插入,再删除 + // 删除位置在插入位置前面:先插入,再删除 + const deletedLength = deleteEnd - deleteStart + const adjustedInsertPos = insertPosition - deletedLength - // 插入到目标内部 - const insertText = adjustedSourceText + '\n' + targetIndent - const withInsert = templateContent.substring(0, insertPositionInTemplate) + - 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 + let result = templateContent.substring(0, deleteStart) + templateContent.substring(deleteEnd) + result = result.substring(0, adjustedInsertPos) + insertText + result.substring(adjustedInsertPos) + return result } } @@ -222,21 +240,16 @@ export function moveElement(vueContent, options) { const templateContent = templateBlock.content // 找到 ', templateTagEnd) - // template内容在原文件中的位置 const contentStart = templateTagEnd const contentEnd = templateCloseStart // 2. 解析template获取AST const templateAST = parseTemplate(templateContent, { - // 保留注释 comments: true, - // 保留空白(用于保持格式) whitespace: 'preserve' }) @@ -245,20 +258,14 @@ export function moveElement(vueContent, options) { const targetNode = findElementByPath(templateAST, targetPath) if (!sourceNode) { - return { - success: false, - error: `未找到源元素: ${sourcePath}` - } + return { success: false, error: `未找到源元素: ${sourcePath}` } } if (!targetNode) { - return { - success: false, - error: `未找到目标元素: ${targetPath}` - } + return { success: false, error: `未找到目标元素: ${targetPath}` } } - // 4. 获取元素在template中的位置 + // 4. 获取元素在template中的精确位置 const sourceStart = sourceNode.loc.start.offset const sourceEnd = sourceNode.loc.end.offset const sourceText = templateContent.substring(sourceStart, sourceEnd) @@ -270,18 +277,32 @@ export function moveElement(vueContent, options) { console.log(`[moveElement] 目标: ${targetPath} [${targetStart}-${targetEnd}]`) console.log(`[moveElement] 方向: ${direction}`) - // 5. 计算插入位置和新内容 - let newTemplateContent + // 5. 计算删除范围(包括前面的缩进和后面的换行) + // 找到源元素所在行的开始 + let deleteStart = sourceStart + let deleteEnd = sourceEnd - // 检测源元素前面的空白(用于保持缩进) - const sourceLineStart = templateContent.lastIndexOf('\n', sourceStart - 1) + 1 - const sourceIndent = templateContent.substring(sourceLineStart, sourceStart) + // 向前查找这一行的开始(换行符后的第一个字符) + 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 + } + + // 获取目标元素的缩进 const targetLineStart = templateContent.lastIndexOf('\n', targetStart - 1) + 1 const targetIndent = templateContent.substring(targetLineStart, targetStart) - // 处理 'inside' 方向(放入目标元素内部) + let newTemplateContent + + // 6. 处理 'inside' 方向 if (direction === 'inside') { newTemplateContent = moveElementInside( templateContent, @@ -290,94 +311,51 @@ export function moveElement(vueContent, options) { sourcePath, 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 { - // 源在目标后面:先插入,再删除 - const insertPosition = (direction === 'bottom' || direction === 'right') - ? targetEnd - : targetStart + // 7. 计算插入位置 + const insertAfterTarget = (direction === 'bottom' || direction === 'right') + const insertPosition = insertAfterTarget ? targetEnd : targetStart - // 先在目标位置插入 - const insertText = (direction === 'bottom' || direction === 'right') - ? '\n' + targetIndent + sourceText - : sourceText + '\n' + targetIndent + // 构建插入文本(使用目标缩进,保持相对缩进) + const adjustedSource = adjustIndentation(sourceText, targetIndent) - const withInsert = - templateContent.substring(0, insertPosition) + - insertText + - templateContent.substring(insertPosition) + const insertText = insertAfterTarget + ? '\n' + adjustedSource + : adjustedSource + '\n' - // 计算源元素新位置(因为插入了内容,位置会变化) - const insertedLength = insertText.length - const newSourceStart = sourceLineStart + insertedLength - const newSourceEnd = sourceEnd + insertedLength - - // 删除源元素 - const beforeNewSource = withInsert.substring(0, newSourceStart) - const afterNewSource = withInsert.substring(newSourceEnd) - - // 跳过换行 - let afterTrimmed = afterNewSource - if (afterTrimmed.startsWith('\n')) { - afterTrimmed = afterTrimmed.substring(1) + // 8. 执行操作(从后向前处理,避免偏移量问题) + if (deleteStart > insertPosition) { + // 删除位置在插入位置后面:先删除,再插入 + newTemplateContent = templateContent.substring(0, deleteStart) + + templateContent.substring(deleteEnd) + newTemplateContent = newTemplateContent.substring(0, insertPosition) + + insertText + + newTemplateContent.substring(insertPosition) + } else { + // 删除位置在插入位置前面:先插入,再删除(需要调整偏移) + const deletedLength = deleteEnd - deleteStart + const adjustedInsertPos = insertPosition - deletedLength + + newTemplateContent = templateContent.substring(0, deleteStart) + + templateContent.substring(deleteEnd) + newTemplateContent = newTemplateContent.substring(0, adjustedInsertPos) + + insertText + + newTemplateContent.substring(adjustedInsertPos) } - - newTemplateContent = beforeNewSource + afterTrimmed } - // 6. 重建Vue文件:保留