feat(viral-video): 积分动态预估——调用 /viral-video/estimate-credits,STEP3参数变化防抖300ms刷新,402积分不足提示
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m49s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m9s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) 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 / 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 / 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 / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (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 Staging API Image (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 / Frontend Lint (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled

- 新增 estimateViralVideoCredits API 封装(POST /api/v1/viral-video/estimate-credits)
- TabTask 新增 estimatedCredits / estimatedCreditsError 两个字段
- STEP2文案完成(step2_copy_ready)后首次拉取;STEP3视频模型/清晰度/比例/时长变化防抖300ms重拉
- 接口失败显示「-- 积分」,加载中显示「… 积分」,不阻断操作
- 提交生成视频时识别 402 Payment Required,message.error 显示后端文案(默认「积分不足,请充值」)
- 未到STEP2阶段不显示积分区域
This commit is contained in:
Coze Agent
2026-10-02 20:43:56 +08:00
parent d8e995afff
commit 67c8b0a080
2 changed files with 92 additions and 9 deletions
+11
View File
@@ -51,6 +51,17 @@ export function analyzeViralStyle(id: string) {
return apiClient.post<ViralVideoJob>(`/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 辅助函数(后端新接口上线后可替换) ── */
/**
@@ -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<ReturnType<typeof setTimeout> | null>(null)
const creditsAbortRef = useRef<number>(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 = () => {
</div>
</div>
<div className="vv-credits">
<span>
<SoundOutlined style={{ marginRight: 4 }} />
预计消耗
</span>
<strong>50 积分</strong>
</div>
{step3Enabled && (
<div className="vv-credits">
<span>
<SoundOutlined style={{ marginRight: 4 }} />
预计消耗
</span>
<strong>
{task.estimatedCreditsError
? "-- 积分"
: task.estimatedCredits === null
? "… 积分"
: `${task.estimatedCredits} 积分`}
</strong>
</div>
)}
{/* 视频预览区 */}
<div className="vv-section">