# 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)