Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0eaea0a9fd | |||
| 410584dd1a | |||
| e48c96633d | |||
| f1348e9fac | |||
| 7591f08faa | |||
| c579b48e05 | |||
| 1709f1c165 | |||
| e323d6ebeb | |||
| 4dc88c1721 | |||
| 942d81e3e7 | |||
| 6556016471 | |||
| e8c7562a28 | |||
| ee0a2f7222 | |||
| a759be19f9 | |||
| 8f0a202130 |
@@ -11,10 +11,7 @@ export interface AssetUploadZoneProps {
|
||||
onUpload: (file: File) => void
|
||||
}
|
||||
|
||||
export const AssetUploadZone: React.FC<AssetUploadZoneProps> = ({
|
||||
uploading,
|
||||
onUpload,
|
||||
}) => {
|
||||
export const AssetUploadZone: React.FC<AssetUploadZoneProps> = ({ uploading, onUpload }) => {
|
||||
return (
|
||||
<Upload.Dragger
|
||||
beforeUpload={(file) => {
|
||||
|
||||
@@ -17,7 +17,11 @@ interface UseBatchDeleteOptions {
|
||||
showResult: (result: BatchOperationResult, title: string, clear?: boolean) => void
|
||||
}
|
||||
|
||||
export const useBatchDelete = ({ selectedIds, invalidateAssets, showResult }: UseBatchDeleteOptions) => {
|
||||
export const useBatchDelete = ({
|
||||
selectedIds,
|
||||
invalidateAssets,
|
||||
showResult,
|
||||
}: UseBatchDeleteOptions) => {
|
||||
const [batchLoading, setBatchLoading] = useState(false)
|
||||
|
||||
const handleBatchDelete = useCallback(async () => {
|
||||
@@ -135,7 +139,11 @@ interface UseBatchClassifyOptions {
|
||||
showResult: (result: BatchOperationResult, title: string, clear?: boolean) => void
|
||||
}
|
||||
|
||||
export const useBatchClassify = ({ selectedIds, queryClient, showResult }: UseBatchClassifyOptions) => {
|
||||
export const useBatchClassify = ({
|
||||
selectedIds,
|
||||
queryClient,
|
||||
showResult,
|
||||
}: UseBatchClassifyOptions) => {
|
||||
const [classifyModalOpen, setClassifyModalOpen] = useState(false)
|
||||
const [batchCategory, setBatchCategory] = useState("")
|
||||
const [batchLoading, setBatchLoading] = useState(false)
|
||||
|
||||
@@ -73,10 +73,7 @@ interface UseBatchHelpersOptions {
|
||||
* 批量操作辅助函数
|
||||
* 刷新数据、显示操作结果
|
||||
*/
|
||||
export const useBatchHelpers = ({
|
||||
queryClient,
|
||||
setSelectedIds,
|
||||
}: UseBatchHelpersOptions) => {
|
||||
export const useBatchHelpers = ({ queryClient, setSelectedIds }: UseBatchHelpersOptions) => {
|
||||
const invalidateAssets = useCallback(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ["assets"] })
|
||||
queryClient.invalidateQueries({ queryKey: ["asset-libraries"] })
|
||||
|
||||
@@ -78,7 +78,10 @@ export function useAssetOperations({ selectedIds, setSelectedIds }: UseAssetOper
|
||||
|
||||
// 取任一批量操作的 loading 状态(任意一个在加载都算加载中)
|
||||
const batchLoading =
|
||||
deleteLoading || tagResult.batchLoading || classifyResult.batchLoading || markResult.batchLoading
|
||||
deleteLoading ||
|
||||
tagResult.batchLoading ||
|
||||
classifyResult.batchLoading ||
|
||||
markResult.batchLoading
|
||||
|
||||
/* ── 关闭结果 Drawer ── */
|
||||
const handleResultDrawerClose = useCallback(() => {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 错误提取工具
|
||||
* 从各种响应格式中安全提取错误消息
|
||||
*/
|
||||
|
||||
/** 安全提取字符串错误信息 */
|
||||
export const safeExtractError = (val: unknown): string => {
|
||||
if (typeof val === "string") return val
|
||||
if (typeof val === "object" && val !== null) {
|
||||
const obj = val as Record<string, unknown>
|
||||
if (typeof obj.message === "string") return obj.message
|
||||
if (typeof obj.msg === "string") return obj.msg
|
||||
if (typeof obj.detail === "string") return obj.detail
|
||||
if (typeof obj.message === "object" && obj.message !== null)
|
||||
return safeExtractError(obj.message)
|
||||
return JSON.stringify(val)
|
||||
}
|
||||
return String(val ?? "")
|
||||
}
|
||||
|
||||
/** 错误信息翻译/友好化 */
|
||||
export const translateError = (msg: string): string => {
|
||||
if (!msg) return "生成失败,请检查网络后重试或联系管理员"
|
||||
if (msg.includes("editing") || msg.includes("draft") || msg.includes("状态")) {
|
||||
return "正在准备生成,请稍候再试"
|
||||
}
|
||||
if (msg.includes("template_id") || msg.includes("not found") || msg.includes("不存在")) {
|
||||
return "所选模板或素材不可用,请重新选择"
|
||||
}
|
||||
if (msg.includes("asset") && (msg.includes("not found") || msg.includes("missing"))) {
|
||||
return "素材数据异常,请返回视频库重新检查"
|
||||
}
|
||||
if (msg.includes("timeout") || msg.includes("network") || msg.includes("ECONN")) {
|
||||
return "网络连接超时,请检查网络后重试"
|
||||
}
|
||||
if (msg.includes("quota") || msg.includes("limit") || msg.includes("exceed")) {
|
||||
return "已达到生成次数上限,请稍后再试或联系客服"
|
||||
}
|
||||
if (msg.length > 0 && msg.length < 100 && !msg.includes("{")) return msg
|
||||
return "生成失败,请稍后重试或联系管理员"
|
||||
}
|
||||
|
||||
/** 从 axios 错误对象中提取后端消息 */
|
||||
export const extractBackendError = (err: unknown): string => {
|
||||
const axiosErr = err as {
|
||||
response?: {
|
||||
data?: {
|
||||
message?: string | object
|
||||
error?: string | object
|
||||
detail?: string | object
|
||||
msg?: string | object
|
||||
}
|
||||
}
|
||||
message?: string
|
||||
}
|
||||
return (
|
||||
safeExtractError(axiosErr.response?.data?.message) ||
|
||||
safeExtractError(axiosErr.response?.data?.error) ||
|
||||
safeExtractError(axiosErr.response?.data?.detail) ||
|
||||
safeExtractError(axiosErr.response?.data?.msg) ||
|
||||
axiosErr.message ||
|
||||
""
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { GenerationPhase } from "./types"
|
||||
|
||||
/** 生成阶段映射 */
|
||||
export const getGenerationPhase = (p: number): GenerationPhase => {
|
||||
if (p < 20) return { label: "分析素材与配置", icon: "🔍" }
|
||||
if (p < 50) return { label: "智能剪辑合成", icon: "🎬" }
|
||||
if (p < 80) return { label: "渲染视频中", icon: "⚡" }
|
||||
return { label: "即将完成", icon: "✨" }
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { GeneratedVideo, EditPlanConfig } from "@/api/template-editor"
|
||||
import type { CoverConfig } from "../../../editing-planner/types"
|
||||
import type { TitleSettings } from "../../types"
|
||||
|
||||
/** useGenerateVideo 入参 */
|
||||
export interface UseGenerateVideoProps {
|
||||
titleSettings: TitleSettings
|
||||
selectedTemplate: string
|
||||
selectedMaterials: string[]
|
||||
materialMode: "manual" | "auto"
|
||||
smartSelectedIds: string[]
|
||||
voiceMode: "preset" | "custom" | "clone"
|
||||
selectedVoice: string
|
||||
selectedClonedVoice: string
|
||||
coverSettings: CoverConfig
|
||||
videoRatio: string
|
||||
style: string
|
||||
duration: number
|
||||
autoSubtitles: boolean
|
||||
bgm: boolean
|
||||
generateCount: number
|
||||
}
|
||||
|
||||
/** 生成阶段 */
|
||||
export interface GenerationPhase {
|
||||
label: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
/** useGenerateVideo 返回值 */
|
||||
export interface UseGenerateVideoResult {
|
||||
generating: boolean
|
||||
progress: number
|
||||
generated: boolean
|
||||
generateError: string | null
|
||||
generatedVideos: GeneratedVideo[]
|
||||
generate: () => Promise<void>
|
||||
retry: () => void
|
||||
dismissError: () => void
|
||||
download: () => Promise<void>
|
||||
share: () => Promise<void>
|
||||
getGenerationPhase: (p: number) => GenerationPhase
|
||||
}
|
||||
|
||||
export type EditPlanVoiceConfig = Pick<
|
||||
EditPlanConfig,
|
||||
"voice_id" | "voice_clone_profile_id" | "custom_audio_url" | "custom_text"
|
||||
>
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useRef, useCallback } from "react"
|
||||
import { message } from "antd"
|
||||
import { getGenerationStatus, getGenerationTaskResults } from "@/api/template-editor"
|
||||
import { safeExtractError } from "./errorUtils"
|
||||
|
||||
interface UseGenerationPollingOptions {
|
||||
templateId: string
|
||||
onProgress: (progress: number) => void
|
||||
onComplete: (videos: unknown[]) => void
|
||||
onFailed: (errorMsg: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成状态轮询 Hook
|
||||
* 轮询生成状态,更新进度,处理完成/失败
|
||||
*/
|
||||
export const useGenerationPolling = ({
|
||||
templateId,
|
||||
onProgress,
|
||||
onComplete,
|
||||
onFailed,
|
||||
}: UseGenerationPollingOptions) => {
|
||||
const progressTimer = useRef<ReturnType<typeof setTimeout>>()
|
||||
|
||||
const clearTimer = useCallback(() => {
|
||||
if (progressTimer.current) {
|
||||
clearTimeout(progressTimer.current)
|
||||
progressTimer.current = undefined
|
||||
}
|
||||
}, [])
|
||||
|
||||
const startPolling = useCallback(() => {
|
||||
const poll = async () => {
|
||||
try {
|
||||
const data = await getGenerationStatus(templateId)
|
||||
|
||||
if (data.plan_status === "completed") {
|
||||
onProgress(100)
|
||||
// 获取生成的视频结果
|
||||
let videos: unknown[] = []
|
||||
if (data.generation_task_id) {
|
||||
try {
|
||||
videos = await getGenerationTaskResults(data.generation_task_id)
|
||||
} catch (err) {
|
||||
console.error("[获取生成结果失败]", err)
|
||||
}
|
||||
}
|
||||
onComplete(videos)
|
||||
message.success("视频生成完成!")
|
||||
return
|
||||
}
|
||||
if (data.plan_status === "failed") {
|
||||
const dataAny = data as unknown as Record<string, unknown>
|
||||
const rawMsg =
|
||||
dataAny.error_message ||
|
||||
dataAny.error ||
|
||||
dataAny.message ||
|
||||
(Array.isArray(data.clips)
|
||||
? (data.clips as { status: string; error_message?: string }[]).find(
|
||||
(c) => c.status === "failed",
|
||||
)?.error_message
|
||||
: undefined) ||
|
||||
"视频生成失败,请联系管理员或重试"
|
||||
const errorMsg = safeExtractError(rawMsg)
|
||||
console.error("[生成失败] templateId:", templateId, "响应:", data)
|
||||
onFailed(errorMsg)
|
||||
message.error(errorMsg)
|
||||
return
|
||||
}
|
||||
|
||||
const clips = data.clips || []
|
||||
const total = clips.length || 1
|
||||
const done = (clips as { status: string }[]).filter((c) => c.status === "completed").length
|
||||
onProgress(Math.round((done / total) * 100))
|
||||
|
||||
progressTimer.current = setTimeout(poll, 2000)
|
||||
} catch (pollErr) {
|
||||
console.error("[轮询出错] templateId:", templateId, pollErr)
|
||||
progressTimer.current = setTimeout(poll, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
progressTimer.current = setTimeout(poll, 2000)
|
||||
}, [templateId, onProgress, onComplete, onFailed])
|
||||
|
||||
return { startPolling, clearTimer }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { EditPlanConfig } from "@/api/template-editor"
|
||||
|
||||
type VoiceConfig = Pick<
|
||||
EditPlanConfig,
|
||||
"voice_id" | "voice_clone_profile_id" | "custom_audio_url" | "custom_text"
|
||||
>
|
||||
|
||||
interface BuildVoiceConfigOptions {
|
||||
voiceMode: "preset" | "custom" | "clone"
|
||||
selectedVoice: string
|
||||
selectedClonedVoice: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 voiceMode 构建配音配置
|
||||
*/
|
||||
export const buildVoiceConfig = ({
|
||||
voiceMode,
|
||||
selectedVoice,
|
||||
selectedClonedVoice,
|
||||
}: BuildVoiceConfigOptions): VoiceConfig => {
|
||||
const voiceConfig: VoiceConfig = {}
|
||||
if (voiceMode === "preset") {
|
||||
voiceConfig.voice_id = selectedVoice || undefined
|
||||
} else if (voiceMode === "clone") {
|
||||
voiceConfig.voice_clone_profile_id = selectedClonedVoice || undefined
|
||||
} else if (voiceMode === "custom") {
|
||||
voiceConfig.voice_id = selectedVoice || undefined
|
||||
// 注意:customAudioUrl / customVoiceText 在 step5 hook 中,
|
||||
// 自定义配音模式需从 step5 组件传回
|
||||
}
|
||||
return voiceConfig
|
||||
}
|
||||
@@ -2,54 +2,35 @@
|
||||
* 视频生成 Hook
|
||||
* 封装视频生成的核心逻辑、状态管理、轮询等
|
||||
*/
|
||||
import { useState, useRef, useCallback } from "react"
|
||||
import { useState, useCallback } from "react"
|
||||
import { message } from "antd"
|
||||
import type { GeneratedVideo, EditPlanConfig } from "@/api/template-editor"
|
||||
import {
|
||||
generateEditPlan,
|
||||
updateEditPlan,
|
||||
getGenerationTaskResults,
|
||||
getGenerationStatus,
|
||||
getEditPlan,
|
||||
} from "@/api/template-editor"
|
||||
import type { CoverConfig } from "../../editing-planner/types"
|
||||
import type { TitleSettings } from "../types"
|
||||
import type { GeneratedVideo } from "@/api/template-editor"
|
||||
import { generateEditPlan, updateEditPlan, getEditPlan } from "@/api/template-editor"
|
||||
import type { UseGenerateVideoProps } from "./generate-video/types"
|
||||
import { getGenerationPhase } from "./generate-video/phase"
|
||||
import { useGenerationPolling } from "./generate-video/useGenerationPolling"
|
||||
import { buildVoiceConfig } from "./generate-video/voiceConfig"
|
||||
import { extractBackendError, translateError } from "./generate-video/errorUtils"
|
||||
|
||||
interface UseGenerateVideoProps {
|
||||
titleSettings: TitleSettings
|
||||
selectedTemplate: string
|
||||
selectedMaterials: string[]
|
||||
materialMode: "manual" | "auto"
|
||||
smartSelectedIds: string[]
|
||||
voiceMode: "preset" | "custom" | "clone"
|
||||
selectedVoice: string
|
||||
selectedClonedVoice: string
|
||||
coverSettings: CoverConfig
|
||||
videoRatio: string
|
||||
style: string
|
||||
duration: number
|
||||
autoSubtitles: boolean
|
||||
bgm: boolean
|
||||
generateCount: number
|
||||
}
|
||||
export function useGenerateVideo(props: UseGenerateVideoProps) {
|
||||
const {
|
||||
titleSettings,
|
||||
selectedTemplate,
|
||||
selectedMaterials,
|
||||
materialMode,
|
||||
smartSelectedIds,
|
||||
voiceMode,
|
||||
selectedVoice,
|
||||
selectedClonedVoice,
|
||||
coverSettings,
|
||||
videoRatio,
|
||||
style,
|
||||
duration,
|
||||
autoSubtitles,
|
||||
bgm,
|
||||
generateCount,
|
||||
} = props
|
||||
|
||||
export function useGenerateVideo({
|
||||
titleSettings,
|
||||
selectedTemplate,
|
||||
selectedMaterials,
|
||||
materialMode,
|
||||
smartSelectedIds,
|
||||
voiceMode,
|
||||
selectedVoice,
|
||||
selectedClonedVoice,
|
||||
coverSettings,
|
||||
videoRatio,
|
||||
style,
|
||||
duration,
|
||||
autoSubtitles,
|
||||
bgm,
|
||||
generateCount,
|
||||
}: UseGenerateVideoProps) {
|
||||
/* ── 生成状态 ── */
|
||||
const [generating, setGenerating] = useState(false)
|
||||
const [progress, setProgress] = useState(0)
|
||||
@@ -57,24 +38,26 @@ export function useGenerateVideo({
|
||||
const [generateError, setGenerateError] = useState<string | null>(null)
|
||||
const [generatedVideos, setGeneratedVideos] = useState<GeneratedVideo[]>([])
|
||||
|
||||
const progressTimer = useRef<ReturnType<typeof setInterval>>(undefined)
|
||||
const handleProgress = useCallback((p: number) => setProgress(p), [])
|
||||
const handleComplete = useCallback((videos: unknown[]) => {
|
||||
setGenerating(false)
|
||||
setGenerated(true)
|
||||
setGeneratedVideos(videos as GeneratedVideo[])
|
||||
}, [])
|
||||
const handleFailed = useCallback((errorMsg: string) => {
|
||||
setGenerating(false)
|
||||
setGenerateError(errorMsg)
|
||||
}, [])
|
||||
|
||||
/* ── 生成阶段映射 ── */
|
||||
const getGenerationPhase = (p: number) => {
|
||||
if (p < 20) return { label: "分析素材与配置", icon: "🔍" }
|
||||
if (p < 50) return { label: "智能剪辑合成", icon: "🎬" }
|
||||
if (p < 80) return { label: "渲染视频中", icon: "⚡" }
|
||||
return { label: "即将完成", icon: "✨" }
|
||||
}
|
||||
const { startPolling, clearTimer } = useGenerationPolling({
|
||||
templateId: selectedTemplate,
|
||||
onProgress: handleProgress,
|
||||
onComplete: handleComplete,
|
||||
onFailed: handleFailed,
|
||||
})
|
||||
|
||||
/* ── 生成视频 ── */
|
||||
const generate = useCallback(async () => {
|
||||
console.log("[handleGenerate] 开始生成, 参数:", {
|
||||
titleSettings,
|
||||
selectedTemplate,
|
||||
selectedMaterials,
|
||||
voiceMode,
|
||||
})
|
||||
if (!titleSettings.title.trim()) {
|
||||
message.warning("请先选择或输入标题")
|
||||
return
|
||||
@@ -83,7 +66,6 @@ export function useGenerateVideo({
|
||||
message.warning("请至少选择一个素材")
|
||||
return
|
||||
}
|
||||
|
||||
if (voiceMode === "clone" && !selectedClonedVoice) {
|
||||
message.warning("请先选择一个克隆音色")
|
||||
return
|
||||
@@ -93,21 +75,14 @@ export function useGenerateVideo({
|
||||
setProgress(0)
|
||||
setGenerated(false)
|
||||
setGenerateError(null)
|
||||
clearTimer()
|
||||
|
||||
try {
|
||||
const voiceConfig: Pick<
|
||||
EditPlanConfig,
|
||||
"voice_id" | "voice_clone_profile_id" | "custom_audio_url" | "custom_text"
|
||||
> = {}
|
||||
if (voiceMode === "preset") {
|
||||
voiceConfig.voice_id = selectedVoice || undefined
|
||||
} else if (voiceMode === "clone") {
|
||||
voiceConfig.voice_clone_profile_id = selectedClonedVoice || undefined
|
||||
} else if (voiceMode === "custom") {
|
||||
voiceConfig.voice_id = selectedVoice || undefined
|
||||
// 注意:customAudioUrl / customVoiceText 在 step5 hook 中,
|
||||
// 自定义配音模式需从 step5 组件传回
|
||||
}
|
||||
const voiceConfig = buildVoiceConfig({
|
||||
voiceMode,
|
||||
selectedVoice,
|
||||
selectedClonedVoice,
|
||||
})
|
||||
|
||||
// 获取或创建草稿
|
||||
await getEditPlan(selectedTemplate)
|
||||
@@ -140,149 +115,13 @@ export function useGenerateVideo({
|
||||
})
|
||||
|
||||
await generateEditPlan(selectedTemplate)
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
const data = await getGenerationStatus(selectedTemplate)
|
||||
|
||||
if (data.plan_status === "completed") {
|
||||
setProgress(100)
|
||||
setGenerating(false)
|
||||
setGenerated(true)
|
||||
|
||||
// 获取生成的视频结果
|
||||
if (data.generation_task_id) {
|
||||
try {
|
||||
const videos = await getGenerationTaskResults(data.generation_task_id)
|
||||
setGeneratedVideos(videos)
|
||||
} catch (err) {
|
||||
console.error("[获取生成结果失败]", err)
|
||||
}
|
||||
}
|
||||
|
||||
message.success("视频生成完成!")
|
||||
return
|
||||
}
|
||||
if (data.plan_status === "failed") {
|
||||
setGenerating(false)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 防御性错误提取
|
||||
const dataAny = data as Record<string, any>
|
||||
const rawMsg =
|
||||
dataAny.error_message ||
|
||||
dataAny.error ||
|
||||
dataAny.message ||
|
||||
(data.clips || []).find((c: { status: string }) => c.status === "failed")
|
||||
?.error_message ||
|
||||
"视频生成失败,请联系管理员或重试"
|
||||
const safeExtract = (val: unknown): string => {
|
||||
if (typeof val === "string") return val
|
||||
if (typeof val === "object" && val !== null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 防御性错误提取
|
||||
const obj = val as Record<string, any>
|
||||
if (typeof obj.message === "string") return obj.message
|
||||
if (typeof obj.msg === "string") return obj.msg
|
||||
if (typeof obj.detail === "string") return obj.detail
|
||||
if (obj.message && typeof obj.message === "object") return safeExtract(obj.message)
|
||||
return JSON.stringify(val)
|
||||
}
|
||||
return String(val ?? "")
|
||||
}
|
||||
const errorMsg = safeExtract(rawMsg)
|
||||
console.error("[生成失败] templateId:", selectedTemplate, "响应:", data)
|
||||
setGenerateError(errorMsg)
|
||||
message.error(errorMsg)
|
||||
return
|
||||
}
|
||||
|
||||
const clips = data.clips || []
|
||||
const total = clips.length || 1
|
||||
const done = clips.filter((c: { status: string }) => c.status === "completed").length
|
||||
setProgress(Math.round((done / total) * 100))
|
||||
|
||||
progressTimer.current = setTimeout(poll, 2000) as unknown as ReturnType<
|
||||
typeof setInterval
|
||||
>
|
||||
} catch (pollErr) {
|
||||
console.error("[轮询出错] templateId:", selectedTemplate, pollErr)
|
||||
progressTimer.current = setTimeout(poll, 3000) as unknown as ReturnType<
|
||||
typeof setInterval
|
||||
>
|
||||
}
|
||||
}
|
||||
|
||||
progressTimer.current = setTimeout(poll, 2000) as unknown as ReturnType<typeof setInterval>
|
||||
startPolling()
|
||||
} catch (err: unknown) {
|
||||
console.error("[handleGenerate] 生成失败:", err)
|
||||
setGenerating(false)
|
||||
const axiosErr = err as {
|
||||
response?: {
|
||||
data?: {
|
||||
message?: string | object
|
||||
error?: string | object
|
||||
detail?: string | object
|
||||
msg?: string | object
|
||||
}
|
||||
}
|
||||
message?: string
|
||||
}
|
||||
const extractString = (val: unknown): string => {
|
||||
if (typeof val === "string") return val
|
||||
if (typeof val === "object" && val !== null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 防御性错误提取
|
||||
const obj = val as Record<string, any>
|
||||
if (typeof obj.message === "string") return obj.message
|
||||
if (typeof obj.msg === "string") return obj.msg
|
||||
if (typeof obj.detail === "string") return obj.detail
|
||||
if (typeof obj.message === "object" && obj.message !== null)
|
||||
return extractString(obj.message)
|
||||
if (typeof obj.msg === "object" && obj.msg !== null) return extractString(obj.msg)
|
||||
return JSON.stringify(val)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
const backendMsg =
|
||||
extractString(axiosErr.response?.data?.message) ||
|
||||
extractString(axiosErr.response?.data?.error) ||
|
||||
extractString(axiosErr.response?.data?.detail) ||
|
||||
extractString(axiosErr.response?.data?.msg) ||
|
||||
axiosErr.message ||
|
||||
""
|
||||
console.error("[handleGenerate] 错误信息:", backendMsg, "完整错误:", axiosErr)
|
||||
const safeExtractErr = (val: unknown): string => {
|
||||
if (typeof val === "string") return val
|
||||
if (typeof val === "object" && val !== null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 防御性错误提取
|
||||
const obj = val as Record<string, any>
|
||||
if (typeof obj.message === "string") return obj.message
|
||||
if (typeof obj.msg === "string") return obj.msg
|
||||
if (typeof obj.detail === "string") return obj.detail
|
||||
if (typeof obj.message === "object") return safeExtractErr(obj.message)
|
||||
return JSON.stringify(val)
|
||||
}
|
||||
return String(val ?? "")
|
||||
}
|
||||
const rawError = safeExtractErr(backendMsg)
|
||||
const translateError = (msg: string): string => {
|
||||
if (!msg) return "生成失败,请检查网络后重试或联系管理员"
|
||||
if (msg.includes("editing") || msg.includes("draft") || msg.includes("状态")) {
|
||||
return "正在准备生成,请稍候再试"
|
||||
}
|
||||
if (msg.includes("template_id") || msg.includes("not found") || msg.includes("不存在")) {
|
||||
return "所选模板或素材不可用,请重新选择"
|
||||
}
|
||||
if (msg.includes("asset") && (msg.includes("not found") || msg.includes("missing"))) {
|
||||
return "素材数据异常,请返回视频库重新检查"
|
||||
}
|
||||
if (msg.includes("timeout") || msg.includes("network") || msg.includes("ECONN")) {
|
||||
return "网络连接超时,请检查网络后重试"
|
||||
}
|
||||
if (msg.includes("quota") || msg.includes("limit") || msg.includes("exceed")) {
|
||||
return "已达到生成次数上限,请稍后再试或联系客服"
|
||||
}
|
||||
if (msg.length > 0 && msg.length < 100 && !msg.includes("{")) return msg
|
||||
return "生成失败,请稍后重试或联系管理员"
|
||||
}
|
||||
const finalMsg = translateError(rawError)
|
||||
const backendMsg = extractBackendError(err)
|
||||
console.error("[handleGenerate] 错误信息:", backendMsg, "完整错误:", err)
|
||||
const finalMsg = translateError(backendMsg)
|
||||
setGenerateError(finalMsg)
|
||||
message.error(finalMsg)
|
||||
}
|
||||
@@ -302,6 +141,8 @@ export function useGenerateVideo({
|
||||
materialMode,
|
||||
coverSettings,
|
||||
smartSelectedIds,
|
||||
clearTimer,
|
||||
startPolling,
|
||||
])
|
||||
|
||||
/* 重新生成(失败后重试) */
|
||||
|
||||
@@ -59,7 +59,7 @@ export const useTitleEdit = ({ updateMutation, createMutation }: UseTitleEditPro
|
||||
message.success("标题创建成功")
|
||||
},
|
||||
})
|
||||
}, [newTitleContent, newTitleType, createMutation])
|
||||
}, [newTitleContent, createMutation])
|
||||
|
||||
/* 关闭新建弹窗 */
|
||||
const handleCloseCreateModal = useCallback(() => {
|
||||
|
||||
+1
-1
@@ -213,4 +213,4 @@ export function useVoiceMaterialActions({
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Regular → Executable
+18
-2
@@ -1,32 +1,48 @@
|
||||
/**
|
||||
* GeneratePage 模块 smoke test
|
||||
* 建立完整依赖链,确保 vitest related 模式能匹配到
|
||||
* generate 目录下所有文件的改动(包括 Phase 3 子组件)
|
||||
* generate 目录下所有文件的改动(包括 Phase 3 子组件和 Hook)
|
||||
*/
|
||||
import { describe, it, expect } from "vitest"
|
||||
|
||||
// 直接引入所有 Step 组件,建立完整依赖链
|
||||
// 页面与 Step 组件
|
||||
import "@/pages/generate/GeneratePage"
|
||||
import "@/pages/generate/components/Step2MaterialSelect"
|
||||
import "@/pages/generate/components/Step4TitleSettings"
|
||||
import "@/pages/generate/components/Step5VoiceSelect"
|
||||
|
||||
// 音色选择子组件
|
||||
import "@/pages/generate/components/voice/VoiceRecommendSection"
|
||||
import "@/pages/generate/components/voice/VoiceChoiceCard"
|
||||
import "@/pages/generate/components/voice/PresetVoiceDetail"
|
||||
import "@/pages/generate/components/voice/CustomVoicePanel"
|
||||
import "@/pages/generate/components/voice/SaveVoiceModal"
|
||||
import "@/pages/generate/components/voice/CloneVoiceSection"
|
||||
|
||||
// 素材选择子组件
|
||||
import "@/pages/generate/components/material/MaterialModeTabs"
|
||||
import "@/pages/generate/components/material/ManualMaterialList"
|
||||
import "@/pages/generate/components/material/SmartMatchInput"
|
||||
import "@/pages/generate/components/material/SmartMatchResults"
|
||||
import "@/pages/generate/components/material/SmartMatchCard"
|
||||
|
||||
// 标题设置子组件
|
||||
import "@/pages/generate/components/title/AiTitleGenerator"
|
||||
import "@/pages/generate/components/title/AiTitleCard"
|
||||
import "@/pages/generate/components/title/TitleStylePanel"
|
||||
import "@/pages/generate/components/title/TitlePresetsGrid"
|
||||
|
||||
// 工具函数
|
||||
import "@/pages/generate/utils/formatDuration"
|
||||
|
||||
// useGenerateVideo Hook(Phase 3 拆分产物)
|
||||
import "@/pages/generate/hooks/useGenerateVideo"
|
||||
import "@/pages/generate/hooks/generate-video/types"
|
||||
import "@/pages/generate/hooks/generate-video/phase"
|
||||
import "@/pages/generate/hooks/generate-video/errorUtils"
|
||||
import "@/pages/generate/hooks/generate-video/voiceConfig"
|
||||
import "@/pages/generate/hooks/generate-video/useGenerationPolling"
|
||||
|
||||
describe("GeneratePage module smoke test", () => {
|
||||
it("should load all generate modules", () => {
|
||||
// 纯模块加载测试,确保所有组件/工具函数能正常 import
|
||||
|
||||
@@ -24,3 +24,10 @@ describe("VoiceMaterialLibrary module smoke test", () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// Hooks
|
||||
import "@/pages/voice-materials/hooks/useVoiceMaterials"
|
||||
import "@/pages/voice-materials/hooks/useVoiceMaterials/useVoiceMaterialActions"
|
||||
import "@/pages/voice-materials/hooks/useTtsSynthesize"
|
||||
import "@/pages/voice-materials/hooks/useAudioPlayer"
|
||||
import "@/pages/voice-materials/hooks/useBatchOperations"
|
||||
|
||||
Reference in New Issue
Block a user