feat(viral-video): 爆款视频积分动态预估(estimate-credits) (#2151)
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 5s
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) 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 / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 26s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 56s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 41s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 56s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m9s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 38s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m57s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m2s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m15s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m21s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 3m35s
CI/CD Pipeline / Validate - Style (push) Successful in 4m38s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m14s
AI Code Review / AI Code Review (pull_request) Successful in 6m51s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m36s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m18s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 7m36s
CI/CD Pipeline / Validate - Security (push) Successful in 9m0s
CI/CD Pipeline / Unit Tests (push) Successful in 10m35s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped

Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
This commit was merged in pull request #2151.
This commit is contained in:
2026-10-03 00:25:59 +08:00
committed by auto-approve-bot
parent 5d6a4675fb
commit 7800ff4c3d
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=加载中/未发起;浮点数,展示时 toFixed(2)
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.toFixed(2)} 积分`}
</strong>
</div>
)}
{/* 视频预览区 */}
<div className="vv-section">