Merge pull request 'fix(#1197): 修复预览结果渲染 React Error #31 — 对象被直接渲染为 React child' (#1215) from fix/1197-react-error-31 into develop
CI/CD Pipeline / Check if frontend-only change (push) 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 / Validate - Type Check (mypy) (push) Successful in 1m3s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m28s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m27s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m54s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m42s
CI/CD Pipeline / Integration Tests (push) Successful in 1m44s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 7m12s
CI/CD Pipeline / Unit Tests (push) Successful in 11m18s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 14m41s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 31s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 46s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 47s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m4s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped

This commit was merged in pull request #1215.
This commit is contained in:
2026-08-02 08:25:58 +08:00
3 changed files with 44 additions and 15 deletions
@@ -68,7 +68,9 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
<div className="xx-preview-video xx-preview-video--error">
<p style={{ color: "rgba(255,255,255,0.8)", fontSize: 14 }}></p>
</div>
<p className="xx-preview-error-msg">{previewError || "请重试"}</p>
<p className="xx-preview-error-msg">
{typeof previewError === "string" && previewError ? previewError : "请重试"}
</p>
<button className="xx-btn xx-btn-ghost xx-btn-block" onClick={onRegenerate}>
</button>
@@ -87,7 +89,10 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
<div className="xx-preview-info">
<div className="xx-preview-info-row">
<span></span>
<span>{previewResult.duration.toFixed(1)} </span>
<span>
{(typeof previewResult.duration === "number" ? previewResult.duration : 0).toFixed(1)}{" "}
</span>
</div>
<div className="xx-preview-info-row">
<span></span>
@@ -86,7 +86,9 @@ const Step3GeneratePreview: React.FC<Step3GeneratePreviewProps> = ({
<div className="xx-preview-error">
<ExclamationCircleFilled style={{ fontSize: 28, color: "#ef4444" }} />
<p className="xx-preview-error-text"></p>
<p className="xx-preview-error-desc">{previewError || "请稍后重试"}</p>
<p className="xx-preview-error-desc">
{typeof previewError === "string" && previewError ? previewError : "请稍后重试"}
</p>
<button className="xx-btn xx-btn-primary" onClick={onRegeneratePreview}>
<ReloadOutlined />
</button>
@@ -145,7 +147,11 @@ const Step3GeneratePreview: React.FC<Step3GeneratePreviewProps> = ({
<div className="xx-preview-plan-row">
<span className="xx-preview-plan-label"></span>
<span className="xx-preview-plan-value">
{previewResult.duration.toFixed(1)}
{(typeof previewResult.duration === "number"
? previewResult.duration
: 0
).toFixed(1)}{" "}
</span>
</div>
<div className="xx-preview-plan-row">
@@ -6,6 +6,24 @@ import { useState, useCallback, useMemo, useEffect, useRef } from "react"
import { createPreview, getPreviewStatus } from "@/api/generation"
import type { PreviewTaskResponse, PreviewStatus as ApiPreviewStatus } from "@/api/generation"
import type { EditingTemplate } from "@/api/editing-planner"
import { safeExtractError } from "./generate-video/errorUtils"
/** 安全地将值转为字符串,防止对象被直接渲染导致 React Error #31 */
const safeString = (val: unknown, fallback: string): string => {
if (val == null) return fallback
const s = safeExtractError(val)
return s || fallback
}
/** 安全地将值转为数字,防止非数字值进入渲染 */
const safeNumber = (val: unknown, fallback = 0): number => {
if (typeof val === "number" && !Number.isNaN(val)) return val
if (typeof val === "string") {
const n = Number(val)
return Number.isNaN(n) ? fallback : n
}
return fallback
}
interface UseStep3PreviewProps {
templates: EditingTemplate[]
@@ -165,14 +183,14 @@ export function useStep3Preview({
if (status === "completed") {
setProgress(100)
setPreviewResult({
taskId: data.task_id,
videoUrl: data.video_url ?? "",
clipCount: data.clip_count ?? 0,
transitionCount: data.transition_count ?? 0,
materialUsage: data.material_usage ?? 0,
duration: data.duration ?? 0,
fileSize: data.file_size ?? 0,
generateDuration: data.generate_duration ?? 0,
taskId: safeString(data.task_id, ""),
videoUrl: safeString(data.video_url, ""),
clipCount: safeNumber(data.clip_count),
transitionCount: safeNumber(data.transition_count),
materialUsage: safeNumber(data.material_usage),
duration: safeNumber(data.duration),
fileSize: safeNumber(data.file_size),
generateDuration: safeNumber(data.generate_duration),
progress: 100,
})
setPreviewStatus("ready")
@@ -180,7 +198,7 @@ export function useStep3Preview({
}
if (status === "failed") {
setPreviewError(data.error_message || "预览生成失败,请重试")
setPreviewError(safeString(data.error_message, "预览生成失败,请重试"))
setPreviewStatus("error")
return
}
@@ -192,7 +210,7 @@ export function useStep3Preview({
}
// pending / generating 状态继续轮询
setProgress(data.progress || 0)
setProgress(safeNumber(data.progress))
if (status === "pending") {
setPreviewStatus("pending")
pollTimerRef.current = setTimeout(poll, 5000)
@@ -248,7 +266,7 @@ export function useStep3Preview({
currentTaskIdRef.current = response.task_id
pollPreviewStatus(response.task_id)
} catch (e) {
setPreviewError(e instanceof Error ? e.message : "预览生成失败")
setPreviewError(safeString(e instanceof Error ? e.message : e, "预览生成失败"))
setPreviewStatus("error")
}
}, [