From 17a72288e60023d7c6a4a3952e339907ffa62e04 Mon Sep 17 00:00:00 2001 From: Coze Agent Date: Fri, 2 Oct 2026 20:43:56 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(viral-video):=20=E7=A7=AF=E5=88=86?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E9=A2=84=E4=BC=B0=E2=80=94=E2=80=94=E8=B0=83?= =?UTF-8?q?=E7=94=A8=20/viral-video/estimate-credits=EF=BC=8CSTEP3?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E5=8F=98=E5=8C=96=E9=98=B2=E6=8A=96300ms?= =?UTF-8?q?=E5=88=B7=E6=96=B0=EF=BC=8C402=E7=A7=AF=E5=88=86=E4=B8=8D?= =?UTF-8?q?=E8=B6=B3=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 estimateViralVideoCredits API 封装(POST /api/v1/viral-video/estimate-credits) - TabTask 新增 estimatedCredits / estimatedCreditsError 两个字段 - STEP2文案完成(step2_copy_ready)后首次拉取;STEP3视频模型/清晰度/比例/时长变化防抖300ms重拉 - 接口失败显示「-- 积分」,加载中显示「… 积分」,不阻断操作 - 提交生成视频时识别 402 Payment Required,message.error 显示后端文案(默认「积分不足,请充值」) - 未到STEP2阶段不显示积分区域 --- apps/web/src/api/viral-video/index.ts | 11 +++ .../src/pages/viral-video/ViralVideoPage.tsx | 90 +++++++++++++++++-- 2 files changed, 92 insertions(+), 9 deletions(-) diff --git a/apps/web/src/api/viral-video/index.ts b/apps/web/src/api/viral-video/index.ts index 35c49321a..ac6526517 100644 --- a/apps/web/src/api/viral-video/index.ts +++ b/apps/web/src/api/viral-video/index.ts @@ -51,6 +51,17 @@ export function analyzeViralStyle(id: string) { return apiClient.post(`/viral-video/${id}/analyze-style`).then((r) => r.data) } +/** 动态预估积分消耗(STEP3 参数变化时调用) */ +export function estimateViralVideoCredits(params: { + video_model: string + resolution: string + video_ratio: string + duration: number +}) { + return apiClient + .post<{ estimated_credits: number }>("/viral-video/estimate-credits", params) + .then((r) => r.data) +} /** ── 三步拆分:前端 mock 辅助函数(后端新接口上线后可替换) ── */ /** diff --git a/apps/web/src/pages/viral-video/ViralVideoPage.tsx b/apps/web/src/pages/viral-video/ViralVideoPage.tsx index 770fa642c..3095fda5f 100644 --- a/apps/web/src/pages/viral-video/ViralVideoPage.tsx +++ b/apps/web/src/pages/viral-video/ViralVideoPage.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useEffect, useRef, useState } from "react" +import axios from "axios" import { PlusOutlined, CloseOutlined, @@ -46,6 +47,7 @@ import { analyzeViralImages, generateViralCopy, confirmViralCopy, + estimateViralVideoCredits, } from "@/api/viral-video" import { useViralVideoPolling } from "./hooks/useViralVideoPolling" import CloneModal from "@/components/voice/CloneModal" @@ -123,6 +125,9 @@ type TabTask = { videoModel: string quality: string upscale: string + // 积分预估 + estimatedCredits: number | null // null=加载中/未发起 + estimatedCreditsError: boolean // true=接口失败显示 -- // UI 状态 uiStep: UIStep imageAnalysis: ImageAnalysisResult | null @@ -395,6 +400,8 @@ const emptyTask = (id: string, title: string): TabTask => ({ videoModel: "seedance-2.5", quality: "480p", upscale: "default", + estimatedCredits: null, + estimatedCreditsError: false, uiStep: "step1_upload", imageAnalysis: null, storyboard: null, @@ -565,6 +572,50 @@ const ViralVideoPage: React.FC = () => { ) useViralVideoPolling(task.jobId, onPollUpdate) + /* ── 积分动态预估:STEP2文案生成完成后首次调用;STEP3参数变化时防抖300ms刷新 ── */ + const creditsTimerRef = useRef | null>(null) + const creditsAbortRef = useRef(0) + useEffect(() => { + const creditsReady = + task.uiStep === "step2_copy_ready" || + task.uiStep === "step3_ready" || + task.uiStep === "step3_generating" || + task.uiStep === "step3_done" || + task.uiStep === "failed" + if (!creditsReady) { + // 未到可预估阶段:清空状态 + if (task.estimatedCredits !== null || task.estimatedCreditsError) { + setTask({ estimatedCredits: null, estimatedCreditsError: false }) + } + return + } + // 防抖 300ms + if (creditsTimerRef.current) clearTimeout(creditsTimerRef.current) + const seq = ++creditsAbortRef.current + creditsTimerRef.current = setTimeout(async () => { + try { + const res = await estimateViralVideoCredits({ + video_model: task.videoModel, + resolution: task.quality, + video_ratio: task.videoRatio, + duration: task.duration, + }) + if (seq !== creditsAbortRef.current) return + setTask({ + estimatedCredits: res.estimated_credits, + estimatedCreditsError: false, + }) + } catch { + if (seq !== creditsAbortRef.current) return + setTask({ estimatedCredits: null, estimatedCreditsError: true }) + } + }, 300) + return () => { + if (creditsTimerRef.current) clearTimeout(creditsTimerRef.current) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [task.videoModel, task.quality, task.videoRatio, task.duration, task.uiStep]) + /* ── OSS 上传 ── */ const uploadFile = useCallback( async (file: File, kind: "image" | "video" | "voice", onProgress?: (pct: number) => void) => { @@ -947,8 +998,21 @@ const ViralVideoPage: React.FC = () => { setTask({ job, jobId: job.id }) message.success("视频已提交生成,预计 1-3 分钟…") } catch (err: unknown) { - message.error(err instanceof Error ? err.message : "提交失败") - setTask({ uiStep: "failed", videoError: err instanceof Error ? err.message : "提交失败" }) + // 402 Payment Required → 积分不足 + if (axios.isAxiosError(err) && err.response?.status === 402) { + const detail = + (err.response.data as { detail?: string; message?: string; msg?: string } | undefined) + ?.detail || + (err.response.data as { detail?: string; message?: string; msg?: string } | undefined) + ?.message || + "积分不足,请充值" + message.error(detail) + setTask({ uiStep: "step3_ready", videoError: detail }) + return + } + const msg = err instanceof Error ? err.message : "提交失败" + message.error(msg) + setTask({ uiStep: "failed", videoError: msg }) } } @@ -2031,13 +2095,21 @@ const ViralVideoPage: React.FC = () => { -
- - - 预计消耗 - - 50 积分 -
+ {step3Enabled && ( +
+ + + 预计消耗 + + + {task.estimatedCreditsError + ? "-- 积分" + : task.estimatedCredits === null + ? "… 积分" + : `${task.estimatedCredits} 积分`} + +
+ )} {/* 视频预览区 */}
-- 2.54.0 From 87cd9a5300b053f91a9053875d73e01bcc3ca6e9 Mon Sep 17 00:00:00 2001 From: Coze Agent Date: Fri, 2 Oct 2026 20:48:23 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(viral-video):=20=E7=A7=AF=E5=88=86?= =?UTF-8?q?=E9=A2=84=E4=BC=B0=E6=98=BE=E7=A4=BA=E4=B8=A4=E4=BD=8D=E5=B0=8F?= =?UTF-8?q?=E6=95=B0=EF=BC=88toFixed(2)=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/pages/viral-video/ViralVideoPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/pages/viral-video/ViralVideoPage.tsx b/apps/web/src/pages/viral-video/ViralVideoPage.tsx index 3095fda5f..8d384b318 100644 --- a/apps/web/src/pages/viral-video/ViralVideoPage.tsx +++ b/apps/web/src/pages/viral-video/ViralVideoPage.tsx @@ -126,7 +126,7 @@ type TabTask = { quality: string upscale: string // 积分预估 - estimatedCredits: number | null // null=加载中/未发起 + estimatedCredits: number | null // null=加载中/未发起;浮点数,展示时 toFixed(2) estimatedCreditsError: boolean // true=接口失败显示 -- // UI 状态 uiStep: UIStep @@ -2106,7 +2106,7 @@ const ViralVideoPage: React.FC = () => { ? "-- 积分" : task.estimatedCredits === null ? "… 积分" - : `${task.estimatedCredits} 积分`} + : `${task.estimatedCredits.toFixed(2)} 积分`}
)} -- 2.54.0