2
This commit is contained in:
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
6
.idea/MarsCodeWorkspaceAppSettings.xml
generated
Normal file
6
.idea/MarsCodeWorkspaceAppSettings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="com.codeverse.userSettings.MarscodeWorkspaceAppSettingsState">
|
||||||
|
<option name="progress" value="1.0" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
9
.idea/fauto-design.iml
generated
Normal file
9
.idea/fauto-design.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/misc.xml
generated
Normal file
6
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/fauto-design.iml" filepath="$PROJECT_DIR$/.idea/fauto-design.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
702
draggable-panels/IMPLEMENTATION_PLAN.md
Normal file
702
draggable-panels/IMPLEMENTATION_PLAN.md
Normal file
@@ -0,0 +1,702 @@
|
|||||||
|
# 🎯 可视化拖拽设计器 - 整体实施计划
|
||||||
|
|
||||||
|
> **目标**:将当前的设计器升级为可直接解析和编辑真实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组件
|
||||||
|
|
||||||
|
**让我们开始实施吧!** 🚀
|
||||||
253
draggable-panels/STAGE1_COMPLETED.md
Normal file
253
draggable-panels/STAGE1_COMPLETED.md
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
# 第一阶段完成说明
|
||||||
|
|
||||||
|
## ✅ 已完成的功能
|
||||||
|
|
||||||
|
### 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. 观察不同页面的布局样式
|
||||||
@@ -3,20 +3,20 @@
|
|||||||
"leftPanel": {
|
"leftPanel": {
|
||||||
"id": "left",
|
"id": "left",
|
||||||
"tabs": [
|
"tabs": [
|
||||||
|
{
|
||||||
|
"id": "6hfm9ux",
|
||||||
|
"title": "页面管理",
|
||||||
|
"content": "新窗口内容",
|
||||||
|
"materialId": "PageManager"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "up60643",
|
"id": "up60643",
|
||||||
"title": "设计组件列表",
|
"title": "设计组件列表",
|
||||||
"content": "新窗口内容",
|
"content": "新窗口内容",
|
||||||
"materialId": "DesignComponentList"
|
"materialId": "DesignComponentList"
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "78ikdz5",
|
|
||||||
"title": "测试组件B",
|
|
||||||
"content": "新窗口内容",
|
|
||||||
"materialId": "TestWidget2"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"activeTabId": "up60643"
|
"activeTabId": "6hfm9ux"
|
||||||
},
|
},
|
||||||
"centerPanel": {
|
"centerPanel": {
|
||||||
"id": "center",
|
"id": "center",
|
||||||
@@ -144,8 +144,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"activeTabId": "vrh9bl2"
|
"activeTabId": "mxfx11j"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lastUpdated": "2025-12-20T14:50:20.652Z"
|
"lastUpdated": "2025-12-21T12:24:13.100Z"
|
||||||
}
|
}
|
||||||
@@ -55,5 +55,5 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"selectedId": "xazr6j9",
|
"selectedId": "xazr6j9",
|
||||||
"lastUpdated": "2025-12-20T15:21:31.411Z"
|
"lastUpdated": "2025-12-21T12:22:52.464Z"
|
||||||
}
|
}
|
||||||
236
draggable-panels/package-lock.json
generated
236
draggable-panels/package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "draggable-panels",
|
"name": "draggable-panels",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"element-plus": "^2.8.8",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.24",
|
"vue": "^3.5.24",
|
||||||
"vue-router": "^4.6.4",
|
"vue-router": "^4.6.4",
|
||||||
@@ -68,6 +69,24 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ctrl/tinycolor": {
|
||||||
|
"version": "3.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
|
||||||
|
"integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@element-plus/icons-vue": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.27.2",
|
"version": "0.27.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
|
||||||
@@ -510,12 +529,48 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
|
||||||
|
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
|
||||||
|
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.7.3",
|
||||||
|
"@floating-ui/utils": "^0.2.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||||
|
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@popperjs/core": {
|
||||||
|
"name": "@sxzz/popperjs-es",
|
||||||
|
"version": "2.11.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
|
||||||
|
"integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/popperjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-beta.53",
|
"version": "1.0.0-beta.53",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
|
||||||
@@ -838,6 +893,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/lodash-es": {
|
||||||
|
"version": "4.17.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||||
|
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/lodash": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.10.4",
|
"version": "24.10.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz",
|
||||||
@@ -848,6 +918,12 @@
|
|||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/web-bluetooth": {
|
||||||
|
"version": "0.0.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
|
||||||
|
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-vue": {
|
"node_modules/@vitejs/plugin-vue": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz",
|
||||||
@@ -1070,6 +1146,94 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vueuse/core": {
|
||||||
|
"version": "10.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
|
||||||
|
"integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/web-bluetooth": "^0.0.20",
|
||||||
|
"@vueuse/metadata": "10.11.1",
|
||||||
|
"@vueuse/shared": "10.11.1",
|
||||||
|
"vue-demi": ">=0.14.8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/core/node_modules/vue-demi": {
|
||||||
|
"version": "0.14.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||||
|
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||||
|
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.0.0-rc.1",
|
||||||
|
"vue": "^3.0.0-0 || ^2.6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/metadata": {
|
||||||
|
"version": "10.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz",
|
||||||
|
"integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/shared": {
|
||||||
|
"version": "10.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz",
|
||||||
|
"integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"vue-demi": ">=0.14.8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/shared/node_modules/vue-demi": {
|
||||||
|
"version": "0.14.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||||
|
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||||
|
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.0.0-rc.1",
|
||||||
|
"vue": "^3.0.0-0 || ^2.6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/alien-signals": {
|
"node_modules/alien-signals": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.1.tgz",
|
||||||
@@ -1077,6 +1241,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/async-validator": {
|
||||||
|
"version": "4.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
|
||||||
|
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/birpc": {
|
"node_modules/birpc": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
|
||||||
@@ -1107,6 +1277,37 @@
|
|||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dayjs": {
|
||||||
|
"version": "1.11.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
|
||||||
|
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/element-plus": {
|
||||||
|
"version": "2.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.0.tgz",
|
||||||
|
"integrity": "sha512-qjxS+SBChvqCl6lU6ShiliLMN6WqFHiXQENYbAY3GKNflG+FS3jqn8JmQq0CBZq4koFqsi95NT1M6SL4whZfrA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@ctrl/tinycolor": "^3.4.1",
|
||||||
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
|
"@floating-ui/dom": "^1.0.1",
|
||||||
|
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
|
||||||
|
"@types/lodash": "^4.17.20",
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
|
"@vueuse/core": "^10.11.0",
|
||||||
|
"async-validator": "^4.2.5",
|
||||||
|
"dayjs": "^1.11.19",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"lodash-unified": "^1.0.3",
|
||||||
|
"memoize-one": "^6.0.0",
|
||||||
|
"normalize-wheel-es": "^1.2.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/entities": {
|
"node_modules/entities": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz",
|
||||||
@@ -1218,6 +1419,29 @@
|
|||||||
"url": "https://github.com/sponsors/mesqueeb"
|
"url": "https://github.com/sponsors/mesqueeb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash-es": {
|
||||||
|
"version": "4.17.22",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz",
|
||||||
|
"integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash-unified": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/lodash-es": "*",
|
||||||
|
"lodash": "*",
|
||||||
|
"lodash-es": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
@@ -1227,6 +1451,12 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/memoize-one": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/mitt": {
|
"node_modules/mitt": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||||
@@ -1258,6 +1488,12 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/normalize-wheel-es": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/path-browserify": {
|
"node_modules/path-browserify": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"element-plus": "^2.8.8",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.24",
|
"vue": "^3.5.24",
|
||||||
"vue-router": "^4.6.4",
|
"vue-router": "^4.6.4",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "设计中心",
|
"name": "设计中心",
|
||||||
"description": "展示已添加的设计组件实例"
|
"description": "实时预览选中的Vue页面或设计组件"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineAsyncComponent, markRaw } from 'vue'
|
import { defineAsyncComponent, markRaw, computed, watch } from 'vue'
|
||||||
import { useDesignStore } from '../../stores/designStore'
|
import { useDesignStore } from '../../stores/designStore'
|
||||||
|
import { useVueFileStore } from '../../stores/vueFileStore'
|
||||||
import config from './index.json'
|
import config from './index.json'
|
||||||
|
|
||||||
const designStore = useDesignStore()
|
const designStore = useDesignStore()
|
||||||
|
const vueFileStore = useVueFileStore()
|
||||||
|
|
||||||
// 自动扫描所有设计组件(eager 模式确保同步加载)
|
// 自动扫描所有设计组件(eager 模式确保同步加载)
|
||||||
const designComponentModules = import.meta.glob('../../designComponents/*/index.vue', { eager: true })
|
const designComponentModules = import.meta.glob('../../designComponents/*/index.vue', { eager: true })
|
||||||
@@ -33,15 +35,50 @@ const handleRemove = (instanceId: string, event: Event) => {
|
|||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
designStore.removeComponent(instanceId)
|
designStore.removeComponent(instanceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 扫描所有views目录下的Vue文件
|
||||||
|
const viewModules = import.meta.glob('../../../views/**/*.vue')
|
||||||
|
|
||||||
|
// 当前选中的Vue页面组件
|
||||||
|
const selectedPageComponent = computed(() => {
|
||||||
|
if (!vueFileStore.selectedFilePath) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态加载选中的组件
|
||||||
|
const loader = viewModules[vueFileStore.selectedFilePath]
|
||||||
|
if (loader) {
|
||||||
|
return defineAsyncComponent(loader as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听选中文件变化
|
||||||
|
watch(() => vueFileStore.selectedFilePath, (newPath) => {
|
||||||
|
console.log('设计中心:选中的文件路径变化', newPath)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="design-center">
|
<div class="design-center">
|
||||||
<div class="center-header">
|
<div class="center-header">
|
||||||
<span class="title">{{ config.name }}</span>
|
<span class="title">{{ config.name }}</span>
|
||||||
<span class="count">{{ designStore.components.length }} 个实例</span>
|
<span class="count" v-if="!vueFileStore.selectedFilePath">
|
||||||
|
{{ designStore.components.length }} 个实例
|
||||||
|
</span>
|
||||||
|
<span class="file-info" v-else>
|
||||||
|
📄 {{ vueFileStore.selectedFileName }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="center-body">
|
<div class="center-body">
|
||||||
|
<!-- 动态渲染选中的Vue页面 -->
|
||||||
|
<div v-if="selectedPageComponent" class="page-preview">
|
||||||
|
<component :is="selectedPageComponent" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 原有的设计组件实例列表 -->
|
||||||
|
<div v-else-if="designStore.components.length > 0" class="component-list">
|
||||||
<div
|
<div
|
||||||
v-for="instance in designStore.components"
|
v-for="instance in designStore.components"
|
||||||
:key="instance.id"
|
:key="instance.id"
|
||||||
@@ -61,11 +98,15 @@ const handleRemove = (instanceId: string, event: Event) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="designStore.components.length === 0" class="empty-tip">
|
<!-- 空状态 -->
|
||||||
|
<div v-else class="empty-tip">
|
||||||
<div class="empty-icon">🎨</div>
|
<div class="empty-icon">🎨</div>
|
||||||
<div>暂无设计组件</div>
|
<div>暂无内容</div>
|
||||||
<div class="empty-hint">从左侧列表点击添加</div>
|
<div class="empty-hint">
|
||||||
|
点击“页面管理”选择Vue页面,或从左侧列表添加设计组件
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,12 +140,32 @@ const handleRemove = (instanceId: string, event: Event) => {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
color: #4fc3f7;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.center-body {
|
.center-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-preview {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.component-row {
|
.component-row {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"name": "页面管理",
|
||||||
|
"description": "浏览和管理src/views目录下的Vue页面文件"
|
||||||
|
}
|
||||||
254
draggable-panels/src/fauto/materials/PageManager/index.vue
Normal file
254
draggable-panels/src/fauto/materials/PageManager/index.vue
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useVueFileStore, type VueFileNode } from '../../stores/vueFileStore'
|
||||||
|
import config from './index.json'
|
||||||
|
|
||||||
|
const vueFileStore = useVueFileStore()
|
||||||
|
|
||||||
|
// 扩展的节点数据(用于el-tree)
|
||||||
|
interface TreeNodeData {
|
||||||
|
label: string
|
||||||
|
path: string
|
||||||
|
type: 'file' | 'folder'
|
||||||
|
children?: TreeNodeData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const treeData = ref<TreeNodeData[]>([])
|
||||||
|
const defaultExpandedKeys = ref<string[]>([])
|
||||||
|
|
||||||
|
// 扫描views目录下的所有Vue文件
|
||||||
|
const scanViewsDirectory = () => {
|
||||||
|
// 使用Vite的import.meta.glob扫描views目录
|
||||||
|
const viewModules = import.meta.glob('../../../views/**/*.vue')
|
||||||
|
|
||||||
|
console.log('扫描到的文件:', Object.keys(viewModules))
|
||||||
|
|
||||||
|
// 构建文件树
|
||||||
|
const fileTree = vueFileStore.buildFileTree(viewModules)
|
||||||
|
|
||||||
|
// 转换为el-tree需要的格式
|
||||||
|
const convertToTreeData = (nodes: VueFileNode[]): TreeNodeData[] => {
|
||||||
|
return nodes.map(node => ({
|
||||||
|
label: node.name,
|
||||||
|
path: node.path,
|
||||||
|
type: node.type,
|
||||||
|
children: node.children ? convertToTreeData(node.children) : undefined
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
treeData.value = convertToTreeData(fileTree)
|
||||||
|
|
||||||
|
// 默认展开所有文件夹
|
||||||
|
defaultExpandedKeys.value = getAllFolderPaths(treeData.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有文件夹路径(用于默认展开)
|
||||||
|
const getAllFolderPaths = (nodes: TreeNodeData[]): string[] => {
|
||||||
|
const paths: string[] = []
|
||||||
|
|
||||||
|
const traverse = (items: TreeNodeData[]) => {
|
||||||
|
items.forEach(item => {
|
||||||
|
if (item.type === 'folder') {
|
||||||
|
paths.push(item.path)
|
||||||
|
if (item.children) {
|
||||||
|
traverse(item.children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse(nodes)
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理节点点击事件
|
||||||
|
const handleNodeClick = (data: TreeNodeData) => {
|
||||||
|
if (data.type === 'file') {
|
||||||
|
// 只有文件才能被选中
|
||||||
|
vueFileStore.selectFile(data.path, data.label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义树节点图标
|
||||||
|
const getNodeIcon = (data: TreeNodeData) => {
|
||||||
|
if (data.type === 'folder') {
|
||||||
|
return '📁'
|
||||||
|
}
|
||||||
|
return '📄'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
scanViewsDirectory()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-manager">
|
||||||
|
<div class="manager-header">
|
||||||
|
<span class="title">{{ config.name }}</span>
|
||||||
|
<span class="count">{{ treeData.length }} 项</span>
|
||||||
|
</div>
|
||||||
|
<div class="manager-body">
|
||||||
|
<div class="current-file" v-if="vueFileStore.selectedFileName">
|
||||||
|
<div class="label">当前文件:</div>
|
||||||
|
<div class="file-name">{{ vueFileStore.selectedFileName }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tree-container">
|
||||||
|
<el-tree
|
||||||
|
:data="treeData"
|
||||||
|
:props="{ label: 'label', children: 'children' }"
|
||||||
|
:default-expanded-keys="defaultExpandedKeys"
|
||||||
|
node-key="path"
|
||||||
|
highlight-current
|
||||||
|
@node-click="handleNodeClick"
|
||||||
|
>
|
||||||
|
<template #default="{ node, data }">
|
||||||
|
<div class="custom-tree-node">
|
||||||
|
<span class="node-icon">{{ getNodeIcon(data) }}</span>
|
||||||
|
<span
|
||||||
|
class="node-label"
|
||||||
|
:class="{
|
||||||
|
'is-file': data.type === 'file',
|
||||||
|
'is-selected': data.path === vueFileStore.selectedFilePath
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ node.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
|
||||||
|
<div v-if="treeData.length === 0" class="empty-tip">
|
||||||
|
<div class="empty-icon">📂</div>
|
||||||
|
<div>暂无Vue文件</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-manager {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manager-header {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #2d2d2d;
|
||||||
|
border-bottom: 1px solid #3c3c3c;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
color: #888888;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manager-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-file {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #094771;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 3px solid #007acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: #888888;
|
||||||
|
font-size: 11px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-container {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element Plus Tree 样式覆盖 */
|
||||||
|
:deep(.el-tree) {
|
||||||
|
background: transparent;
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tree-node__content) {
|
||||||
|
background: transparent;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tree-node__content:hover) {
|
||||||
|
background: #2a2d2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tree-node.is-current > .el-tree-node__content) {
|
||||||
|
background: #094771;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tree-node__expand-icon) {
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tree-node {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-label {
|
||||||
|
flex: 1;
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-label.is-file {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-label.is-file:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-label.is-selected {
|
||||||
|
color: #4fc3f7;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-tip {
|
||||||
|
color: #666666;
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 40px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
110
draggable-panels/src/fauto/stores/vueFileStore.ts
Normal file
110
draggable-panels/src/fauto/stores/vueFileStore.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
// Vue文件树节点
|
||||||
|
export interface VueFileNode {
|
||||||
|
name: string // 文件/文件夹名称
|
||||||
|
path: string // 完整路径
|
||||||
|
type: 'file' | 'folder' // 类型
|
||||||
|
children?: VueFileNode[] // 子节点
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useVueFileStore = defineStore('vueFile', () => {
|
||||||
|
// 当前选中的Vue文件路径
|
||||||
|
const selectedFilePath = ref<string | null>(null)
|
||||||
|
|
||||||
|
// 当前选中的文件名
|
||||||
|
const selectedFileName = ref<string | null>(null)
|
||||||
|
|
||||||
|
// 文件树结构
|
||||||
|
const fileTree = ref<VueFileNode[]>([])
|
||||||
|
|
||||||
|
// 设置选中的文件
|
||||||
|
const selectFile = (path: string, name: string) => {
|
||||||
|
selectedFilePath.value = path
|
||||||
|
selectedFileName.value = name
|
||||||
|
console.log('选中文件:', { path, name })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除选中
|
||||||
|
const clearSelection = () => {
|
||||||
|
selectedFilePath.value = null
|
||||||
|
selectedFileName.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建文件树(从import.meta.glob结果)
|
||||||
|
const buildFileTree = (modules: Record<string, any>): VueFileNode[] => {
|
||||||
|
const tree: VueFileNode[] = []
|
||||||
|
const folderMap = new Map<string, VueFileNode>()
|
||||||
|
|
||||||
|
for (const path in modules) {
|
||||||
|
// 移除开头的 '../views/' 或 './views/'
|
||||||
|
const relativePath = path.replace(/^\.\.?\/views\//, '')
|
||||||
|
const parts = relativePath.split('/')
|
||||||
|
|
||||||
|
let currentLevel = tree
|
||||||
|
let currentPath = ''
|
||||||
|
|
||||||
|
// 处理路径的每一部分
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
const part = parts[i]
|
||||||
|
currentPath = currentPath ? `${currentPath}/${part}` : part
|
||||||
|
|
||||||
|
const isFile = i === parts.length - 1 && part.endsWith('.vue')
|
||||||
|
|
||||||
|
if (isFile) {
|
||||||
|
// 添加文件节点
|
||||||
|
currentLevel.push({
|
||||||
|
name: part,
|
||||||
|
path: path,
|
||||||
|
type: 'file'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 添加文件夹节点
|
||||||
|
let folder = folderMap.get(currentPath)
|
||||||
|
|
||||||
|
if (!folder) {
|
||||||
|
folder = {
|
||||||
|
name: part,
|
||||||
|
path: currentPath,
|
||||||
|
type: 'folder',
|
||||||
|
children: []
|
||||||
|
}
|
||||||
|
currentLevel.push(folder)
|
||||||
|
folderMap.set(currentPath, folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLevel = folder.children!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排序:文件夹在前,文件在后
|
||||||
|
const sortTree = (nodes: VueFileNode[]) => {
|
||||||
|
nodes.sort((a, b) => {
|
||||||
|
if (a.type !== b.type) {
|
||||||
|
return a.type === 'folder' ? -1 : 1
|
||||||
|
}
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
})
|
||||||
|
|
||||||
|
nodes.forEach(node => {
|
||||||
|
if (node.children) {
|
||||||
|
sortTree(node.children)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sortTree(tree)
|
||||||
|
return tree
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedFilePath,
|
||||||
|
selectedFileName,
|
||||||
|
fileTree,
|
||||||
|
selectFile,
|
||||||
|
clearSelection,
|
||||||
|
buildFileTree
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
import './style.css'
|
import './style.css'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
@@ -9,4 +11,5 @@ const pinia = createPinia()
|
|||||||
|
|
||||||
app.use(pinia)
|
app.use(pinia)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
app.use(ElementPlus)
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|||||||
52
draggable-panels/src/views/TestPage1.vue
Normal file
52
draggable-panels/src/views/TestPage1.vue
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<div class="test-page">
|
||||||
|
<h2>测试页面 1</h2>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="grid-content">左侧内容区域</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="grid-content">右侧内容区域</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20" style="margin-top: 20px;">
|
||||||
|
<el-col :span="8">
|
||||||
|
<div class="grid-content">列 1</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<div class="grid-content">列 2</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<div class="grid-content">列 3</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const message = ref('这是测试页面1')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.test-page {
|
||||||
|
padding: 20px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-content {
|
||||||
|
background: #409eff;
|
||||||
|
color: white;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
64
draggable-panels/src/views/TestPage2.vue
Normal file
64
draggable-panels/src/views/TestPage2.vue
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<template>
|
||||||
|
<div class="test-page">
|
||||||
|
<h2>测试页面 2 - 表单布局</h2>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="24">
|
||||||
|
<div class="grid-content">表单标题区域</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20" style="margin-top: 20px;">
|
||||||
|
<el-col :span="6">
|
||||||
|
<div class="grid-content">标签</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="18">
|
||||||
|
<div class="grid-content">输入框</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20" style="margin-top: 20px;">
|
||||||
|
<el-col :span="6">
|
||||||
|
<div class="grid-content">标签</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="18">
|
||||||
|
<div class="grid-content">输入框</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20" style="margin-top: 20px;">
|
||||||
|
<el-col :span="24">
|
||||||
|
<div class="grid-content">提交按钮</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const formData = ref({
|
||||||
|
name: '',
|
||||||
|
email: ''
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.test-page {
|
||||||
|
padding: 20px;
|
||||||
|
background: #fafafa;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-content {
|
||||||
|
background: #67c23a;
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
95
draggable-panels/src/views/dashboard/Overview.vue
Normal file
95
draggable-panels/src/views/dashboard/Overview.vue
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<div class="overview-page">
|
||||||
|
<h2>仪表板概览</h2>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="6">
|
||||||
|
<div class="grid-content stat-card">
|
||||||
|
<div class="stat-title">总用户</div>
|
||||||
|
<div class="stat-value">1,234</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<div class="grid-content stat-card">
|
||||||
|
<div class="stat-title">活跃用户</div>
|
||||||
|
<div class="stat-value">856</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<div class="grid-content stat-card">
|
||||||
|
<div class="stat-title">订单数</div>
|
||||||
|
<div class="stat-value">432</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<div class="grid-content stat-card">
|
||||||
|
<div class="stat-title">收入</div>
|
||||||
|
<div class="stat-value">¥12,345</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20" style="margin-top: 20px;">
|
||||||
|
<el-col :span="16">
|
||||||
|
<div class="grid-content chart-area">图表区域</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<div class="grid-content">侧边栏信息</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const stats = ref({
|
||||||
|
users: 1234,
|
||||||
|
active: 856,
|
||||||
|
orders: 432,
|
||||||
|
revenue: 12345
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.overview-page {
|
||||||
|
padding: 20px;
|
||||||
|
background: #f0f2f5;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-content {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-title {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
color: #333;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-area {
|
||||||
|
height: 300px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
93
draggable-panels/src/views/user/Profile.vue
Normal file
93
draggable-panels/src/views/user/Profile.vue
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<div class="profile-page">
|
||||||
|
<h2>用户资料</h2>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="8">
|
||||||
|
<div class="grid-content avatar-section">
|
||||||
|
<div class="avatar">头像</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="16">
|
||||||
|
<div class="grid-content info-section">
|
||||||
|
<el-row :gutter="10">
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="info-item">姓名: 张三</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="info-item">邮箱: zhang@example.com</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="10" style="margin-top: 10px;">
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="info-item">电话: 138****8888</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="info-item">部门: 技术部</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const userInfo = ref({
|
||||||
|
name: '张三',
|
||||||
|
email: 'zhang@example.com',
|
||||||
|
phone: '138****8888',
|
||||||
|
department: '技术部'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.profile-page {
|
||||||
|
padding: 20px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-content {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
background: #409eff;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
padding: 10px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user