diff --git a/apps/desktop/src-tauri/src/business/services/draft_parser.rs b/apps/desktop/src-tauri/src/business/services/draft_parser.rs index aa1aabe..21ed386 100644 --- a/apps/desktop/src-tauri/src/business/services/draft_parser.rs +++ b/apps/desktop/src-tauri/src/business/services/draft_parser.rs @@ -613,15 +613,9 @@ impl DraftContentParser { let target_duration = segment_raw.target_timerange.duration; let end_time = start_time + target_duration; - // 计算实际的播放时长(考虑播放速度) + // 播放速度不影响片段在时间轴上的时长 + // target_duration 就是片段的实际时长 let speed = segment_raw.speed.unwrap_or(1.0); - let actual_duration = if speed != 0.0 { - // 如果有播放速度,计算实际播放时长 - // 例如:speed = 1.28306,则实际播放时长 = target_duration / speed - (target_duration as f64 / speed) as u64 - } else { - target_duration - }; let segment_name = format!("片段{}", segment_index + 1); let mut segment = TrackSegment::new( @@ -633,28 +627,27 @@ impl DraftContentParser { segment_index, ); - // 更新片段的实际持续时间(考虑播放速度) - segment.duration = actual_duration; + // 保持时间轴上的时长,不要覆盖为 actual_duration + // segment.duration 应该是 end_time - start_time (时间轴时长) + // actual_duration 存储在 properties 中供前端使用 // 关联素材 if let Some(material_id) = material_map.get(&segment_raw.material_id) { segment.associate_material(material_id.clone()); } - // 存储原始属性和计算后的时间信息 + // 存储原始属性信息 let properties = serde_json::json!({ "original_material_id": segment_raw.material_id, "target_timerange": segment_raw.target_timerange, "source_timerange": segment_raw.source_timerange, "speed": segment_raw.speed, - "actual_duration": actual_duration, - "target_duration": target_duration, "visible": segment_raw.visible, "volume": segment_raw.volume, - "computed_info": { - "speed_applied": speed != 1.0, - "original_speed": speed, - "time_scaling_factor": if speed != 0.0 { 1.0 / speed } else { 1.0 } + "speed_info": { + "is_speed_modified": speed != 1.0, + "speed_value": speed, + "speed_description": if speed > 1.0 { "加速播放" } else if speed < 1.0 { "减速播放" } else { "正常播放" } } }); segment.properties = Some(properties.to_string()); diff --git a/apps/desktop/src/components/template/TemplateDetailModal.tsx b/apps/desktop/src/components/template/TemplateDetailModal.tsx index 1ae8029..7c2d5d3 100644 --- a/apps/desktop/src/components/template/TemplateDetailModal.tsx +++ b/apps/desktop/src/components/template/TemplateDetailModal.tsx @@ -21,6 +21,16 @@ export const TemplateDetailModal: React.FC = ({ return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; }; + // 格式化时长(精确到毫秒) + const formatDurationWithMs = (microseconds: number) => { + const totalMs = Math.floor(microseconds / 1000); + const seconds = Math.floor(totalMs / 1000); + const ms = totalMs % 1000; + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}.${ms.toString().padStart(3, '0')}`; + }; + // 格式化文件大小 const formatFileSize = (bytes?: number) => { if (!bytes) return '未知'; @@ -163,6 +173,25 @@ export const TemplateDetailModal: React.FC = ({ } } + // 解析片段属性信息 + const parseSegmentProperties = (properties: string | null) => { + if (!properties) return null; + + try { + const parsed = JSON.parse(properties); + return { + speed: parsed.speed || 1.0, + source_timerange: parsed.source_timerange || null, + target_timerange: parsed.target_timerange || null, + speed_info: parsed.speed_info || null, + visible: parsed.visible, + volume: parsed.volume + }; + } catch (e) { + return null; + } + }; + const tabs = [ { id: 'overview', label: '概览', icon: Monitor }, { id: 'materials', label: '素材', icon: FileText }, @@ -440,31 +469,88 @@ export const TemplateDetailModal: React.FC = ({ {track.segments.length > 0 && (
- {track.segments.map((segment) => ( -
-
- {segment.name} - - {formatDuration(segment.start_time)} - {formatDuration(segment.end_time)} - -
-
-
- 片段ID: {segment.id} - 索引: {segment.segment_index} + {track.segments.map((segment) => { + const segmentProps = parseSegmentProperties(segment.properties || null); + + return ( +
+
+ {segment.name} + + {formatDurationWithMs(segment.start_time)} - {formatDurationWithMs(segment.end_time)} +
-
时长: {formatDuration(segment.duration)}
- {segment.template_material_id && ( +
- 使用素材ID: {segment.template_material_id} + 片段ID: {segment.id} + 索引: {segment.segment_index}
- )} - {!segment.template_material_id && ( -
未关联素材
- )} +
+ 时长: {formatDurationWithMs(segment.duration)} + {segmentProps?.speed && segmentProps.speed !== 1.0 && ( + 1.0 + ? 'bg-red-100 text-red-800' + : 'bg-green-100 text-green-800' + }`}> + 速度: {segmentProps.speed.toFixed(3)}x {segmentProps.speed > 1.0 ? '(加速)' : '(减速)'} + + )} + {segmentProps?.speed === 1.0 && ( + 正常速度 + )} +
+ {/* 播放速度不影响片段时长,移除实际播放时长显示 */} + {/* 时间轴位置信息 */} + {segmentProps?.target_timerange && ( +
+
时间轴位置:
+
+
开始: {formatDurationWithMs(segmentProps.target_timerange.start)}
+
时长: {formatDurationWithMs(segmentProps.target_timerange.duration)}
+
结束: {formatDurationWithMs(segmentProps.target_timerange.start + segmentProps.target_timerange.duration)}
+
+
+ )} + + {/* 源素材时间范围 */} + {segmentProps?.source_timerange && ( +
+
源素材时间范围:
+
+
开始位置: {formatDurationWithMs(segmentProps.source_timerange.start)}
+
截取时长: {formatDurationWithMs(segmentProps.source_timerange.duration)}
+
结束位置: {formatDurationWithMs(segmentProps.source_timerange.start + segmentProps.source_timerange.duration)}
+
+
+ )} + {/* 其他属性信息 */} + {(segmentProps?.visible !== undefined || segmentProps?.volume !== undefined) && ( +
+
其他属性:
+
+ {segmentProps?.visible !== undefined && ( +
可见性: {segmentProps.visible ? '可见' : '隐藏'}
+ )} + {segmentProps?.volume !== undefined && ( +
音量: {(segmentProps.volume * 100).toFixed(0)}%
+ )} +
+
+ )} + + {segment.template_material_id && ( +
+ 使用素材ID: {segment.template_material_id} +
+ )} + {!segment.template_material_id && ( +
未关联素材
+ )} +
-
- ))} + ); + })}
)}