# 容器拖拽重构 & 嵌套展望

## 一、本次解决的问题

### 1.1 背景

原「新增分组」流程：用户点击设计区底部的「新增分组」按钮 → 弹出 `el-dialog` 选择分组类型（表单/子表单/插槽） → 点击确认 → 创建分组。

**痛点**：操作路径长，打断设计节奏；弹窗交互与拖拽式设计体验不一致。

### 1.2 解决方案

将弹窗选择改为从左侧控件面板「容器」分组直接拖入设计区：

```
左侧面板                              设计区
┌───────────┐                    ┌──────────────────────┐
│ 控件       │                    │ 表单设计区            │
│ ▼ 容器     │  ─── 拖拽 ──→     │                      │
│   表单     │                    │ ┌──────────────────┐ │
│   子表单   │                    │ │ 拖入容器创建分组  │ │
│   插槽     │                    │ └──────────────────┘ │
│ ▶ 基础控件 │                    │                      │
└───────────┘                    └──────────────────────┘
```

核心设计决策：使用独立 SortableJS group name `'containers'` 与基础控件的 `'formWidgets'` 隔离，防止基础控件被误拖到根层。

### 1.3 涉及文件

| 文件 | 改动 |
|------|------|
| `utils/defaultConfigs.ts` | 新增 `CONTAINER_WIDGET_LIST` 常量 |
| `components/WidgetPanel.vue` | 新增「容器」分组，group name = `containers` |
| `components/FormDesignArea.vue` | 根层新增容器 drop zone，移除弹窗按钮 |
| `composables/useGroupManagement.ts` | 新增 `addGroupByContainerType()`，移除弹窗相关方法 |
| `composables/useDropHandlers.ts` | 新增 `onContainerDrop()`，处理容器拖入 |
| `framework.vue` | 串联组件，移除弹窗引用 |

### 1.4 关键实现细节

#### 1.4.1 拖拽组隔离

SortableJS 通过 `group.name` 决定跨列表拖拽的合法性。我们定义了两个互不干扰的拖拽通道：

| 通道 | group.name | 源（左侧面板） | 目标（设计区） |
|------|------------|---------------|---------------|
| 容器 | `containers` | 容器面板项 | 根层 container-drop-zone |
| 基础控件 | `formWidgets` | 基础/自定义面板项 | 分组内 form-inner-drag-zone |

容器面板配置：
```html
<!-- WidgetPanel.vue — 容器面板（源） -->
<draggable
  :group="{ name: 'containers', pull: 'clone', put: false }"
  :clone="clonePaletteItem"
/>
```

```html
<!-- FormDesignArea.vue — 容器放置区（目标） -->
<draggable
  :list="containerDropList"
  :group="{ name: 'containers', put: true }"
  :sort="false"
  @add="onContainerAdd()"
/>
```

#### 1.4.2 数据流

```
用户拖拽容器项
  → SortableJS clone → clonePaletteItem() 生成 DroppedItem { type, id, label }
  → SortableJS add  → containerDropList 接收 DroppedItem
  → Vue @add       → onContainerAdd() → emit('drop-container')
  → framework.vue  → onContainerDrop()
                    → 读取 containerDropList，清空列表
                    → addGroupByContainerType(type) 创建分组
```

#### 1.4.3 vuedraggable v4 的 slot 陷阱

vuedraggable v4（vue.draggable.next）**只渲染 `#item` 和 `#footer` 具名 slot**，默认 slot 内容会被忽略：

```html
<!-- 错误：提示文字不会渲染 -->
<draggable :list="items" item-key="id">
  <template #item="{ element }"><span /></template>
  <span class="drop-hint">提示文字</span>  <!-- 被忽略！ -->
</draggable>

<!-- 正确：使用 #footer slot -->
<draggable :list="items" item-key="id">
  <template #item="{ element }"><span :key="element.id" /></template>
  <template #footer>
    <span class="drop-hint">提示文字</span>
  </template>
</draggable>
```

---

## 二、当前数据模型

### 2.1 三层固定树

```
LayoutItem[] (顶层分组)
  └── FormChildItem[] (表单 form / 子表单 subForm / 插槽 slot)
        └── FormChildItem[] (基础控件 control / 表单插槽 formSlot)
```

- `LayoutItem` — 顶层分组节点，`nodeKind: 'group'`
- `FormChildItem` — 内部节点，`nodeKind` 区分 `form` / `subForm` / `slot` / `control`

### 2.2 类型定义（types.ts 关键片段）

```typescript
interface LayoutItem {
  i: string; x: number; y: number; w: number; h: number
  type?: 'group' | 'flmForm' | BasicControlType
  label?: string
  config?: Record<string, unknown>
  children?: FormChildItem[]
  nodeKind?: 'group' | 'form' | 'subForm' | ...
}

interface FormChildItem {
  i: string; x: number; y: number; w: number; h: number
  type: BasicControlType | SlotControlType
  children?: FormChildItem[]   // 子表单/表单内部的控件列表
  nodeKind?: 'form' | 'subForm' | 'slot' | 'control'
  formItem?: FormItemSettings
  config?: Record<string, unknown>
}
```

