# RFC 136: 时间轴多元素块系统 **状态**: 实施中 **创建时间**: 2026-01-28 **优先级**: 高 ## 背景 当前时间轴架构中,每个分镜在每个轨道上只有一个元素块。新需求要求支持一个分镜在某些轨道上有多个独立的元素块,每个块显示不同的内容。 ## 需求 ### 1. 对白轨道(原"台词"轨道) - 轨道名称从"台词"改为"对白" - 一个分镜可以有多段对白(对话形式) - 每段对白是独立的块,显示对白文字 - 多个对白块在分镜时间范围内等分宽度 - 没有对白时不显示块 ### 2. 视频轨道 - 一个分镜可以有多个视频片段 - 每个视频是独立的块 - 多个视频块等分宽度 ### 3. 音效轨道 - 显示音效名称 - 一个分镜可以有多个音效 - 多个音效块等分宽度 ### 4. 配音轨道 - 一个分镜可以有多个配音 - 多个配音块等分宽度 ### 5. 宽度计算规则 - 分镜宽度 = 240px(示例) - 2个元素块 = 每个 120px(等分) - 需要添加间隔(2px gap) ## 技术方案 ### 数据结构 #### 1. 分镜对白 ```typescript export interface StoryboardDialogue { id: string; text: string; characterName?: string; displayOrder: number; } export interface Storyboard { // ... 其他字段 dialogues?: StoryboardDialogue[]; } ``` #### 2. 关联资源 ```typescript export interface Video { // ... 其他字段 storyboardId?: string; displayOrder?: number; } export interface SoundEffect { // ... 其他字段 storyboardId?: string; displayOrder?: number; } export interface Voiceover { // ... 其他字段 storyboardId?: string; displayOrder?: number; } ``` ### 渲染逻辑 #### TimelineTrack 组件变更 **当前逻辑**: ```typescript // 一个分镜 = 一个 TimelineItem track.items.map((item) => ( )) ``` **新逻辑**: ```typescript // 一个分镜可能 = 多个 TimelineItem track.items.map((item) => { const elements = getElementsForTrack(item, track.type); if (elements.length === 0) return null; return elements.map((element, index) => { const { startTime, endTime } = calculateElementPosition( item.startTime, item.endTime, index, elements.length ); return ( ); }); }) ``` #### 元素获取函数 ```typescript function getElementsForTrack( item: TimelineItem, trackType: TrackType, storyboard?: StoryboardWithUI, videos?: Video[], soundEffects?: SoundEffect[], voiceovers?: Voiceover[] ): ElementBlock[] { switch (trackType) { case 'subtitle': // 对白轨道 return storyboard?.dialogues?.map(d => ({ id: d.id, content: d.text, type: 'dialogue' })) || []; case 'video': return videos ?.filter(v => v.storyboardId === item.storyboardId) .sort((a, b) => (a.displayOrder || 0) - (b.displayOrder || 0)) .map(v => ({ id: v.id, content: v.name, type: 'video' })) || []; case 'sound': return soundEffects ?.filter(s => s.storyboardId === item.storyboardId) .sort((a, b) => (a.displayOrder || 0) - (b.displayOrder || 0)) .map(s => ({ id: s.id, content: s.name, type: 'sound' })) || []; case 'voice': return voiceovers ?.filter(v => v.storyboardId === item.storyboardId) .sort((a, b) => (a.displayOrder || 0) - (b.displayOrder || 0)) .map(v => ({ id: v.id, content: v.text, type: 'voiceover' })) || []; default: return [{ id: item.id, content: item.title, type: 'default' }]; } } ``` #### 位置计算函数 ```typescript function calculateElementPosition( storyboardStart: number, storyboardEnd: number, elementIndex: number, totalElements: number ): { startTime: number; endTime: number } { const duration = storyboardEnd - storyboardStart; const elementDuration = duration / totalElements; return { startTime: storyboardStart + (elementDuration * elementIndex), endTime: storyboardStart + (elementDuration * (elementIndex + 1)) }; } ``` ### TimelineItem 组件变更 添加 `content` prop 用于显示元素内容: ```typescript interface TimelineItemProps { // ... 现有 props content?: string; // 元素内容(对白文字、音效名称等) contentType?: 'dialogue' | 'video' | 'sound' | 'voiceover'; } // 渲染内容 {contentType === 'dialogue' && ( {content} )} {contentType === 'sound' && ( {content} )} ``` ## 实施步骤 ### 阶段 1:数据层 ✅ - [x] 扩展类型定义 - [x] 创建 Mock 数据 - [x] 更新导出 ### 阶段 2:工具函数(进行中) - [ ] 创建 `utils/timeline-elements.ts` - [ ] 实现 `getElementsForTrack()` - [ ] 实现 `calculateElementPosition()` ### 阶段 3:组件更新 - [ ] 更新 `TimelineTrack.tsx` 支持多元素渲染 - [ ] 更新 `TimelineItem.tsx` 显示元素内容 - [ ] 处理间隔和宽度计算 ### 阶段 4:轨道名称 - [ ] 将"台词"改为"对白" - [ ] 更新 i18n 翻译 ### 阶段 5:测试 - [ ] 单个元素块测试 - [ ] 多个元素块测试 - [ ] 等分宽度验证 - [ ] 间隔显示验证 ## 技术挑战 ### 1. 宽度计算精度 - 等分可能产生小数像素 - 需要处理舍入误差 - 间隔会影响实际可用宽度 **解决方案**: ```typescript const GAP = 2; // 间隔 2px const totalGaps = (totalElements - 1) * GAP; const availableWidth = totalWidth - totalGaps; const elementWidth = availableWidth / totalElements; ``` ### 2. 性能优化 - 多个元素块增加渲染负担 - 需要优化 memo 和 key **解决方案**: - 使用稳定的 key(element.id) - memo 包裹 TimelineItem - 避免不必要的重渲染 ### 3. 交互处理 - 点击、拖拽需要识别具体元素 - 选中状态管理更复杂 **解决方案**: - 使用复合 ID:`${storyboardId}-${elementId}` - 扩展选中状态数据结构 ## 向后兼容 - 现有单元素块轨道(分镜、资源)不受影响 - 新字段都是可选的 - 没有对白/视频/音效时显示空轨道 ## 后续优化 1. **拖拽调整顺序**:支持拖拽改变元素块顺序 2. **独立时间调整**:每个元素块可以独立调整时长 3. **批量操作**:选中多个元素块进行批量操作 4. **视觉优化**:不同类型元素块使用不同样式 ## 参考 - [时间轴性能优化](../changelogs/2026-01-28-timeline-performance-optimization.md) - [时间轴元素状态系统](./135-timeline-element-status-system.md)