3
This commit is contained in:
@@ -1,20 +1,22 @@
|
|||||||
# 拖拽面板设计器 - 项目上下文总结
|
# Vue页面可视化设计器 - 项目上下文
|
||||||
|
|
||||||
> **用途**:用于在新电脑快速恢复 AI 协作上下文
|
> **用途**:用于在新环境快速恢复 AI 协作上下文
|
||||||
> **生成时间**:2025-12-20
|
> **更新时间**:2025-12-22
|
||||||
> **项目路径**:`/Volumes/mypan/ui/draggable-panels`
|
> **项目路径**:`d:/workspace/fauto-design/draggable-panels`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📋 项目概述
|
## 📋 项目概述
|
||||||
|
|
||||||
这是一个基于 **Vue3 + TypeScript + Vite** 的**可拖拽面板设计器**项目,类似 IDE 界面(IDEA/VS Code)的多面板布局,支持:
|
这是一个基于 **Vue3 + TypeScript + Vite + Element Plus** 的**可视化页面设计器**,通过拖拽操作直接编辑真实的Vue页面文件。
|
||||||
|
|
||||||
- 🎯 三栏面板布局(左/中/右可调整宽度)
|
### 核心特性
|
||||||
- 🎨 Tab 可拖拽、跨面板移动
|
|
||||||
- 🧩 物料组件系统(自动扫描注册)
|
1. **直接解析Vue文件** - 动态扫描并渲染`src/views`下的页面
|
||||||
- 🎭 设计组件系统(可视化编辑、属性绑定)
|
2. **拖拽式设计** - 将设计组件拖拽到页面的el-row/el-col上
|
||||||
- 💾 状态持久化(通过模拟 API)
|
3. **非侵入式交互** - 通过注入方式绑定事件,不修改页面代码
|
||||||
|
4. **智能层级选择** - 键盘方向键切换嵌套元素层级
|
||||||
|
5. **实时视觉反馈** - 拖拽预览 + 拖放区域显示(上/下/左/右)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -23,304 +25,281 @@
|
|||||||
```
|
```
|
||||||
draggable-panels/
|
draggable-panels/
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── fauto/ # 🔥 设计器核心代码(所有功能都在这里)
|
│ ├── fauto/ # 🔥 设计器核心代码
|
||||||
│ │ ├── Designer.vue # 设计器主入口页面
|
│ │ ├── Designer.vue # 设计器主入口
|
||||||
│ │ ├── components/ # 设计器基础组件
|
│ │ │
|
||||||
│ │ │ ├── Header.vue # 顶部菜单栏(打开项目/窗口菜单)
|
│ │ ├── components/ # 基础UI组件
|
||||||
│ │ │ ├── Footer.vue # 底部状态栏
|
│ │ │ ├── Header.vue # 顶部菜单栏
|
||||||
|
│ │ │ ├── Footer.vue # 底部状态栏(显示拖拽状态)
|
||||||
│ │ │ ├── MainLayout.vue # 三栏布局容器
|
│ │ │ ├── MainLayout.vue # 三栏布局容器
|
||||||
│ │ │ ├── Panel.vue # 单个面板(含 Tab 列表 + 内容区)
|
│ │ │ ├── Panel.vue # 面板容器
|
||||||
│ │ │ └── Resizer.vue # 面板间拖拽分割线
|
│ │ │ └── Resizer.vue # 面板分隔器
|
||||||
│ │ │
|
│ │ │
|
||||||
│ │ ├── materials/ # 🎁 物料组件(8个)
|
│ │ ├── materials/ # 🎁 物料组件系统
|
||||||
│ │ │ ├── index.ts # ✅ 自动扫描所有 */index.vue 和 */index.json
|
│ │ │ ├── PageManagement/ # 页面管理(树形选择Vue文件)
|
||||||
│ │ │ ├── DesignComponentList/ # 可选设计组件列表
|
│ │ │ ├── DesignComponentList/ # 设计组件列表
|
||||||
│ │ │ ├── TreeViewer/ # 树形结构查看器(已添加实例列表)
|
│ │ │ └── DesignCenter/ # 设计中心(动态渲染页面)
|
||||||
│ │ │ ├── DesignCenter/ # 设计中心(组件预览面板)
|
│ │ │ ├── index.vue # 主组件
|
||||||
│ │ │ ├── DataTable/ # 属性表格编辑器
|
│ │ │ ├── InteractiveWrapper.vue # 交互包装器(注入事件)
|
||||||
│ │ │ ├── TextEditor/ # 文本编辑器(支持状态持久化)
|
│ │ │ ├── DropZone.vue # 拖放区域指示器
|
||||||
│ │ │ ├── TestWidget1/2/3/ # 测试组件
|
│ │ │ └── DragPreview.vue # 拖拽预览
|
||||||
│ │ │ └── [每个物料包含]
|
|
||||||
│ │ │ ├── index.vue # 组件实现
|
|
||||||
│ │ │ └── index.json # 组件元数据(name/description)
|
|
||||||
│ │ │
|
│ │ │
|
||||||
│ │ ├── designComponents/ # 🎨 设计组件(3个)
|
│ │ ├── designComponents/ # 🎨 设计组件库
|
||||||
│ │ │ ├── TextInput/ # 文本输入框
|
│ │ │ ├── TextInput/ # 文本输入框
|
||||||
│ │ │ ├── RadioSelect/ # 单选器
|
│ │ │ ├── RadioSelect/ # 单选器
|
||||||
│ │ │ ├── GridTable/ # 表格
|
│ │ │ └── GridTable/ # 表格
|
||||||
│ │ │ └── [每个设计组件包含]
|
|
||||||
│ │ │ ├── index.vue # Vue 组件定义
|
|
||||||
│ │ │ └── index.json # 元数据(name/description/props)
|
|
||||||
│ │ │
|
│ │ │
|
||||||
│ │ ├── stores/ # 🗄️ Pinia 状态管理
|
│ │ ├── plugins/ # 🔌 插件系统(核心功能)
|
||||||
│ │ │ ├── panelStore.ts # 面板布局 + 物料状态管理
|
│ │ │ ├── index.ts # 统一导出
|
||||||
│ │ │ └── designStore.ts # 设计组件实例管理(✅ 本地自动扫描)
|
│ │ │ ├── interactionStore.ts # 全局交互事件钩子
|
||||||
|
│ │ │ ├── dragStore.ts # 拖拽状态管理(层级选择)
|
||||||
|
│ │ │ └── pathUtils.ts # 结构化路径工具
|
||||||
│ │ │
|
│ │ │
|
||||||
│ │ └── types/ # 📝 TypeScript 类型定义
|
│ │ ├── stores/ # 🗄️ Pinia状态管理
|
||||||
│ │ └── index.ts # Panel/TabItem/MaterialInfo 等
|
│ │ │ ├── panelStore.ts # 面板布局状态
|
||||||
|
│ │ │ ├── designStore.ts # 设计组件元数据
|
||||||
|
│ │ │ └── vueFileStore.ts # Vue文件选择状态
|
||||||
|
│ │ │
|
||||||
|
│ │ └── types/ # 📝 类型定义
|
||||||
│ │
|
│ │
|
||||||
│ ├── views/ # 普通视图
|
│ ├── views/ # 📄 示例页面
|
||||||
│ │ └── HelloWorld.vue # 首页欢迎页
|
│ │ ├── TestPage1.vue # 测试页面1
|
||||||
|
│ │ ├── TestPage2.vue # 测试页面2
|
||||||
|
│ │ ├── user/Profile.vue # 用户资料页
|
||||||
|
│ │ └── dashboard/Overview.vue # 仪表板
|
||||||
│ │
|
│ │
|
||||||
│ ├── router.ts # 🛤️ 路由配置(/ 和 /draggable)
|
│ ├── router.ts # 路由配置
|
||||||
│ ├── main.ts # 应用入口
|
│ ├── main.ts # 应用入口
|
||||||
│ ├── App.vue # 根组件(仅路由容器)
|
│ ├── App.vue # 根组件
|
||||||
│ └── style.css # 全局样式
|
│ └── style.css # 全局样式
|
||||||
│
|
│
|
||||||
├── package.json # 依赖配置
|
└── vite.config.ts # Vite配置
|
||||||
└── vite.config.ts # Vite 配置
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔑 核心技术要点
|
## 🔑 核心技术要点
|
||||||
|
|
||||||
### 1. **路由设计**
|
### 1. **Vue页面规范** ⭐
|
||||||
|
|
||||||
```typescript
|
**强制规则**:template的第一层级**有且仅有一个el-row**
|
||||||
// src/router.ts
|
|
||||||
'/' → HelloWorld 欢迎页
|
**示例**:
|
||||||
'/draggable' → Designer 设计器主页面(fauto/Designer.vue)
|
```vue
|
||||||
|
<template>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="design-component">左侧内容</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-row :gutter="10">
|
||||||
|
<el-col :span="24">
|
||||||
|
<div class="design-component">标题</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. **物料组件自动注册** ⭐
|
**目的**:保证布局可解析性,方便自动生成结构化路径ID
|
||||||
|
|
||||||
**文件**:`src/fauto/materials/index.ts`
|
### 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
|
```typescript
|
||||||
// ✅ 使用 import.meta.glob 自动扫描
|
// 1. 动态渲染组件
|
||||||
const vueModules = import.meta.glob('./*/index.vue')
|
<component :is="selectedPageComponent" />
|
||||||
const jsonModules = import.meta.glob('./*/index.json', { eager: true })
|
|
||||||
|
|
||||||
// 自动构建组件映射表和信息列表
|
// 2. 挂载后扫描DOM
|
||||||
export const materialComponents: Record<string, Component> = {}
|
const injectInteractionEvents = () => {
|
||||||
export const materialList: MaterialInfo[] = []
|
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. **层级选择机制** ⭐
|
||||||
1. 在 `materials/` 下创建新目录(如 `MyWidget/`)
|
|
||||||
2. 添加 `index.vue`(组件实现)
|
|
||||||
3. 添加 `index.json`(元数据)
|
|
||||||
4. **无需修改任何注册代码**,自动生效!
|
|
||||||
|
|
||||||
### 3. **设计组件自动加载** ⭐
|
**核心思想**:当鼠标悬停在嵌套元素上时(如`r1c1r1`),会同时触发`r1c1r1`、`r1c1`、`r1`三个层级。
|
||||||
|
|
||||||
**最近修复**(2025-12-20):
|
**解决方案**(`dragStore.ts`):
|
||||||
|
|
||||||
**designStore.ts** - 从远程 API 改为本地扫描:
|
|
||||||
```typescript
|
```typescript
|
||||||
// ✅ 使用 eager: true 同步加载
|
// 1. 收集所有层级节点
|
||||||
const designComponentMetaModules = import.meta.glob('../designComponents/*/index.json', { eager: true })
|
const updateHierarchy = (element: HTMLElement) => {
|
||||||
|
const nodes = []
|
||||||
|
let current = element
|
||||||
|
|
||||||
// 自动构建元数据列表
|
while (current) {
|
||||||
const loadComponentMetas = async () => {
|
if (current.classList.contains('el-row') || current.classList.contains('el-col')) {
|
||||||
const metas: DesignComponentMeta[] = []
|
nodes.push({ path, type, element, depth })
|
||||||
for (const path in designComponentMetaModules) {
|
|
||||||
const id = match[1] // 如 'TextInput'
|
|
||||||
metas.push({ id, name, description, props })
|
|
||||||
}
|
}
|
||||||
componentMetas.value = metas
|
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;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**DesignCenter.vue** - 组件预览渲染:
|
#### 拖拽预览效果
|
||||||
```typescript
|
- 跟随鼠标的半透明卡片
|
||||||
// ✅ 使用 eager: true 确保同步加载
|
- 显示组件图标 + 名称
|
||||||
const designComponentModules = import.meta.glob('../../designComponents/*/index.vue', { eager: true })
|
- Teleport到body避免z-index问题
|
||||||
|
|
||||||
// 构建组件映射(用于 <component :is="...">)
|
|
||||||
const designComponentMap: Record<string, any> = {}
|
|
||||||
for (const path in designComponentModules) {
|
|
||||||
const mod = designComponentModules[path] as any
|
|
||||||
designComponentMap[id] = markRaw(mod.default || mod)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. **状态同步机制** ⭐
|
|
||||||
|
|
||||||
**TreeViewer.vue** - 实例列表实时同步(最近修复):
|
|
||||||
```typescript
|
|
||||||
// ✅ 改用 watch 替代 $subscribe(更稳定)
|
|
||||||
watch(
|
|
||||||
() => designStore.components,
|
|
||||||
(newVal) => {
|
|
||||||
localComponents.value = [...newVal]
|
|
||||||
},
|
|
||||||
{ deep: true, immediate: true }
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. **状态持久化**
|
|
||||||
|
|
||||||
- **panelStore**:管理面板布局 + 物料组件状态
|
|
||||||
- `loadConfig()` / `saveConfig()` → `/api/config`
|
|
||||||
- `getMaterialState()` / `updateMaterialState()` → `/api/material-states`
|
|
||||||
|
|
||||||
- **designStore**:管理设计组件实例
|
|
||||||
- `loadState()` / `saveState()` → `/api/design-state`
|
|
||||||
|
|
||||||
**注意**:目前使用 `fetch('/api/...')` 模拟持久化,实际会失败但不影响运行。如需真实持久化,可改用 localStorage 或搭建后端。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 数据流图
|
## 🎯 数据流图
|
||||||
|
|
||||||
```
|
```
|
||||||
用户操作
|
1. 页面管理
|
||||||
|
用户点击Vue文件
|
||||||
↓
|
↓
|
||||||
┌─────────────────────────────────────────────────┐
|
vueFileStore.selectFile(path)
|
||||||
│ Header.vue - 点击"窗口"菜单 │
|
|
||||||
│ → panelStore.openMaterial('TreeViewer') │
|
|
||||||
└─────────────────────────────────────────────────┘
|
|
||||||
↓
|
↓
|
||||||
┌─────────────────────────────────────────────────┐
|
DesignCenter动态渲染该页面
|
||||||
│ Panel.vue - 渲染 Tab + 加载物料组件 │
|
|
||||||
│ → <component :is="getMaterialComponent(id)"> │
|
|
||||||
└─────────────────────────────────────────────────┘
|
|
||||||
↓
|
↓
|
||||||
┌─────────────────────────────────────────────────┐
|
InteractiveWrapper注入交互事件
|
||||||
│ DesignComponentList - 点击"文本输入框" │
|
|
||||||
│ → designStore.addComponent('TextInput') │
|
2. 拖拽交互
|
||||||
└─────────────────────────────────────────────────┘
|
用户拖拽设计组件
|
||||||
↓
|
↓
|
||||||
┌─────────────────────────────────────────────────┐
|
dragStore.startDragFromComponentList(id, name)
|
||||||
│ TreeViewer - watch 监听 components 变化 │
|
|
||||||
│ → 自动刷新实例列表 │
|
|
||||||
└─────────────────────────────────────────────────┘
|
|
||||||
↓
|
↓
|
||||||
┌─────────────────────────────────────────────────┐
|
鼠标移动到页面元素上
|
||||||
│ DesignCenter - 渲染组件预览 │
|
|
||||||
│ → <component :is="getComponent(componentId)"> │
|
|
||||||
└─────────────────────────────────────────────────┘
|
|
||||||
↓
|
↓
|
||||||
┌─────────────────────────────────────────────────┐
|
dragStore.updateHierarchy(element) // 识别层级
|
||||||
│ DataTable - 编辑属性 │
|
↓
|
||||||
│ → designStore.updateComponentProps(id, key, val)│
|
用户按↑↓键切换层级
|
||||||
└─────────────────────────────────────────────────┘
|
↓
|
||||||
|
DropZone显示可放置区域(上/下/左/右)
|
||||||
|
↓
|
||||||
|
用户点击区域确认
|
||||||
|
↓
|
||||||
|
dragStore.confirmDrop()
|
||||||
|
↓
|
||||||
|
Footer显示拖放记录:TextInput → r1c1 左侧
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🐛 已解决的关键问题
|
## 🛠️ 快速开始
|
||||||
|
|
||||||
### 问题 1:设计组件列表不显示
|
### 访问设计器
|
||||||
**时间**:2025-12-20
|
```
|
||||||
**原因**:`designStore.loadComponentMetas()` 使用 `fetch('/api/design-components')`,无后端导致 `componentMetas` 为空
|
http://localhost:5173/draggable
|
||||||
**解决**:改用 `import.meta.glob` 自动扫描本地 `designComponents/*/index.json`
|
|
||||||
|
|
||||||
### 问题 2:TreeViewer 列表不更新
|
|
||||||
**原因**:使用 Pinia 的 `$subscribe` 监听 mutation,逻辑复杂且不稳定
|
|
||||||
**解决**:改用 Vue 的 `watch(() => designStore.components)` 直接监听
|
|
||||||
|
|
||||||
### 问题 3:DesignCenter 组件不渲染
|
|
||||||
**原因**:`import.meta.glob` 未设置 `eager: true`,导致组件映射表构建失败
|
|
||||||
**解决**:添加 `{ eager: true }` 并使用 `mod.default || mod` 获取组件定义
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 依赖清单
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"dependencies": {
|
|
||||||
"vue": "^3.5.24", // Vue 3 框架
|
|
||||||
"vue-router": "^4.6.4", // 路由管理
|
|
||||||
"pinia": "^3.0.4", // 状态管理
|
|
||||||
"vuedraggable": "^4.1.0" // 拖拽功能
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"vite": "^7.2.4", // 构建工具
|
|
||||||
"typescript": "~5.9.3", // TypeScript
|
|
||||||
"@vitejs/plugin-vue": "^6.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 使用流程
|
||||||
|
1. 点击左侧"页面管理"物料
|
||||||
|
2. 在树形列表中选择一个Vue页面
|
||||||
|
3. 页面在"设计中心"中渲染
|
||||||
|
4. 从"设计组件列表"拖拽组件
|
||||||
|
5. 移动到页面的el-row或el-col上
|
||||||
|
6. 使用↑↓键切换层级
|
||||||
|
7. 点击放置方向(上/下/左/右)
|
||||||
|
8. 查看Footer中的拖放记录
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 启动命令
|
## 📝 开发规范
|
||||||
|
|
||||||
```bash
|
### 创建符合规范的Vue页面
|
||||||
# 1. 安装依赖
|
```vue
|
||||||
npm install
|
<template>
|
||||||
|
<!-- 第一层级必须且只能有一个el-row -->
|
||||||
# 2. 启动开发服务器
|
<el-row :gutter="20">
|
||||||
npm run dev
|
<el-col :span="24">
|
||||||
# 访问 http://localhost:5173/draggable
|
<!-- 内容 -->
|
||||||
|
</el-col>
|
||||||
# 3. 构建生产版本
|
</el-row>
|
||||||
npm run build
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
### 添加设计组件
|
||||||
|
1. 在`src/fauto/designComponents/`下创建目录
|
||||||
|
2. 添加`index.vue`和`index.json`
|
||||||
|
3. 自动在列表中显示
|
||||||
|
|
||||||
## 📝 后续可优化方向
|
### 插件开发
|
||||||
|
所有与页面解析、交互相关的代码放在`src/fauto/plugins/`
|
||||||
### 🔧 功能增强
|
|
||||||
1. **真实持久化**
|
|
||||||
- 选项 A:改用 `localStorage` 替代模拟 API
|
|
||||||
- 选项 B:搭建 Node.js 后端(Express + fs.writeFile)
|
|
||||||
|
|
||||||
2. **拖拽增强**
|
|
||||||
- 支持面板间 Tab 跨级拖动
|
|
||||||
- 树形组件支持父子节点拖拽
|
|
||||||
|
|
||||||
3. **设计器功能**
|
|
||||||
- 实现拖拽生成 Vue 文件(模板引擎)
|
|
||||||
- 接入 Element Plus 组件库作为设计组件源
|
|
||||||
|
|
||||||
4. **开发体验**
|
|
||||||
- 添加 ESLint + Prettier 代码规范
|
|
||||||
- 添加单元测试(Vitest)
|
|
||||||
|
|
||||||
### 🎨 界面优化
|
|
||||||
- 添加主题切换(暗色/亮色)
|
|
||||||
- 面板大小记忆功能
|
|
||||||
- 快捷键支持(Ctrl+S 保存等)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 💡 给 AI 的关键提示
|
## 🚀 下一步计划
|
||||||
|
|
||||||
### 如果遇到"自动扫描不生效"
|
1. **拖放后修改源文件** - 通过API将操作写入.vue文件
|
||||||
1. 检查是否添加了 `{ eager: true }` 选项
|
2. **属性编辑器** - 展示和编辑组件属性
|
||||||
2. 确认路径匹配正则是否正确(`/designComponents\/(.+)\/index\.vue$/`)
|
3. **热更新同步** - 修改后自动刷新页面
|
||||||
3. 验证文件夹下是否有 `index.vue` 和 `index.json`
|
4. **撤销/重做** - 操作历史记录
|
||||||
|
|
||||||
### 如果组件不渲染
|
|
||||||
1. 检查 `import.meta.glob` 是否使用 `eager: true`
|
|
||||||
2. 确认使用 `mod.default || mod` 获取组件
|
|
||||||
3. 对于动态组件,确保使用 `markRaw()` 包装
|
|
||||||
|
|
||||||
### 如果状态不同步
|
|
||||||
1. 优先使用 Vue 的 `watch` 而非 Pinia 的 `$subscribe`
|
|
||||||
2. 设置 `{ deep: true, immediate: true }` 确保深度监听
|
|
||||||
3. 使用 `[...array]` 创建新数组触发响应式更新
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📞 联系方式
|
## 📚 相关文档
|
||||||
|
|
||||||
**项目负责人**:[你的信息]
|
- **详细设计文档**:`项目设计文档.md`
|
||||||
**Git 仓库**:[如果有的话]
|
- **技术选型**:Vue3 + TypeScript + Element Plus + Pinia
|
||||||
**文档更新**:2025-12-20
|
- **构建工具**:Vite 7.3.0
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✅ 快速检查清单(给新 AI)
|
## 💡 核心创新点
|
||||||
|
|
||||||
当你接手这个项目时,请确认以下内容:
|
1. **非侵入式设计** - 无需修改Vue页面代码即可实现拖拽设计
|
||||||
|
2. **智能层级选择** - 自动识别嵌套结构,精准定位目标位置
|
||||||
- [ ] 项目已正常启动(`npm run dev`)
|
3. **实时视觉反馈** - 完善的拖拽交互体验
|
||||||
- [ ] 访问 `/draggable` 能看到设计器界面
|
4. **结构化路径** - 人类可读的元素定位方式
|
||||||
- [ ] 点击"窗口"菜单能打开物料组件
|
|
||||||
- [ ] "设计组件列表"显示 3 个组件(TextInput/RadioSelect/GridTable)
|
|
||||||
- [ ] 点击设计组件后,TreeViewer 能显示新实例
|
|
||||||
- [ ] DesignCenter 能正确渲染组件预览
|
|
||||||
- [ ] DataTable 能编辑组件属性
|
|
||||||
|
|
||||||
如果以上任一项失败,请参考"已解决的关键问题"章节。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**🎉 恭喜!你已经完全掌握了这个项目的上下文,可以继续开发了!**
|
**最后更新**:2025-12-22
|
||||||
|
**AI协作建议**:优先阅读本文档的"核心技术要点"部分,理解项目的设计理念和实现方式
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"materialId": "DesignComponentList"
|
"materialId": "DesignComponentList"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"activeTabId": "up60643"
|
"activeTabId": "6hfm9ux"
|
||||||
},
|
},
|
||||||
"centerPanel": {
|
"centerPanel": {
|
||||||
"id": "center",
|
"id": "center",
|
||||||
@@ -141,5 +141,5 @@
|
|||||||
"activeTabId": "mxfx11j"
|
"activeTabId": "mxfx11j"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lastUpdated": "2025-12-22T14:24:28.202Z"
|
"lastUpdated": "2025-12-22T15:06:06.209Z"
|
||||||
}
|
}
|
||||||
@@ -67,5 +67,5 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"selectedId": "jy87mdv",
|
"selectedId": "jy87mdv",
|
||||||
"lastUpdated": "2025-12-22T14:24:28.783Z"
|
"lastUpdated": "2025-12-22T14:42:41.050Z"
|
||||||
}
|
}
|
||||||
@@ -60,18 +60,12 @@ const dragInfo = computed(() => {
|
|||||||
if (source.type === 'design-component') {
|
if (source.type === 'design-component') {
|
||||||
return `拖拽: ${source.componentName}`
|
return `拖拽: ${source.componentName}`
|
||||||
}
|
}
|
||||||
return `拖抽: ${source.path}`
|
return `拖拽: ${source.path}`
|
||||||
})
|
})
|
||||||
|
|
||||||
// 当前拖放目标信息
|
// 删除不再使用的代码
|
||||||
const dropTargetInfo = computed(() => {
|
// const dropTargetInfo = ...
|
||||||
if (!dragStore.isDragging) return null
|
// const hierarchyInfo = ...
|
||||||
if (!dragStore.selectedNode) return null
|
|
||||||
|
|
||||||
const node = dragStore.selectedNode
|
|
||||||
const typeText = node.type === 'er' ? 'Row' : 'Col'
|
|
||||||
return `目标: ${typeText} [${node.path}]`
|
|
||||||
})
|
|
||||||
|
|
||||||
// 最后一次拖放记录
|
// 最后一次拖放记录
|
||||||
const lastDropInfo = computed(() => {
|
const lastDropInfo = computed(() => {
|
||||||
@@ -98,11 +92,8 @@ onUnmounted(() => {
|
|||||||
<span class="info-item drag-info">
|
<span class="info-item drag-info">
|
||||||
🎯 {{ dragInfo }}
|
🎯 {{ dragInfo }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="dropTargetInfo" class="info-item target-info">
|
<span v-if="hoverInfo" class="info-item hover-info">
|
||||||
➡️ {{ dropTargetInfo }}
|
👁️ {{ hoverInfo }}
|
||||||
</span>
|
|
||||||
<span v-if="dragStore.hoverDirection" class="info-item direction-info">
|
|
||||||
📍 {{ dragStore.hoverDirection === 'top' ? '上方' : dragStore.hoverDirection === 'bottom' ? '下方' : dragStore.hoverDirection === 'left' ? '左侧' : '右侧' }}
|
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,15 @@ const bindElementEvents = (element: HTMLElement, type: ElementType, path: string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 鼠标移动(拖拽时持续更新层级)
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
// 只在拖拽状态下处理
|
||||||
|
if (dragStore.isDragging) {
|
||||||
|
e.stopPropagation()
|
||||||
|
dragStore.updateHierarchy(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 鼠标离开
|
// 鼠标离开
|
||||||
const handleMouseLeave = (e: MouseEvent) => {
|
const handleMouseLeave = (e: MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@@ -98,6 +107,7 @@ const bindElementEvents = (element: HTMLElement, type: ElementType, path: string
|
|||||||
|
|
||||||
// 绑定事件
|
// 绑定事件
|
||||||
element.addEventListener('mouseenter', handleMouseEnter)
|
element.addEventListener('mouseenter', handleMouseEnter)
|
||||||
|
element.addEventListener('mousemove', handleMouseMove) // 新增:拖拽时持续更新
|
||||||
element.addEventListener('mouseleave', handleMouseLeave)
|
element.addEventListener('mouseleave', handleMouseLeave)
|
||||||
element.addEventListener('click', handleClick)
|
element.addEventListener('click', handleClick)
|
||||||
element.addEventListener('mousedown', handleMouseDown)
|
element.addEventListener('mousedown', handleMouseDown)
|
||||||
@@ -107,6 +117,7 @@ const bindElementEvents = (element: HTMLElement, type: ElementType, path: string
|
|||||||
// 返回清理函数
|
// 返回清理函数
|
||||||
return () => {
|
return () => {
|
||||||
element.removeEventListener('mouseenter', handleMouseEnter)
|
element.removeEventListener('mouseenter', handleMouseEnter)
|
||||||
|
element.removeEventListener('mousemove', handleMouseMove)
|
||||||
element.removeEventListener('mouseleave', handleMouseLeave)
|
element.removeEventListener('mouseleave', handleMouseLeave)
|
||||||
element.removeEventListener('click', handleClick)
|
element.removeEventListener('click', handleClick)
|
||||||
element.removeEventListener('mousedown', handleMouseDown)
|
element.removeEventListener('mousedown', handleMouseDown)
|
||||||
|
|||||||
@@ -1,42 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineAsyncComponent, markRaw, computed, watch } from 'vue'
|
import { defineAsyncComponent, computed, watch } from 'vue'
|
||||||
import { useDesignStore } from '../../stores/designStore'
|
|
||||||
import { useVueFileStore } from '../../stores/vueFileStore'
|
import { useVueFileStore } from '../../stores/vueFileStore'
|
||||||
import InteractiveWrapper from './InteractiveWrapper.vue'
|
import InteractiveWrapper from './InteractiveWrapper.vue'
|
||||||
import config from './index.json'
|
import config from './index.json'
|
||||||
|
|
||||||
const designStore = useDesignStore()
|
|
||||||
const vueFileStore = useVueFileStore()
|
const vueFileStore = useVueFileStore()
|
||||||
|
|
||||||
// 自动扫描所有设计组件(eager 模式确保同步加载)
|
|
||||||
const designComponentModules = import.meta.glob('../../designComponents/*/index.vue', { eager: true })
|
|
||||||
|
|
||||||
// 自动构建设计组件映射表
|
|
||||||
const designComponentMap: Record<string, any> = {}
|
|
||||||
|
|
||||||
for (const path in designComponentModules) {
|
|
||||||
// 从路径中提取组件 ID,例如 '../../designComponents/TextInput/index.vue' => 'TextInput'
|
|
||||||
const match = path.match(/\/designComponents\/(.+)\/index\.vue$/)
|
|
||||||
if (match) {
|
|
||||||
const id = match[1]
|
|
||||||
const mod = designComponentModules[path] as any
|
|
||||||
designComponentMap[id] = markRaw(mod.default || mod)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getComponent = (componentId: string) => {
|
|
||||||
return designComponentMap[componentId]
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSelect = (instanceId: string) => {
|
|
||||||
designStore.selectComponent(instanceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRemove = (instanceId: string, event: Event) => {
|
|
||||||
event.stopPropagation()
|
|
||||||
designStore.removeComponent(instanceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 扫描所有views目录下的Vue文件
|
// 扫描所有views目录下的Vue文件
|
||||||
const viewModules = import.meta.glob('../../../views/**/*.vue')
|
const viewModules = import.meta.glob('../../../views/**/*.vue')
|
||||||
|
|
||||||
@@ -65,10 +34,7 @@ watch(() => vueFileStore.selectedFilePath, (newPath) => {
|
|||||||
<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" v-if="!vueFileStore.selectedFilePath">
|
<span class="file-info" v-if="vueFileStore.selectedFilePath">
|
||||||
{{ designStore.components.length }} 个实例
|
|
||||||
</span>
|
|
||||||
<span class="file-info" v-else>
|
|
||||||
📄 {{ vueFileStore.selectedFileName }}
|
📄 {{ vueFileStore.selectedFileName }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,35 +44,12 @@ watch(() => vueFileStore.selectedFilePath, (newPath) => {
|
|||||||
<InteractiveWrapper :component="selectedPageComponent" />
|
<InteractiveWrapper :component="selectedPageComponent" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 原有的设计组件实例列表 -->
|
|
||||||
<div v-else-if="designStore.components.length > 0" class="component-list">
|
|
||||||
<div
|
|
||||||
v-for="instance in designStore.components"
|
|
||||||
:key="instance.id"
|
|
||||||
class="component-row"
|
|
||||||
:class="{ selected: designStore.selectedId === instance.id }"
|
|
||||||
@click="handleSelect(instance.id)"
|
|
||||||
>
|
|
||||||
<div class="component-label">
|
|
||||||
<span class="component-name">{{ instance.name }}</span>
|
|
||||||
<button class="remove-btn" @click="handleRemove(instance.id, $event)">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="component-preview">
|
|
||||||
<component
|
|
||||||
v-if="getComponent(instance.componentId)"
|
|
||||||
:is="getComponent(instance.componentId)"
|
|
||||||
v-bind="instance.props"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div v-else 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 class="empty-hint">
|
||||||
点击“页面管理”选择Vue页面,或从左侧列表添加设计组件
|
点击“页面管理”选择Vue页面开始设计
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -136,11 +79,6 @@ watch(() => vueFileStore.selectedFilePath, (newPath) => {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.count {
|
|
||||||
color: #888888;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-info {
|
.file-info {
|
||||||
color: #4fc3f7;
|
color: #4fc3f7;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -161,67 +99,6 @@ watch(() => vueFileStore.selectedFilePath, (newPath) => {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.component-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.component-row {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
transition: border-color 0.2s;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.component-row:hover {
|
|
||||||
border-color: #3c3c3c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.component-row.selected {
|
|
||||||
border-color: #007acc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.component-label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 6px 10px;
|
|
||||||
background: #252526;
|
|
||||||
border-radius: 4px 4px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.component-name {
|
|
||||||
color: #e0e0e0;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-btn {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: #888888;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 4px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-btn:hover {
|
|
||||||
background: #ff4444;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.component-preview {
|
|
||||||
padding: 8px;
|
|
||||||
background: #1e1e1e;
|
|
||||||
border-radius: 0 0 4px 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-tip {
|
.empty-tip {
|
||||||
color: #666666;
|
color: #666666;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, watch, computed } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
// 设计组件实例
|
|
||||||
export interface DesignComponentInstance {
|
|
||||||
id: string // 实例唯一ID
|
|
||||||
componentId: string // 设计组件类型ID
|
|
||||||
name: string // 显示名称
|
|
||||||
props: Record<string, any> // 属性值
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设计组件定义
|
// 设计组件定义
|
||||||
export interface DesignComponentMeta {
|
export interface DesignComponentMeta {
|
||||||
@@ -17,37 +9,13 @@ export interface DesignComponentMeta {
|
|||||||
props: Record<string, any>
|
props: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成唯一ID
|
|
||||||
const generateId = () => Math.random().toString(36).substring(2, 9)
|
|
||||||
|
|
||||||
export const useDesignStore = defineStore('design', () => {
|
export const useDesignStore = defineStore('design', () => {
|
||||||
// 设计中心已添加的组件实例列表
|
|
||||||
const components = ref<DesignComponentInstance[]>([])
|
|
||||||
|
|
||||||
// 当前选中的组件实例ID
|
|
||||||
const selectedId = ref<string | null>(null)
|
|
||||||
|
|
||||||
// 设计组件元数据缓存
|
// 设计组件元数据缓存
|
||||||
const componentMetas = ref<DesignComponentMeta[]>([])
|
const componentMetas = ref<DesignComponentMeta[]>([])
|
||||||
|
|
||||||
// 自动扫描所有设计组件的 .json 配置文件
|
// 自动扫描所有设计组件的 .json 配置文件
|
||||||
const designComponentMetaModules = import.meta.glob('../designComponents/*/index.json', { eager: true })
|
const designComponentMetaModules = import.meta.glob('../designComponents/*/index.json', { eager: true })
|
||||||
|
|
||||||
// 是否已加载
|
|
||||||
const isLoaded = ref(false)
|
|
||||||
|
|
||||||
// 当前选中的组件实例
|
|
||||||
const selectedComponent = computed(() => {
|
|
||||||
if (!selectedId.value) return null
|
|
||||||
return components.value.find(c => c.id === selectedId.value) || null
|
|
||||||
})
|
|
||||||
|
|
||||||
// 当前选中组件的元数据(属性定义)
|
|
||||||
const selectedComponentMeta = computed(() => {
|
|
||||||
if (!selectedComponent.value) return null
|
|
||||||
return componentMetas.value.find(m => m.id === selectedComponent.value!.componentId) || null
|
|
||||||
})
|
|
||||||
|
|
||||||
// 加载设计组件元数据(从本地文件自动扫描)
|
// 加载设计组件元数据(从本地文件自动扫描)
|
||||||
const loadComponentMetas = async () => {
|
const loadComponentMetas = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -71,132 +39,20 @@ export const useDesignStore = defineStore('design', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentMetas.value = metas
|
componentMetas.value = metas
|
||||||
|
console.log('加载设计组件元数据:', metas.length, '个组件')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载设计组件元数据失败:', error)
|
console.error('加载设计组件元数据失败:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载设计中心状态
|
|
||||||
const loadState = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/design-state')
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json()
|
|
||||||
if (data.components) {
|
|
||||||
components.value = data.components
|
|
||||||
}
|
|
||||||
if (data.selectedId) {
|
|
||||||
selectedId.value = data.selectedId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('使用默认设计状态')
|
|
||||||
}
|
|
||||||
isLoaded.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存设计中心状态
|
|
||||||
const saveState = async () => {
|
|
||||||
try {
|
|
||||||
await fetch('/api/design-state', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
components: components.value,
|
|
||||||
selectedId: selectedId.value,
|
|
||||||
lastUpdated: new Date().toISOString()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('保存设计状态失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听变化自动保存
|
|
||||||
watch([components, selectedId], () => {
|
|
||||||
if (isLoaded.value) {
|
|
||||||
saveState()
|
|
||||||
}
|
|
||||||
}, { deep: true })
|
|
||||||
|
|
||||||
// 添加设计组件到设计中心
|
|
||||||
const addComponent = (componentId: string) => {
|
|
||||||
const meta = componentMetas.value.find(m => m.id === componentId)
|
|
||||||
if (!meta) {
|
|
||||||
console.warn('未找到设计组件:', componentId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算同类型组件的数量
|
|
||||||
const sameTypeCount = components.value.filter(c => c.componentId === componentId).length
|
|
||||||
|
|
||||||
const instance: DesignComponentInstance = {
|
|
||||||
id: generateId(),
|
|
||||||
componentId: componentId,
|
|
||||||
name: `${meta.name} ${sameTypeCount + 1}`,
|
|
||||||
props: JSON.parse(JSON.stringify(meta.props))
|
|
||||||
}
|
|
||||||
|
|
||||||
components.value.push(instance)
|
|
||||||
selectedId.value = instance.id
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除设计组件
|
|
||||||
const removeComponent = (instanceId: string) => {
|
|
||||||
const index = components.value.findIndex(c => c.id === instanceId)
|
|
||||||
if (index > -1) {
|
|
||||||
components.value.splice(index, 1)
|
|
||||||
if (selectedId.value === instanceId) {
|
|
||||||
selectedId.value = components.value.length > 0 ? components.value[0].id : null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 选中设计组件
|
|
||||||
const selectComponent = (instanceId: string | null) => {
|
|
||||||
selectedId.value = instanceId
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新组件属性
|
|
||||||
const updateComponentProps = (instanceId: string, key: string, value: any) => {
|
|
||||||
const component = components.value.find(c => c.id === instanceId)
|
|
||||||
if (component) {
|
|
||||||
component.props[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重新排序组件(拖拽后)
|
|
||||||
const reorderComponents = (newOrder: DesignComponentInstance[]) => {
|
|
||||||
components.value = newOrder
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取组件元数据
|
// 获取组件元数据
|
||||||
const getComponentMeta = (componentId: string) => {
|
const getComponentMeta = (componentId: string) => {
|
||||||
return componentMetas.value.find(m => m.id === componentId)
|
return componentMetas.value.find(m => m.id === componentId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化
|
|
||||||
const init = async () => {
|
|
||||||
await loadComponentMetas()
|
|
||||||
await loadState()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
components,
|
|
||||||
selectedId,
|
|
||||||
selectedComponent,
|
|
||||||
selectedComponentMeta,
|
|
||||||
componentMetas,
|
componentMetas,
|
||||||
isLoaded,
|
|
||||||
init,
|
|
||||||
loadComponentMetas,
|
loadComponentMetas,
|
||||||
loadState,
|
|
||||||
saveState,
|
|
||||||
addComponent,
|
|
||||||
removeComponent,
|
|
||||||
selectComponent,
|
|
||||||
updateComponentProps,
|
|
||||||
reorderComponents,
|
|
||||||
getComponentMeta
|
getComponentMeta
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
# 可拖拽子窗口项目设计文档
|
# Vue页面可视化设计器项目文档
|
||||||
|
|
||||||
## 项目概述
|
## 项目概述
|
||||||
|
|
||||||
本项目是一个基于 Vite + Vue3 + TypeScript 的可拖拽子窗口系统,模仿 IDE(如 IntelliJ IDEA、Visual Studio)的界面设计,提供多区域可拖拽的窗口管理功能。
|
本项目是一个基于 Vite + Vue3 + TypeScript 的**可视化页面设计器**,通过拖拽操作直接编辑真实的Vue页面文件,实现低代码页面快速构建。
|
||||||
|
|
||||||
|
## 核心特性
|
||||||
|
|
||||||
|
1. **直接解析Vue文件** - 动态渲染真实的.vue页面
|
||||||
|
2. **拖拽式设计** - 将设计组件拖拽到页面的el-row/el-col上
|
||||||
|
3. **层级选择** - 通过键盘方向键切换嵌套元素的选中层级
|
||||||
|
4. **实时预览** - 拖拽时显示放置方向(上/下/左/右)
|
||||||
|
5. **结构化ID** - 自动生成路径ID(如r1c2r1c1)
|
||||||
|
|
||||||
## 技术架构
|
## 技术架构
|
||||||
|
|
||||||
@@ -17,127 +25,196 @@
|
|||||||
### 项目结构
|
### 项目结构
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── assets/ # 静态资源
|
├── fauto/ # 核心功能模块
|
||||||
├── components/ # 核心UI组件
|
│ ├── components/ # UI组件
|
||||||
│ ├── Header.vue # 顶部菜单栏
|
│ │ ├── Header.vue # 顶部菜单栏
|
||||||
│ ├── Footer.vue # 底部状态栏
|
│ │ ├── Footer.vue # 底部状态栏(显示拖拽状态)
|
||||||
│ ├── MainLayout.vue # 主布局容器
|
│ │ ├── MainLayout.vue # 主布局容器
|
||||||
│ ├── Panel.vue # 面板容器
|
│ │ ├── Panel.vue # 面板容器
|
||||||
│ └── Resizer.vue # 面板分隔器
|
│ │ └── Resizer.vue # 面板分隔器
|
||||||
├── materials/ # 物料组件系统
|
│ ├── materials/ # 物料组件系统
|
||||||
│ ├── index.ts # 物料组件注册中心
|
│ │ ├── PageManagement/ # 页面管理(树形选择Vue文件)
|
||||||
│ ├── TextEditor/ # 文本编辑器
|
│ │ ├── DesignComponentList/ # 设计组件列表
|
||||||
│ ├── TreeViewer/ # 树形展示器
|
│ │ └── DesignCenter/ # 设计中心(动态渲染页面)
|
||||||
│ ├── DataTable/ # 数据表格
|
│ ├── designComponents/ # 设计组件库
|
||||||
│ ├── TestWidget*/ # 测试组件
|
│ │ ├── TextInput/ # 文本输入框
|
||||||
│ ├── DesignComponentList/ # 设计组件列表
|
│ │ ├── RadioSelect/ # 单选器
|
||||||
│ └── DesignCenter/ # 设计中心
|
│ │ └── GridTable/ # 表格组件
|
||||||
├── designComponents/ # 设计组件库
|
│ ├── plugins/ # 插件系统(交互事件、拖拽管理)
|
||||||
│ ├── TextInput/ # 文本输入框
|
│ │ ├── interactionStore.ts # 全局交互钩子
|
||||||
│ ├── RadioSelect/ # 单选器
|
│ │ ├── dragStore.ts # 拖拽状态管理
|
||||||
│ └── GridTable/ # 表格组件
|
│ │ └── pathUtils.ts # 结构化路径工具
|
||||||
├── stores/ # 状态管理
|
│ ├── stores/ # 状态管理
|
||||||
│ ├── panelStore.ts # 面板布局状态
|
│ │ ├── panelStore.ts # 面板布局状态
|
||||||
│ └── designStore.ts # 设计中心状态
|
│ │ ├── designStore.ts # 设计组件元数据
|
||||||
├── types/ # TypeScript 类型定义
|
│ │ └── vueFileStore.ts # Vue文件选择状态
|
||||||
|
│ └── Designer.vue # 设计器主入口
|
||||||
|
├── views/ # 示例页面(测试用)
|
||||||
└── App.vue # 应用入口
|
└── App.vue # 应用入口
|
||||||
```
|
```
|
||||||
|
|
||||||
## 核心功能模块
|
## 核心功能模块
|
||||||
|
|
||||||
### 1. 面板系统 (Panel System)
|
### 1. 页面管理系统
|
||||||
|
|
||||||
#### 布局结构
|
#### 功能说明
|
||||||
- **Header**: 固定顶部,包含菜单和新增窗口按钮
|
- 动态扫描`src/views`目录下的所有Vue文件
|
||||||
- **MainLayout**: 三区域布局(左/中/右)
|
- 以树形结构展示文件夹和页面
|
||||||
- **Panel**: 可拖拽的面板容器
|
- 点击选中后在设计中心动态渲染
|
||||||
- **Footer**: 固定底部,显示时间和状态信息
|
|
||||||
|
|
||||||
#### 拖拽实现
|
#### 技术实现
|
||||||
使用 `vuedraggable` 库实现跨面板的 Tab 拖拽:
|
```typescript
|
||||||
```vue
|
// 使用import.meta.glob动态扫描
|
||||||
<draggable
|
const viewModules = import.meta.glob('../../../views/**/*.vue')
|
||||||
:list="panel.tabs"
|
|
||||||
group="tabs"
|
// 异步加载选中的组件
|
||||||
item-key="id"
|
const loader = viewModules[selectedFilePath]
|
||||||
:animation="150"
|
const component = defineAsyncComponent(loader)
|
||||||
>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 物料组件系统 (Material System)
|
### 2. 设计中心系统
|
||||||
|
|
||||||
#### 规范标准
|
#### InteractiveWrapper(交互包装器)
|
||||||
遵循统一的物料组件规范:
|
**作用**:为动态渲染的Vue页面注入交互事件
|
||||||
1. 每个组件独立文件夹
|
|
||||||
2. 包含 `index.vue`(组件实现)和 `index.json`(元数据)
|
|
||||||
3. `index.json` 定义:名称、描述、属性及默认值
|
|
||||||
|
|
||||||
#### 组件状态管理
|
**核心功能**:
|
||||||
- 使用 `materialState` 属性传递组件状态
|
1. 自动扫描页面中的el-row和el-col
|
||||||
- 通过 `update:state` 事件实现状态更新
|
2. 为每个元素注入结构化路径ID(如`r1c2`)
|
||||||
- 物料组件状态独立存储,组件关闭后状态仍保留
|
3. 绑定交互事件(悬停、点击、拖拽)
|
||||||
|
4. 使用MutationObserver监听DOM变化
|
||||||
|
|
||||||
#### 状态持久化
|
#### DropZone(拖放区域指示器)
|
||||||
通过自定义 Vite 插件实现 API 中间件:
|
**作用**:拖拽时显示可放置区域
|
||||||
- `/api/config`: 布局配置读写
|
|
||||||
- `/api/material-states`: 物料组件状态读写
|
|
||||||
- `/api/design-components`: 设计组件元数据读取
|
|
||||||
- `/api/design-state`: 设计中心状态读写
|
|
||||||
|
|
||||||
### 3. 设计组件系统 (Design System)
|
**显示规则**:
|
||||||
|
- el-row:显示 **上方** 和 **下方** 两个区域
|
||||||
|
- el-col:显示 **左侧** 和 **右侧** 两个区域
|
||||||
|
|
||||||
#### 组件构成
|
#### DragPreview(拖拽预览)
|
||||||
1. **设计组件库** (`designComponents/`)
|
跟随鼠标显示正在拖拽的内容,提供视觉反馈。
|
||||||
- TextInput: 文本输入框(属性:label, width, maxLength)
|
|
||||||
- RadioSelect: 单选器(属性:options)
|
|
||||||
- GridTable: 表格(属性:rows, columns, headers)
|
|
||||||
|
|
||||||
2. **物料组件**
|
### 3. 插件系统 (fauto/plugins)
|
||||||
- DesignComponentList: 展示可用设计组件
|
|
||||||
- DesignCenter: 展示已添加的设计组件实例
|
|
||||||
- TreeViewer: 展示设计中心组件列表(支持拖拽排序)
|
|
||||||
- DataTable: 展示选中组件的属性(支持编辑)
|
|
||||||
|
|
||||||
#### 跨组件联动机制
|
#### interactionStore(交互状态管理)
|
||||||
1. 点击 DesignComponentList 中的组件添加到 DesignCenter
|
管理所有交互事件的全局状态:
|
||||||
2. 点击 DesignCenter 中的组件,在 DataTable 中展示其属性
|
- `hoverTarget`: 当前悬停的元素
|
||||||
3. 在 TreeViewer 中拖拽组件调整 DesignCenter 的顺序
|
- `selectedTarget`: 当前选中的元素
|
||||||
4. 在 DataTable 中双击属性值进行编辑
|
- 提供全局事件钩子(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 (面板状态)
|
### PanelStore(面板状态)
|
||||||
管理整个应用的布局和物料组件状态:
|
管理整个应用的布局和物料组件状态:
|
||||||
- `layout`: 三区域面板配置
|
- `layout`: 三区域面板配置
|
||||||
- `materialStates`: 物料组件状态独立存储
|
- `materialStates`: 物料组件状态独立存储
|
||||||
- 提供 Tab 操作 API(添加、关闭、移动、激活)
|
- 提供 Tab 操作 API(添加、关闭、移动、激活)
|
||||||
|
|
||||||
### DesignStore (设计中心状态)
|
### DesignStore(设计组件元数据)
|
||||||
管理设计组件系统的状态:
|
管理设计组件的元数据:
|
||||||
- `components`: 已添加的设计组件实例列表
|
- `componentMetas`: 设计组件元数据列表
|
||||||
- `selectedId`: 当前选中的组件实例ID
|
- `loadComponentMetas()`: 动态扫描设计组件目录
|
||||||
- `componentMetas`: 设计组件元数据缓存
|
- `getComponentMeta()`: 获取指定组件的元数据
|
||||||
|
|
||||||
|
### VueFileStore(Vue文件状态)
|
||||||
|
管理页面管理的选中状态:
|
||||||
|
- `selectedFilePath`: 当前选中的文件路径
|
||||||
|
- `selectedFileName`: 当前选中的文件名
|
||||||
|
- `selectFile()`: 选中指定文件
|
||||||
|
|
||||||
|
### InteractionStore / DragStore
|
||||||
|
见**插件系统**部分
|
||||||
|
|
||||||
## 实现细节
|
## 实现细节
|
||||||
|
|
||||||
### 1. 拖拽功能优化
|
### 1. Vue页面规范
|
||||||
- 使用 `:list` 属性而非 `v-model` 解决拖拽位置问题
|
**强制规则**:template的第一层级**有且仅有一个el-row**
|
||||||
- 实现跨面板 Tab 拖拽
|
|
||||||
- 面板宽度可调整(通过 Resizer 组件)
|
|
||||||
|
|
||||||
### 2. 状态持久化策略
|
**目的**:保证布局可解析性,方便自动生成结构化路径
|
||||||
- 布局配置和物料状态分别存储
|
|
||||||
- 使用防抖机制避免频繁保存(TextEditor 500ms)
|
|
||||||
- 组件移除后状态仍保留
|
|
||||||
|
|
||||||
### 3. 组件通信机制
|
示例:
|
||||||
- 父子组件:Props / Events
|
```vue
|
||||||
- 兄弟组件:通过 Pinia Store
|
<template>
|
||||||
- 物料组件:`materialState` / `update:state`
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="design-component">左侧内容</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-row :gutter="10">
|
||||||
|
<el-col :span="24">
|
||||||
|
<div class="design-component">标题</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 拖拽交互优化
|
||||||
|
|
||||||
|
#### 禁用文本选择
|
||||||
|
拖拽时给body添加`is-dragging`类,设置`user-select: none`
|
||||||
|
|
||||||
|
#### 拖拽预览效果
|
||||||
|
DragPreview组件跟随鼠标显示:
|
||||||
|
- 组件图标 + 名称
|
||||||
|
- 动画效果(缩放 + 淡入)
|
||||||
|
- Teleport到body,避免z-index问题
|
||||||
|
|
||||||
|
### 3. 层级选择实现
|
||||||
|
```typescript
|
||||||
|
// 递归遍历DOM生成层级节点
|
||||||
|
const updateHierarchy = (element: HTMLElement) => {
|
||||||
|
const nodes: HierarchyNode[] = []
|
||||||
|
let current: HTMLElement | null = element
|
||||||
|
|
||||||
|
while (current) {
|
||||||
|
if (current.classList.contains('el-row') ||
|
||||||
|
current.classList.contains('el-col')) {
|
||||||
|
nodes.push({ path, type, element, depth })
|
||||||
|
}
|
||||||
|
current = current.parentElement
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按深度排序,最深的在前
|
||||||
|
nodes.sort((a, b) => b.depth - a.depth)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### 4. 性能优化
|
### 4. 性能优化
|
||||||
- 使用 `defineAsyncComponent` 异步加载物料组件
|
- 使用 `defineAsyncComponent` 异步加载页面
|
||||||
- 使用 `markRaw` 避免不必要的响应式转换
|
- 使用 `MutationObserver` 监听DOM变化
|
||||||
|
- 防止重复绑定(`data-fauto-bindend`属性)
|
||||||
- 使用 `computed` 缓存派生数据
|
- 使用 `computed` 缓存派生数据
|
||||||
|
|
||||||
## API 设计
|
## API 设计
|
||||||
@@ -159,6 +236,11 @@ POST /api/design-state # 保存设计中心状态
|
|||||||
|
|
||||||
## 开发规范
|
## 开发规范
|
||||||
|
|
||||||
|
### Vue页面结构规范
|
||||||
|
1. template第一层级有且仅有一个el-row
|
||||||
|
2. el-row内只能包含el-col或div(设计组件)
|
||||||
|
3. div使用class="design-component"标识
|
||||||
|
|
||||||
### 代码规范
|
### 代码规范
|
||||||
1. 使用 TypeScript 严格模式
|
1. 使用 TypeScript 严格模式
|
||||||
2. 组件 Props 必须明确定义类型
|
2. 组件 Props 必须明确定义类型
|
||||||
@@ -167,33 +249,32 @@ POST /api/design-state # 保存设计中心状态
|
|||||||
|
|
||||||
### 组件开发规范
|
### 组件开发规范
|
||||||
1. 遵循物料组件标准化规范
|
1. 遵循物料组件标准化规范
|
||||||
2. 状态通过 `materialState` 接收,通过 `update:state` 更新
|
2. 每个组件包含`index.vue`和`index.json`
|
||||||
3. 实现防抖保存机制避免频繁 IO
|
3. 插件相关代码放在`fauto/plugins`目录
|
||||||
4. 提供清晰的组件文档(index.json)
|
|
||||||
|
|
||||||
### 状态管理规范
|
### 状态管理规范
|
||||||
1. 使用 Pinia 进行全局状态管理
|
1. 使用 Pinia 进行全局状态管理
|
||||||
2. 状态变更必须通过 Store 的方法
|
2. 状态变更必须通过 Store 的方法
|
||||||
3. 复杂状态逻辑封装在 Store 内部
|
3. 复杂状态逻辑封装在 Store 内部
|
||||||
4. 状态持久化与 UI 逻辑分离
|
|
||||||
|
|
||||||
## 扩展性设计
|
## 扩展性设计
|
||||||
|
|
||||||
### 新增物料组件
|
|
||||||
1. 在 `materials/` 目录下创建组件文件夹
|
|
||||||
2. 实现 `index.vue` 和 `index.json`
|
|
||||||
3. 在 `materials/index.ts` 中注册组件
|
|
||||||
|
|
||||||
### 新增设计组件
|
### 新增设计组件
|
||||||
1. 在 `designComponents/` 目录下创建组件文件夹
|
1. 在 `designComponents/` 目录下创建组件文件夹
|
||||||
2. 实现 `index.vue` 和 `index.json`
|
2. 实现 `index.vue` 和 `index.json`
|
||||||
3. 在 DesignComponentList 中会自动显示
|
3. 自动在 DesignComponentList 中显示
|
||||||
|
|
||||||
### 功能扩展
|
### 功能扩展
|
||||||
1. 面板系统支持更多布局模式
|
1. **拖放后修改源文件** - 通过API将拖放操作写入.vue文件
|
||||||
2. 物料组件支持更多交互类型
|
2. **属性编辑器** - 展示和编辑设计组件属性
|
||||||
3. 设计系统支持更复杂的属性编辑器
|
3. **热更新同步** - 修改后自动刷新页面
|
||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
本项目实现了完整的可拖拽子窗口系统,具备良好的架构设计和扩展性。通过物料组件系统和设计组件系统的分离,既满足了基础的窗口管理需求,又提供了高级的设计能力。状态持久化机制确保了用户体验的连续性,而规范化的开发流程保证了项目的可维护性。
|
本项目实现了一个完整的**Vue页面可视化设计器**,通过直接解析和编辑真实的Vue文件,实现了低代码的页面构建方式。
|
||||||
|
|
||||||
|
**核心亮点**:
|
||||||
|
1. **非侵入式设计** - 通过注入式交互,无需修改Vue页面代码
|
||||||
|
2. **智能层级选择** - 自动识别嵌套结构,键盘切换层级
|
||||||
|
3. **实时视觉反馈** - 拖拽预览 + 拖放区域指示
|
||||||
|
4. **结构化路径** - 自动生成可读的元素ID
|
||||||
Reference in New Issue
Block a user