feat(viral-video): v1.6 暗色主题 + 分镜脚本线性文档风格 + STEP3改版 #2135
File diff suppressed because it is too large
Load Diff
@@ -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 正在生成视频中…
|
||||
|
||||
Reference in New Issue
Block a user