### 2.3 关键约束

1. **每个分组只能有一个子项**：form 或 subForm 或 slot（互斥）
2. **树深度固定为三层**：group → form/subForm/slot → controls
3. **容器不可嵌套**：group 下不能有 group，form 下不能有 form

---

## 三、容器嵌套实现方案（展望）

### 3.1 什么是容器嵌套

允许分组内包含子分组，形成任意深度的树：

```
分组 A
  ├── 表单
  │     ├── 输入框
  │     └── 选择器
  └── 分组 B（子分组）
        └── 子表单
              ├── 日期选择器
              └── 开关
```

典型业务场景：
- **卡片式分组**：一个分组内包含多个卡片（子分组），每个卡片有独立标题
- **Tab 页签**：分组内包含多个 Tab，每个 Tab 是一个子分组
- **折叠面板**：分组内包含多个可折叠区域

### 3.2 需要修改的文件与方向

#### 3.2.1 类型定义（types.ts）

**目标**：让分组支持递归嵌套。

```typescript
// 方案 A：LayoutItem 自引用
interface LayoutItem {
  i: string; x: number; y: number; w: number; h: number
  type: 'group'
  label?: string
  config?: Record<string, unknown>
  children?: LayoutItem[] | FormChildItem[]  // ← 关键：允许嵌套分组或表单子项
  nodeKind: 'group'
}
```

```typescript
// 方案 B（推荐）：统一树节点，通过 nodeKind 区分
interface TreeNode {
  i: string; x: number; y: number; w: number; h: number
  type: string
  label?: string
  nodeKind: 'group' | 'form' | 'subForm' | 'slot' | 'control'
  children?: TreeNode[]
  config?: Record<string, unknown>
  formItem?: FormItemSettings
  parentGroup?: string  // ← 新增：指向父分组 id
}
```

**推荐方案 B**，理由：
- 统一类型便于递归处理（渲染、遍历、序列化）
- `parentGroup` 便于向上查找，对选择、删除、复制等操作有帮助
- 渐进式迁移：可先让 `LayoutItem = TreeNode`，`FormChildItem = TreeNode` 作为别名

#### 3.2.2 渲染层（FormDesignArea.vue）

**目标**：支持递归渲染分组。

当前 FormDesignArea 是手写的扁平结构，改为递归组件：

```vue
<!-- 新组件：GroupNode.vue -->
<template>
  <div class="group-node" :class="{ selected }">
    <div class="group-header" @click="select">
      <span>{{ item.label || '分组' }}</span>
      <div class="group-actions">
        <el-button @click.stop="addChildGroup">添加子分组</el-button>
        <el-button @click.stop="remove">删除</el-button>
      </div>
    </div>

    <!-- 分组内容：表单/子表单/插槽（同现有逻辑） -->
    <div v-if="formChild" class="group-body">
      <template v-if="formChild.nodeKind === 'form' || formChild.nodeKind === 'subForm'">
        <el-form>
          <GridLayout :layout="formChild.children" ...>
            <GridItem v-for="ctrl in formChild.children" :key="ctrl.i" ...>
              <!-- 控件渲染 -->
            </GridItem>
          </GridLayout>
        </el-form>
      </template>
    </div>

    <!-- 递归：子分组 -->
    <draggable
      v-model="childGroups"
      item-key="i"
      handle=".group-header"
      :group="{ name: 'containers', put: true }"
      :sort="true"
    >
      <template #item="{ element }">
        <GroupNode :item="element" ... />  <!-- 递归 -->
      </template>
    </draggable>
  </div>
</template>
```

**关键变化**：
- 分组渲染逻辑提取为独立组件 `GroupNode.vue`
- 子分组区域接收 `'containers'` 拖拽，实现嵌套拖入
- `handle=".group-header"` 保证只有拖标题才能排序，不会误触控件

#### 3.2.3 拖拽处理（useDropHandlers.ts）

**目标**：支持向任意深度的分组拖入子分组。

当前 `onContainerDrop` 只处理根层拖入，嵌套需要：

```typescript
function onContainerDrop(targetGroupId?: string) {
  const items = [...containerDropList.value]
  containerDropList.value = []

  for (const dropped of items) {
    const type = dropped.type === 'groupSlot' ? 'slot' : dropped.type
    if (targetGroupId) {
      // 嵌套场景：找到目标分组，push 子分组
      const parent = findGroupById(layout.value, targetGroupId)
      if (parent) {
        parent.children.push(createGroupItem(type))
      }
    } else {
      // 根层场景（现有逻辑）
      addGroupByContainerType(type)
    }
  }
}
```

新增递归查找辅助：

```typescript
function findGroupById(nodes: TreeNode[], id: string): TreeNode | null {
  for (const n of nodes) {
    if (n.i === id && n.nodeKind === 'group') return n
    if (n.children) {
      const found = findGroupById(n.children, id)
      if (found) return found
    }
  }
  return null
}
```

#### 3.2.4 配置序列化（useConfigSerialization.ts）

**目标**：`buildFrameworkConfig` 和 `parseConfigToLayout` 支持递归。

**序列化（layout → config）**：

