Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d37bddb0c | |||
| f4ed07dedd |
Regular → Executable
+16
-101
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* Step 6 封面设置组件
|
||||
*/
|
||||
import React from "react"
|
||||
import type { CoverConfig } from "../../editing-planner/types"
|
||||
import { useStep6Cover } from "../hooks/useStep6Cover"
|
||||
import { CoverModeSelector } from "./cover-settings/CoverModeSelector"
|
||||
import { FrameCoverPicker } from "./cover-settings/FrameCoverPicker"
|
||||
import { UploadCoverPicker } from "./cover-settings/UploadCoverPicker"
|
||||
|
||||
interface Step6CoverSettingsProps {
|
||||
coverSettings: CoverConfig
|
||||
@@ -28,7 +28,6 @@ const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
|
||||
<div className="xx-form-section">
|
||||
<h3>🖼️ 选择封面</h3>
|
||||
|
||||
{/* 启用开关 */}
|
||||
<div className="xx-cover-header">
|
||||
<span className="xx-cover-header-label">启用自定义封面</span>
|
||||
<label className="xx-switch">
|
||||
@@ -43,22 +42,14 @@ const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
|
||||
|
||||
{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>
|
||||
<CoverModeSelector
|
||||
mode={coverSettings.mode}
|
||||
onModeChange={setMode}
|
||||
modeLabels={COVER_MODE_LABELS}
|
||||
modeIcons={COVER_MODE_ICONS}
|
||||
/>
|
||||
|
||||
{/* 智能封面 */}
|
||||
{coverSettings.mode === "auto" && (
|
||||
<div className="xx-cover-auto">
|
||||
<div className="xx-cover-auto-desc">
|
||||
@@ -71,95 +62,19 @@ const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
|
||||
</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>
|
||||
<FrameCoverPicker
|
||||
frameTime={coverSettings.frame_time}
|
||||
totalDuration={totalDuration}
|
||||
formatTime={formatTime}
|
||||
onFrameTimeChange={setFrameTime}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 上传封面 */}
|
||||
{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>
|
||||
<UploadCoverPicker uploadUrl={coverSettings.upload_url} onUpload={handleUpload} />
|
||||
)}
|
||||
|
||||
{/* 封面预览 */}
|
||||
<div className="xx-section-title">封面预览</div>
|
||||
<div className="xx-cover-preview-box">
|
||||
{coverSettings.upload_url ? (
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import React from "react"
|
||||
import type { CoverMode } from "../../../editing-planner/types"
|
||||
|
||||
interface CoverModeSelectorProps {
|
||||
mode: CoverMode
|
||||
onModeChange: (mode: CoverMode) => void
|
||||
modeLabels: Record<CoverMode, string>
|
||||
modeIcons: Record<CoverMode, string>
|
||||
}
|
||||
|
||||
export const CoverModeSelector: React.FC<CoverModeSelectorProps> = ({
|
||||
mode,
|
||||
onModeChange,
|
||||
modeLabels,
|
||||
modeIcons,
|
||||
}) => {
|
||||
const modes: CoverMode[] = ["auto", "frame", "upload"]
|
||||
return (
|
||||
<div className="xx-cover-mode-tabs">
|
||||
{modes.map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
className={`xx-cover-mode-tab${mode === m ? " active" : ""}`}
|
||||
onClick={() => onModeChange(m)}
|
||||
>
|
||||
<span className="xx-cover-mode-icon">{modeIcons[m]}</span>
|
||||
<span className="xx-cover-mode-label">{modeLabels[m]}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from "react"
|
||||
|
||||
interface FrameCoverPickerProps {
|
||||
frameTime: number
|
||||
totalDuration: number
|
||||
formatTime: (seconds: number) => string
|
||||
onFrameTimeChange: (time: number) => void
|
||||
}
|
||||
|
||||
export const FrameCoverPicker: React.FC<FrameCoverPickerProps> = ({
|
||||
frameTime,
|
||||
totalDuration,
|
||||
formatTime,
|
||||
onFrameTimeChange,
|
||||
}) => {
|
||||
const quickRatios = [0, 0.25, 0.5, 0.75]
|
||||
|
||||
return (
|
||||
<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(frameTime)}</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(frameTime)}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={Math.max(totalDuration, 1)}
|
||||
step={0.1}
|
||||
value={frameTime}
|
||||
onChange={(e) => onFrameTimeChange(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>
|
||||
{quickRatios.map((ratio) => {
|
||||
const t = totalDuration * ratio
|
||||
return (
|
||||
<button key={ratio} className="xx-cover-quick-btn" onClick={() => onFrameTimeChange(t)}>
|
||||
{formatTime(t)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import React from "react"
|
||||
|
||||
interface UploadCoverPickerProps {
|
||||
uploadUrl: string
|
||||
onUpload: (file: File) => void
|
||||
}
|
||||
|
||||
export const UploadCoverPicker: React.FC<UploadCoverPickerProps> = ({ uploadUrl, onUpload }) => {
|
||||
const handleClick = () => {
|
||||
const input = document.getElementById("cover-upload-input")
|
||||
input?.click()
|
||||
}
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (file) {
|
||||
onUpload(file)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="xx-cover-upload">
|
||||
<div className="xx-cover-upload-area" onClick={handleClick}>
|
||||
{uploadUrl ? (
|
||||
<div className="xx-cover-upload-preview">
|
||||
<img src={uploadUrl} 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={handleFileChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
# ── 单轨时间计算 ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
from packages.domain.speed_config import (
|
||||
DEFAULT_SPEED,
|
||||
SpeedConfig,
|
||||
|
||||
@@ -6,7 +6,6 @@ domain 层纯逻辑模块,0 FFmpeg 依赖,快速轻量。
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.domain.transition_presets import (
|
||||
|
||||
Reference in New Issue
Block a user