diff --git a/apps/web/src/api/viral-video/index.ts b/apps/web/src/api/viral-video/index.ts index 69ba8ae70..f7b86e824 100644 --- a/apps/web/src/api/viral-video/index.ts +++ b/apps/web/src/api/viral-video/index.ts @@ -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("/viral-video/models").then((r) => r.data) + return apiClient.get("/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 辅助函数(后端新接口上线后可替换) ── */ diff --git a/apps/web/src/api/viral-video/types.ts b/apps/web/src/api/viral-video/types.ts index 92a791ec1..835c4f362 100644 --- a/apps/web/src/api/viral-video/types.ts +++ b/apps/web/src/api/viral-video/types.ts @@ -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 */ diff --git a/apps/web/src/pages/viral-video/ViralVideoPage.tsx b/apps/web/src/pages/viral-video/ViralVideoPage.tsx index bd8309789..237cbf65e 100644 --- a/apps/web/src/pages/viral-video/ViralVideoPage.tsx +++ b/apps/web/src/pages/viral-video/ViralVideoPage.tsx @@ -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, + }))} />
@@ -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