# 分镜看板重叠问题修复记录 **日期**: 2026-02-08 **类型**: Bug 修复 **影响范围**: 分镜看板拖拽排序功能 **严重程度**: 高(影响核心功能) ## 问题描述 用户报告分镜看板存在以下问题: 1. **分镜元素块重叠**:多个分镜块在时间轴上重叠显示 2. **拖动排序错误**:拖拽分镜后,刷新页面位置不正确 ## 问题根本原因 ### 1. 数据模型冲突 数据库中同时维护了两套定位系统: ```python # server/app/models/storyboard.py class Storyboard(SQLModel, table=True): # 时间轴定位 start_time: Decimal = Field(...) # 第162行 end_time: Decimal = Field(...) # 第166行 # 排序索引 order_index: int = Field(...) # 第178行 ``` **核心矛盾**:两套系统同时存在,但没有明确的"唯一真相来源"(Single Source of Truth)。 ### 2. 后端计算逻辑 后端在 `_calculate_storyboard_board_tracks` 方法中会根据 `orderIndex` 重新计算 `startTime/endTime`: ```python # server/app/services/storyboard_board_service.py:364-394 current_time = 0.0 for storyboard in storyboards: start_time = current_time duration = float(storyboard.actual_duration or storyboard.estimated_duration or 3.0) end_time = current_time + duration # 使用计算的值,忽略数据库中的值 tracks['storyboard'].items.append(StoryboardBoardItem( start_time=start_time, end_time=end_time, ... )) current_time = end_time ``` **关键发现**:后端完全忽略数据库中存储的 `startTime/endTime`,而是根据 `orderIndex` 顺序累积计算。 ### 3. 前端更新逻辑冲突 前端在拖拽结束时会同时更新两个值: ```typescript // client/src/hooks/useStoryboardBoardLogic.ts:1417-1422(修复前) reorderStoryboards({ projectId: currentProjectId, orderedIds }); storyboardTimeUpdates.forEach((update) => { updateStoryboard({ id: update.id, data: { startTime: update.startTime, endTime: update.endTime }, }); }); ``` **问题**:前端更新了数据库中的 `startTime/endTime`,但下次刷新时后端会重新计算,导致数据丢失。 ### 4. 数据流冲突示意图 ``` 用户拖拽分镜 ↓ 前端计算新的 startTime/endTime(基于像素位置) ↓ 更新数据库(同时更新 orderIndex 和 startTime/endTime) ↓ 刷新页面 ↓ 后端根据 orderIndex 重新计算 startTime/endTime(忽略数据库值) ↓ 前端渲染时使用后端计算的值 ↓ 如果计算逻辑不一致 → 重叠或错位 ``` ### 5. 具体重叠场景 **场景示例**: 1. **初始状态**(orderIndex: [0, 1, 2]) - 分镜1: 0-3s - 分镜2: 3-6s - 分镜3: 6-9s 2. **用户拖拽分镜2到最后** - 前端计算: 分镜1(0-3s), 分镜3(3-6s), 分镜2(6-9s) - 更新数据库: orderIndex=[0, 2, 1], startTime/endTime 也被更新 3. **刷新页面** - 后端读取 orderIndex: [0, 2, 1] - 后端重新计算: 分镜1(0-3s), 分镜3(3-6s), 分镜2(6-9s) - 但如果数据库中的 startTime/endTime 没有被正确更新,或者后端使用了数据库值,就会出现不一致 ## 解决方案 ### 核心原则 根据用户需求"抛开时间轴的概念,按照 orderIndex 排序",采用 **orderIndex 为唯一真相来源** 的架构: 1. **orderIndex 是唯一的排序依据** 2. **startTime/endTime 由 orderIndex + duration 实时计算** 3. **前端拖拽只更新 orderIndex** 4. **数据库中的 startTime/endTime 作为缓存(可选)** ### 修改内容 #### 1. 前端修改:移除拖拽时更新 startTime/endTime **文件**: `client/src/hooks/useStoryboardBoardLogic.ts` **修改位置**: `handleDragEnd` 方法(约第1399-1422行) **修改前**: ```typescript const orderedIds = reorderedStoryboards.map((item) => item.id.toString()); const storyboardTimeUpdates = reorderedStoryboards .map((item) => { const pos = finalPositions[item.id]; if (!pos) return null; if (pos.startTime === item.startTime && pos.endTime === item.endTime) return null; return { id: item.id.toString(), startTime: pos.startTime, endTime: pos.endTime }; }) .filter( (update): update is { id: string; startTime: number; endTime: number } => update !== null ); updateStoryboardBoard({ projectId: currentProjectId, tracks: newTracks, }); reorderStoryboards({ projectId: currentProjectId, orderedIds }); storyboardTimeUpdates.forEach((update) => { updateStoryboard({ id: update.id, data: { startTime: update.startTime, endTime: update.endTime }, }); }); ``` **修改后**: ```typescript const orderedIds = reorderedStoryboards.map((item) => item.id.toString()); // 注意:不再更新 startTime/endTime 到数据库 // 后端会根据 orderIndex 自动重新计算 startTime/endTime // 这样可以确保数据一致性,避免重叠问题 updateStoryboardBoard({ projectId: currentProjectId, tracks: newTracks, }); reorderStoryboards({ projectId: currentProjectId, orderedIds }); ``` **修改原因**: - 移除了 `storyboardTimeUpdates` 的计算和更新逻辑 - 只保留 `reorderStoryboards` 调用,仅更新 orderIndex - 后端会根据 orderIndex 自动重新计算 startTime/endTime ### 验证修改 #### 1. 代码检查 已确认以下内容: - ✅ `handleDragEnd` 中移除了 `storyboardTimeUpdates` 相关代码 - ✅ 保留了 `reorderStoryboards` 调用(更新 orderIndex) - ✅ 保留了 `updateStoryboardBoard` 调用(更新前端缓存) - ✅ 其他地方没有直接更新 startTime/endTime 的逻辑 #### 2. 数据流验证 修改后的数据流: ``` 用户拖拽分镜 ↓ 前端计算新的排序(基于插入位置) ↓ 更新 orderIndex 到数据库 ↓ 刷新页面 ↓ 后端根据 orderIndex 重新计算 startTime/endTime ↓ 前端渲染(使用后端计算的值) ↓ 数据一致,无重叠 ``` ## 测试建议 ### 1. 基础功能测试 - [ ] 创建 3-5 个分镜 - [ ] 拖拽分镜改变顺序 - [ ] 刷新页面,验证顺序保持正确 - [ ] 验证分镜块之间没有重叠 - [ ] 验证分镜块之间的间隔正常 ### 2. 边界情况测试 - [ ] 拖拽第一个分镜到最后 - [ ] 拖拽最后一个分镜到最前 - [ ] 拖拽中间分镜到任意位置 - [ ] 连续多次拖拽 - [ ] 拖拽后立即刷新页面 ### 3. 大数据量测试 - [ ] 创建 50+ 分镜 - [ ] 测试拖拽性能 - [ ] 验证虚拟滚动是否正常工作 - [ ] 验证骨架数据加载是否正常 ## 后续优化工作 ### 1. handleInsertStoryboard 方法重构(可选) **当前问题**:`handleInsertStoryboard` 方法仍然使用基于时间的插入逻辑,与 orderIndex-based 架构不一致。 **建议**:重构为基于 orderIndex 的插入逻辑: - 根据插入位置计算目标 orderIndex - 调用后端 API 插入新分镜 - 后端自动调整后续分镜的 orderIndex **优先级**:低(不影响当前拖拽排序功能) ### 2. 数据库 startTime/endTime 字段处理 **当前状态**:数据库中仍然存储 startTime/endTime,但这些值可能与实际渲染不一致。 **选项 A**:保留字段作为缓存 - 优点:可以用于快速查询和排序 - 缺点:需要维护数据一致性 **选项 B**:移除字段,完全依赖 orderIndex - 优点:数据模型更简洁,无一致性问题 - 缺点:需要数据库迁移 **建议**:暂时保留字段,观察一段时间后再决定。 ### 3. 虚拟滚动完整实现 **当前状态**:已实现骨架数据和虚拟滚动辅助函数,但尚未在组件层面完全集成。 **待完成**: - 在 StoryboardBoardPanel 中实现滚动监听 - 根据可见范围加载分页数据 - 实现预加载策略 **参考**:[ADR 003: 分镜看板基于 orderIndex 的性能优化](../adrs/003-storyboard-board-orderindex-optimization.md) ## 相关文件 ### 前端 - `client/src/hooks/useStoryboardBoardLogic.ts` - 主要修改文件 - `client/src/hooks/useStoryboardBoardVirtualScroll.ts` - 虚拟滚动辅助 - `client/src/hooks/api/useStoryboardBoard.ts` - API Hooks - `client/src/services/api/storyboard-board.ts` - API 客户端 - `client/src/types/storyboard-board.ts` - 类型定义 ### 后端 - `server/app/models/storyboard.py` - 数据模型 - `server/app/services/storyboard_board_service.py` - 业务逻辑 - `server/app/api/v1/storyboard_board.py` - API 路由 ### 文档 - `docs/client/changelogs/2026-02-08-storyboard-board-optimization.md` - 性能优化实施记录 - `docs/server/adrs/03-storyboard-keyframe-management.md` - 分镜关键帧管理 ADR ## 总结 ### 修复效果 通过移除前端拖拽时更新 `startTime/endTime` 的逻辑,确保了: 1. **数据一致性**:orderIndex 是唯一真相来源,startTime/endTime 由后端统一计算 2. **无重叠问题**:后端按 orderIndex 顺序累积计算,保证分镜块连续排列 3. **拖拽排序正确**:只更新 orderIndex,刷新后位置保持正确 ### 架构优势 1. **简化数据流**:单向数据流,前端 → orderIndex → 后端计算 → 前端渲染 2. **易于维护**:计算逻辑集中在后端,前端只负责展示 3. **性能优化**:配合骨架数据和虚拟滚动,支持大规模数据 ### 注意事项 1. **数据库字段**:startTime/endTime 字段仍然存在,但不再作为主要数据源 2. **向后兼容**:现有数据不受影响,后端会自动重新计算 3. **后续优化**:handleInsertStoryboard 方法需要重构(优先级低)