fix(viral-video) P0: 修复GET /models响应包装导致页面白屏,数组操作加防御 #2163

Merged
xiaoxia merged 1 commits from fix/viral-video-p0-models-api-wrapper into develop 2026-10-03 20:42:11 +08:00
3 changed files with 53 additions and 20 deletions
+10 -2
View File
@@ -10,6 +10,7 @@ import type {
GenerateCopyRequest,
ConfirmCopyRequest,
ViralVideoModel,
ViralVideoModelsResponse,
} from "./types"
/** 创建爆款视频任务 */
@@ -64,9 +65,16 @@ export function estimateViralVideoCredits(params: {
.then((r) => r.data)
}
/** 获取支持的视频模型列表(GET /viral-video/models) */
/** 获取支持的视频模型列表(GET /viral-video/models)。后端返回 {models: [...]} 包装 */
export function getViralVideoModels() {
return apiClient.get<ViralVideoModel[]>("/viral-video/models").then((r) => r.data)
return apiClient.get<ViralVideoModelsResponse>("/viral-video/models").then((r) => {
const data = r.data as ViralVideoModelsResponse | ViralVideoModel[] | null | undefined
if (Array.isArray(data)) return data
if (data && Array.isArray((data as ViralVideoModelsResponse).models)) {
return (data as ViralVideoModelsResponse).models
}
return []
})
}
/** ── 三步拆分:前端 mock 辅助函数(后端新接口上线后可替换) ── */
+6 -1
View File
@@ -287,11 +287,16 @@ export interface ViralVideoModel {
supports_audio: boolean
supported_resolutions: string[]
max_duration: number
/** 计费模式(可选):per_second / per_video 等,后端预留字段 */
/** 计费模式(可选):per_second / per_video / token 等 */
billing_mode?: string
is_default?: boolean
}
/** GET /viral-video/models 响应包装 */
export interface ViralVideoModelsResponse {
models: ViralVideoModel[]
}
/** v1.6 阶段3请求:用户确认/编辑口播文案后开始单次 Seedance 出片(POST /viral-video/{id}/confirm-copy) */
export interface ConfirmCopyRequest {
/** 用户编辑后的口播文案;为空则使用 AI 生成的 voiceover_script */
@@ -550,20 +550,30 @@ const ViralVideoPage: React.FC = () => {
useEffect(() => {
getViralVideoModels()
.then((list) => {
if (!list || list.length === 0) return
setVideoModels(list)
// 防御:确保 list 是数组
const safeList = Array.isArray(list) ? list : []
if (safeList.length === 0) return
setVideoModels(safeList)
// 如果当前选中的模型不在返回列表里,切换到默认模型并联动修正参数
setTask((t) => {
const inList = list.some((m) => m.key === t.videoModel)
const inList = safeList.some((m) => m && typeof m === "object" && m.key === t.videoModel)
if (inList) return t
const def = list.find((m) => m.is_default) || list[0]
const sortedRes = def.supported_resolutions.slice().sort((a, b) => {
const def =
safeList.find((m) => m && typeof m === "object" && m.is_default) || safeList[0]
if (!def) return t
const defRes = Array.isArray(def.supported_resolutions)
? def.supported_resolutions
: ["480p", "720p", "1080p"]
const sortedRes = defRes.slice().sort((a, b) => {
const ra = RESOLUTION_ORDER.indexOf(a)
const rb = RESOLUTION_ORDER.indexOf(b)
return (rb === -1 ? -1 : rb) - (ra === -1 ? -1 : ra)
})
const newRes = sortedRes[0] || "480p"
const newDur = Math.min(t.duration, def.max_duration)
const newDur = Math.min(
t.duration,
typeof def.max_duration === "number" ? def.max_duration : 15,
)
return { ...t, videoModel: def.key, quality: newRes, duration: newDur }
})
})
@@ -2379,27 +2389,35 @@ const ViralVideoPage: React.FC = () => {
style={{ width: "100%" }}
value={task.videoModel}
onChange={(v) => {
const m = videoModels.find((x) => x.key === v)
const m = videoModels.find((x) => x && x.key === v)
if (!m) {
setTask({ videoModel: v })
return
}
// 切换模型时自动联动修正分辨率/时长
const sortedRes = m.supported_resolutions.slice().sort((a, b) => {
// 切换模型时自动联动修正分辨率/时长(防御:确保 supported_resolutions 为数组)
const mRes = Array.isArray(m.supported_resolutions)
? m.supported_resolutions
: ["480p", "720p", "1080p"]
const sortedRes = mRes.slice().sort((a, b) => {
const ra = RESOLUTION_ORDER.indexOf(a)
const rb = RESOLUTION_ORDER.indexOf(b)
return (rb === -1 ? -1 : rb) - (ra === -1 ? -1 : ra)
})
const newRes = m.supported_resolutions.includes(task.quality)
const newRes = mRes.includes(task.quality)
? task.quality
: sortedRes[0] || "480p"
const newDur = Math.min(task.duration, m.max_duration)
const newDur = Math.min(
task.duration,
typeof m.max_duration === "number" ? m.max_duration : 15,
)
setTask({ videoModel: v, quality: newRes, duration: newDur })
}}
options={videoModels.map((m) => ({
value: m.key,
label: m.display_name,
}))}
options={videoModels
.filter((m) => m && typeof m === "object" && m.key)
.map((m) => ({
value: m.key,
label: m.display_name || m.key,
}))}
/>
</div>
<div className="vv-form-row">
@@ -2410,8 +2428,10 @@ const ViralVideoPage: React.FC = () => {
value={task.quality}
onChange={(v) => setTask({ quality: v })}
options={(() => {
const curM = videoModels.find((m) => m.key === task.videoModel)
const supported = curM?.supported_resolutions
const curM = videoModels.find((m) => m && m.key === task.videoModel)
const supported = Array.isArray(curM?.supported_resolutions)
? curM!.supported_resolutions
: null
const pool =
supported && supported.length > 0
? supported