This commit is contained in:
wfz
2025-12-22 23:20:40 +08:00
parent ff8a6a28f8
commit 9829b91321
8 changed files with 439 additions and 644 deletions

View File

@@ -1,20 +1,22 @@
# 拖拽面板设计器 - 项目上下文总结
# Vue页面可视化设计器 - 项目上下文
> **用途**:用于在新电脑快速恢复 AI 协作上下文
> **生成时间**2025-12-20
> **项目路径**`/Volumes/mypan/ui/draggable-panels`
> **用途**:用于在新环境快速恢复 AI 协作上下文
> **更新时间**2025-12-22
> **项目路径**`d:/workspace/fauto-design/draggable-panels`
---
## 📋 项目概述
这是一个基于 **Vue3 + TypeScript + Vite** 的**可拖拽面板设计器**项目,类似 IDE 界面IDEA/VS Code的多面板布局支持
这是一个基于 **Vue3 + TypeScript + Vite + Element Plus** 的**可视化页面设计器**通过拖拽操作直接编辑真实的Vue页面文件。
- 🎯 三栏面板布局(左/中/右可调整宽度)
- 🎨 Tab 可拖拽、跨面板移动
- 🧩 物料组件系统(自动扫描注册)
- 🎭 设计组件系统(可视化编辑、属性绑定)
- 💾 状态持久化(通过模拟 API
### 核心特性
1. **直接解析Vue文件** - 动态扫描并渲染`src/views`下的页面
2. **拖拽式设计** - 将设计组件拖拽到页面的el-row/el-col上
3. **非侵入式交互** - 通过注入方式绑定事件,不修改页面代码
4. **智能层级选择** - 键盘方向键切换嵌套元素层级
5. **实时视觉反馈** - 拖拽预览 + 拖放区域显示(上/下/左/右)
---
@@ -23,51 +25,54 @@
```
draggable-panels/
├── src/
│ ├── fauto/ # 🔥 设计器核心代码(所有功能都在这里)
│ │ ├── Designer.vue # 设计器主入口页面
│ │ ├── components/ # 设计器基础组件
│ │ │ ├── Header.vue # 顶部菜单栏(打开项目/窗口菜单)
│ │ │ ├── Footer.vue # 底部状态
│ ├── fauto/ # 🔥 设计器核心代码
│ │ ├── Designer.vue # 设计器主入口
│ │
│ │ ├── components/ # 基础UI组件
│ │ │ ├── Header.vue # 顶部菜单
│ │ │ ├── Footer.vue # 底部状态栏(显示拖拽状态)
│ │ │ ├── MainLayout.vue # 三栏布局容器
│ │ │ ├── Panel.vue # 单个面板(含 Tab 列表 + 内容区)
│ │ │ └── Resizer.vue # 面板间拖拽分割线
│ │ │ ├── Panel.vue # 面板容器
│ │ │ └── Resizer.vue # 面板分隔器
│ │ │
│ │ ├── materials/ # 🎁 物料组件8个
│ │ │ ├── index.ts # ✅ 自动扫描所有 */index.vue 和 */index.json
│ │ │ ├── DesignComponentList/ # 可选设计组件列表
│ │ │ ── TreeViewer/ # 树形结构查看器(已添加实例列表
│ │ │ ├── DesignCenter/ # 设计中心(组件预览面板)
│ │ │ ├── DataTable/ # 属性表格编辑器
│ │ │ ├── TextEditor/ # 文本编辑器(支持状态持久化)
│ │ │ ├── TestWidget1/2/3/ # 测试组件
│ │ │ └── [每个物料包含]
│ │ │ ├── index.vue # 组件实现
│ │ │ └── index.json # 组件元数据name/description
│ │ ├── materials/ # 🎁 物料组件系统
│ │ │ ├── PageManagement/ # 页面管理树形选择Vue文件
│ │ │ ├── DesignComponentList/ # 设计组件列表
│ │ │ ── DesignCenter/ # 设计中心(动态渲染页面
│ │ │ ├── index.vue # 主组件
│ │ │ ├── InteractiveWrapper.vue # 交互包装器(注入事件)
│ │ │ ├── DropZone.vue # 拖放区域指示器
│ │ │ └── DragPreview.vue # 拖拽预览
│ │ │
│ │ ├── designComponents/ # 🎨 设计组件3个
│ │ ├── designComponents/ # 🎨 设计组件
│ │ │ ├── TextInput/ # 文本输入框
│ │ │ ├── RadioSelect/ # 单选器
│ │ │ ── GridTable/ # 表格
│ │ │ └── [每个设计组件包含]
│ │ │ ├── index.vue # Vue 组件定义
│ │ │ ── index.json # 元数据name/description/props
│ │ │ ── GridTable/ # 表格
│ │ │
│ │ ├── plugins/ # 🔌 插件系统(核心功能)
│ │ │ ── index.ts # 统一导出
│ │ │ ├── interactionStore.ts # 全局交互事件钩子
│ │ │ ├── dragStore.ts # 拖拽状态管理(层级选择)
│ │ │ └── pathUtils.ts # 结构化路径工具
│ │ │
│ │ ├── stores/ # 🗄️ Pinia状态管理
│ │ │ ├── panelStore.ts # 面板布局 + 物料状态管理
│ │ │ ── designStore.ts # 设计组件实例管理(✅ 本地自动扫描)
│ │ │ ├── panelStore.ts # 面板布局状态
│ │ │ ── designStore.ts # 设计组件元数据
│ │ │ └── vueFileStore.ts # Vue文件选择状态
│ │ │
│ │ └── types/ # 📝 TypeScript 类型定义
│ │ └── index.ts # Panel/TabItem/MaterialInfo 等
│ │ └── types/ # 📝 类型定义
│ │
│ ├── views/ # 普通视图
│ │ ── HelloWorld.vue # 首页欢迎页
│ ├── views/ # 📄 示例页面
│ │ ── TestPage1.vue # 测试页面1
│ │ ├── TestPage2.vue # 测试页面2
│ │ ├── user/Profile.vue # 用户资料页
│ │ └── dashboard/Overview.vue # 仪表板
│ │
│ ├── router.ts # 🛤️ 路由配置/ 和 /draggable
│ ├── router.ts # 路由配置
│ ├── main.ts # 应用入口
│ ├── App.vue # 根组件(仅路由容器)
│ ├── App.vue # 根组件
│ └── style.css # 全局样式
├── package.json # 依赖配置
└── vite.config.ts # Vite配置
```
@@ -75,252 +80,226 @@ draggable-panels/
## 🔑 核心技术要点
### 1. **路由设计**
### 1. **Vue页面规范** ⭐
```typescript
// src/router.ts
'/' HelloWorld
'/draggable' Designer fauto/Designer.vue
**强制规则**template的第一层级**有且仅有一个el-row**
**示例**
```vue
<template>
<el-row :gutter="20">
<el-col :span="12">
<div class="design-component">左侧内容</div>
</el-col>
<el-col :span="12">
<el-row :gutter="10">
<el-col :span="24">
<div class="design-component">标题</div>
</el-col>
</el-row>
</el-col>
</el-row>
</template>
```
### 2. **物料组件自动注册** ⭐
**目的**保证布局可解析性方便自动生成结构化路径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
// ✅ 使用 import.meta.glob 自动扫描
const vueModules = import.meta.glob('./*/index.vue')
const jsonModules = import.meta.glob('./*/index.json', { eager: true })
// 1. 动态渲染组件
<component :is="selectedPageComponent" />
// 自动构建组件映射表和信息列表
export const materialComponents: Record<string, Component> = {}
export const materialList: MaterialInfo[] = []
```
// 2. 挂载后扫描DOM
const injectInteractionEvents = () => {
const rows = containerRef.value.querySelectorAll('.el-row')
const cols = containerRef.value.querySelectorAll('.el-col')
**新增物料步骤**
1.`materials/` 下创建新目录(如 `MyWidget/`
2. 添加 `index.vue`(组件实现)
3. 添加 `index.json`(元数据)
4. **无需修改任何注册代码**,自动生效!
### 3. **设计组件自动加载** ⭐
**最近修复**2025-12-20
**designStore.ts** - 从远程 API 改为本地扫描:
```typescript
// ✅ 使用 eager: true 同步加载
const designComponentMetaModules = import.meta.glob('../designComponents/*/index.json', { eager: true })
// 自动构建元数据列表
const loadComponentMetas = async () => {
const metas: DesignComponentMeta[] = []
for (const path in designComponentMetaModules) {
const id = match[1] // 如 'TextInput'
metas.push({ id, name, description, props })
// 3. 为每个元素注入事件
rows.forEach((row) => {
const path = generateElementPath(row) // 生成路径ID
bindElementEvents(row, 'er', path) // 绑定事件
})
}
componentMetas.value = metas
// 4. 使用MutationObserver监听DOM变化
observer.observe(containerRef.value, { childList: true, subtree: true })
```
### 4. **层级选择机制** ⭐
**核心思想**:当鼠标悬停在嵌套元素上时(如`r1c1r1`),会同时触发`r1c1r1``r1c1``r1`三个层级。
**解决方案**`dragStore.ts`
```typescript
// 1. 收集所有层级节点
const updateHierarchy = (element: HTMLElement) => {
const nodes = []
let current = element
while (current) {
if (current.classList.contains('el-row') || current.classList.contains('el-col')) {
nodes.push({ path, type, element, depth })
}
current = current.parentElement
}
// 按深度排序,最深的在前
nodes.sort((a, b) => b.depth - a.depth)
}
// 2. 键盘切换层级
selectParentLevel() // 切换到父级
selectChildLevel() // 切换到子级
Esc cancelDrag() // 取消拖拽
```
### 5. **拖拽交互优化** ⭐
#### 禁用文本选择
```typescript
// DragPreview.vue
watch(() => dragStore.isDragging, (isDragging) => {
if (isDragging) {
document.body.classList.add('is-dragging')
} else {
document.body.classList.remove('is-dragging')
}
})
```
```css
/* style.css */
body.is-dragging {
user-select: none !important;
cursor: grabbing !important;
}
```
**DesignCenter.vue** - 组件预览渲染:
```typescript
// ✅ 使用 eager: true 确保同步加载
const designComponentModules = import.meta.glob('../../designComponents/*/index.vue', { eager: true })
// 构建组件映射(用于 <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 或搭建后端。
#### 拖拽预览效果
- 跟随鼠标的半透明卡片
- 显示组件图标 + 名称
- Teleport到body避免z-index问题
---
## 🎯 数据流图
```
用户操作
1. 页面管理
用户点击Vue文件
┌─────────────────────────────────────────────────┐
│ Header.vue - 点击"窗口"菜单 │
│ → panelStore.openMaterial('TreeViewer') │
└─────────────────────────────────────────────────┘
vueFileStore.selectFile(path)
┌─────────────────────────────────────────────────┐
│ Panel.vue - 渲染 Tab + 加载物料组件 │
│ → <component :is="getMaterialComponent(id)"> │
└─────────────────────────────────────────────────┘
DesignCenter动态渲染该页面
┌─────────────────────────────────────────────────┐
│ DesignComponentList - 点击"文本输入框" │
│ → designStore.addComponent('TextInput') │
└─────────────────────────────────────────────────┘
InteractiveWrapper注入交互事件
2. 拖拽交互
用户拖拽设计组件
┌─────────────────────────────────────────────────┐
│ TreeViewer - watch 监听 components 变化 │
│ → 自动刷新实例列表 │
└─────────────────────────────────────────────────┘
dragStore.startDragFromComponentList(id, name)
┌─────────────────────────────────────────────────┐
│ DesignCenter - 渲染组件预览 │
│ → <component :is="getComponent(componentId)"> │
└─────────────────────────────────────────────────┘
鼠标移动到页面元素上
┌─────────────────────────────────────────────────┐
│ DataTable - 编辑属性 │
→ designStore.updateComponentProps(id, key, val)│
└─────────────────────────────────────────────────┘
dragStore.updateHierarchy(element) // 识别层级
用户按↑↓键切换层级
DropZone显示可放置区域上/下/左/右)
用户点击区域确认
dragStore.confirmDrop()
Footer显示拖放记录TextInput → r1c1 左侧
```
---
## 🐛 已解决的关键问题
## 🛠️ 快速开始
### 问题 1设计组件列表不显示
**时间**2025-12-20
**原因**`designStore.loadComponentMetas()` 使用 `fetch('/api/design-components')`,无后端导致 `componentMetas` 为空
**解决**:改用 `import.meta.glob` 自动扫描本地 `designComponents/*/index.json`
### 问题 2TreeViewer 列表不更新
**原因**:使用 Pinia 的 `$subscribe` 监听 mutation逻辑复杂且不稳定
**解决**:改用 Vue 的 `watch(() => designStore.components)` 直接监听
### 问题 3DesignCenter 组件不渲染
**原因**`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"
}
}
### 访问设计器
```
http://localhost:5173/draggable
```
### 使用流程
1. 点击左侧"页面管理"物料
2. 在树形列表中选择一个Vue页面
3. 页面在"设计中心"中渲染
4. 从"设计组件列表"拖拽组件
5. 移动到页面的el-row或el-col上
6. 使用↑↓键切换层级
7. 点击放置方向(上/下/左/右)
8. 查看Footer中的拖放记录
---
## 🚀 启动命令
## 📝 开发规范
```bash
# 1. 安装依赖
npm install
# 2. 启动开发服务器
npm run dev
# 访问 http://localhost:5173/draggable
# 3. 构建生产版本
npm run build
### 创建符合规范的Vue页面
```vue
<template>
<!-- 第一层级必须且只能有一个el-row -->
<el-row :gutter="20">
<el-col :span="24">
<!-- 内容 -->
</el-col>
</el-row>
</template>
```
---
### 添加设计组件
1.`src/fauto/designComponents/`下创建目录
2. 添加`index.vue``index.json`
3. 自动在列表中显示
## 📝 后续可优化方向
### 🔧 功能增强
1. **真实持久化**
- 选项 A改用 `localStorage` 替代模拟 API
- 选项 B搭建 Node.js 后端Express + fs.writeFile
2. **拖拽增强**
- 支持面板间 Tab 跨级拖动
- 树形组件支持父子节点拖拽
3. **设计器功能**
- 实现拖拽生成 Vue 文件(模板引擎)
- 接入 Element Plus 组件库作为设计组件源
4. **开发体验**
- 添加 ESLint + Prettier 代码规范
- 添加单元测试Vitest
### 🎨 界面优化
- 添加主题切换(暗色/亮色)
- 面板大小记忆功能
- 快捷键支持Ctrl+S 保存等)
### 插件开发
所有与页面解析、交互相关的代码放在`src/fauto/plugins/`
---
## 💡 给 AI 的关键提示
## 🚀 下一步计划
### 如果遇到"自动扫描不生效"
1. 检查是否添加了 `{ eager: true }` 选项
2. 确认路径匹配正则是否正确(`/designComponents\/(.+)\/index\.vue$/`
3. 验证文件夹下是否有 `index.vue``index.json`
### 如果组件不渲染
1. 检查 `import.meta.glob` 是否使用 `eager: true`
2. 确认使用 `mod.default || mod` 获取组件
3. 对于动态组件,确保使用 `markRaw()` 包装
### 如果状态不同步
1. 优先使用 Vue 的 `watch` 而非 Pinia 的 `$subscribe`
2. 设置 `{ deep: true, immediate: true }` 确保深度监听
3. 使用 `[...array]` 创建新数组触发响应式更新
1. **拖放后修改源文件** - 通过API将操作写入.vue文件
2. **属性编辑器** - 展示和编辑组件属性
3. **热更新同步** - 修改后自动刷新页面
4. **撤销/重做** - 操作历史记录
---
## 📞 联系方式
## 📚 相关文档
**项目负责人**[你的信息]
**Git 仓库**[如果有的话]
**文档更新**2025-12-20
- **详细设计文档**`项目设计文档.md`
- **技术选型**Vue3 + TypeScript + Element Plus + Pinia
- **构建工具**Vite 7.3.0
---
## ✅ 快速检查清单(给新 AI
## 💡 核心创新点
当你接手这个项目时,请确认以下内容:
- [ ] 项目已正常启动(`npm run dev`
- [ ] 访问 `/draggable` 能看到设计器界面
- [ ] 点击"窗口"菜单能打开物料组件
- [ ] "设计组件列表"显示 3 个组件TextInput/RadioSelect/GridTable
- [ ] 点击设计组件后TreeViewer 能显示新实例
- [ ] DesignCenter 能正确渲染组件预览
- [ ] DataTable 能编辑组件属性
如果以上任一项失败,请参考"已解决的关键问题"章节。
1. **非侵入式设计** - 无需修改Vue页面代码即可实现拖拽设计
2. **智能层级选择** - 自动识别嵌套结构,精准定位目标位置
3. **实时视觉反馈** - 完善的拖拽交互体验
4. **结构化路径** - 人类可读的元素定位方式
---
**🎉 恭喜!你已经完全掌握了这个项目的上下文,可以继续开发了!**
**最后更新**2025-12-22
**AI协作建议**:优先阅读本文档的"核心技术要点"部分,理解项目的设计理念和实现方式

View File

@@ -16,7 +16,7 @@
"materialId": "DesignComponentList"
}
],
"activeTabId": "up60643"
"activeTabId": "6hfm9ux"
},
"centerPanel": {
"id": "center",
@@ -141,5 +141,5 @@
"activeTabId": "mxfx11j"
}
},
"lastUpdated": "2025-12-22T14:24:28.202Z"
"lastUpdated": "2025-12-22T15:06:06.209Z"
}

View File

@@ -67,5 +67,5 @@
}
],
"selectedId": "jy87mdv",
"lastUpdated": "2025-12-22T14:24:28.783Z"
"lastUpdated": "2025-12-22T14:42:41.050Z"
}

View File

@@ -60,18 +60,12 @@ const dragInfo = computed(() => {
if (source.type === 'design-component') {
return `拖拽: ${source.componentName}`
}
return `: ${source.path}`
return `: ${source.path}`
})
// 当前拖放目标信息
const dropTargetInfo = computed(() => {
if (!dragStore.isDragging) return null
if (!dragStore.selectedNode) return null
const node = dragStore.selectedNode
const typeText = node.type === 'er' ? 'Row' : 'Col'
return `目标: ${typeText} [${node.path}]`
})
// 删除不再使用的代码
// const dropTargetInfo = ...
// const hierarchyInfo = ...
// 最后一次拖放记录
const lastDropInfo = computed(() => {
@@ -98,11 +92,8 @@ onUnmounted(() => {
<span class="info-item drag-info">
🎯 {{ dragInfo }}
</span>
<span v-if="dropTargetInfo" class="info-item target-info">
{{ dropTargetInfo }}
</span>
<span v-if="dragStore.hoverDirection" class="info-item direction-info">
📍 {{ dragStore.hoverDirection === 'top' ? '上方' : dragStore.hoverDirection === 'bottom' ? '下方' : dragStore.hoverDirection === 'left' ? '左侧' : '右侧' }}
<span v-if="hoverInfo" class="info-item hover-info">
👁 {{ hoverInfo }}
</span>
</template>

View File

@@ -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) => {
e.stopPropagation()
@@ -98,6 +107,7 @@ const bindElementEvents = (element: HTMLElement, type: ElementType, path: string
// 绑定事件
element.addEventListener('mouseenter', handleMouseEnter)
element.addEventListener('mousemove', handleMouseMove) // 新增:拖拽时持续更新
element.addEventListener('mouseleave', handleMouseLeave)
element.addEventListener('click', handleClick)
element.addEventListener('mousedown', handleMouseDown)
@@ -107,6 +117,7 @@ const bindElementEvents = (element: HTMLElement, type: ElementType, path: string
// 返回清理函数
return () => {
element.removeEventListener('mouseenter', handleMouseEnter)
element.removeEventListener('mousemove', handleMouseMove)
element.removeEventListener('mouseleave', handleMouseLeave)
element.removeEventListener('click', handleClick)
element.removeEventListener('mousedown', handleMouseDown)

View File

@@ -1,42 +1,11 @@
<script setup lang="ts">
import { defineAsyncComponent, markRaw, computed, watch } from 'vue'
import { useDesignStore } from '../../stores/designStore'
import { defineAsyncComponent, computed, watch } from 'vue'
import { useVueFileStore } from '../../stores/vueFileStore'
import InteractiveWrapper from './InteractiveWrapper.vue'
import config from './index.json'
const designStore = useDesignStore()
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文件
const viewModules = import.meta.glob('../../../views/**/*.vue')
@@ -65,10 +34,7 @@ watch(() => vueFileStore.selectedFilePath, (newPath) => {
<div class="design-center">
<div class="center-header">
<span class="title">{{ config.name }}</span>
<span class="count" v-if="!vueFileStore.selectedFilePath">
{{ designStore.components.length }} 个实例
</span>
<span class="file-info" v-else>
<span class="file-info" v-if="vueFileStore.selectedFilePath">
📄 {{ vueFileStore.selectedFileName }}
</span>
</div>
@@ -78,35 +44,12 @@ watch(() => vueFileStore.selectedFilePath, (newPath) => {
<InteractiveWrapper :component="selectedPageComponent" />
</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 class="empty-icon">🎨</div>
<div>暂无内容</div>
<div class="empty-hint">
点击页面管理选择Vue页面或从左侧列表添加设计组件
点击页面管理选择Vue页面开始设计
</div>
</div>
</div>
@@ -136,11 +79,6 @@ watch(() => vueFileStore.selectedFilePath, (newPath) => {
font-weight: 500;
}
.count {
color: #888888;
font-size: 11px;
}
.file-info {
color: #4fc3f7;
font-size: 12px;
@@ -161,67 +99,6 @@ watch(() => vueFileStore.selectedFilePath, (newPath) => {
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 {
color: #666666;
text-align: center;

View File

@@ -1,13 +1,5 @@
import { defineStore } from 'pinia'
import { ref, watch, computed } from 'vue'
// 设计组件实例
export interface DesignComponentInstance {
id: string // 实例唯一ID
componentId: string // 设计组件类型ID
name: string // 显示名称
props: Record<string, any> // 属性值
}
import { ref } from 'vue'
// 设计组件定义
export interface DesignComponentMeta {
@@ -17,37 +9,13 @@ export interface DesignComponentMeta {
props: Record<string, any>
}
// 生成唯一ID
const generateId = () => Math.random().toString(36).substring(2, 9)
export const useDesignStore = defineStore('design', () => {
// 设计中心已添加的组件实例列表
const components = ref<DesignComponentInstance[]>([])
// 当前选中的组件实例ID
const selectedId = ref<string | null>(null)
// 设计组件元数据缓存
const componentMetas = ref<DesignComponentMeta[]>([])
// 自动扫描所有设计组件的 .json 配置文件
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 () => {
try {
@@ -71,132 +39,20 @@ export const useDesignStore = defineStore('design', () => {
}
componentMetas.value = metas
console.log('加载设计组件元数据:', metas.length, '个组件')
} catch (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) => {
return componentMetas.value.find(m => m.id === componentId)
}
// 初始化
const init = async () => {
await loadComponentMetas()
await loadState()
}
return {
components,
selectedId,
selectedComponent,
selectedComponentMeta,
componentMetas,
isLoaded,
init,
loadComponentMetas,
loadState,
saveState,
addComponent,
removeComponent,
selectComponent,
updateComponentProps,
reorderComponents,
getComponentMeta
}
})

View File

@@ -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/
├── assets/ # 静态资源
├── components/ # 核心UI组件
│ ├── Header.vue # 顶部菜单栏
│ ├── Footer.vue # 底部状态栏
│ ├── MainLayout.vue # 主布局容器
│ ├── Panel.vue # 面板容器
│ └── Resizer.vue # 面板分隔器
├── materials/ # 物料组件系统
│ ├── index.ts # 物料组件注册中心
│ ├── TextEditor/ # 文本编辑器
├── TreeViewer/ # 树形展示器
│ ├── DataTable/ # 数据表格
│ ├── TestWidget*/ # 测试组件
│ ├── DesignComponentList/ # 设计组件列表
│ └── DesignCenter/ # 设计中心
├── designComponents/ # 设计组件库
│ ├── TextInput/ # 文本输入框
│ ├── RadioSelect/ # 单选器
│ └── GridTable/ # 表格组件
├── stores/ # 状态管理
│ ├── panelStore.ts # 面板布局状态
── designStore.ts # 设计中心状态
├── types/ # TypeScript 类型定义
├── fauto/ # 核心功能模块
├── components/ # UI组件
│ ├── Header.vue # 顶部菜单栏
│ ├── Footer.vue # 底部状态栏(显示拖拽状态)
│ ├── MainLayout.vue # 主布局容器
│ ├── Panel.vue # 面板容器
│ └── Resizer.vue # 面板分隔器
├── materials/ # 物料组件系统
│ ├── PageManagement/ # 页面管理树形选择Vue文件
│ ├── DesignComponentList/ # 设计组件列表
│ └── DesignCenter/ # 设计中心(动态渲染页面)
│ ├── designComponents/ # 设计组件库
│ ├── TextInput/ # 文本输入框
│ ├── RadioSelect/ # 单选器
│ └── GridTable/ # 表格组件
│ ├── plugins/ # 插件系统(交互事件、拖拽管理)
│ ├── interactionStore.ts # 全局交互钩子
│ ├── dragStore.ts # 拖拽状态管理
│ └── pathUtils.ts # 结构化路径工具
├── stores/ # 状态管理
│ ├── panelStore.ts # 面板布局状态
│ ├── designStore.ts # 设计组件元数据
└── vueFileStore.ts # Vue文件选择状态
│ └── Designer.vue # 设计器主入口
├── views/ # 示例页面(测试用)
└── App.vue # 应用入口
```
## 核心功能模块
### 1. 面板系统 (Panel System)
### 1. 页面管理系统
#### 布局结构
- **Header**: 固定顶部,包含菜单和新增窗口按钮
- **MainLayout**: 三区域布局(左/中/右)
- **Panel**: 可拖拽的面板容器
- **Footer**: 固定底部,显示时间和状态信息
#### 功能说明
- 动态扫描`src/views`目录下的所有Vue文件
- 以树形结构展示文件夹和页面
- 点击选中后在设计中心动态渲染
#### 拖拽实现
使用 `vuedraggable` 库实现跨面板的 Tab 拖拽:
```vue
<draggable
:list="panel.tabs"
group="tabs"
item-key="id"
:animation="150"
>
#### 技术实现
```typescript
// 使用import.meta.glob动态扫描
const viewModules = import.meta.glob('../../../views/**/*.vue')
// 异步加载选中的组件
const loader = viewModules[selectedFilePath]
const component = defineAsyncComponent(loader)
```
### 2. 物料组件系统 (Material System)
### 2. 设计中心系统
#### 规范标准
遵循统一的物料组件规范:
1. 每个组件独立文件夹
2. 包含 `index.vue`(组件实现)和 `index.json`(元数据)
3. `index.json` 定义:名称、描述、属性及默认值
#### InteractiveWrapper交互包装器
**作用**为动态渲染的Vue页面注入交互事件
#### 组件状态管理
- 使用 `materialState` 属性传递组件状态
- 通过 `update:state` 事件实现状态更新
- 物料组件状态独立存储,组件关闭后状态仍保留
**核心功能**
1. 自动扫描页面中的el-row和el-col
2. 为每个元素注入结构化路径ID`r1c2`
3. 绑定交互事件(悬停、点击、拖拽)
4. 使用MutationObserver监听DOM变化
#### 状态持久化
通过自定义 Vite 插件实现 API 中间件:
- `/api/config`: 布局配置读写
- `/api/material-states`: 物料组件状态读写
- `/api/design-components`: 设计组件元数据读取
- `/api/design-state`: 设计中心状态读写
#### DropZone拖放区域指示器
**作用**:拖拽时显示可放置区域
### 3. 设计组件系统 (Design System)
**显示规则**
- el-row显示 **上方****下方** 两个区域
- el-col显示 **左侧****右侧** 两个区域
#### 组件构成
1. **设计组件库** (`designComponents/`)
- TextInput: 文本输入框属性label, width, maxLength
- RadioSelect: 单选器属性options
- GridTable: 表格属性rows, columns, headers
#### DragPreview拖拽预览
跟随鼠标显示正在拖拽的内容,提供视觉反馈。
2. **物料组件**
- DesignComponentList: 展示可用设计组件
- DesignCenter: 展示已添加的设计组件实例
- TreeViewer: 展示设计中心组件列表(支持拖拽排序)
- DataTable: 展示选中组件的属性(支持编辑)
### 3. 插件系统 (fauto/plugins)
#### 跨组件联动机制
1. 点击 DesignComponentList 中的组件添加到 DesignCenter
2. 点击 DesignCenter 中的组件,在 DataTable 中展示其属性
3. 在 TreeViewer 中拖拽组件调整 DesignCenter 的顺序
4. 在 DataTable 中双击属性值进行编辑
#### interactionStore交互状态管理
管理所有交互事件的全局状态:
- `hoverTarget`: 当前悬停的元素
- `selectedTarget`: 当前选中的元素
- 提供全局事件钩子hover, click, longpress, drag, release
#### dragStore拖拽状态管理
管理拖拽操作的复杂状态:
**核心状态**
- `dragSource`: 拖拽源(设计组件/画布元素)
- `hierarchyNodes`: 当前悬停位置的所有层级节点
- `selectedHierarchyIndex`: 当前选中的层级索引
- `hoverDirection`: 当前悬停的拖放方向
- `lastDropRecord`: 最后一次拖放记录
**层级选择机制**
- 鼠标悬停在嵌套元素上时,自动识别所有父级节点
- 默认选中最深层级
- **↑键**:切换到父级
- **↓键**:切换到子级
- **Esc键**:取消拖拽
#### pathUtils路径工具
生成和解析结构化路径ID
**路径格式**`r{n}c{m}r{x}c{y}...`
- `r`: el-row
- `c`: el-col
- 数字在同级元素中的索引从1开始
**示例**`r1c2r1c3` 表示:
- 第1个el-row
- 的第2个el-col
- 内的第1个el-row
- 内的第3个el-col
## 状态管理设计
### PanelStore (面板状态)
### PanelStore面板状态
管理整个应用的布局和物料组件状态:
- `layout`: 三区域面板配置
- `materialStates`: 物料组件状态独立存储
- 提供 Tab 操作 API添加、关闭、移动、激活
### DesignStore (设计中心状态)
管理设计组件系统的状态
- `components`: 已添加的设计组件实例列表
- `selectedId`: 当前选中的组件实例ID
- `componentMetas`: 设计组件元数据缓存
### DesignStore(设计组件元数据)
管理设计组件的元数据
- `componentMetas`: 设计组件元数据列表
- `loadComponentMetas()`: 动态扫描设计组件目录
- `getComponentMeta()`: 获取指定组件元数据
### VueFileStoreVue文件状态
管理页面管理的选中状态:
- `selectedFilePath`: 当前选中的文件路径
- `selectedFileName`: 当前选中的文件名
- `selectFile()`: 选中指定文件
### InteractionStore / DragStore
见**插件系统**部分
## 实现细节
### 1. 拖拽功能优化
- 使用 `:list` 属性而非 `v-model` 解决拖拽位置问题
- 实现跨面板 Tab 拖拽
- 面板宽度可调整(通过 Resizer 组件)
### 1. Vue页面规范
**强制规则**template的第一层级**有且仅有一个el-row**
### 2. 状态持久化策略
- 布局配置和物料状态分别存储
- 使用防抖机制避免频繁保存TextEditor 500ms
- 组件移除后状态仍保留
**目的**:保证布局可解析性,方便自动生成结构化路径
### 3. 组件通信机制
- 父子组件Props / Events
- 兄弟组件:通过 Pinia Store
- 物料组件:`materialState` / `update:state`
示例:
```vue
<template>
<el-row :gutter="20">
<el-col :span="12">
<div class="design-component">左侧内容</div>
</el-col>
<el-col :span="12">
<el-row :gutter="10">
<el-col :span="24">
<div class="design-component">标题</div>
</el-col>
</el-row>
</el-col>
</el-row>
</template>
```
### 2. 拖拽交互优化
#### 禁用文本选择
拖拽时给body添加`is-dragging`类,设置`user-select: none`
#### 拖拽预览效果
DragPreview组件跟随鼠标显示
- 组件图标 + 名称
- 动画效果(缩放 + 淡入)
- Teleport到body避免z-index问题
### 3. 层级选择实现
```typescript
// 递归遍历DOM生成层级节点
const updateHierarchy = (element: HTMLElement) => {
const nodes: HierarchyNode[] = []
let current: HTMLElement | null = element
while (current) {
if (current.classList.contains('el-row') ||
current.classList.contains('el-col')) {
nodes.push({ path, type, element, depth })
}
current = current.parentElement
}
// 按深度排序,最深的在前
nodes.sort((a, b) => b.depth - a.depth)
}
```
### 4. 性能优化
- 使用 `defineAsyncComponent` 异步加载物料组件
- 使用 `markRaw` 避免不必要的响应式转换
- 使用 `defineAsyncComponent` 异步加载页面
- 使用 `MutationObserver` 监听DOM变化
- 防止重复绑定(`data-fauto-bindend`属性)
- 使用 `computed` 缓存派生数据
## 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 严格模式
2. 组件 Props 必须明确定义类型
@@ -167,33 +249,32 @@ POST /api/design-state # 保存设计中心状态
### 组件开发规范
1. 遵循物料组件标准化规范
2. 状态通过 `materialState` 接收,通过 `update:state` 更新
3. 实现防抖保存机制避免频繁 IO
4. 提供清晰的组件文档index.json
2. 每个组件包含`index.vue``index.json`
3. 插件相关代码放在`fauto/plugins`目录
### 状态管理规范
1. 使用 Pinia 进行全局状态管理
2. 状态变更必须通过 Store 的方法
3. 复杂状态逻辑封装在 Store 内部
4. 状态持久化与 UI 逻辑分离
## 扩展性设计
### 新增物料组件
1.`materials/` 目录下创建组件文件夹
2. 实现 `index.vue``index.json`
3.`materials/index.ts` 中注册组件
### 新增设计组件
1.`designComponents/` 目录下创建组件文件夹
2. 实现 `index.vue``index.json`
3. 在 DesignComponentList 中会自动显示
3. 自动在 DesignComponentList 中显示
### 功能扩展
1. 面板系统支持更多布局模式
2. 物料组件支持更多交互类型
3. 设计系统支持更复杂的属性编辑器
1. **拖放后修改源文件** - 通过API将拖放操作写入.vue文件
2. **属性编辑器** - 展示和编辑设计组件属性
3. **热更新同步** - 修改后自动刷新页面
## 总结
本项目实现了完整的可拖拽子窗口系统,具备良好的架构设计和扩展性。通过物料组件系统和设计组件系统的分离,既满足了基础的窗口管理需求,又提供了高级的设计能力。状态持久化机制确保了用户体验的连续性,而规范化的开发流程保证了项目的可维护性
本项目实现了一个完整的**Vue页面可视化设计器**通过直接解析和编辑真实的Vue文件实现了低代码的页面构建方式
**核心亮点**
1. **非侵入式设计** - 通过注入式交互无需修改Vue页面代码
2. **智能层级选择** - 自动识别嵌套结构,键盘切换层级
3. **实时视觉反馈** - 拖拽预览 + 拖放区域指示
4. **结构化路径** - 自动生成可读的元素ID