```typescript
function serializeGroup(group: TreeNode): FrameworkGroupConfig {
  const formChild = group.children?.find(c =>
    c.nodeKind === 'form' || c.nodeKind === 'subForm'
  )
  const childGroups = group.children?.filter(c => c.nodeKind === 'group') || []

  return {
    id: group.i,
    title: group.label,
    type: formChild?.nodeKind === 'subForm' ? 'subForm' : 'form',
    items: serializeControls(formChild?.children),
    children: childGroups.map(g => serializeGroup(g)),  // ← 递归
    ...formChild?.config
  }
}
```

**反序列化（config → layout）**：

```typescript
function parseGroup(cfg: any, idx: number): TreeNode {
  const group: TreeNode = {
    i: cfg.id || `group_${idx}`,
    nodeKind: 'group',
    children: []
  }
  // 解析表单子项
  if (cfg.type !== 'slot') {
    group.children.push(parseFormChild(cfg))
  }
  // 递归解析子分组
  if (Array.isArray(cfg.children)) {
    cfg.children.forEach((child, ci) => {
      group.children.push(parseGroup(child, ci))  // ← 递归
    })
  }
  return group
}
```

**FrameworkGroupConfig 需扩展**：

```typescript
interface FrameworkGroupConfig {
  id: string
  title: string
  type: FrameworkGroupType
  items?: FormConfig['items']
  children?: FrameworkGroupConfig[]  // ← 新增：子分组
  // ...
}
```

#### 3.2.5 选中状态（useSelection.ts）

**目标**：支持深层节点的选中与定位。

```typescript
// 当前：selectedFormChild = { formId, childId }
// 嵌套：需要完整路径
interface SelectionPath {
  groupPath: string[]   // 从根到目标分组的 id 路径，如 ['group_1', 'group_1_2']
  formId?: string       // 所属表单/子表单 id
  childId?: string      // 具体控件 id
}

const selection = ref<SelectionPath | null>(null)
```

`selectedItem` computed 需沿路径递归查找：

```typescript
const selectedItem = computed(() => {
  const sel = selection.value
  if (!sel) return null
  return findByPath(layout.value, sel.groupPath, sel.childId)
})
```

#### 3.2.6 属性面板（framework.vue + SettingsPanel.vue）

无需结构改动——属性面板已通过 `selectedItem` 驱动，只要 `selectedItem` 返回正确的节点引用即可。

但分组属性区可能需要增加「子分组管理」入口，如在基础属性中显示子分组列表，支持排序、删除。

### 3.3 嵌套的最大深度限制

建议设定最大嵌套深度（如 3 层），原因：
- **用户体验**：过深的嵌套导致设计区视觉混乱
- **性能**：每层分组都有 vue-grid-layout 实例，嵌套过多影响渲染
- **配置可读性**：JSON 配置过深不易维护

```typescript
const MAX_NESTING_DEPTH = 3

function canNest(parentGroup: TreeNode): boolean {
  let depth = 0
  let current = parentGroup
  while (current) {
    depth++
    current = findParentGroup(current.i)
  }
  return depth < MAX_NESTING_DEPTH
}
```

### 3.4 实施路线

建议分三个阶段渐进式推进：

#### 阶段 1：类型统一（低风险）

- 将 `LayoutItem` 和 `FormChildItem` 统一为 `TreeNode`
- 现有功能不变，只是类型重命名
- 验证：全部现有测试通过

#### 阶段 2：单层嵌套（中等风险）

- `TreeNode.children` 允许同时包含分组和表单子项
- 渲染层支持一层嵌套
- 拖拽支持向分组内拖入子分组
- 序列化支持 `children` 字段
- 验证：能创建一层子分组

#### 阶段 3：递归嵌套（较高风险）

- 抽取 `GroupNode.vue` 递归组件
- 选中状态改为路径模式
- 所有 composable 支持递归操作
- 加入深度限制
- 验证：多层嵌套完整可用

### 3.5 注意事项

1. **vue-grid-layout 实例数量**：每个表单/子表单区域都是一个 GridLayout 实例，嵌套会线性增长。深层嵌套时注意性能。
2. **拖拽冲突**：子分组的拖拽排序与父级拖拽排序可能冲突（handle 选择器需严格限定）。
3. **预览渲染**：PreviewDialog 渲染嵌套配置时需递归处理。
4. **向后兼容**：`FrameworkConfig.children` 应为可选字段，旧配置（无 children）仍可正常回显。
5. **ID 唯一性**：嵌套后 id 生成需包含父级路径信息（如 `group_1_child_2`），避免全局冲突。

---

## 四、总结

本次改动将「新增分组」从弹窗交互改为拖拽交互，通过 SortableJS group 隔离机制实现了容器与基础控件的独立拖拽通道。整体改动集中在拖拽链路和分组管理两个 composable，未改变数据模型和渲染结构。

容器嵌套是对当前三层固定树的扩展，核心挑战在于将现有的扁平渲染和操作逻辑改为递归模式。建议按照「类型统一 → 单层嵌套 → 递归嵌套」的渐进路线实施，每个阶段可独立交付验证。
