# 执行计划 - 前端适配模块父子层级结构

> **文档类型**: 执行计划文档
> **来源文档**: `_PRD_需求文档_前端适配模块父子层级.md`
> **创建日期**: 2026-07-22
> **作者**: czj
> **预计工期**: 2 小时
> **实施状态**: ⏳ 待实施

---

## 一、执行概述

### 1.1 目标

前端适配后端已实现的模块父子层级结构，包括类型定义、树形选择器、编辑弹窗父模块选择。

### 1.2 改动范围

| 层级 | 文件数 | 改动类型 |
|------|--------|----------|
| 类型定义 | 1 | Module 接口增加字段 |
| API 层 | 1 | 新增 buildTree 函数 |
| 页面组件 | 3 | el-select → el-tree-select |
| 新增代码 | - | buildTree 辅助函数 |

---

## 二、执行步骤

### Phase 1: TypeScript 类型定义 (预计 10 分钟)

#### Step 1.1: 更新 Module 接口

**文件**: `frontend/src/types/module.ts`

```typescript
/** 测试模块接口 */
export interface Module {
  /** 模块唯一标识 */
  id: string
  /** 模块名称 */
  name: string
  /** 模块描述 */
  description: string
  /** 模块图标标识 */
  icon: string
  /** 排序序号 */
  order: number
  /** 模块类型 standard/custom */
  moduleType?: string
  /** 父模块ID（支持层级结构） */
  parentId?: string | null
  /** 子模块列表（树形结构） */
  children?: Module[]
  /** 用例数量（扩展字段） */
  caseCount?: number
  /** 通过率（扩展字段） */
  passRate?: number
  /** 创建时间 */
  createdAt: string
  /** 更新时间 */
  updatedAt: string
}

/** 创建模块请求参数 */
export interface CreateModuleRequest {
  /** 模块名称 */
  name: string
  /** 模块描述 */
  description?: string
  /** 模块图标 */
  icon?: string
  /** 模块类型 standard/custom */
  module_type?: string
  /** 父模块ID */
  parent_id?: string | null
}

/** 更新模块请求参数 */
export interface UpdateModuleRequest {
  name?: string
  description?: string
  icon?: string
  order?: number
  module_type?: string
  /** 父模块ID（空字符串表示清除父模块） */
  parent_id?: string | null
}
```

---

### Phase 2: API 层增加 buildTree 函数 (预计 10 分钟)

#### Step 2.1: 新增并导出 buildTree 函数

**文件**: `frontend/src/api/modules.ts`

在文件末尾添加：

```typescript
/**
 * 将扁平模块列表构建为树形结构
 * 
 * @param items 扁平模块列表
 * @returns 树形结构数据
 */
export function buildModuleTree(items: Module[]): Module[] {
  const map = new Map<string, Module>()
  const roots: Module[] = []

  // 建立映射：先复制并初始化 children
  items.forEach(item => {
    map.set(item.id, { ...item, children: [] })
  })

  // 构建树：根据 parentId 归类到父模块的 children 中
  items.forEach(item => {
    const node = map.get(item.id)!
    if (item.parentId && map.has(item.parentId)) {
      const parent = map.get(item.parentId)!
      if (!parent.children) {
        parent.children = []
      }
      parent.children.push(node)
    } else {
      // 无父模块或父模块不存在，作为根节点
      roots.push(node)
    }
  })

  return roots
}
```

---

### Phase 3: Cases.vue 模块选择器树形化 (预计 20 分钟)

#### Step 3.1: 筛选区模块选择器

**文件**: `frontend/src/views/Cases.vue`

**原代码**（约第14-27行）：

```vue
<el-select
  v-model="filterModuleId"
  placeholder="选择模块"
  clearable
  style="width: 200px"
  @change="loadCases"
>
  <el-option
    v-for="m in modules"
    :key="m.id"
    :label="m.name"
    :value="m.id"
  />
</el-select>
```

**改为**：

```vue
<el-tree-select
  v-model="filterModuleId"
  :data="moduleTreeData"
  :props="{ label: 'name', value: 'id', children: 'children' }"
  placeholder="选择模块"
  clearable
  check-strictly
  style="width: 200px"
  @change="loadCases"
/>
```

#### Step 3.2: 编辑弹窗模块选择器

**原代码**（约第230-237行）：

```vue
<el-form-item label="所属模块" prop="module_id">
  <el-select v-model="editForm.module_id" placeholder="选择模块" style="width: 100%">
    <el-option
      v-for="m in modules"
      :key="m.id"
      :label="m.name"
      :value="m.id"
    />
  </el-select>
</el-form-item>
```

**改为**：

```vue
<el-form-item label="所属模块" prop="module_id">
  <el-tree-select
    v-model="editForm.module_id"
    :data="moduleTreeData"
    :props="{ label: 'name', value: 'id', children: 'children' }"
    placeholder="选择模块"
    check-strictly
    style="width: 100%"
  />
</el-form-item>
```

#### Step 3.3: 增加 moduleTreeData 计算属性

在 `<script setup>` 中增加：

```typescript
import { buildModuleTree } from '@/api/modules'

// 模块树形数据（用于 el-tree-select）
const moduleTreeData = computed(() => buildModuleTree(modules.value))
```

---

### Phase 4: Recorder.vue 模块选择器树形化 (预计 15 分钟)

#### Step 4.1: 模块选择器

**文件**: `frontend/src/views/Recorder.vue`

