feat: 剪辑计划编辑器前端实现 #106

Merged
xiaoxia merged 2 commits from feat/editing-planner-frontend into develop 2026-06-29 15:28:45 +08:00
Owner

变更内容

新增功能

  • 剪辑计划编辑器页面 (/editing-planner)
    • 三栏布局:左侧模板面板 / 中间预览+时间线 / 右侧设置面板
    • 支持 4 种模式切换(画中画/人物口播/一镜到底/口播+混剪)
    • 时间线片段支持拖拽排序
    • 标题/字幕/BGM 设置面板
  • 我的模板页面 (/my-templates)
    • 卡片视图展示用户模板
    • 支持搜索、分类筛选
    • 编辑/复制/删除/使用模板生成

技术实现

  • API 模块使用 mock 数据,后端 API 就绪后可无缝替换
  • 遵循项目现有代码风格(Ant Design + Zustand + React Query)
  • TypeScript 类型检查通过

文件变更

  • apps/web/src/api/editingPlanner.ts — API 模块 + mock 数据
  • apps/web/src/pages/editing-planner/ — 编辑器页面
  • apps/web/src/pages/my-templates/ — 我的模板页面
  • apps/web/src/router/index.tsx — 新增路由
  • apps/web/src/components/layout/Header.tsx — 新增导航项

🤖 Generated with Claude Code

## 变更内容 ### 新增功能 - **剪辑计划编辑器页面** (`/editing-planner`) - 三栏布局:左侧模板面板 / 中间预览+时间线 / 右侧设置面板 - 支持 4 种模式切换(画中画/人物口播/一镜到底/口播+混剪) - 时间线片段支持拖拽排序 - 标题/字幕/BGM 设置面板 - **我的模板页面** (`/my-templates`) - 卡片视图展示用户模板 - 支持搜索、分类筛选 - 编辑/复制/删除/使用模板生成 ### 技术实现 - API 模块使用 mock 数据,后端 API 就绪后可无缝替换 - 遵循项目现有代码风格(Ant Design + Zustand + React Query) - TypeScript 类型检查通过 ### 文件变更 - `apps/web/src/api/editingPlanner.ts` — API 模块 + mock 数据 - `apps/web/src/pages/editing-planner/` — 编辑器页面 - `apps/web/src/pages/my-templates/` — 我的模板页面 - `apps/web/src/router/index.tsx` — 新增路由 - `apps/web/src/components/layout/Header.tsx` — 新增导航项 🤖 Generated with [Claude Code](https://claude.com/claude-code)
xiaoxia added 1 commit 2026-06-29 14:35:55 +08:00
feat: 剪辑计划编辑器前端实现
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
fff47a7a3d
- 新增剪辑计划编辑器页面 (三栏布局)
  - 左侧:模板面板(搜索/筛选/加载模板)
  - 中间:预览区 + 时间线(片段拖拽排序)
  - 右侧:标题/字幕/BGM 设置面板
- 支持 4 种模式切换(画中画/人物口播/一镜到底/口播+混剪)
- 新增「我的模板」页面(卡片视图/编辑/复制/删除/生成)
- 新增 API 模块(mock 数据,后端就绪后替换)
- 更新路由和导航菜单

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Owner

🔍 代码审查报告 — PR #106 剪辑计划编辑器前端

Head SHA: fff47a7a3d1d71dcd78272ecd4060ae17be21586
变更范围: 7 文件,+1654 行,全新代码
审查结论: 不通过(2 个 P0 需修复)

⚠️ 注:PR 编号为 #106,非 #104(#104 是已关闭的 Alembic 双头修复 PR)


