diff --git a/apps/web/src/pages/viral-video/ViralVideoPage.css b/apps/web/src/pages/viral-video/ViralVideoPage.css index fdbb0202b..3c600dc1f 100644 --- a/apps/web/src/pages/viral-video/ViralVideoPage.css +++ b/apps/web/src/pages/viral-video/ViralVideoPage.css @@ -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; + } +} diff --git a/apps/web/src/pages/viral-video/ViralVideoPage.tsx b/apps/web/src/pages/viral-video/ViralVideoPage.tsx index 1fe1b6cbc..adba29306 100644 --- a/apps/web/src/pages/viral-video/ViralVideoPage.tsx +++ b/apps/web/src/pages/viral-video/ViralVideoPage.tsx @@ -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 (
AI 生成文案 {task.generatedTitle && {task.generatedTitle}} - {locked && (已锁定)}