From 922e0bdc58f846863b60e65dcfc5e382f53bd63c Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 07:37:56 +0800 Subject: [PATCH 1/5] =?UTF-8?q?refactor(editing-planner):=20=E6=8B=86?= =?UTF-8?q?=E5=88=86=20CoverSelector=20=E7=BB=84=E4=BB=B6=EF=BC=8C?= =?UTF-8?q?=E6=8A=BD=E7=A6=BB=20Hook=20+=20=E6=A8=A1=E5=BC=8F=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主文件 286→160 行(-44%),拆分为: - useCoverSelector: 状态管理、文件上传、模式切换等逻辑 - CoverModePanels: 智能封面/抽帧/上传 三个模式面板 - index: 组合入口 + Drawer 布局 导入路径保持向后兼容 --- .../cover-selector/CoverModePanels.tsx | 143 +++++++++++++++++ .../components/cover-selector/index.tsx | 146 ++++++++++++++++++ .../cover-selector/useCoverSelector.ts | 98 ++++++++++++ 3 files changed, 387 insertions(+) create mode 100644 apps/web/src/pages/editing-planner/components/cover-selector/CoverModePanels.tsx create mode 100644 apps/web/src/pages/editing-planner/components/cover-selector/index.tsx create mode 100644 apps/web/src/pages/editing-planner/components/cover-selector/useCoverSelector.ts 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..48495a2bf --- /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..6fbc41444 --- /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..69dd1003d --- /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, + } +} -- 2.54.0 From 2781663c612025d422f16a7f5427dedf795f64a0 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 12:37:55 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix(cover-selector):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20types=20=E5=AF=BC=E5=85=A5=E8=B7=AF=E5=BE=84=EF=BC=88../type?= =?UTF-8?q?s=20=E2=86=92=20../../types=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/editing-planner/components/cover-selector/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 6fbc41444..6bb5a6d96 100644 --- a/apps/web/src/pages/editing-planner/components/cover-selector/index.tsx +++ b/apps/web/src/pages/editing-planner/components/cover-selector/index.tsx @@ -4,7 +4,7 @@ */ import React from "react" import { Drawer } from "antd" -import type { CoverConfig, CoverMode } from "../types" +import type { CoverConfig, CoverMode } from "../../types" import { useCoverSelector, MODE_LABELS, MODE_ICONS } from "./useCoverSelector" import { CoverAutoMode, CoverFrameMode, CoverUploadMode } from "./CoverModePanels" -- 2.54.0 From 71701e27aed2ec952393f636810d79c239430c5c Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 12:37:57 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix(cover-selector):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20types=20=E5=AF=BC=E5=85=A5=E8=B7=AF=E5=BE=84=EF=BC=88../type?= =?UTF-8?q?s=20=E2=86=92=20../../types=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/cover-selector/useCoverSelector.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 69dd1003d..dbb649e22 100644 --- a/apps/web/src/pages/editing-planner/components/cover-selector/useCoverSelector.ts +++ b/apps/web/src/pages/editing-planner/components/cover-selector/useCoverSelector.ts @@ -1,6 +1,6 @@ import { useCallback, useRef, useState } from "react" -import type { CoverConfig, CoverMode } from "../types" -import { DEFAULT_COVER_CONFIG } from "../types" +import type { CoverConfig, CoverMode } from "../../types" +import { DEFAULT_COVER_CONFIG } from "../../types" /** 封面模式标签 */ export const MODE_LABELS: Record = { -- 2.54.0 From be1a1dae60bbcc949eb6a335dc940d4b97da4460 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 12:37:58 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(cover-selector):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20types=20=E5=AF=BC=E5=85=A5=E8=B7=AF=E5=BE=84=EF=BC=88../type?= =?UTF-8?q?s=20=E2=86=92=20../../types=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/cover-selector/CoverModePanels.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 48495a2bf..d13dd5072 100644 --- a/apps/web/src/pages/editing-planner/components/cover-selector/CoverModePanels.tsx +++ b/apps/web/src/pages/editing-planner/components/cover-selector/CoverModePanels.tsx @@ -1,5 +1,5 @@ import React from "react" -import type { CoverConfig } from "../types" +import type { CoverConfig } from "../../types" interface CoverAutoModeProps { config: CoverConfig -- 2.54.0 From 51a79230d5143e8f0e591ed29e8481107156fb47 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 12:39:00 +0800 Subject: [PATCH 5/5] =?UTF-8?q?refactor(cover-selector):=20=E4=B8=BB?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E6=94=B9=E4=B8=BA=20re-export=20=E5=AD=90?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=EF=BC=8C=E5=AE=8C=E6=88=90=E6=8B=86=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/CoverSelector.tsx | 287 +----------------- 1 file changed, 3 insertions(+), 284 deletions(-) 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" -- 2.54.0