9.4 KiB
分镜看板重叠问题修复记录
日期: 2026-02-08 类型: Bug 修复 影响范围: 分镜看板拖拽排序功能 严重程度: 高(影响核心功能)
问题描述
用户报告分镜看板存在以下问题:
- 分镜元素块重叠:多个分镜块在时间轴上重叠显示
- 拖动排序错误:拖拽分镜后,刷新页面位置不正确
问题根本原因
1. 数据模型冲突
数据库中同时维护了两套定位系统:
# 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:
# 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. 前端更新逻辑冲突
前端在拖拽结束时会同时更新两个值:
// 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. 具体重叠场景
场景示例:
-
初始状态(orderIndex: [0, 1, 2])
- 分镜1: 0-3s
- 分镜2: 3-6s
- 分镜3: 6-9s
-
用户拖拽分镜2到最后
- 前端计算: 分镜1(0-3s), 分镜3(3-6s), 分镜2(6-9s)
- 更新数据库: orderIndex=[0, 2, 1], startTime/endTime 也被更新
-
刷新页面
- 后端读取 orderIndex: [0, 2, 1]
- 后端重新计算: 分镜1(0-3s), 分镜3(3-6s), 分镜2(6-9s)
- 但如果数据库中的 startTime/endTime 没有被正确更新,或者后端使用了数据库值,就会出现不一致
解决方案
核心原则
根据用户需求"抛开时间轴的概念,按照 orderIndex 排序",采用 orderIndex 为唯一真相来源 的架构:
- orderIndex 是唯一的排序依据
- startTime/endTime 由 orderIndex + duration 实时计算
- 前端拖拽只更新 orderIndex
- 数据库中的 startTime/endTime 作为缓存(可选)
修改内容
1. 前端修改:移除拖拽时更新 startTime/endTime
文件: client/src/hooks/useStoryboardBoardLogic.ts
修改位置: handleDragEnd 方法(约第1399-1422行)
修改前:
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 },
});
});
修改后:
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 的性能优化
相关文件
前端
client/src/hooks/useStoryboardBoardLogic.ts- 主要修改文件client/src/hooks/useStoryboardBoardVirtualScroll.ts- 虚拟滚动辅助client/src/hooks/api/useStoryboardBoard.ts- API Hooksclient/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 的逻辑,确保了:
- 数据一致性:orderIndex 是唯一真相来源,startTime/endTime 由后端统一计算
- 无重叠问题:后端按 orderIndex 顺序累积计算,保证分镜块连续排列
- 拖拽排序正确:只更新 orderIndex,刷新后位置保持正确
架构优势
- 简化数据流:单向数据流,前端 → orderIndex → 后端计算 → 前端渲染
- 易于维护:计算逻辑集中在后端,前端只负责展示
- 性能优化:配合骨架数据和虚拟滚动,支持大规模数据
注意事项
- 数据库字段:startTime/endTime 字段仍然存在,但不再作为主要数据源
- 向后兼容:现有数据不受影响,后端会自动重新计算
- 后续优化:handleInsertStoryboard 方法需要重构(优先级低)