9a51cad137
CI/CD Pipeline / Frontend Lint (push) Successful in 48s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 2m24s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m25s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m27s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m8s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m16s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m20s
CI/CD Pipeline / Integration Tests (push) Successful in 3m53s
CI/CD Pipeline / Unit Tests (push) Failing after 7m53s
CI/CD Pipeline / Build Staging API Image (push) Successful in 11m29s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m13s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 26s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m3s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m15s
CI/CD Pipeline / Production Browser E2E (push) Failing after 1294h4m36s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 1294h5m22s
CI/CD Pipeline / Build Production Web Image (push) Failing after 1294h5m22s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 1294h7m45s
CI/CD Pipeline / PR Build Web Image (push) Failing after 1294h7m47s
CI/CD Pipeline / PR Build API Image (push) Failing after 1294h7m47s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 1294h7m49s
CI/CD Pipeline / Deploy Production (push) Failing after 1294h37m5s
CI/CD Pipeline / Build Production API Image (push) Failing after 1294h37m51s
188 lines
6.5 KiB
TypeScript
188 lines
6.5 KiB
TypeScript
/**
|
|
* Step 6 封面设置组件
|
|
*/
|
|
import React from "react"
|
|
import type { CoverConfig } from "../../editing-planner/types"
|
|
import { useStep6Cover } from "../hooks/useStep6Cover"
|
|
|
|
interface Step6CoverSettingsProps {
|
|
coverSettings: CoverConfig
|
|
onCoverSettingsChange: (settings: CoverConfig) => void
|
|
duration: number
|
|
}
|
|
|
|
const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
|
|
const {
|
|
coverSettings,
|
|
formatTime,
|
|
toggleEnabled,
|
|
setMode,
|
|
setFrameTime,
|
|
handleUpload,
|
|
totalDuration,
|
|
COVER_MODE_LABELS,
|
|
COVER_MODE_ICONS,
|
|
} = useStep6Cover(props)
|
|
|
|
return (
|
|
<div className="xx-form-section">
|
|
<h3>🖼️ 选择封面</h3>
|
|
|
|
{/* 启用开关 */}
|
|
<div className="xx-cover-header">
|
|
<span className="xx-cover-header-label">启用自定义封面</span>
|
|
<label className="xx-switch">
|
|
<input
|
|
type="checkbox"
|
|
checked={coverSettings.enabled}
|
|
onChange={(e) => toggleEnabled(e.target.checked)}
|
|
/>
|
|
<span className="xx-switch-slider" />
|
|
</label>
|
|
</div>
|
|
|
|
{coverSettings.enabled && (
|
|
<>
|
|
{/* 模式选择 */}
|
|
<div className="xx-section-title">封面来源</div>
|
|
<div className="xx-cover-mode-tabs">
|
|
{(["auto", "frame", "upload"] as const).map((m) => (
|
|
<button
|
|
key={m}
|
|
className={`xx-cover-mode-tab${coverSettings.mode === m ? " active" : ""}`}
|
|
onClick={() => setMode(m)}
|
|
>
|
|
<span className="xx-cover-mode-icon">{COVER_MODE_ICONS[m]}</span>
|
|
<span className="xx-cover-mode-label">{COVER_MODE_LABELS[m]}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* 智能封面 */}
|
|
{coverSettings.mode === "auto" && (
|
|
<div className="xx-cover-auto">
|
|
<div className="xx-cover-auto-desc">
|
|
AI 将分析视频内容,自动选择最具吸引力的画面作为封面。
|
|
</div>
|
|
<div className="xx-cover-auto-badge">
|
|
<span style={{ fontSize: 24 }}>🤖</span>
|
|
<span>AI 智能选帧</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 抽帧选封面 */}
|
|
{coverSettings.mode === "frame" && (
|
|
<div className="xx-cover-frame">
|
|
<div className="xx-cover-frame-preview">
|
|
<div className="xx-cover-frame-placeholder">
|
|
<span className="xx-cover-frame-icon">🎞️</span>
|
|
<span className="xx-cover-frame-time">
|
|
{formatTime(coverSettings.frame_time)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="xx-cover-frame-slider">
|
|
<div className="xx-cover-frame-slider-header">
|
|
<span>拖动选择封面帧</span>
|
|
<span className="xx-cover-frame-value">
|
|
{formatTime(coverSettings.frame_time)}
|
|
</span>
|
|
</div>
|
|
<input
|
|
type="range"
|
|
min={0}
|
|
max={Math.max(totalDuration, 1)}
|
|
step={0.1}
|
|
value={coverSettings.frame_time}
|
|
onChange={(e) => setFrameTime(Number(e.target.value))}
|
|
className="xx-cover-range"
|
|
/>
|
|
<div className="xx-cover-frame-range">
|
|
<span>00:00</span>
|
|
<span>{formatTime(totalDuration)}</span>
|
|
</div>
|
|
</div>
|
|
<div className="xx-cover-frame-quick">
|
|
<span className="xx-cover-quick-label">快捷选帧:</span>
|
|
{[0, 0.25, 0.5, 0.75].map((ratio) => {
|
|
const t = totalDuration * ratio
|
|
return (
|
|
<button
|
|
key={ratio}
|
|
className="xx-cover-quick-btn"
|
|
onClick={() => setFrameTime(t)}
|
|
>
|
|
{formatTime(t)}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 上传封面 */}
|
|
{coverSettings.mode === "upload" && (
|
|
<div className="xx-cover-upload">
|
|
<div
|
|
className="xx-cover-upload-area"
|
|
onClick={() => {
|
|
const input = document.getElementById("cover-upload-input")
|
|
input?.click()
|
|
}}
|
|
>
|
|
{coverSettings.upload_url ? (
|
|
<div className="xx-cover-upload-preview">
|
|
<img src={coverSettings.upload_url} alt="封面预览" />
|
|
<div className="xx-cover-upload-overlay">点击更换</div>
|
|
</div>
|
|
) : (
|
|
<div className="xx-cover-upload-placeholder">
|
|
<span style={{ fontSize: 32 }}>📤</span>
|
|
<span className="xx-cover-upload-text">点击上传封面图片</span>
|
|
<span className="xx-cover-upload-hint">支持 JPG / PNG,建议 16:9 比例</span>
|
|
</div>
|
|
)}
|
|
<input
|
|
id="cover-upload-input"
|
|
type="file"
|
|
accept="image/*"
|
|
style={{ display: "none" }}
|
|
onChange={(e) => {
|
|
const file = e.target.files?.[0]
|
|
if (file) {
|
|
handleUpload(file)
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 封面预览 */}
|
|
<div className="xx-section-title">封面预览</div>
|
|
<div className="xx-cover-preview-box">
|
|
{coverSettings.upload_url ? (
|
|
<img src={coverSettings.upload_url} alt="封面预览" className="xx-cover-preview-img" />
|
|
) : (
|
|
<div className="xx-cover-preview-placeholder">
|
|
<span style={{ fontSize: 28 }}>🖼️</span>
|
|
<span>
|
|
{coverSettings.mode === "auto"
|
|
? "AI 智能选择"
|
|
: coverSettings.mode === "frame"
|
|
? `帧 ${formatTime(coverSettings.frame_time)}`
|
|
: "未上传封面"}
|
|
</span>
|
|
</div>
|
|
)}
|
|
<div className="xx-cover-preview-ratio">16:9</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default Step6CoverSettings
|