fix(viral-video): STEP2 UI 重做 - 2列表单/删BGM/文案区重设按钮流 (#2124)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 15s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 7s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
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 / Build Staging API Image (push) Successful in 38s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 25s
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 / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (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 2m37s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m56s
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
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 / 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 / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (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
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled

This commit was merged in pull request #2124.
This commit is contained in:
2026-10-01 12:50:13 +08:00
parent 033c4a2eab
commit 19024da223
2 changed files with 177 additions and 103 deletions
@@ -1596,3 +1596,108 @@
grid-template-columns: repeat(2, 1fr);
}
}
/* ── STEP2 2列表单网格 ── */
.vv-form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px 18px;
margin-top: 12px;
}
.vv-form-row {
display: flex;
flex-direction: column;
gap: 6px;
}
.vv-label {
font-size: 13px;
font-weight: 500;
color: #374151;
display: flex;
align-items: center;
gap: 4px;
}
.vv-label-optional {
font-size: 12px;
color: #9ca3af;
font-weight: 400;
}
.vv-input {
width: 100%;
height: 36px;
border-radius: 8px;
border: 1px solid #e5e7eb;
background: #fff;
padding: 0 12px;
font-size: 13px;
color: #111827;
outline: none;
transition:
border-color 0.15s,
box-shadow 0.15s;
}
.vv-input:focus {
border-color: #7c3aed;
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.15);
}
.vv-input:disabled {
background: #f9fafb;
color: #9ca3af;
cursor: not-allowed;
}
.vv-input::placeholder {
color: #9ca3af;
}
/* 自定义 Select 样式(浅色紫调) */
.vv-select-purple .ant-select-selector {
border-radius: 8px !important;
border: 1px solid #e5e7eb !important;
background: #fff !important;
min-height: 36px !important;
padding: 0 28px 0 12px !important;
box-shadow: none !important;
transition:
border-color 0.15s,
box-shadow 0.15s !important;
}
.vv-select-purple.ant-select-focused .ant-select-selector,
.vv-select-purple .ant-select-focused .ant-select-selector {
border-color: #7c3aed !important;
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.15) !important;
}
.vv-select-purple .ant-select-selection-item,
.vv-select-purple .ant-select-selection-placeholder {
line-height: 34px !important;
font-size: 13px !important;
color: #111827 !important;
}
.vv-select-purple .ant-select-selection-placeholder {
color: #9ca3af !important;
}
.vv-select-purple .ant-select-arrow {
color: #6b7280;
right: 10px !important;
}
.vv-select-purple.ant-select-disabled .ant-select-selector {
background: #f9fafb !important;
color: #9ca3af !important;
cursor: not-allowed;
}
/* ghost/outline 按钮 */
.vv-btn-ghost {
background: #fff;
color: #7c3aed;
border: 1px solid #d6c6fb;
}
.vv-btn-ghost:hover {
background: #f8f4ff;
border-color: #7c3aed;
}
@media (max-width: 640px) {
.vv-form-grid {
grid-template-columns: 1fr;
}
}
+72 -103
View File
@@ -109,11 +109,11 @@ type TabTask = {
userCopy: string
industry: string
targetCustomer: string
persona: string
language: string
viralStructure: string
marketingPurpose: string
duration: number
bgm: string
// STEP3 设置
videoRatio: string
videoModel: string
@@ -134,17 +134,18 @@ type TabTask = {
/* ─────────── 常量 ─────────── */
const INDUSTRIES = [
"美妆个护",
"服饰鞋包",
"数码3C",
"食品饮料",
"家居日用",
"母婴亲子",
"教育培训",
"其他",
]
const LANGUAGES = ["中文(普通话)", "粤语", "英语", "日语", "韩语"]
const TARGET_CUSTOMERS = [
"小镇青年",
"小镇中老年",
"Z世代",
"精致妈妈",
"新锐白领",
"资深中产",
"都市蓝领",
"都市银发",
]
const PERSONAS = ["亲切邻家姐姐", "专业顾问", "搞笑段子手", "真实测评博主", "闺蜜种草", "行业专家"]
const STRUCTURES = [
"痛点开场→卖点→促单",
"反差对比→证据→促单",
@@ -152,7 +153,7 @@ const STRUCTURES = [
"专家背书→数据→促单",
]
const PURPOSES = ["品牌种草", "新品上市", "促销转化", "活动引流"]
const BGMS = ["无BGM", "欢快活泼", "温柔治愈", "励志燃向", "搞笑魔性", "科技感"]
const DURATIONS = [15, 20, 30, 45, 60]
const RATIOS = [
{ v: "9:16", label: "9:16 竖屏(抖音/视频号)" },
{ v: "16:9", label: "16:9 横屏(B站/YouTube)" },
@@ -187,8 +188,8 @@ const emptyTask = (id: string, title: string): TabTask => ({
language: "中文(普通话)",
viralStructure: STRUCTURES[0],
marketingPurpose: PURPOSES[0],
duration: 30,
bgm: BGMS[0],
duration: 15,
persona: "",
videoRatio: "9:16",
videoModel: "seedance-2.5",
uiStep: "step1_upload",
@@ -655,11 +656,10 @@ const ViralVideoPage: React.FC = () => {
const job = await generateViralCopy(task.jobId, {
industry: task.industry || undefined,
target_customer: task.targetCustomer || undefined,
persona_id: undefined,
viral_structure: task.viralStructure || undefined,
marketing_purpose: task.marketingPurpose || undefined,
bgm_preference: task.bgm === "无BGM" ? undefined : task.bgm,
duration: task.duration,
persona_id: task.persona || undefined,
user_copy_text:
task.fusionLevel === "user_primary" && task.userCopy ? task.userCopy : undefined,
fusion_level: task.fusionLevel,
@@ -673,33 +673,6 @@ const ViralVideoPage: React.FC = () => {
}
}
/* ── STEP 2:确认文案 → 直接触发 STEP 3 视频生成(confirm-copy 接口) ── */
const handleConfirmCopy = async () => {
if (!task.generatedCopy.trim()) {
message.warning("文案不能为空")
return
}
if (!task.jobId) {
message.warning("请先完成文案生成")
return
}
setTask({ uiStep: "step3_generating", videoError: undefined })
try {
const edited = task.generatedCopy.trim()
const originalCopy = task.job?.copy_result?.final_copy || task.generatedCopy
const job = await confirmViralCopy(task.jobId, {
// 若用户未修改则不传 edited_copy,后端用 AI 文案;若修改过则传编辑后文案
edited_copy: edited !== originalCopy.trim() ? edited : undefined,
})
setTask({ job })
// 轮询继续,status=completed 时推进 step3_done
message.success("已提交视频生成,预计 1-3 分钟…")
} catch (err: unknown) {
message.error(err instanceof Error ? err.message : "提交失败")
setTask({ uiStep: "failed", videoError: err instanceof Error ? err.message : "提交失败" })
}
}
/* ── STEP 3:开始生成视频(重试/兜底入口,正常流程由 handleConfirmCopy 触发) ──
* 三步拆分后端下,点击「确认文案并生成视频」会直接走 confirmViralCopy 触发 TTS+渲染。
* 此处保留「开始生成视频」按钮作为网络中断/失败后重试、或旧状态兼容入口:
@@ -711,8 +684,10 @@ const ViralVideoPage: React.FC = () => {
let job
if (task.jobId && task.generatedCopy.trim()) {
// 新三步链路:基于已有 job 触发 confirm-copy
const edited = task.generatedCopy.trim()
const originalCopy = task.job?.copy_result?.final_copy || ""
job = await confirmViralCopy(task.jobId, {
edited_copy: task.generatedCopy.trim() || undefined,
edited_copy: edited && edited !== originalCopy.trim() ? edited : undefined,
})
} else {
// 兜底:走旧 /generate 接口(一次性跑完)
@@ -727,7 +702,6 @@ const ViralVideoPage: React.FC = () => {
fusion_level: task.fusionLevel,
voice_id: task.refAudio?.id || undefined,
voice_source: task.refAudio?.source,
bgm_preference: task.bgm === "无BGM" ? undefined : task.bgm,
industry: task.industry || undefined,
target_customer: task.targetCustomer || undefined,
language: task.language || undefined,
@@ -800,6 +774,7 @@ const ViralVideoPage: React.FC = () => {
task.uiStep === "step3_done" ||
task.uiStep === "failed"
const step3Enabled =
task.uiStep === "step2_copy_ready" ||
task.uiStep === "step3_ready" ||
task.uiStep === "step3_generating" ||
task.uiStep === "step3_done" ||
@@ -876,25 +851,29 @@ const ViralVideoPage: React.FC = () => {
task.uiStep === "step3_generating" ||
task.uiStep === "step3_done"
) {
const locked = task.uiStep !== "step2_copy_ready"
// 文案始终可编辑(即便是视频生成中也可以看但锁编辑)
const locked = task.uiStep === "step3_generating" || task.uiStep === "step3_done"
return (
<div className="vv-copy-box">
<div className="vv-copy-head">
<EditOutlined style={{ color: "#7c3aed" }} />
<span className="vv-copy-title">AI 生成文案</span>
{task.generatedTitle && <span className="vv-copy-sub">{task.generatedTitle}</span>}
{locked && <span className="vv-copy-locked">(已锁定)</span>}
</div>
<textarea
className="vv-textarea vv-copy-textarea"
value={task.generatedCopy}
onChange={(e) => setTask({ generatedCopy: e.target.value })}
disabled={locked}
placeholder="AI 生成的营销文案将显示在这里…"
placeholder="AI 生成的营销文案将显示在这里,可直接编辑修改…"
/>
{task.uiStep === "step2_copy_ready" && (
<button className="vv-btn vv-btn-primary vv-btn-block" onClick={handleConfirmCopy}>
<VideoCameraOutlined /> 确认文案并生成视频
<button
className="vv-btn vv-btn-ghost vv-btn-block"
onClick={handleGenerateCopy}
style={{ marginTop: 8 }}
>
<ReloadOutlined /> 重新生成文案
</button>
)}
</div>
@@ -1298,7 +1277,7 @@ const ViralVideoPage: React.FC = () => {
<div className="vv-section">
<div className="vv-step-subhead">
<span className={`vv-step-badge ${step2Enabled ? "" : "vv-step-badge-dim"}`}>2</span>
<span className="vv-step-subtitle">STEP 2 文案配置并生成文案</span>
<span className="vv-step-subtitle">STEP 2 生成视频文案</span>
{!step2Enabled && <span className="vv-step-lock">请先完成 STEP 1</span>}
</div>
<div className="vv-section-body">
@@ -1330,38 +1309,37 @@ const ViralVideoPage: React.FC = () => {
</div>
)}
{/* 视频参数 */}
<div className="vv-section-subtitle" style={{ marginTop: 16 }}>
视频参数
</div>
{/* 文案参数(2列网格) */}
<div className="vv-form-grid">
<div className="vv-form-row">
<label className="vv-label">行业</label>
<label className="vv-label">
行业与产品 <span className="vv-label-optional">(选填)</span>
</label>
<input
className="vv-input"
placeholder="如:家具店、宠物店、装修公司"
value={task.industry}
onChange={(e) => setTask({ industry: e.target.value })}
disabled={!canEditStep2 || !step2Enabled}
/>
</div>
<div className="vv-form-row">
<label className="vv-label">目标客户</label>
<Select
className="vv-select"
className="vv-select vv-select-purple"
style={{ width: "100%" }}
placeholder="请选择"
value={task.industry || undefined}
onChange={(v) => setTask({ industry: v })}
options={INDUSTRIES.map((i) => ({ value: i, label: i }))}
placeholder="请选择目标客户"
value={task.targetCustomer || undefined}
onChange={(v) => setTask({ targetCustomer: v })}
options={TARGET_CUSTOMERS.map((i) => ({ value: i, label: i }))}
allowClear
disabled={!canEditStep2 || !step2Enabled}
/>
</div>
<div className="vv-form-row">
<label className="vv-label">目标人群</label>
<input
className="vv-input"
placeholder="例:25-35岁职场女性"
value={task.targetCustomer}
onChange={(e) => setTask({ targetCustomer: e.target.value })}
disabled={!canEditStep2 || !step2Enabled}
/>
</div>
<div className="vv-form-row">
<label className="vv-label">语言</label>
<label className="vv-label">台词语言</label>
<Select
className="vv-select"
className="vv-select vv-select-purple"
style={{ width: "100%" }}
value={task.language}
onChange={(v) => setTask({ language: v })}
@@ -1370,20 +1348,22 @@ const ViralVideoPage: React.FC = () => {
/>
</div>
<div className="vv-form-row">
<label className="vv-label">视频时长</label>
<label className="vv-label">我的人设</label>
<Select
className="vv-select"
className="vv-select vv-select-purple"
style={{ width: "100%" }}
value={task.duration}
onChange={(v) => setTask({ duration: v })}
options={[15, 20, 30, 45, 60].map((n) => ({ value: n, label: `${n}秒` }))}
placeholder="请选择人设"
value={task.persona || undefined}
onChange={(v) => setTask({ persona: v })}
options={PERSONAS.map((i) => ({ value: i, label: i }))}
allowClear
disabled={!canEditStep2 || !step2Enabled}
/>
</div>
<div className="vv-form-row">
<label className="vv-label">叙事结构</label>
<label className="vv-label">爆款结构</label>
<Select
className="vv-select"
className="vv-select vv-select-purple"
style={{ width: "100%" }}
value={task.viralStructure}
onChange={(v) => setTask({ viralStructure: v })}
@@ -1394,7 +1374,7 @@ const ViralVideoPage: React.FC = () => {
<div className="vv-form-row">
<label className="vv-label">营销目的</label>
<Select
className="vv-select"
className="vv-select vv-select-purple"
style={{ width: "100%" }}
value={task.marketingPurpose}
onChange={(v) => setTask({ marketingPurpose: v })}
@@ -1403,19 +1383,15 @@ const ViralVideoPage: React.FC = () => {
/>
</div>
<div className="vv-form-row" style={{ gridColumn: "1 / -1" }}>
<label className="vv-label">BGM 风格</label>
<div className="vv-pill-row">
{BGMS.map((b) => (
<button
key={b}
className={`vv-pill ${task.bgm === b ? "active" : ""}`}
onClick={() => setTask({ bgm: b })}
disabled={!canEditStep2 || !step2Enabled}
>
{b}
</button>
))}
</div>
<label className="vv-label">文案视频时长</label>
<Select
className="vv-select vv-select-purple"
style={{ width: "100%" }}
value={task.duration}
onChange={(v) => setTask({ duration: v })}
options={DURATIONS.map((n) => ({ value: n, label: `${n}秒` }))}
disabled={!canEditStep2 || !step2Enabled}
/>
</div>
</div>
@@ -1575,13 +1551,7 @@ const ViralVideoPage: React.FC = () => {
className={`vv-btn vv-btn-primary ${!step3Enabled ? "vv-btn-disabled-gray" : ""}`}
onClick={handleGenerateVideo}
disabled={task.uiStep === "step3_generating" || !step3Enabled}
title={
!step3Enabled
? !step2Enabled
? "请先完成图片分析"
: "请先在 STEP 2 生成并确认文案"
: ""
}
title={!step3Enabled ? "请先在 STEP 2 生成文案" : ""}
>
{task.uiStep === "step3_generating" ? (
<>
@@ -1589,8 +1559,7 @@ const ViralVideoPage: React.FC = () => {
</>
) : !step3Enabled ? (
<>
<VideoCameraOutlined /> 开始生成视频
{!step2Enabled ? "(请先完成图片分析)" : "(请先确认文案)"}
<VideoCameraOutlined /> 开始生成视频(请先生成文案)
</>
) : (
<>