做得好的地方

  1. 三栏布局设计合理:左侧模板面板 / 中间预览+时间线 / 右侧设置面板,交互清晰
  2. React Query + Mutation 模式正确:queryKey 设计合理,invalidateQueries 使用正确,错误处理用了 __msgShown 防重复 toast(与 PR #99 全局拦截器配合)
  3. 模式切换的业务规则联动:一镜到底锁定 1 片段、口播+混剪自动填充 material_type,与后端校验逻辑一致
  4. CSS 响应式设计:有 1200px/900px/768px 断点适配,移动端有汉堡菜单和侧边抽屉
  5. 拖拽排序实现简洁:用原生 HTML5 drag API 实现片段重排,自动重新编号 segment_order
  6. Mock 数据设计:3 个 mock 模板覆盖不同模式,切换真实 API 时改动集中在 editingPlanner.ts 一处

🔴 P0 — 必须修复

P0-1: TemplateMode 类型值与后端 API 完全不匹配

前端定义:

export type TemplateMode = '画中画' | '人物口播' | '一镜到底' | '口播+混剪';

后端 API Schema(PR #105)存储和传输的是英文字符串:pip / voice_over / one_take / voice_pip(后端正在修复 person_narrationvoice_overvoice_over_mixvoice_pip)。

如果 mock 切到真实 API,前端发送 mode: "画中画",后端会返回 422(不在 VALID_MODES 中)。 后端返回 mode: "pip",前端类型校验也会失败。

修复方案:

export type TemplateMode = 'pip' | 'voice_over' | 'one_take' | 'voice_pip';

// 显示层用映射
const MODE_LABELS: Record<TemplateMode, string> = {
  pip: '画中画',
  voice_over: '人物口播',
  one_take: '一镜到底',
  voice_pip: '口播+混剪',
};

在 UI 展示处用 MODE_LABELS[tpl.mode] 显示中文,数据存储和 API 传输统一用英文值。

P0-2: EditingPlanner 不读取 URL 参数,MyTemplates 的编辑/生成跳转失效

MyTemplates 页面通过 URL 参数跳转:

navigate(\`/editing-planner?template=\${tpl.id}\`);          // 编辑
navigate(\`/editing-planner?template=\${tpl.id}&generate=1\`);  // 使用模板生成

但 EditingPlanner 组件完全没有 useSearchParamsuseEffectuseLocation,不会读取 URL 参数。结果是:

  • 点击「编辑」→ 跳到空白编辑器,未加载指定模板
  • 点击「使用」→ 跳到空白编辑器,不会自动弹出生成弹窗

修复方案: 在 EditingPlanner 中添加 useEffect,在模板列表加载完成后读取 template 参数并调用 loadTemplate,读取 generate=1 参数并自动打开生成弹窗。


🟡 P1 — 建议修复

P1-1: EditingPlanner.tsx 单文件 848 行,应拆分

编辑器组件过于庞大,包含了模式切换、片段管理、标题/字幕/BGM 设置面板、保存弹窗、生成弹窗等全部逻辑。建议拆分为:

  • ModeSelector — 模式切换按钮组
  • TimelinePanel — 时间线 + 片段拖拽
  • TitleSettingsPanel — 标题设置
  • SubtitleSettingsPanel — 字幕设置
  • BgmSettingsPanel — BGM 设置
  • SaveTemplateModal — 保存弹窗
  • GenerateModal — 生成弹窗

P1-2: GenerateFromTemplatePayload 与后端不对齐

前端:{ voiceover_id: string } — 发送配音 ID
后端 PR #105:{ voiceover_duration: Optional[float] } — 接收配音时长(秒)

语义完全不同。后端需要配音时长来校验 ±30% 偏差,前端发送的是配音文件 ID。

修复: 对齐为 { voiceover_duration: number },在弹窗中让用户输入配音时长,或在用户选择配音后自动传入时长。

P1-3: 分类保存用 Input 手动输入,应关联 TemplateCategory

保存模板弹窗中,分类字段用 Input 手动输入文字。但后端有 template_categories 表 + 分类 CRUD API。用户可能输入已存在的分类名变体(如「生活 vlog」vs「生活」),导致分类碎片化。

修复: 改用 Select,选项来源为 getTemplateCategories(),同时支持创建新分类。

P1-4: SaveTemplatePayload 缺少 estimated_duration 字段

后端 Schema 中 CreateTemplateRequest 包含 estimated_duration。前端 SaveTemplatePayload 没有这个字段,mock 函数中手动计算了。切换到真实 API 时,需要在前端计算并传入,或者让后端自动计算(但这需要后端修改)。


🔵 P2 — 建议改进

P2-1: Slider 未校验 duration_min <= duration_max

片段的最短/最长时长各用一个 Slider,没有联动约束。用户可以把最短设为 30s、最长设为 5s,导致后端校验拒绝或剪辑逻辑异常。建议用 RangeSlider 或在 updateSegment 中做约束。

P2-2: Mock 数据的 getEditingTemplates 不支持分页

后端 API 有 skip + limit 分页,mock 函数返回全量数据。切换到真实 API 后,前端可能需要添加分页逻辑或确认 limit=200 足够。

P2-3: 模块级全局计数器 _segId

let _segId = 0 在模块级别,每次页面刷新重置。在 mock 模式下不会冲突,但如果多组件实例共存(如未来路由 keep-alive),可能产生 ID 碰撞。建议用 useRefcrypto.randomUUID()


📋 总结

级别 数量 关键项
P0 2 TemplateMode 值不匹配后端、URL 参数未读取导致跳转失效
P1 4 组件过大需拆分、generate payload 不对齐、分类应关联 API、缺少 estimated_duration
P2 3 Slider 未约束 min/max、mock 无分页、全局计数器

P0 修复后即可通过审查。 UI 设计和交互逻辑整体质量不错,主要是前后端对齐问题和 URL 参数处理的遗漏。

## 🔍 代码审查报告 — PR #106 剪辑计划编辑器前端 **Head SHA:** `fff47a7a3d1d71dcd78272ecd4060ae17be21586` **变更范围:** 7 文件,+1654 行,全新代码 **审查结论:❌ 不通过(2 个 P0 需修复)** > ⚠️ 注:PR 编号为 #106,非 #104(#104 是已关闭的 Alembic 双头修复 PR) --- ### ✅ 做得好的地方 1. **三栏布局设计合理**:左侧模板面板 / 中间预览+时间线 / 右侧设置面板,交互清晰 2. **React Query + Mutation 模式正确**:queryKey 设计合理,invalidateQueries 使用正确,错误处理用了 `__msgShown` 防重复 toast(与 PR #99 全局拦截器配合) 3. **模式切换的业务规则联动**:一镜到底锁定 1 片段、口播+混剪自动填充 material_type,与后端校验逻辑一致 4. **CSS 响应式设计**:有 1200px/900px/768px 断点适配,移动端有汉堡菜单和侧边抽屉 5. **拖拽排序实现简洁**:用原生 HTML5 drag API 实现片段重排,自动重新编号 segment_order 6. **Mock 数据设计**:3 个 mock 模板覆盖不同模式,切换真实 API 时改动集中在 editingPlanner.ts 一处 --- ### 🔴 P0 — 必须修复 #### P0-1: TemplateMode 类型值与后端 API 完全不匹配 前端定义: ```typescript export type TemplateMode = '画中画' | '人物口播' | '一镜到底' | '口播+混剪'; ``` 后端 API Schema(PR #105)存储和传输的是英文字符串:`pip` / `voice_over` / `one_take` / `voice_pip`(后端正在修复 `person_narration` → `voice_over`,`voice_over_mix` → `voice_pip`)。 **如果 mock 切到真实 API,前端发送 `mode: "画中画"`,后端会返回 422(不在 VALID_MODES 中)。** 后端返回 `mode: "pip"`,前端类型校验也会失败。 **修复方案:** ```typescript export type TemplateMode = 'pip' | 'voice_over' | 'one_take' | 'voice_pip'; // 显示层用映射 const MODE_LABELS: Record<TemplateMode, string> = { pip: '画中画', voice_over: '人物口播', one_take: '一镜到底', voice_pip: '口播+混剪', }; ``` 在 UI 展示处用 `MODE_LABELS[tpl.mode]` 显示中文,数据存储和 API 传输统一用英文值。 #### P0-2: EditingPlanner 不读取 URL 参数,MyTemplates 的编辑/生成跳转失效 MyTemplates 页面通过 URL 参数跳转: ```typescript navigate(\`/editing-planner?template=\${tpl.id}\`); // 编辑 navigate(\`/editing-planner?template=\${tpl.id}&generate=1\`); // 使用模板生成 ``` 但 EditingPlanner 组件**完全没有 `useSearchParams`、`useEffect` 或 `useLocation`**,不会读取 URL 参数。结果是: - 点击「编辑」→ 跳到空白编辑器,未加载指定模板 - 点击「使用」→ 跳到空白编辑器,不会自动弹出生成弹窗 **修复方案:** 在 EditingPlanner 中添加 `useEffect`,在模板列表加载完成后读取 `template` 参数并调用 `loadTemplate`,读取 `generate=1` 参数并自动打开生成弹窗。 --- ### 🟡 P1 — 建议修复 #### P1-1: EditingPlanner.tsx 单文件 848 行,应拆分 编辑器组件过于庞大,包含了模式切换、片段管理、标题/字幕/BGM 设置面板、保存弹窗、生成弹窗等全部逻辑。建议拆分为: - `ModeSelector` — 模式切换按钮组 - `TimelinePanel` — 时间线 + 片段拖拽 - `TitleSettingsPanel` — 标题设置 - `SubtitleSettingsPanel` — 字幕设置 - `BgmSettingsPanel` — BGM 设置 - `SaveTemplateModal` — 保存弹窗 - `GenerateModal` — 生成弹窗 #### P1-2: `GenerateFromTemplatePayload` 与后端不对齐 前端:`{ voiceover_id: string }` — 发送配音 ID 后端 PR #105:`{ voiceover_duration: Optional[float] }` — 接收配音时长(秒) 语义完全不同。后端需要配音时长来校验 ±30% 偏差,前端发送的是配音文件 ID。 **修复:** 对齐为 `{ voiceover_duration: number }`,在弹窗中让用户输入配音时长,或在用户选择配音后自动传入时长。 #### P1-3: 分类保存用 Input 手动输入,应关联 TemplateCategory 保存模板弹窗中,分类字段用 `Input` 手动输入文字。但后端有 `template_categories` 表 + 分类 CRUD API。用户可能输入已存在的分类名变体(如「生活 vlog」vs「生活」),导致分类碎片化。 **修复:** 改用 `Select`,选项来源为 `getTemplateCategories()`,同时支持创建新分类。 #### P1-4: `SaveTemplatePayload` 缺少 `estimated_duration` 字段 后端 Schema 中 `CreateTemplateRequest` 包含 `estimated_duration`。前端 `SaveTemplatePayload` 没有这个字段,mock 函数中手动计算了。切换到真实 API 时,需要在前端计算并传入,或者让后端自动计算(但这需要后端修改)。 --- ### 🔵 P2 — 建议改进 #### P2-1: Slider 未校验 `duration_min <= duration_max` 片段的最短/最长时长各用一个 Slider,没有联动约束。用户可以把最短设为 30s、最长设为 5s,导致后端校验拒绝或剪辑逻辑异常。建议用 `RangeSlider` 或在 `updateSegment` 中做约束。 #### P2-2: Mock 数据的 `getEditingTemplates` 不支持分页 后端 API 有 `skip` + `limit` 分页,mock 函数返回全量数据。切换到真实 API 后,前端可能需要添加分页逻辑或确认 limit=200 足够。 #### P2-3: 模块级全局计数器 `_segId` `let _segId = 0` 在模块级别,每次页面刷新重置。在 mock 模式下不会冲突,但如果多组件实例共存(如未来路由 keep-alive),可能产生 ID 碰撞。建议用 `useRef` 或 `crypto.randomUUID()`。 --- ### 📋 总结 | 级别 | 数量 | 关键项 | |------|------|--------| | P0 | 2 | TemplateMode 值不匹配后端、URL 参数未读取导致跳转失效 | | P1 | 4 | 组件过大需拆分、generate payload 不对齐、分类应关联 API、缺少 estimated_duration | | P2 | 3 | Slider 未约束 min/max、mock 无分页、全局计数器 | **P0 修复后即可通过审查。** UI 设计和交互逻辑整体质量不错,主要是前后端对齐问题和 URL 参数处理的遗漏。
xiaoxia added 1 commit 2026-06-29 15:18:50 +08:00
fix: 修复 PR#106 代码审计问题
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
067439a1dd
P0-1: TemplateMode 改为英文枚举值(pip/voice_over/one_take/voice_pip),显示层通过 MODE_LABELS 映射中文
P0-2: EditingPlanner 通过 useSearchParams 读取 ?template=xxx&generate=1,自动加载模板并打开发成弹窗
P1-3: 848行组件拆分为 5 个子组件(TemplatePanel/TimelinePanel/SettingsPanel/SaveModal/GenerateModal)
P1-4: GenerateFromTemplatePayload voiceover_id(string) → voiceover_duration(number)
P1-5: SaveModal 分类字段 Input → Select,关联后端 categories API
P1-6: SaveTemplatePayload 补充 estimated_duration 字段
P2: TimelinePanel Slider 约束 min≤max

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Owner

所有代码审计问题已修复,请复审。

已修复项:

  • P0-1: TemplateMode 改为英文枚举值,显示层映射中文
  • P0-2: EditingPlanner 读取 URL 参数 ?template=xxx&generate=1
  • P1-3: 848行组件拆分为 5 个子组件
  • P1-4: voiceover_id → voiceover_duration
  • P1-5: 分类 Input → Select
  • P1-6: SaveTemplatePayload 补充 estimated_duration
  • P2: Slider min≤max 约束

TypeScript 检查通过,零错误。

所有代码审计问题已修复,请复审。 **已修复项:** - P0-1: TemplateMode 改为英文枚举值,显示层映射中文 - P0-2: EditingPlanner 读取 URL 参数 ?template=xxx&generate=1 - P1-3: 848行组件拆分为 5 个子组件 - P1-4: voiceover_id → voiceover_duration - P1-5: 分类 Input → Select - P1-6: SaveTemplatePayload 补充 estimated_duration - P2: Slider min≤max 约束 TypeScript 检查通过,零错误。
Author
Owner

PR #106 复审通过

Head SHA: 067439a1ddef

上轮问题修复确认

# 级别 问题 状态
1 P0 TemplateMode 中文值 → 英文枚举 "pip" | "voice_over" | "one_take" | "voice_pip",与后端 EditingMode 枚举完全对齐;MODE_LABELS 提供中文显示映射
2 P0 EditingPlanner 不读取 URL 参数 已加 useSearchParams,useEffect 自动加载 ?template=xxx 并在 generate=1 时打开生成弹窗
3 P1 组件 848 行过大 拆分为 5 个子组件:TemplatePanel(130) / TimelinePanel(195) / SettingsPanel(229) / SaveModal(88) / GenerateModal(58),主组件降至 438 行
4 P1 voiceover_id → voiceover_duration GenerateFromTemplatePayload.voiceover_duration: number,mutation 正确传参
5 P1 分类 Input → Select SaveModal 使用 Select 关联 getTemplateCategories() API
6 P1 SaveTemplatePayload 缺 estimated_duration 已补充,保存时 calcEstimatedDuration(segments) 自动计算
7 P2 Slider min≤max 约束 最短 Slider max={seg.duration_max},最长 Slider min={seg.duration_min},互锁

代码质量评价

  • 组件拆分合理,职责清晰,props 传递规范
  • TypeScript 类型定义完整,与后端 Schema 对齐
  • Mock 数据结构与真实 API 返回格式一致,切换成本低
  • 拖拽排序、模式切换联动(one_take 锁定单片段、voice_pip 强制 material_type)逻辑正确

同意合并。

## ✅ PR #106 复审通过 **Head SHA:** `067439a1ddef` ### 上轮问题修复确认 | # | 级别 | 问题 | 状态 | |---|------|------|------| | 1 | P0 | TemplateMode 中文值 → 英文枚举 | ✅ `"pip" \| "voice_over" \| "one_take" \| "voice_pip"`,与后端 EditingMode 枚举完全对齐;MODE_LABELS 提供中文显示映射 | | 2 | P0 | EditingPlanner 不读取 URL 参数 | ✅ 已加 `useSearchParams`,useEffect 自动加载 `?template=xxx` 并在 `generate=1` 时打开生成弹窗 | | 3 | P1 | 组件 848 行过大 | ✅ 拆分为 5 个子组件:TemplatePanel(130) / TimelinePanel(195) / SettingsPanel(229) / SaveModal(88) / GenerateModal(58),主组件降至 438 行 | | 4 | P1 | voiceover_id → voiceover_duration | ✅ `GenerateFromTemplatePayload.voiceover_duration: number`,mutation 正确传参 | | 5 | P1 | 分类 Input → Select | ✅ SaveModal 使用 `Select` 关联 `getTemplateCategories()` API | | 6 | P1 | SaveTemplatePayload 缺 estimated_duration | ✅ 已补充,保存时 `calcEstimatedDuration(segments)` 自动计算 | | 7 | P2 | Slider min≤max 约束 | ✅ 最短 Slider `max={seg.duration_max}`,最长 Slider `min={seg.duration_min}`,互锁 | ### 代码质量评价 - 组件拆分合理,职责清晰,props 传递规范 - TypeScript 类型定义完整,与后端 Schema 对齐 - Mock 数据结构与真实 API 返回格式一致,切换成本低 - 拖拽排序、模式切换联动(one_take 锁定单片段、voice_pip 强制 material_type)逻辑正确 **同意合并。**
xiaoxia merged commit 9631272cfb into develop 2026-06-29 15:28:45 +08:00
Author
Owner

v0.1.91 测试问题修复 (a29e30eb)

问题1 [高优先级]: editingPlanner.ts mock → 真实 API

  • 移除全部 mock 数据(MOCK_TEMPLATES、MOCK_CATEGORIES、delay 等)
  • 7 个函数全部改为通过 apiClient 调用 /api/v1/templates 端点
  • 保留所有类型定义不变,前端页面无需修改

问题2 [低优先级]: 清理遗留文件

  • 已删除 src/api/editPlans.ts(确认无任何引用)

⚠️ 注意: 本地代码库中未发现后端 /api/v1/templates 的路由定义(apps/api/app/api/routes/ 下无 templates 路由文件)。如果端点确实已上线,请确认后端代码是否已合并到当前分支。TypeScript 检查通过

## v0.1.91 测试问题修复 (a29e30eb) **问题1 [高优先级]**: editingPlanner.ts mock → 真实 API - 移除全部 mock 数据(MOCK_TEMPLATES、MOCK_CATEGORIES、delay 等) - 7 个函数全部改为通过 apiClient 调用 `/api/v1/templates` 端点 - 保留所有类型定义不变,前端页面无需修改 **问题2 [低优先级]**: 清理遗留文件 - 已删除 `src/api/editPlans.ts`(确认无任何引用) ⚠️ **注意**: 本地代码库中未发现后端 `/api/v1/templates` 的路由定义(`apps/api/app/api/routes/` 下无 templates 路由文件)。如果端点确实已上线,请确认后端代码是否已合并到当前分支。TypeScript 检查通过 ✅
Sign in to join this conversation.