feat(viral-video): v1.6 暗色主题 + 分镜脚本线性文档风格 + STEP3改版 (#2135)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 15s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m0s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m39s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m24s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m24s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 3m9s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m36s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled

Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
This commit was merged in pull request #2135.
This commit is contained in:
2026-10-02 10:31:36 +08:00
committed by auto-approve-bot
parent ecb049a57e
commit dbb4e57810
2 changed files with 1422 additions and 1499 deletions
File diff suppressed because it is too large Load Diff
+235 -161
View File
@@ -9,8 +9,6 @@ import {
CaretRightOutlined,
VideoCameraOutlined,
SoundOutlined,
BulbOutlined,
ThunderboltOutlined,
ReloadOutlined,
WarningFilled,
SearchOutlined,
@@ -23,6 +21,7 @@ import {
LoadingOutlined,
CheckCircleFilled,
EditOutlined,
HistoryOutlined,
} from "@ant-design/icons"
import { Select, Input, message } from "antd"
import { uploadAssetDirect, getAssetLibraries, getAssetsByKind, type AssetItem } from "@/api/assets"
@@ -122,6 +121,8 @@ type TabTask = {
// STEP3 设置
videoRatio: string
videoModel: string
quality: string
upscale: string
// UI 状态
uiStep: UIStep
imageAnalysis: ImageAnalysisResult | null
@@ -183,6 +184,15 @@ const MODELS = [
{ v: "seedance-2.5", label: "Seedance 2.5(推荐)" },
{ v: "seedance-2.0", label: "Seedance 2.0" },
]
const QUALITY_OPTIONS = [
{ v: "480p", label: "480p(快速)" },
{ v: "720p", label: "720p(清晰)" },
{ v: "1080p", label: "1080p(高清)" },
]
const UPSCALE_OPTIONS = [
{ v: "关闭", label: "关闭" },
{ v: "开启", label: "开启" },
]
const VOICE_TIPS =
"支持 MP3/WAV/M4A/AAC/OGG 格式,最大 10MB,时长 ≤30 秒。建议清晰人声、无背景音乐、环境安静;录音请保持距麦克风 15-20cm,音量适中。"
@@ -347,6 +357,8 @@ const emptyTask = (id: string, title: string): TabTask => ({
persona: "",
videoRatio: "9:16",
videoModel: "seedance-2.5",
quality: "480p",
upscale: "关闭",
uiStep: "step1_upload",
imageAnalysis: null,
storyboard: null,
@@ -358,6 +370,32 @@ const emptyTask = (id: string, title: string): TabTask => ({
})
/* ─────────── 主页面 ─────────── */
function getCopyStageText(stage: string | undefined): string {
switch (stage) {
case "image_analysis":
case "video_analysis":
case "analyzing_images":
return "🔍 正在分析商品特征..."
case "intent_parsing":
case "generating_outline":
case "planning":
return "📋 正在构思脚本框架..."
case "script_generation":
case "generating_storyboard":
case "writing_shots":
return "🎬 正在生成分镜脚本..."
case "review":
case "generating_copy":
case "writing_copy":
return "✍️ 正在撰写口播文案..."
case "finalizing":
return "✨ 正在优化细节..."
default:
return "🎬 AI 正在创作视频分镜脚本..."
}
}
const ViralVideoPage: React.FC = () => {
const [tasks, setTasks] = useState<TabTask[]>(() => [emptyTask("t1", "生成 1")])
const [activeId, setActiveId] = useState<string>("t1")
@@ -937,7 +975,7 @@ const ViralVideoPage: React.FC = () => {
return (
<div className="vv-recog">
<div className="vv-recog-title">
<LoadingOutlined style={{ color: "#7c3aed", marginRight: 6 }} />
<LoadingOutlined style={{ color: "#22d3ee", marginRight: 6 }} />
识别描述汇览
</div>
<div className="vv-muted">AI 正在识别商品特征…</div>
@@ -1061,8 +1099,11 @@ const ViralVideoPage: React.FC = () => {
if (task.uiStep === "step2_generating") {
return (
<div className="vv-copy-box vv-copy-loading">
<LoadingOutlined style={{ color: "#7c3aed", fontSize: 18, marginRight: 8 }} />
<span>AI 正在创作视频分镜脚本…</span>
<span className="vv-spinner" />
<span className="vv-copy-loading-text">
{task.job?.progress_message || getCopyStageText(task.job?.progress_stage)}
</span>
<span className="vv-copy-loading-hint">预计 10-30 秒,请稍候</span>
</div>
)
}
@@ -1106,155 +1147,155 @@ const ViralVideoPage: React.FC = () => {
return (
<div className="vv-copy-box vv-storyboard">
<div className="vv-copy-head">
<EditOutlined style={{ color: "#7c3aed" }} />
<span className="vv-copy-title">AI 分镜脚本</span>
{isMockPreview && (
<span className="vv-copy-tag vv-copy-tag-preview">
示例预览 · 等后端输出新结构后自动替换
</span>
)}
</div>
{/* 1. 总览卡片 */}
<div className="vv-sb-overview">
<div className="vv-sb-ov-field">
<label>主题</label>
<div className="vv-sb-doc">
{/* 视频总览 */}
<h4 className="vv-sb-h">视频总览</h4>
<div className="vv-sb-kv">
<span className="vv-sb-k">整体主题:</span>
<input
className="vv-input vv-sb-input"
className="vv-sb-inline vv-sb-inline-input"
value={sb.overview.theme}
disabled={locked}
onChange={(e) => updateOverview({ theme: e.target.value })}
/>
</div>
<div className="vv-sb-ov-field">
<label>总时长</label>
<div className="vv-sb-kv">
<span className="vv-sb-k">总时长:</span>
<input
className="vv-input vv-sb-input"
className="vv-sb-inline vv-sb-inline-input"
value={sb.overview.total_duration}
disabled={locked}
onChange={(e) => updateOverview({ total_duration: e.target.value })}
/>
</div>
<div className="vv-sb-ov-field">
<label>画幅</label>
<div className="vv-sb-kv">
<span className="vv-sb-k">画幅:</span>
<Select
className="vv-select vv-select-purple vv-sb-input"
style={{ width: "100%" }}
className="vv-select vv-sb-inline-select"
style={{ width: 200 }}
value={sb.overview.aspect_ratio}
disabled={locked}
onChange={(v) => updateOverview({ aspect_ratio: v })}
options={RATIOS.map((r) => ({ value: r.v, label: r.label }))}
options={RATIOS.map((r) => ({ value: r.v, label: r.v }))}
variant="borderless"
/>
</div>
</div>
{/* 2. 场景与光线 */}
<div className="vv-sb-section">
<div className="vv-sb-section-title">
<BulbOutlined style={{ color: "#7c3aed" }} /> 场景与光线
</div>
{/* 场景与光线 */}
<h4 className="vv-sb-h">场景与光线</h4>
<textarea
className="vv-textarea"
className="vv-textarea vv-sb-doc-ta"
value={sb.scene_and_lighting}
disabled={locked}
onChange={(e) => updateStoryboard({ scene_and_lighting: e.target.value })}
placeholder="描述整体场景氛围、光线方向与色温…"
/>
</div>
{/* 3. 逐镜头卡片 */}
<div className="vv-sb-section">
<div className="vv-sb-section-title">
<VideoCameraOutlined style={{ color: "#7c3aed" }} /> 逐镜头拆解({sb.shots.length} 镜)
</div>
<div className="vv-sb-shots">
{/* 逐镜头 */}
<h4 className="vv-sb-h">逐秒镜头拆解</h4>
<div className="vv-sb-doc-shots">
{sb.shots.map((sh, idx) => {
const refImg =
typeof sh.reference_image_index === "number"
? task.images[sh.reference_image_index]
: undefined
return (
<div key={idx} className="vv-sb-shot">
<div className="vv-sb-shot-head">
<span className="vv-sb-shot-num">镜头 {idx + 1}</span>
<div key={idx} className="vv-sb-doc-shot">
<div className="vv-sb-doc-shot-head">
<input
className="vv-sb-time"
className="vv-sb-time-doc"
value={sh.time_range}
disabled={locked}
onChange={(e) => updateShot(idx, { time_range: e.target.value })}
placeholder="0-3秒"
/>
</div>
<div className="vv-sb-shot-grid">
<div className="vv-sb-cell">
<label>景别 / 运镜</label>
<textarea
className="vv-textarea vv-sb-cell-ta"
value={sh.shot_type_angle_movement}
disabled={locked}
onChange={(e) =>
updateShot(idx, { shot_type_angle_movement: e.target.value })
}
/>
</div>
<div className="vv-sb-cell vv-sb-cell-wide">
<label>场景与对白</label>
<textarea
className="vv-textarea vv-sb-cell-ta"
value={sh.scene_and_dialogue}
disabled={locked}
onChange={(e) => updateShot(idx, { scene_and_dialogue: e.target.value })}
/>
</div>
<div className="vv-sb-cell">
<label>动作细节</label>
<textarea
className="vv-textarea vv-sb-cell-ta"
value={sh.action_details}
disabled={locked}
onChange={(e) => updateShot(idx, { action_details: e.target.value })}
/>
</div>
<div className="vv-sb-cell">
<label>音效 / BGM</label>
<textarea
className="vv-textarea vv-sb-cell-ta"
value={sh.audio_bgm}
disabled={locked}
onChange={(e) => updateShot(idx, { audio_bgm: e.target.value })}
/>
</div>
<div className="vv-sb-cell">
<label>转场</label>
<textarea
className="vv-textarea vv-sb-cell-ta"
value={sh.transition}
disabled={locked}
onChange={(e) => updateShot(idx, { transition: e.target.value })}
/>
</div>
<div className="vv-sb-cell vv-sb-ref">
<label>参考图</label>
<div className="vv-sb-ref-box">
{refImg?.url ? (
<img src={refImg.url} alt={`ref-${idx}`} className="vv-sb-ref-img" />
) : (
<span className="vv-sb-ref-empty">未指定</span>
<div className="vv-sb-kv">
<span className="vv-sb-k">景别/角度与运镜:</span>
<textarea
className="vv-textarea vv-sb-doc-ta vv-sb-doc-ta-sm"
value={sh.shot_type_angle_movement}
disabled={locked}
onChange={(e) =>
updateShot(idx, { shot_type_angle_movement: e.target.value })
}
/>
</div>
<div className="vv-sb-kv">
<span className="vv-sb-k">场景与对白:</span>
<textarea
className="vv-textarea vv-sb-doc-ta"
value={sh.scene_and_dialogue}
disabled={locked}
onChange={(e) => updateShot(idx, { scene_and_dialogue: e.target.value })}
/>
</div>
<div className="vv-sb-kv">
<span className="vv-sb-k">动作与真人细节:</span>
<textarea
className="vv-textarea vv-sb-doc-ta vv-sb-doc-ta-sm"
value={sh.action_details}
disabled={locked}
onChange={(e) => updateShot(idx, { action_details: e.target.value })}
/>
</div>
<div className="vv-sb-kv">
<span className="vv-sb-k">音效/BGM:</span>
<textarea
className="vv-textarea vv-sb-doc-ta vv-sb-doc-ta-sm"
value={sh.audio_bgm}
disabled={locked}
onChange={(e) => updateShot(idx, { audio_bgm: e.target.value })}
/>
</div>
<div className="vv-sb-kv">
<span className="vv-sb-k">转场:</span>
<textarea
className="vv-textarea vv-sb-doc-ta vv-sb-doc-ta-sm"
value={sh.transition}
disabled={locked}
onChange={(e) => updateShot(idx, { transition: e.target.value })}
/>
</div>
<div className="vv-sb-kv vv-sb-kv-ref">
<span className="vv-sb-k">参考图片:</span>
{refImg?.url ? (
<span className="vv-sb-ref-badge">
<img src={refImg.url} alt={`ref-${idx}`} className="vv-sb-ref-thumb" />
{!locked && (
<button
className="vv-sb-ref-x"
onClick={() => updateShot(idx, { reference_image_index: undefined })}
aria-label="移除参考图"
>
×
</button>
)}
</div>
</div>
</span>
) : !locked && task.images.length > 0 ? (
<span className="vv-sb-ref-pick">
{task.images.map((im, i) => (
<button
key={im.localId}
className="vv-sb-ref-opt"
onClick={() => updateShot(idx, { reference_image_index: i })}
title={`@图片${i + 1}`}
>
<img src={im.url} alt={`img${i}`} />
</button>
))}
</span>
) : (
<span className="vv-sb-muted">未指定</span>
)}
</div>
</div>
)
})}
</div>
</div>
{/* 4. 硬性约束 */}
<div className="vv-sb-section">
<div className="vv-sb-section-title">
<ThunderboltOutlined style={{ color: "#7c3aed" }} /> 硬性约束
</div>
{/* 硬性约束 */}
<h4 className="vv-sb-h">硬性约束</h4>
<div className="vv-sb-taglist">
{sb.hard_constraints.map((c, i) => (
<span key={i} className="vv-sb-tag vv-sb-tag-hard">
@@ -1281,13 +1322,9 @@ const ViralVideoPage: React.FC = () => {
</button>
)}
</div>
</div>
{/* 5. 负面提示词 */}
<div className="vv-sb-section">
<div className="vv-sb-section-title">
<CloseOutlined style={{ color: "#ef4444" }} /> 负面提示词
</div>
{/* 负面提示词 */}
<h4 className="vv-sb-h">负面提示词</h4>
<div className="vv-sb-taglist">
{sb.negative_prompts.map((c, i) => (
<span key={i} className="vv-sb-tag vv-sb-tag-neg">
@@ -1314,20 +1351,18 @@ const ViralVideoPage: React.FC = () => {
</button>
)}
</div>
</div>
{/* 6. 完整口播稿(折叠) */}
<div className="vv-sb-section">
<button className="vv-sb-collapse" onClick={() => setVoOpen(!voOpen)}>
<span>
<SoundOutlined style={{ color: "#7c3aed", marginRight: 6 }} />
{/* 完整口播稿(可折叠) */}
<h4 className="vv-sb-h">
<button className="vv-sb-doc-collapse" onClick={() => setVoOpen(!voOpen)} type="button">
<SoundOutlined style={{ color: "#22d3ee", marginRight: 6 }} />
完整口播稿(TTS 合成使用)
</span>
<span className={`vv-sb-caret ${voOpen ? "open" : ""}`}>▾</span>
</button>
<span className={`vv-sb-caret ${voOpen ? "open" : ""}`}>▾</span>
</button>
</h4>
{voOpen && (
<textarea
className="vv-textarea vv-sb-vo"
className="vv-textarea vv-sb-doc-ta vv-sb-vo"
value={sb.voiceover_script}
disabled={locked}
onChange={(e) => updateStoryboard({ voiceover_script: e.target.value })}
@@ -1336,16 +1371,18 @@ const ViralVideoPage: React.FC = () => {
)}
</div>
{/* 7. 重新生成按钮(ghost) */}
{task.uiStep === "step2_copy_ready" && (
<button
className="vv-btn vv-btn-ghost vv-btn-block"
onClick={handleGenerateCopy}
style={{ marginTop: 12 }}
>
<ReloadOutlined /> 重新生成文案
</button>
)}
<div className="vv-sb-actions">
{isMockPreview && (
<span className="vv-copy-tag vv-copy-tag-preview">
示例预览 · 后端新结构就绪后自动替换
</span>
)}
{task.uiStep === "step2_copy_ready" && (
<button className="vv-btn vv-btn-ghost" onClick={handleGenerateCopy}>
<ReloadOutlined /> 重新生成文案
</button>
)}
</div>
</div>
)
}
@@ -1639,7 +1676,7 @@ const ViralVideoPage: React.FC = () => {
/>
{task.refAudio && (
<div className="vv-selected-audio">
<SoundOutlined style={{ color: "#7c3aed", marginRight: 6 }} />
<SoundOutlined style={{ color: "#22d3ee", marginRight: 6 }} />
<span style={{ flex: 1 }}>已选:{task.refAudio.name}</span>
<button
className="vv-link-btn"
@@ -1914,33 +1951,70 @@ const ViralVideoPage: React.FC = () => {
{/* ═══════════ 右列 STEP 3 ═══════════ */}
<div className="vv-col">
<div className="vv-section">
<div className="vv-step-subhead">
<span className={`vv-step-badge ${step3Enabled ? "" : "vv-step-badge-dim"}`}>3</span>
<span className="vv-step-subtitle">STEP 3 视频设置并生成视频</span>
<div className="vv-step-subhead vv-step3-head">
<div className="vv-step3-head-left">
<span className={`vv-step-badge ${step3Enabled ? "" : "vv-step-badge-dim"}`}>
3
</span>
<span className="vv-step-subtitle">STEP 3 生成视频</span>
</div>
{step3Enabled && (
<button
className="vv-btn vv-btn-ghost vv-btn-xs vv-history-btn"
onClick={() => message.info("历史作品功能开发中")}
>
<HistoryOutlined /> 历史作品
</button>
)}
{!step3Enabled && <span className="vv-step-lock">请先完成 STEP 2</span>}
</div>
<div className="vv-step3-divider" />
<div className="vv-section-body">
<div className="vv-form-row">
<label className="vv-label">视频比例</label>
<Select
className="vv-select"
style={{ width: "100%" }}
value={task.videoRatio}
onChange={(v) => setTask({ videoRatio: v })}
options={RATIOS.map((r) => ({ value: r.v, label: r.label }))}
disabled={task.uiStep === "step3_generating" || task.uiStep === "step3_done"}
/>
</div>
<div className="vv-form-row">
<label className="vv-label">生成模型</label>
<Select
className="vv-select"
style={{ width: "100%" }}
value={task.videoModel}
onChange={(v) => setTask({ videoModel: v })}
options={MODELS.map((m) => ({ value: m.v, label: m.label }))}
disabled={task.uiStep === "step3_generating" || task.uiStep === "step3_done"}
/>
<div className="vv-step3-grid">
<div className="vv-form-row">
<label className="vv-label">视频比例</label>
<Select
className="vv-select vv-select-step3"
style={{ width: "100%" }}
value={task.videoRatio}
onChange={(v) => setTask({ videoRatio: v })}
options={RATIOS.map((r) => ({ value: r.v, label: r.v }))}
disabled={task.uiStep === "step3_generating" || task.uiStep === "step3_done"}
/>
</div>
<div className="vv-form-row">
<label className="vv-label">视频模型</label>
<Select
className="vv-select vv-select-step3"
style={{ width: "100%" }}
value={task.videoModel}
onChange={(v) => setTask({ videoModel: v })}
options={MODELS.map((m) => ({ value: m.v, label: m.label }))}
disabled={task.uiStep === "step3_generating" || task.uiStep === "step3_done"}
/>
</div>
<div className="vv-form-row">
<label className="vv-label">清晰度</label>
<Select
className="vv-select vv-select-step3"
style={{ width: "100%" }}
value={task.quality}
onChange={(v) => setTask({ quality: v })}
options={QUALITY_OPTIONS.map((m) => ({ value: m.v, label: m.label }))}
disabled={task.uiStep === "step3_generating" || task.uiStep === "step3_done"}
/>
</div>
<div className="vv-form-row">
<label className="vv-label">成片超分</label>
<Select
className="vv-select vv-select-step3"
style={{ width: "100%" }}
value={task.upscale}
onChange={(v) => setTask({ upscale: v })}
options={UPSCALE_OPTIONS.map((m) => ({ value: m.v, label: m.label }))}
disabled={task.uiStep === "step3_generating" || task.uiStep === "step3_done"}
/>
</div>
</div>
</div>
</div>
@@ -1962,7 +2036,7 @@ const ViralVideoPage: React.FC = () => {
) : task.uiStep === "step3_generating" ? (
<div className="vv-preview-placeholder">
<div className="ph-icon">
<LoadingOutlined style={{ fontSize: 36, color: "#7c3aed" }} />
<LoadingOutlined style={{ fontSize: 36, color: "#22d3ee" }} />
</div>
<div className="ph-txt">
AI 正在生成视频中…