diff --git a/apps/web/src/pages/editing-planner/components/CoverSelector.tsx b/apps/web/src/pages/editing-planner/components/CoverSelector.tsx index 7c1784175..f4c64ef26 100644 --- a/apps/web/src/pages/editing-planner/components/CoverSelector.tsx +++ b/apps/web/src/pages/editing-planner/components/CoverSelector.tsx @@ -1,286 +1,5 @@ /** - * 封面选择器 - * 抽帧选封面 + 上传自定义封面 + 智能封面推荐 + * 封面选择器入口(向后兼容) + * 实际实现位于 ./cover-selector/ 目录 */ -import React, { useCallback, useRef, useState } from "react" -import { Drawer } from "antd" -import type { CoverConfig, CoverMode } from "../types" -import { DEFAULT_COVER_CONFIG } from "../types" - -interface CoverSelectorProps { - open: boolean - onClose: () => void - config: CoverConfig - onChange: (config: CoverConfig) => void - totalDuration: number -} - -/** 封面模式标签 */ -const MODE_LABELS: Record = { - auto: "智能封面", - frame: "抽帧选封面", - upload: "上传封面", -} - -/** 封面模式图标 */ -const MODE_ICONS: Record = { - auto: "🤖", - frame: "🎞️", - upload: "📤", -} - -const CoverSelector: React.FC = ({ - open, - onClose, - config, - onChange, - totalDuration, -}) => { - const fileInputRef = useRef(null) - const [isDragging, setIsDragging] = useState(false) - - const update = useCallback( - (partial: Partial) => { - onChange({ ...config, ...partial }) - }, - [config, onChange], - ) - - const handleReset = useCallback(() => { - onChange({ ...DEFAULT_COVER_CONFIG, enabled: config.enabled }) - }, [config.enabled, onChange]) - - /** 切换模式 */ - const handleModeChange = useCallback( - (mode: CoverMode) => { - update({ mode }) - }, - [update], - ) - - /** 处理文件上传 */ - const handleFileUpload = useCallback( - (file: File) => { - if (!file.type.startsWith("image/")) return - const reader = new FileReader() - reader.onload = (e) => { - const url = e.target?.result as string - update({ upload_url: url, thumbnail_url: url, mode: "upload" }) - } - reader.readAsDataURL(file) - }, - [update], - ) - - /** 拖拽上传 */ - const handleDrop = useCallback( - (e: React.DragEvent) => { - e.preventDefault() - setIsDragging(false) - const file = e.dataTransfer.files[0] - if (file) handleFileUpload(file) - }, - [handleFileUpload], - ) - - /** 使用 AI 推荐时间 */ - const handleUseAiSuggestion = useCallback(() => { - if (config.ai_suggested_time !== null) { - update({ frame_time: config.ai_suggested_time, mode: "frame" }) - } - }, [config.ai_suggested_time, update]) - - /** 格式化时间 */ - const formatTime = (seconds: number) => { - const m = Math.floor(seconds / 60) - const s = Math.floor(seconds % 60) - const ms = Math.floor((seconds % 1) * 10) - return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}.${ms}` - } - - return ( - - {/* 顶部开关 */} -
- 启用自定义封面 - -
- - {/* 模式选择 */} -
-
封面来源
-
- {(["auto", "frame", "upload"] as CoverMode[]).map((m) => ( - - ))} -
-
- - {/* 模式内容区 */} -
- {/* 智能封面 */} - {config.mode === "auto" && ( -
-
- AI 将分析视频内容,自动选择最具吸引力的画面作为封面。 -
- {config.ai_suggested_time !== null ? ( -
-
AI 推荐
-
- 推荐时间点:{formatTime(config.ai_suggested_time)} -
- -
- ) : ( -
-
- AI 分析中...(生成视频后自动推荐) -
- )} -
- )} - - {/* 抽帧选封面 */} - {config.mode === "frame" && ( -
-
-
- 🎞️ - {formatTime(config.frame_time)} -
-
-
-
- 拖动选择封面帧 - {formatTime(config.frame_time)} -
- update({ frame_time: Number(e.target.value) })} - /> -
- 00:00 - {formatTime(totalDuration)} -
-
- {/* 快捷时间点 */} -
- 快捷选帧: - {[0, 0.25, 0.5, 0.75].map((ratio) => { - const t = totalDuration * ratio - return ( - - ) - })} -
-
- )} - - {/* 上传封面 */} - {config.mode === "upload" && ( -
-
{ - e.preventDefault() - setIsDragging(true) - }} - onDragLeave={() => setIsDragging(false)} - onDrop={handleDrop} - onClick={() => fileInputRef.current?.click()} - > - {config.upload_url ? ( -
- 封面预览 -
点击更换
-
- ) : ( -
- 📤 - 点击或拖拽上传封面图片 - 支持 JPG / PNG,建议 16:9 比例 -
- )} - { - const file = e.target.files?.[0] - if (file) handleFileUpload(file) - }} - /> -
-
- )} -
- - {/* 封面预览 */} -
-
封面预览
-
- {config.upload_url ? ( - 封面预览 - ) : ( -
- 🖼️ - - {config.mode === "auto" - ? "AI 智能选择" - : config.mode === "frame" - ? `帧 ${formatTime(config.frame_time)}` - : "未上传封面"} - -
- )} -
16:9
-
-
- - {/* 底部 */} -
- -
- - ) -} - -export default CoverSelector +export { default } from "./cover-selector" diff --git a/apps/web/src/pages/editing-planner/components/cover-selector/CoverModePanels.tsx b/apps/web/src/pages/editing-planner/components/cover-selector/CoverModePanels.tsx new file mode 100644 index 000000000..d13dd5072 --- /dev/null +++ b/apps/web/src/pages/editing-planner/components/cover-selector/CoverModePanels.tsx @@ -0,0 +1,143 @@ +import React from "react" +import type { CoverConfig } from "../../types" + +interface CoverAutoModeProps { + config: CoverConfig + formatTime: (s: number) => string + onUseAiSuggestion: () => void +} + +/** 智能封面模式面板 */ +export const CoverAutoMode: React.FC = ({ + config, + formatTime, + onUseAiSuggestion, +}) => ( +
+
AI 将分析视频内容,自动选择最具吸引力的画面作为封面。
+ {config.ai_suggested_time !== null ? ( +
+
AI 推荐
+
推荐时间点:{formatTime(config.ai_suggested_time)}
+ +
+ ) : ( +
+
+ AI 分析中...(生成视频后自动推荐) +
+ )} +
+) + +interface CoverFrameModeProps { + config: CoverConfig + totalDuration: number + formatTime: (s: number) => string + onFrameTimeChange: (time: number) => void +} + +/** 抽帧选封面模式面板 */ +export const CoverFrameMode: React.FC = ({ + config, + totalDuration, + formatTime, + onFrameTimeChange, +}) => ( +
+
+
+ 🎞️ + {formatTime(config.frame_time)} +
+
+
+
+ 拖动选择封面帧 + {formatTime(config.frame_time)} +
+ onFrameTimeChange(Number(e.target.value))} + /> +
+ 00:00 + {formatTime(totalDuration)} +
+
+
+ 快捷选帧: + {[0, 0.25, 0.5, 0.75].map((ratio) => { + const t = totalDuration * ratio + return ( + + ) + })} +
+
+) + +interface CoverUploadModeProps { + config: CoverConfig + isDragging: boolean + fileInputRef: React.RefObject + onDragOver: (e: React.DragEvent) => void + onDragLeave: () => void + onDrop: (e: React.DragEvent) => void + onAreaClick: () => void + onFileChange: (file: File) => void +} + +/** 上传封面模式面板 */ +export const CoverUploadMode: React.FC = ({ + config, + isDragging, + fileInputRef, + onDragOver, + onDragLeave, + onDrop, + onAreaClick, + onFileChange, +}) => ( +
+
+ {config.upload_url ? ( +
+ 封面预览 +
点击更换
+
+ ) : ( +
+ 📤 + 点击或拖拽上传封面图片 + 支持 JPG / PNG,建议 16:9 比例 +
+ )} + { + const file = e.target.files?.[0] + if (file) onFileChange(file) + }} + /> +
+
+) diff --git a/apps/web/src/pages/editing-planner/components/cover-selector/index.tsx b/apps/web/src/pages/editing-planner/components/cover-selector/index.tsx new file mode 100644 index 000000000..6bb5a6d96 --- /dev/null +++ b/apps/web/src/pages/editing-planner/components/cover-selector/index.tsx @@ -0,0 +1,146 @@ +/** + * 封面选择器 + * 抽帧选封面 + 上传自定义封面 + 智能封面推荐 + */ +import React from "react" +import { Drawer } from "antd" +import type { CoverConfig, CoverMode } from "../../types" +import { useCoverSelector, MODE_LABELS, MODE_ICONS } from "./useCoverSelector" +import { CoverAutoMode, CoverFrameMode, CoverUploadMode } from "./CoverModePanels" + +interface CoverSelectorProps { + open: boolean + onClose: () => void + config: CoverConfig + onChange: (config: CoverConfig) => void + totalDuration: number +} + +const CoverSelector: React.FC = ({ + open, + onClose, + config, + onChange, + totalDuration, +}) => { + const { + fileInputRef, + isDragging, + setIsDragging, + update, + handleReset, + handleModeChange, + handleFileUpload, + handleDrop, + handleUseAiSuggestion, + formatTime, + } = useCoverSelector({ config, onChange }) + + return ( + + {/* 顶部开关 */} +
+ 启用自定义封面 + +
+ + {/* 模式选择 */} +
+
封面来源
+
+ {(["auto", "frame", "upload"] as CoverMode[]).map((m) => ( + + ))} +
+
+ + {/* 模式内容区 */} +
+ {config.mode === "auto" && ( + + )} + + {config.mode === "frame" && ( + update({ frame_time: t })} + /> + )} + + {config.mode === "upload" && ( + { + e.preventDefault() + setIsDragging(true) + }} + onDragLeave={() => setIsDragging(false)} + onDrop={handleDrop} + onAreaClick={() => fileInputRef.current?.click()} + onFileChange={handleFileUpload} + /> + )} +
+ + {/* 封面预览 */} +
+
封面预览
+
+ {config.upload_url ? ( + 封面预览 + ) : ( +
+ 🖼️ + + {config.mode === "auto" + ? "AI 智能选择" + : config.mode === "frame" + ? `帧 ${formatTime(config.frame_time)}` + : "未上传封面"} + +
+ )} +
16:9
+
+
+ + {/* 底部 */} +
+ +
+
+ ) +} + +export default CoverSelector diff --git a/apps/web/src/pages/editing-planner/components/cover-selector/useCoverSelector.ts b/apps/web/src/pages/editing-planner/components/cover-selector/useCoverSelector.ts new file mode 100644 index 000000000..dbb649e22 --- /dev/null +++ b/apps/web/src/pages/editing-planner/components/cover-selector/useCoverSelector.ts @@ -0,0 +1,98 @@ +import { useCallback, useRef, useState } from "react" +import type { CoverConfig, CoverMode } from "../../types" +import { DEFAULT_COVER_CONFIG } from "../../types" + +/** 封面模式标签 */ +export const MODE_LABELS: Record = { + auto: "智能封面", + frame: "抽帧选封面", + upload: "上传封面", +} + +/** 封面模式图标 */ +export const MODE_ICONS: Record = { + auto: "🤖", + frame: "🎞️", + upload: "📤", +} + +interface UseCoverSelectorOptions { + config: CoverConfig + onChange: (config: CoverConfig) => void +} + +/** + * 封面选择器 Hook + * 封装状态管理、文件上传、模式切换等逻辑 + */ +export function useCoverSelector({ config, onChange }: UseCoverSelectorOptions) { + const fileInputRef = useRef(null) + const [isDragging, setIsDragging] = useState(false) + + const update = useCallback( + (partial: Partial) => { + onChange({ ...config, ...partial }) + }, + [config, onChange], + ) + + const handleReset = useCallback(() => { + onChange({ ...DEFAULT_COVER_CONFIG, enabled: config.enabled }) + }, [config.enabled, onChange]) + + const handleModeChange = useCallback( + (mode: CoverMode) => { + update({ mode }) + }, + [update], + ) + + const handleFileUpload = useCallback( + (file: File) => { + if (!file.type.startsWith("image/")) return + const reader = new FileReader() + reader.onload = (e) => { + const url = e.target?.result as string + update({ upload_url: url, thumbnail_url: url, mode: "upload" }) + } + reader.readAsDataURL(file) + }, + [update], + ) + + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault() + setIsDragging(false) + const file = e.dataTransfer.files[0] + if (file) handleFileUpload(file) + }, + [handleFileUpload], + ) + + const handleUseAiSuggestion = useCallback(() => { + if (config.ai_suggested_time !== null) { + update({ frame_time: config.ai_suggested_time, mode: "frame" }) + } + }, [config.ai_suggested_time, update]) + + const formatTime = (seconds: number) => { + const m = Math.floor(seconds / 60) + const s = Math.floor(seconds % 60) + const ms = Math.floor((seconds % 1) * 10) + return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}.${ms}` + } + + return { + fileInputRef, + isDragging, + setIsDragging, + update, + handleReset, + handleModeChange, + handleFileUpload, + handleDrop, + handleUseAiSuggestion, + formatTime, + } +}