找到模块选择器（约第31-44行），将 `el-select` 改为 `el-tree-select`：

```vue
<el-tree-select
  v-model="recorderForm.moduleId"
  :data="moduleTreeData"
  :props="{ label: 'name', value: 'id', children: 'children' }"
  placeholder="选择模块"
  check-strictly
  style="width: 100%"
/>
```

#### Step 4.2: 增加 moduleTreeData 计算属性

```typescript
import { buildModuleTree } from '@/api/modules'

const moduleTreeData = computed(() => buildModuleTree(modules.value))
```

---

### Phase 5: Modules.vue 编辑弹窗支持父模块选择 (预计 25 分钟)

#### Step 5.1: formData 增加 parent_id 字段

**文件**: `frontend/src/views/Modules.vue`

在 `formData` 定义中增加：

```typescript
const formData = ref<CreateModuleRequest>({
  name: '',
  description: '',
  icon: 'folder',
  module_type: 'standard',
  parent_id: null  // 新增
})
```

#### Step 5.2: 编辑弹窗增加父模块字段

在编辑弹窗的表单中增加：

```vue
<el-form-item label="父模块">
  <el-tree-select
    v-model="formData.parent_id"
    :data="parentModuleTreeData"
    :props="{ label: 'name', value: 'id', children: 'children' }"
    placeholder="选择父模块（可选）"
    clearable
    check-strictly
    style="width: 100%"
  />
</el-form-item>
```

#### Step 5.3: 增加 parentModuleTreeData 计算属性（排除当前模块及其子模块）

```typescript
import { buildModuleTree } from '@/api/modules'

// 编辑时排除当前模块及其子模块（防循环引用）
const parentModuleTreeData = computed(() => {
  if (!isEdit.value || !editingId.value) {
    return buildModuleTree(modules.value)
  }
  
  // 递归获取所有子模块ID
  const getChildIds = (parentId: string): string[] => {
    const children = modules.value.filter(m => m.parentId === parentId)
    return children.flatMap(c => [c.id, ...getChildIds(c.id)])
  }
  
  const excludeIds = new Set([editingId.value, ...getChildIds(editingId.value)])
  const filteredModules = modules.value.filter(m => !excludeIds.has(m.id))
  
  return buildModuleTree(filteredModules)
})
```

#### Step 5.4: showEditDialog 填充 parent_id

```typescript
const showEditDialog = (module: Module) => {
  isEdit.value = true
  editingId.value = module.id
  formData.value = {
    name: module.name,
    description: module.description,
    icon: module.icon,
    module_type: module.moduleType || 'standard',
    parent_id: module.parentId || null  // 新增
  }
  dialogVisible.value = true
}
```

#### Step 5.5: showCreateDialog 初始化 parent_id

```typescript
const showCreateDialog = () => {
  isEdit.value = false
  editingId.value = ''
  formData.value = {
    name: '',
    description: '',
    icon: 'folder',
    module_type: currentType.value,
    parent_id: null  // 新增
  }
  dialogVisible.value = true
}
```

---

### Phase 6: 验证 (预计 10 分钟)

#### Step 6.1: TypeScript 编译检查

```bash
cd frontend
npm run build
```

预期：无类型错误。

#### Step 6.2: 功能验证清单

| 测试项 | 操作 | 预期结果 |
|--------|------|----------|
| Cases.vue 筛选模块 | 打开用例管理，点击模块筛选下拉 | 展示树形层级 |
| Cases.vue 编辑用例 | 编辑用例，查看模块选择器 | 展示树形层级 |
| Recorder.vue 模块选择 | 打开录制器，查看模块选择器 | 展示树形层级 |
| Modules.vue 编辑父模块 | 编辑模块，选择父模块，保存 | 保存成功，刷新后层级正确 |
| 循环引用防护 | 编辑模块，查看父模块选择器 | 不包含自己及子模块 |
| 清空父模块 | 编辑模块，清空父模块，保存 | 模块变为顶级模块 |

---

## 三、风险评估

| 风险项 | 可能性 | 影响 | 缓解措施 |
|--------|--------|------|----------|
| el-tree-select 兼容性 | 低 | 中 | Element Plus 2.2+ 支持，项目已满足 |
| 大量模块时性能 | 低 | 低 | 虚拟滚动 + 层级折叠 |
| 循环引用遗漏 | 中 | 高 | edit 时严格排除当前模块及其子模块 |
| parentId 字段名不一致 | 低 | 中 | 后端返回 camelCase（parentId），前端直接使用 |

---

## 四、验收清单

- [ ] TypeScript 类型定义更新
- [ ] buildModuleTree 函数实现并导出
- [ ] Cases.vue 筛选区模块选择器树形化
- [ ] Cases.vue 编辑弹窗模块选择器树形化
- [ ] Recorder.vue 模块选择器树形化
- [ ] Modules.vue 编辑弹窗增加父模块选择
- [ ] 循环引用防护实现
- [ ] TypeScript 编译通过
- [ ] 功能验证通过

---

## 五、相关文件

| 文件 | 用途 |
|------|------|
| `frontend/src/types/module.ts` | TypeScript 类型定义 |
| `frontend/src/api/modules.ts` | API 层 + buildModuleTree |
| `frontend/src/views/Cases.vue` | 用例管理页面 |
| `frontend/src/views/Recorder.vue` | 用例录制器页面 |
| `frontend/src/views/Modules.vue` | 模块管理页面 |

---

*本文档由 Claude Code 生成，遵循项目执行计划文档规范。*