diff --git a/apps/web/src/pages/editing-planner/components/clip-properties/ClipDetailSection.tsx b/apps/web/src/pages/editing-planner/components/clip-properties/ClipDetailSection.tsx index f2a48dbe9..af9aa97d3 100644 --- a/apps/web/src/pages/editing-planner/components/clip-properties/ClipDetailSection.tsx +++ b/apps/web/src/pages/editing-planner/components/clip-properties/ClipDetailSection.tsx @@ -1,266 +1,5 @@ /** - * 片段详情区块 + * ClipDetailSection 入口(向后兼容) + * 实际实现位于 ./clip-detail-section/ 目录 */ -import React from "react" -import { useNavigate } from "react-router-dom" -import { TRANSITION_OPTIONS } from "@/api/template-editor" -import type { ClipData, ClipType } from "@/pages/editing-planner/types" -import { CLIP_TYPE_ICONS, CLIP_TYPE_LABELS } from "@/pages/editing-planner/constants/clipProperties" -import { getGenderLabel } from "@/pages/editing-planner/utils/clipProperties" -import type { AssetItem } from "@/api/assets" -import type { TemplateMode } from "@/api/editing-planner" - -interface ClipDetailSectionProps { - clip: ClipData - currentMode: TemplateMode - voiceMaterials?: AssetItem[] - voiceMaterialsLoading?: boolean - onClipUpdate: (clipId: string, data: Partial) => void - onRefreshVoiceMaterials?: () => void - onClipVoiceSelect?: (clipId: string, asset: AssetItem | null) => void - onOpenTransitionDrawer?: (clipId: string) => void - onOpenSpeedDrawer?: (clipId: string) => void - onOpenTtsDrawer?: (clipId: string) => void - previewingId: string | null - onPreviewVoice: (asset: AssetItem) => void - onStopPreview: () => void -} - -const ClipDetailSection: React.FC = ({ - clip, - currentMode, - voiceMaterials = [], - voiceMaterialsLoading = false, - onClipUpdate, - onRefreshVoiceMaterials, - onClipVoiceSelect, - onOpenTransitionDrawer, - onOpenSpeedDrawer, - onOpenTtsDrawer, - previewingId, - onPreviewVoice, - onStopPreview, -}) => { - const navigate = useNavigate() - - const isTypeDisabled = (t: ClipType) => { - if (currentMode === "pip") return t !== "pip" - if (currentMode === "voice_over") return t !== "voice" - return false - } - - const transitionLabel = (() => { - const t = clip.transition - if (!t || t.type === "none") return "无转场" - const opt = TRANSITION_OPTIONS.find((o) => o.value === t.type) - return `${opt?.label ?? t.type} · ${t.duration.toFixed(1)}s` - })() - - const speedLabel = clip.speed ? `${clip.speed.rate.toFixed(2)}x` : "1.00x" - - const ttsLabel = (() => { - const tts = clip.tts_config - if (!tts || tts.mode === "none") return "无配音" - if (tts.mode === "upload") return "上传配音" - return `TTS · ${tts.voice_id ? "已选音色" : "未选音色"}` - })() - - return ( -
-
- 🎞️ - 片段详情 -
- -
- {/* 类型选择器 */} -
-
类型
-
- {(["voice", "pip"] as ClipType[]).map((t) => { - const disabled = isTypeDisabled(t) - return ( - - ) - })} -
-
- - {/* 时长 */} -
-
时长
-
- - onClipUpdate(clip.id, { - duration: Math.max(1, Math.min(120, Number(e.target.value) || 1)), - }) - } - /> - -
-
- - {/* 转场效果入口 */} - {onOpenTransitionDrawer && ( -
- -
- )} - - {/* 播放速度入口 */} - {onOpenSpeedDrawer && ( -
- -
- )} - - {/* TTS 配音入口 */} - {onOpenTtsDrawer && ( -
- -
- )} - - {/* 素材起始时间 — 仅 voice 类型显示 */} - {clip.type === "voice" && ( -
-
素材起始时间
-
- - onClipUpdate(clip.id, { - startOffset: Math.max(0, Math.min(9999, Number(e.target.value) || 0)), - }) - } - /> - -
-
- )} - - {/* 配音素材选择 — 仅 voice 类型显示 */} - {clip.type === "voice" && ( -
-
- 配音素材 - {onRefreshVoiceMaterials && ( - - )} -
- - {voiceMaterialsLoading && voiceMaterials.length === 0 ? ( -
加载中...
- ) : ( - <> -
- - {clip.voice_asset_id && ( - - )} -
- {voiceMaterials.length === 0 && ( -
暂无配音素材,请先上传
- )} - - )} - - -
- )} -
-
- ) -} - -export default ClipDetailSection +export { default } from "./clip-detail-section" diff --git a/apps/web/src/pages/editing-planner/components/clip-properties/clip-detail-section/AdvancedEntries.tsx b/apps/web/src/pages/editing-planner/components/clip-properties/clip-detail-section/AdvancedEntries.tsx new file mode 100644 index 000000000..0ab96c7be --- /dev/null +++ b/apps/web/src/pages/editing-planner/components/clip-properties/clip-detail-section/AdvancedEntries.tsx @@ -0,0 +1,85 @@ +import React from "react" +import { TRANSITION_OPTIONS } from "@/api/template-editor" +import type { ClipData } from "@/pages/editing-planner/types" + +interface AdvancedEntriesProps { + clip: ClipData + onOpenTransitionDrawer?: (clipId: string) => void + onOpenSpeedDrawer?: (clipId: string) => void + onOpenTtsDrawer?: (clipId: string) => void +} + +/** + * 高级功能入口按钮(转场/调速/TTS) + */ +export const AdvancedEntries: React.FC = ({ + clip, + onOpenTransitionDrawer, + onOpenSpeedDrawer, + onOpenTtsDrawer, +}) => { + const transitionLabel = (() => { + const t = clip.transition + if (!t || t.type === "none") return "无转场" + const opt = TRANSITION_OPTIONS.find((o) => o.value === t.type) + return `${opt?.label ?? t.type} · ${t.duration.toFixed(1)}s` + })() + + const speedLabel = clip.speed ? `${clip.speed.rate.toFixed(2)}x` : "1.00x" + + const ttsLabel = (() => { + const tts = clip.tts_config + if (!tts || tts.mode === "none") return "无配音" + if (tts.mode === "upload") return "上传配音" + return `TTS · ${tts.voice_id ? "已选音色" : "未选音色"}` + })() + + return ( + <> + {/* 转场效果入口 */} + {onOpenTransitionDrawer && ( +
+ +
+ )} + + {/* 播放速度入口 */} + {onOpenSpeedDrawer && ( +
+ +
+ )} + + {/* TTS 配音入口 */} + {onOpenTtsDrawer && ( +
+ +
+ )} + + ) +} diff --git a/apps/web/src/pages/editing-planner/components/clip-properties/clip-detail-section/ClipTypeAndDuration.tsx b/apps/web/src/pages/editing-planner/components/clip-properties/clip-detail-section/ClipTypeAndDuration.tsx new file mode 100644 index 000000000..af558e09a --- /dev/null +++ b/apps/web/src/pages/editing-planner/components/clip-properties/clip-detail-section/ClipTypeAndDuration.tsx @@ -0,0 +1,69 @@ +import React from "react" +import type { ClipData, ClipType } from "@/pages/editing-planner/types" +import { CLIP_TYPE_ICONS, CLIP_TYPE_LABELS } from "@/pages/editing-planner/constants/clipProperties" +import type { TemplateMode } from "@/api/editing-planner" + +interface ClipTypeAndDurationProps { + clip: ClipData + currentMode: TemplateMode + onClipUpdate: (clipId: string, data: Partial) => void +} + +/** + * 片段类型选择 + 时长设置 + */ +export const ClipTypeAndDuration: React.FC = ({ + clip, + currentMode, + onClipUpdate, +}) => { + const isTypeDisabled = (t: ClipType) => { + if (currentMode === "pip") return t !== "pip" + if (currentMode === "voice_over") return t !== "voice" + return false + } + + return ( + <> + {/* 类型选择器 */} +
+
类型
+
+ {(["voice", "pip"] as ClipType[]).map((t) => { + const disabled = isTypeDisabled(t) + return ( + + ) + })} +
+
+ + {/* 时长 */} +
+
时长
+
+ + onClipUpdate(clip.id, { + duration: Math.max(1, Math.min(120, Number(e.target.value) || 1)), + }) + } + /> + +
+
+ + ) +} diff --git a/apps/web/src/pages/editing-planner/components/clip-properties/clip-detail-section/VoiceMaterialSection.tsx b/apps/web/src/pages/editing-planner/components/clip-properties/clip-detail-section/VoiceMaterialSection.tsx new file mode 100644 index 000000000..9b77a1624 --- /dev/null +++ b/apps/web/src/pages/editing-planner/components/clip-properties/clip-detail-section/VoiceMaterialSection.tsx @@ -0,0 +1,132 @@ +import React from "react" +import { useNavigate } from "react-router-dom" +import type { ClipData } from "@/pages/editing-planner/types" +import { getGenderLabel } from "@/pages/editing-planner/utils/clipProperties" +import type { AssetItem } from "@/api/assets" + +interface VoiceMaterialSectionProps { + clip: ClipData + voiceMaterials: AssetItem[] + voiceMaterialsLoading: boolean + onClipUpdate: (clipId: string, data: Partial) => void + onRefreshVoiceMaterials?: () => void + onClipVoiceSelect?: (clipId: string, asset: AssetItem | null) => void + previewingId: string | null + onPreviewVoice: (asset: AssetItem) => void + onStopPreview: () => void +} + +/** + * 配音素材选择区(仅 voice 类型显示) + */ +export const VoiceMaterialSection: React.FC = ({ + clip, + voiceMaterials, + voiceMaterialsLoading, + onClipUpdate, + onRefreshVoiceMaterials, + onClipVoiceSelect, + previewingId, + onPreviewVoice, + onStopPreview, +}) => { + const navigate = useNavigate() + + return ( + <> + {/* 素材起始时间 */} +
+
素材起始时间
+
+ + onClipUpdate(clip.id, { + startOffset: Math.max(0, Math.min(9999, Number(e.target.value) || 0)), + }) + } + /> + +
+
+ + {/* 配音素材选择 */} +
+
+ 配音素材 + {onRefreshVoiceMaterials && ( + + )} +
+ + {voiceMaterialsLoading && voiceMaterials.length === 0 ? ( +
加载中...
+ ) : ( + <> +
+ + {clip.voice_asset_id && ( + + )} +
+ {voiceMaterials.length === 0 && ( +
暂无配音素材,请先上传
+ )} + + )} + + +
+ + ) +} diff --git a/apps/web/src/pages/editing-planner/components/clip-properties/clip-detail-section/index.tsx b/apps/web/src/pages/editing-planner/components/clip-properties/clip-detail-section/index.tsx new file mode 100644 index 000000000..5ba4ea27e --- /dev/null +++ b/apps/web/src/pages/editing-planner/components/clip-properties/clip-detail-section/index.tsx @@ -0,0 +1,78 @@ +/** + * 片段详情区块 + */ +import React from "react" +import type { ClipData } from "@/pages/editing-planner/types" +import type { AssetItem } from "@/api/assets" +import type { TemplateMode } from "@/api/editing-planner" +import { ClipTypeAndDuration } from "./ClipTypeAndDuration" +import { AdvancedEntries } from "./AdvancedEntries" +import { VoiceMaterialSection } from "./VoiceMaterialSection" + +interface ClipDetailSectionProps { + clip: ClipData + currentMode: TemplateMode + voiceMaterials?: AssetItem[] + voiceMaterialsLoading?: boolean + onClipUpdate: (clipId: string, data: Partial) => void + onRefreshVoiceMaterials?: () => void + onClipVoiceSelect?: (clipId: string, asset: AssetItem | null) => void + onOpenTransitionDrawer?: (clipId: string) => void + onOpenSpeedDrawer?: (clipId: string) => void + onOpenTtsDrawer?: (clipId: string) => void + previewingId: string | null + onPreviewVoice: (asset: AssetItem) => void + onStopPreview: () => void +} + +const ClipDetailSection: React.FC = ({ + clip, + currentMode, + voiceMaterials = [], + voiceMaterialsLoading = false, + onClipUpdate, + onRefreshVoiceMaterials, + onClipVoiceSelect, + onOpenTransitionDrawer, + onOpenSpeedDrawer, + onOpenTtsDrawer, + previewingId, + onPreviewVoice, + onStopPreview, +}) => { + return ( +
+
+ 🎞️ + 片段详情 +
+ +
+ + + + + {clip.type === "voice" && ( + + )} +
+
+ ) +} + +export default ClipDetailSection