Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 51a79230d5 | |||
| be1a1dae60 | |||
| 71701e27ae | |||
| 2781663c61 | |||
| 185431788c | |||
| 922e0bdc58 |
@@ -1,286 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* 封面选择器
|
* 封面选择器入口(向后兼容)
|
||||||
* 抽帧选封面 + 上传自定义封面 + 智能封面推荐
|
* 实际实现位于 ./cover-selector/ 目录
|
||||||
*/
|
*/
|
||||||
import React, { useCallback, useRef, useState } from "react"
|
export { default } from "./cover-selector"
|
||||||
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<CoverMode, string> = {
|
|
||||||
auto: "智能封面",
|
|
||||||
frame: "抽帧选封面",
|
|
||||||
upload: "上传封面",
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 封面模式图标 */
|
|
||||||
const MODE_ICONS: Record<CoverMode, string> = {
|
|
||||||
auto: "🤖",
|
|
||||||
frame: "🎞️",
|
|
||||||
upload: "📤",
|
|
||||||
}
|
|
||||||
|
|
||||||
const CoverSelector: React.FC<CoverSelectorProps> = ({
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
config,
|
|
||||||
onChange,
|
|
||||||
totalDuration,
|
|
||||||
}) => {
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
|
||||||
|
|
||||||
const update = useCallback(
|
|
||||||
(partial: Partial<CoverConfig>) => {
|
|
||||||
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 (
|
|
||||||
<Drawer
|
|
||||||
title="封面选择"
|
|
||||||
placement="right"
|
|
||||||
width={440}
|
|
||||||
open={open}
|
|
||||||
onClose={onClose}
|
|
||||||
className="cover-selector-drawer"
|
|
||||||
>
|
|
||||||
{/* 顶部开关 */}
|
|
||||||
<div className="cover-header">
|
|
||||||
<span className="cover-header-label">启用自定义封面</span>
|
|
||||||
<label className="cover-switch">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={config.enabled}
|
|
||||||
onChange={(e) => update({ enabled: e.target.checked })}
|
|
||||||
/>
|
|
||||||
<span className="cover-switch-slider" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 模式选择 */}
|
|
||||||
<div className="cover-mode-section">
|
|
||||||
<div className="cover-section-title">封面来源</div>
|
|
||||||
<div className="cover-mode-tabs">
|
|
||||||
{(["auto", "frame", "upload"] as CoverMode[]).map((m) => (
|
|
||||||
<button
|
|
||||||
key={m}
|
|
||||||
className={`cover-mode-tab${config.mode === m ? " active" : ""}`}
|
|
||||||
onClick={() => handleModeChange(m)}
|
|
||||||
>
|
|
||||||
<span className="cover-mode-icon">{MODE_ICONS[m]}</span>
|
|
||||||
<span className="cover-mode-label">{MODE_LABELS[m]}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 模式内容区 */}
|
|
||||||
<div className="cover-mode-content">
|
|
||||||
{/* 智能封面 */}
|
|
||||||
{config.mode === "auto" && (
|
|
||||||
<div className="cover-auto-section">
|
|
||||||
<div className="cover-auto-desc">
|
|
||||||
AI 将分析视频内容,自动选择最具吸引力的画面作为封面。
|
|
||||||
</div>
|
|
||||||
{config.ai_suggested_time !== null ? (
|
|
||||||
<div className="cover-auto-suggestion">
|
|
||||||
<div className="cover-auto-badge">AI 推荐</div>
|
|
||||||
<div className="cover-auto-time">
|
|
||||||
推荐时间点:{formatTime(config.ai_suggested_time)}
|
|
||||||
</div>
|
|
||||||
<button className="cover-auto-use-btn" onClick={handleUseAiSuggestion}>
|
|
||||||
使用此时间点
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="cover-auto-pending">
|
|
||||||
<div className="cover-auto-spinner" />
|
|
||||||
<span>AI 分析中...(生成视频后自动推荐)</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 抽帧选封面 */}
|
|
||||||
{config.mode === "frame" && (
|
|
||||||
<div className="cover-frame-section">
|
|
||||||
<div className="cover-frame-preview">
|
|
||||||
<div className="cover-frame-placeholder">
|
|
||||||
<span className="cover-frame-icon">🎞️</span>
|
|
||||||
<span className="cover-frame-time">{formatTime(config.frame_time)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="cover-frame-timeline">
|
|
||||||
<div className="cover-frame-slider-header">
|
|
||||||
<span className="cover-frame-slider-label">拖动选择封面帧</span>
|
|
||||||
<span className="cover-frame-slider-value">{formatTime(config.frame_time)}</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
className="cover-frame-slider"
|
|
||||||
min={0}
|
|
||||||
max={Math.max(totalDuration, 1)}
|
|
||||||
step={0.1}
|
|
||||||
value={config.frame_time}
|
|
||||||
onChange={(e) => update({ frame_time: Number(e.target.value) })}
|
|
||||||
/>
|
|
||||||
<div className="cover-frame-range">
|
|
||||||
<span>00:00</span>
|
|
||||||
<span>{formatTime(totalDuration)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* 快捷时间点 */}
|
|
||||||
<div className="cover-frame-quick">
|
|
||||||
<span className="cover-quick-label">快捷选帧:</span>
|
|
||||||
{[0, 0.25, 0.5, 0.75].map((ratio) => {
|
|
||||||
const t = totalDuration * ratio
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={ratio}
|
|
||||||
className="cover-quick-btn"
|
|
||||||
onClick={() => update({ frame_time: t })}
|
|
||||||
>
|
|
||||||
{formatTime(t)}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 上传封面 */}
|
|
||||||
{config.mode === "upload" && (
|
|
||||||
<div className="cover-upload-section">
|
|
||||||
<div
|
|
||||||
className={`cover-upload-area${isDragging ? " dragging" : ""}`}
|
|
||||||
onDragOver={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setIsDragging(true)
|
|
||||||
}}
|
|
||||||
onDragLeave={() => setIsDragging(false)}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
>
|
|
||||||
{config.upload_url ? (
|
|
||||||
<div className="cover-upload-preview">
|
|
||||||
<img src={config.upload_url} alt="封面预览" />
|
|
||||||
<div className="cover-upload-overlay">点击更换</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="cover-upload-placeholder">
|
|
||||||
<span className="cover-upload-icon">📤</span>
|
|
||||||
<span className="cover-upload-text">点击或拖拽上传封面图片</span>
|
|
||||||
<span className="cover-upload-hint">支持 JPG / PNG,建议 16:9 比例</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
style={{ display: "none" }}
|
|
||||||
onChange={(e) => {
|
|
||||||
const file = e.target.files?.[0]
|
|
||||||
if (file) handleFileUpload(file)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 封面预览 */}
|
|
||||||
<div className="cover-preview-section">
|
|
||||||
<div className="cover-section-title">封面预览</div>
|
|
||||||
<div className="cover-preview-box">
|
|
||||||
{config.upload_url ? (
|
|
||||||
<img src={config.upload_url} alt="封面预览" className="cover-preview-img" />
|
|
||||||
) : (
|
|
||||||
<div className="cover-preview-placeholder">
|
|
||||||
<span className="cover-preview-icon">🖼️</span>
|
|
||||||
<span className="cover-preview-text">
|
|
||||||
{config.mode === "auto"
|
|
||||||
? "AI 智能选择"
|
|
||||||
: config.mode === "frame"
|
|
||||||
? `帧 ${formatTime(config.frame_time)}`
|
|
||||||
: "未上传封面"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="cover-preview-ratio">16:9</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 底部 */}
|
|
||||||
<div className="cover-footer">
|
|
||||||
<button className="cover-reset-btn" onClick={handleReset}>
|
|
||||||
重置封面
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Drawer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CoverSelector
|
|
||||||
|
|||||||
@@ -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<CoverAutoModeProps> = ({
|
||||||
|
config,
|
||||||
|
formatTime,
|
||||||
|
onUseAiSuggestion,
|
||||||
|
}) => (
|
||||||
|
<div className="cover-auto-section">
|
||||||
|
<div className="cover-auto-desc">AI 将分析视频内容,自动选择最具吸引力的画面作为封面。</div>
|
||||||
|
{config.ai_suggested_time !== null ? (
|
||||||
|
<div className="cover-auto-suggestion">
|
||||||
|
<div className="cover-auto-badge">AI 推荐</div>
|
||||||
|
<div className="cover-auto-time">推荐时间点:{formatTime(config.ai_suggested_time)}</div>
|
||||||
|
<button className="cover-auto-use-btn" onClick={onUseAiSuggestion}>
|
||||||
|
使用此时间点
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="cover-auto-pending">
|
||||||
|
<div className="cover-auto-spinner" />
|
||||||
|
<span>AI 分析中...(生成视频后自动推荐)</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
interface CoverFrameModeProps {
|
||||||
|
config: CoverConfig
|
||||||
|
totalDuration: number
|
||||||
|
formatTime: (s: number) => string
|
||||||
|
onFrameTimeChange: (time: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 抽帧选封面模式面板 */
|
||||||
|
export const CoverFrameMode: React.FC<CoverFrameModeProps> = ({
|
||||||
|
config,
|
||||||
|
totalDuration,
|
||||||
|
formatTime,
|
||||||
|
onFrameTimeChange,
|
||||||
|
}) => (
|
||||||
|
<div className="cover-frame-section">
|
||||||
|
<div className="cover-frame-preview">
|
||||||
|
<div className="cover-frame-placeholder">
|
||||||
|
<span className="cover-frame-icon">🎞️</span>
|
||||||
|
<span className="cover-frame-time">{formatTime(config.frame_time)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="cover-frame-timeline">
|
||||||
|
<div className="cover-frame-slider-header">
|
||||||
|
<span className="cover-frame-slider-label">拖动选择封面帧</span>
|
||||||
|
<span className="cover-frame-slider-value">{formatTime(config.frame_time)}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
className="cover-frame-slider"
|
||||||
|
min={0}
|
||||||
|
max={Math.max(totalDuration, 1)}
|
||||||
|
step={0.1}
|
||||||
|
value={config.frame_time}
|
||||||
|
onChange={(e) => onFrameTimeChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
<div className="cover-frame-range">
|
||||||
|
<span>00:00</span>
|
||||||
|
<span>{formatTime(totalDuration)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="cover-frame-quick">
|
||||||
|
<span className="cover-quick-label">快捷选帧:</span>
|
||||||
|
{[0, 0.25, 0.5, 0.75].map((ratio) => {
|
||||||
|
const t = totalDuration * ratio
|
||||||
|
return (
|
||||||
|
<button key={ratio} className="cover-quick-btn" onClick={() => onFrameTimeChange(t)}>
|
||||||
|
{formatTime(t)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
interface CoverUploadModeProps {
|
||||||
|
config: CoverConfig
|
||||||
|
isDragging: boolean
|
||||||
|
fileInputRef: React.RefObject<HTMLInputElement>
|
||||||
|
onDragOver: (e: React.DragEvent) => void
|
||||||
|
onDragLeave: () => void
|
||||||
|
onDrop: (e: React.DragEvent) => void
|
||||||
|
onAreaClick: () => void
|
||||||
|
onFileChange: (file: File) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 上传封面模式面板 */
|
||||||
|
export const CoverUploadMode: React.FC<CoverUploadModeProps> = ({
|
||||||
|
config,
|
||||||
|
isDragging,
|
||||||
|
fileInputRef,
|
||||||
|
onDragOver,
|
||||||
|
onDragLeave,
|
||||||
|
onDrop,
|
||||||
|
onAreaClick,
|
||||||
|
onFileChange,
|
||||||
|
}) => (
|
||||||
|
<div className="cover-upload-section">
|
||||||
|
<div
|
||||||
|
className={`cover-upload-area${isDragging ? " dragging" : ""}`}
|
||||||
|
onDragOver={onDragOver}
|
||||||
|
onDragLeave={onDragLeave}
|
||||||
|
onDrop={onDrop}
|
||||||
|
onClick={onAreaClick}
|
||||||
|
>
|
||||||
|
{config.upload_url ? (
|
||||||
|
<div className="cover-upload-preview">
|
||||||
|
<img src={config.upload_url} alt="封面预览" />
|
||||||
|
<div className="cover-upload-overlay">点击更换</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="cover-upload-placeholder">
|
||||||
|
<span className="cover-upload-icon">📤</span>
|
||||||
|
<span className="cover-upload-text">点击或拖拽上传封面图片</span>
|
||||||
|
<span className="cover-upload-hint">支持 JPG / PNG,建议 16:9 比例</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
onChange={(e) => {
|
||||||
|
const file = e.target.files?.[0]
|
||||||
|
if (file) onFileChange(file)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
@@ -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<CoverSelectorProps> = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
config,
|
||||||
|
onChange,
|
||||||
|
totalDuration,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
fileInputRef,
|
||||||
|
isDragging,
|
||||||
|
setIsDragging,
|
||||||
|
update,
|
||||||
|
handleReset,
|
||||||
|
handleModeChange,
|
||||||
|
handleFileUpload,
|
||||||
|
handleDrop,
|
||||||
|
handleUseAiSuggestion,
|
||||||
|
formatTime,
|
||||||
|
} = useCoverSelector({ config, onChange })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
title="封面选择"
|
||||||
|
placement="right"
|
||||||
|
width={440}
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
className="cover-selector-drawer"
|
||||||
|
>
|
||||||
|
{/* 顶部开关 */}
|
||||||
|
<div className="cover-header">
|
||||||
|
<span className="cover-header-label">启用自定义封面</span>
|
||||||
|
<label className="cover-switch">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={config.enabled}
|
||||||
|
onChange={(e) => update({ enabled: e.target.checked })}
|
||||||
|
/>
|
||||||
|
<span className="cover-switch-slider" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 模式选择 */}
|
||||||
|
<div className="cover-mode-section">
|
||||||
|
<div className="cover-section-title">封面来源</div>
|
||||||
|
<div className="cover-mode-tabs">
|
||||||
|
{(["auto", "frame", "upload"] as CoverMode[]).map((m) => (
|
||||||
|
<button
|
||||||
|
key={m}
|
||||||
|
className={`cover-mode-tab${config.mode === m ? " active" : ""}`}
|
||||||
|
onClick={() => handleModeChange(m)}
|
||||||
|
>
|
||||||
|
<span className="cover-mode-icon">{MODE_ICONS[m]}</span>
|
||||||
|
<span className="cover-mode-label">{MODE_LABELS[m]}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 模式内容区 */}
|
||||||
|
<div className="cover-mode-content">
|
||||||
|
{config.mode === "auto" && (
|
||||||
|
<CoverAutoMode
|
||||||
|
config={config}
|
||||||
|
formatTime={formatTime}
|
||||||
|
onUseAiSuggestion={handleUseAiSuggestion}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{config.mode === "frame" && (
|
||||||
|
<CoverFrameMode
|
||||||
|
config={config}
|
||||||
|
totalDuration={totalDuration}
|
||||||
|
formatTime={formatTime}
|
||||||
|
onFrameTimeChange={(t) => update({ frame_time: t })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{config.mode === "upload" && (
|
||||||
|
<CoverUploadMode
|
||||||
|
config={config}
|
||||||
|
isDragging={isDragging}
|
||||||
|
fileInputRef={fileInputRef}
|
||||||
|
onDragOver={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setIsDragging(true)
|
||||||
|
}}
|
||||||
|
onDragLeave={() => setIsDragging(false)}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onAreaClick={() => fileInputRef.current?.click()}
|
||||||
|
onFileChange={handleFileUpload}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 封面预览 */}
|
||||||
|
<div className="cover-preview-section">
|
||||||
|
<div className="cover-section-title">封面预览</div>
|
||||||
|
<div className="cover-preview-box">
|
||||||
|
{config.upload_url ? (
|
||||||
|
<img src={config.upload_url} alt="封面预览" className="cover-preview-img" />
|
||||||
|
) : (
|
||||||
|
<div className="cover-preview-placeholder">
|
||||||
|
<span className="cover-preview-icon">🖼️</span>
|
||||||
|
<span className="cover-preview-text">
|
||||||
|
{config.mode === "auto"
|
||||||
|
? "AI 智能选择"
|
||||||
|
: config.mode === "frame"
|
||||||
|
? `帧 ${formatTime(config.frame_time)}`
|
||||||
|
: "未上传封面"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="cover-preview-ratio">16:9</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 底部 */}
|
||||||
|
<div className="cover-footer">
|
||||||
|
<button className="cover-reset-btn" onClick={handleReset}>
|
||||||
|
重置封面
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CoverSelector
|
||||||
@@ -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<CoverMode, string> = {
|
||||||
|
auto: "智能封面",
|
||||||
|
frame: "抽帧选封面",
|
||||||
|
upload: "上传封面",
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 封面模式图标 */
|
||||||
|
export const MODE_ICONS: Record<CoverMode, string> = {
|
||||||
|
auto: "🤖",
|
||||||
|
frame: "🎞️",
|
||||||
|
upload: "📤",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseCoverSelectorOptions {
|
||||||
|
config: CoverConfig
|
||||||
|
onChange: (config: CoverConfig) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 封面选择器 Hook
|
||||||
|
* 封装状态管理、文件上传、模式切换等逻辑
|
||||||
|
*/
|
||||||
|
export function useCoverSelector({ config, onChange }: UseCoverSelectorOptions) {
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
|
|
||||||
|
const update = useCallback(
|
||||||
|
(partial: Partial<CoverConfig>) => {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user