Files
xiaoxia-saas/apps/web/src/pages/editing-planner/components/IntroOutroPanel.tsx
T
xiaoxia 0fcb77b991
CI Build & Deploy Pipeline / Staging E2E Tests (push) Failing after 2m50s
CI Build & Deploy Pipeline / Production Browser E2E (push) Failing after 1437h19m24s
CI Build & Deploy Pipeline / Build Production Web Image (push) Failing after 1437h19m26s
CI Build & Deploy Pipeline / Deploy Production (push) Failing after 1437h19m25s
CI Build & Deploy Pipeline / Build Production API Image (push) Failing after 1437h19m26s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 1437h21m16s
CI Build & Deploy Pipeline / Build Staging API Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Staging Web Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Staging Worker Image (push) Has been skipped
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI Build & Deploy Pipeline / Staging API Integration Tests (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (push) Failing after 1437h51m29s
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been skipped
CI/CD Pipeline / Unit Tests (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
ci: Prettier纳入两层防御体系 (#520)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-07-18 18:08:19 +08:00

292 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 片头片尾配置面板 — Drawer 形式
* 两个区块:片头(Intro)/ 片尾(Outro)
* 每个区块支持:类型选择(无/视频/图片)、素材 URL、时长、过渡动画
*/
import React, { useCallback } from "react"
import { Drawer } from "antd"
import type { IntroOutroConfig, IntroOutroItem, IntroOutroKind, TransitionType } from "../types"
import { DEFAULT_INTRO_OUTRO } from "../types"
import { TRANSITION_OPTIONS } from "@/api/editPlans"
/* ──────────── 常量 ──────────── */
const KIND_OPTIONS: { value: IntroOutroKind; label: string; icon: string }[] = [
{ value: "none", label: "无", icon: "🚫" },
{ value: "video", label: "视频", icon: "🎬" },
{ value: "image", label: "图片", icon: "🖼️" },
]
/* ──────────── Props ──────────── */
interface IntroOutroPanelProps {
open: boolean
onClose: () => void
config: IntroOutroConfig
onChange: (config: IntroOutroConfig) => void
}
const IntroOutroPanel: React.FC<IntroOutroPanelProps> = ({ open, onClose, config, onChange }) => {
/* ── 更新片头 ── */
const handleIntroChange = useCallback(
(partial: Partial<IntroOutroItem>) => {
onChange({ ...config, intro: { ...config.intro, ...partial } })
},
[config, onChange],
)
/* ── 更新片尾 ── */
const handleOutroChange = useCallback(
(partial: Partial<IntroOutroItem>) => {
onChange({ ...config, outro: { ...config.outro, ...partial } })
},
[config, onChange],
)
/* ── 切换片头类型 ── */
const handleIntroKindChange = useCallback(
(kind: IntroOutroKind) => {
handleIntroChange({ kind, url: kind === "none" ? undefined : "" })
},
[handleIntroChange],
)
/* ── 切换片尾类型 ── */
const handleOutroKindChange = useCallback(
(kind: IntroOutroKind) => {
handleOutroChange({ kind, url: kind === "none" ? undefined : "" })
},
[handleOutroChange],
)
/* ── 重置 ── */
const handleReset = useCallback(() => {
onChange({ ...DEFAULT_INTRO_OUTRO })
}, [onChange])
return (
<Drawer
title="🎬 片头片尾设置"
placement="right"
width={420}
open={open}
onClose={onClose}
className="intro-outro-panel-drawer"
>
{/* ═══ 片头区块 ═══ */}
<div className="iop-block">
<div className="iop-block-header">
<span className="iop-block-icon">🎞️</span>
<span className="iop-block-title">片头</span>
</div>
{/* 类型选择 */}
<div className="iop-kind-row">
{KIND_OPTIONS.map((opt) => (
<button
key={opt.value}
className={`iop-kind-btn${config.intro.kind === opt.value ? " active" : ""}`}
onClick={() => handleIntroKindChange(opt.value)}
>
<span className="iop-kind-icon">{opt.icon}</span>
<span className="iop-kind-label">{opt.label}</span>
</button>
))}
</div>
{/* 视频/图片配置 */}
{config.intro.kind !== "none" && (
<>
<div className="iop-field">
<label className="iop-field-label">
{config.intro.kind === "video" ? "视频" : "图片"} URL
</label>
<input
className="iop-input"
type="text"
placeholder={
config.intro.kind === "video"
? "https://example.com/intro.mp4"
: "https://example.com/intro.png"
}
value={config.intro.url ?? ""}
onChange={(e) => handleIntroChange({ url: e.target.value })}
/>
</div>
<div className="iop-field">
<label className="iop-field-label">显示时长</label>
<div className="iop-slider-row">
<input
className="iop-slider"
type="range"
min={1}
max={15}
step={0.5}
value={config.intro.duration}
onChange={(e) => handleIntroChange({ duration: Number(e.target.value) })}
/>
<span className="iop-slider-value">{config.intro.duration}s</span>
</div>
</div>
<div className="iop-field">
<label className="iop-field-label">进入过渡动画</label>
<select
className="iop-select"
value={config.intro.transition ?? "none"}
onChange={(e) =>
handleIntroChange({
transition: e.target.value as TransitionType,
})
}
>
{TRANSITION_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
{config.intro.transition && config.intro.transition !== "none" && (
<div className="iop-field">
<label className="iop-field-label">过渡时长</label>
<div className="iop-slider-row">
<input
className="iop-slider"
type="range"
min={0.3}
max={2.0}
step={0.1}
value={config.intro.transition_duration ?? 0.5}
onChange={(e) =>
handleIntroChange({
transition_duration: Number(e.target.value),
})
}
/>
<span className="iop-slider-value">
{(config.intro.transition_duration ?? 0.5).toFixed(1)}s
</span>
</div>
</div>
)}
</>
)}
</div>
{/* ═══ 片尾区块 ═══ */}
<div className="iop-block">
<div className="iop-block-header">
<span className="iop-block-icon">🏁</span>
<span className="iop-block-title">片尾</span>
</div>
{/* 类型选择 */}
<div className="iop-kind-row">
{KIND_OPTIONS.map((opt) => (
<button
key={opt.value}
className={`iop-kind-btn${config.outro.kind === opt.value ? " active" : ""}`}
onClick={() => handleOutroKindChange(opt.value)}
>
<span className="iop-kind-icon">{opt.icon}</span>
<span className="iop-kind-label">{opt.label}</span>
</button>
))}
</div>
{/* 视频/图片配置 */}
{config.outro.kind !== "none" && (
<>
<div className="iop-field">
<label className="iop-field-label">
{config.outro.kind === "video" ? "视频" : "图片"} URL
</label>
<input
className="iop-input"
type="text"
placeholder={
config.outro.kind === "video"
? "https://example.com/outro.mp4"
: "https://example.com/outro.png"
}
value={config.outro.url ?? ""}
onChange={(e) => handleOutroChange({ url: e.target.value })}
/>
</div>
<div className="iop-field">
<label className="iop-field-label">显示时长</label>
<div className="iop-slider-row">
<input
className="iop-slider"
type="range"
min={1}
max={15}
step={0.5}
value={config.outro.duration}
onChange={(e) => handleOutroChange({ duration: Number(e.target.value) })}
/>
<span className="iop-slider-value">{config.outro.duration}s</span>
</div>
</div>
<div className="iop-field">
<label className="iop-field-label">退出过渡动画</label>
<select
className="iop-select"
value={config.outro.transition ?? "none"}
onChange={(e) =>
handleOutroChange({
transition: e.target.value as TransitionType,
})
}
>
{TRANSITION_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
{config.outro.transition && config.outro.transition !== "none" && (
<div className="iop-field">
<label className="iop-field-label">过渡时长</label>
<div className="iop-slider-row">
<input
className="iop-slider"
type="range"
min={0.3}
max={2.0}
step={0.1}
value={config.outro.transition_duration ?? 0.5}
onChange={(e) =>
handleOutroChange({
transition_duration: Number(e.target.value),
})
}
/>
<span className="iop-slider-value">
{(config.outro.transition_duration ?? 0.5).toFixed(1)}s
</span>
</div>
</div>
)}
</>
)}
</div>
{/* ── 底部操作 ── */}
<div className="iop-footer">
<button className="iop-reset-btn" onClick={handleReset}>
重置
</button>
</div>
</Drawer>
)
}
export default IntroOutroPanel