diff --git a/apps/web/src/api/client.ts b/apps/web/src/api/client.ts index 771b76256..042e54a64 100644 --- a/apps/web/src/api/client.ts +++ b/apps/web/src/api/client.ts @@ -3,6 +3,7 @@ * 封装 Axios 实例,配置拦截器和 Token 管理 */ import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'; +import { message } from 'antd'; import { useAuthStore } from '@/store/authStore'; // 创建 Axios 实例 @@ -28,14 +29,43 @@ apiClient.interceptors.request.use( } ); -// 响应拦截器:处理未授权状态 +// 响应拦截器:统一错误提示 + 处理未授权状态 apiClient.interceptors.response.use( (response) => response, - async (error: AxiosError) => { + async (error: AxiosError<{ detail?: string; message?: string; msg?: string }>) => { + // 401 → 清除登录态 if (error.response?.status === 401) { useAuthStore.getState().clearAuth(); } + // 提取后端返回的错误信息(detail / message / msg) + const data = error.response?.data; + const serverMsg = data?.detail || data?.message || data?.msg; + let handled = false; + + if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) { + message.error('请求超时,请检查网络后重试'); + handled = true; + } else if (!error.response) { + message.error('网络连接异常,请检查网络设置'); + handled = true; + } else if (serverMsg) { + message.error(serverMsg); + handled = true; + } else { + const status = error.response?.status; + if (status && status >= 500) { + message.error('服务器繁忙,请稍后再试'); + handled = true; + } + // 4xx 且无具体信息时不弹通用提示,由各组件自行处理 + } + + // 标记已展示过提示,组件 onError 可据此跳过重复 toast + if (handled) { + (error as any).__msgShown = true; + } + return Promise.reject(error); } ); diff --git a/apps/web/src/pages/assets/AssetLibrary.tsx b/apps/web/src/pages/assets/AssetLibrary.tsx index 16fee17f3..e3506f61a 100644 --- a/apps/web/src/pages/assets/AssetLibrary.tsx +++ b/apps/web/src/pages/assets/AssetLibrary.tsx @@ -94,9 +94,7 @@ const AssetLibrary: React.FC = () => { setNewLibName(''); queryClient.invalidateQueries({ queryKey: ['asset-libraries'] }); }, - onError: () => { - message.error('创建失败'); - }, + onError: (err: any) => { if (!err?.__msgShown) message.error('创建失败') }, }); // 上传素材 @@ -107,9 +105,7 @@ const AssetLibrary: React.FC = () => { queryClient.invalidateQueries({ queryKey: ['assets', activeLibrary] }); queryClient.invalidateQueries({ queryKey: ['asset-libraries'] }); }, - onError: () => { - message.error('上传失败'); - }, + onError: (err: any) => { if (!err?.__msgShown) message.error('上传失败') }, }); // 删除素材 @@ -120,6 +116,7 @@ const AssetLibrary: React.FC = () => { queryClient.invalidateQueries({ queryKey: ['assets', activeLibrary] }); queryClient.invalidateQueries({ queryKey: ['asset-libraries'] }); }, + onError: (err: any) => { if (!err?.__msgShown) message.error('删除失败') }, }); /** 处理上传 */ diff --git a/apps/web/src/pages/auth/ForgotPassword.tsx b/apps/web/src/pages/auth/ForgotPassword.tsx index 246f142ba..1d636eaa5 100644 --- a/apps/web/src/pages/auth/ForgotPassword.tsx +++ b/apps/web/src/pages/auth/ForgotPassword.tsx @@ -20,7 +20,7 @@ const ForgotPassword: React.FC = () => { message.success('重置邮件已发送!'); }, onError: (error: any) => { - message.error(error.response?.data?.message || '发送失败,请重试'); + if (!error?.__msgShown) message.error(error.response?.data?.message || '发送失败,请重试'); }, }); diff --git a/apps/web/src/pages/auth/Login.tsx b/apps/web/src/pages/auth/Login.tsx index 3b6e03a02..2cc6bf166 100644 --- a/apps/web/src/pages/auth/Login.tsx +++ b/apps/web/src/pages/auth/Login.tsx @@ -27,7 +27,7 @@ const Login: React.FC = () => { message.success('登录成功!'); navigate('/'); } catch (error: any) { - message.error(error.response?.data?.message || '登录失败,请检查邮箱和密码'); + if (!error?.__msgShown) message.error(error.response?.data?.message || '登录失败,请检查邮箱和密码'); } }; diff --git a/apps/web/src/pages/auth/Register.tsx b/apps/web/src/pages/auth/Register.tsx index 09b98cddf..cfa18baa9 100644 --- a/apps/web/src/pages/auth/Register.tsx +++ b/apps/web/src/pages/auth/Register.tsx @@ -29,7 +29,7 @@ const Register: React.FC = () => { }); message.success('注册成功!请查收验证邮件。'); } catch (error: any) { - message.error(error.response?.data?.message || '注册失败,请重试'); + if (!error?.__msgShown) message.error(error.response?.data?.message || '注册失败,请重试'); } }; diff --git a/apps/web/src/pages/auth/ResetPassword.tsx b/apps/web/src/pages/auth/ResetPassword.tsx index 8370ea316..5eff3dd66 100644 --- a/apps/web/src/pages/auth/ResetPassword.tsx +++ b/apps/web/src/pages/auth/ResetPassword.tsx @@ -22,7 +22,7 @@ const ResetPassword: React.FC = () => { setTimeout(() => navigate('/login'), 2000); }, onError: (error: any) => { - message.error(error.response?.data?.message || '重置失败,请重试'); + if (!error?.__msgShown) message.error(error.response?.data?.message || '重置失败,请重试'); }, }); diff --git a/apps/web/src/pages/dashboard/Dashboard.tsx b/apps/web/src/pages/dashboard/Dashboard.tsx index 6d2b674ae..891ea5fc5 100644 --- a/apps/web/src/pages/dashboard/Dashboard.tsx +++ b/apps/web/src/pages/dashboard/Dashboard.tsx @@ -16,6 +16,7 @@ import { Button, Progress, Space, + Alert, } from 'antd'; import { FileOutlined, @@ -60,7 +61,7 @@ const StatusTag: React.FC<{ status: string }> = ({ status }) => { const Dashboard: React.FC = () => { const navigate = useNavigate(); - const { data, isLoading } = useQuery({ + const { data, isLoading, isError } = useQuery({ queryKey: ['dashboard-overview'], queryFn: getDashboardOverview, }); @@ -111,6 +112,14 @@ const Dashboard: React.FC = () => { ); } + if (isError) { + return ( +
+ +
+ ); + } + return (
diff --git a/apps/web/src/pages/duplication/DuplicationDetail.tsx b/apps/web/src/pages/duplication/DuplicationDetail.tsx index 888ee7e9e..5bf90291b 100644 --- a/apps/web/src/pages/duplication/DuplicationDetail.tsx +++ b/apps/web/src/pages/duplication/DuplicationDetail.tsx @@ -228,7 +228,7 @@ const DuplicationDetail: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); - const { data: detail, isLoading } = useQuery({ + const { data: detail, isLoading, isError } = useQuery({ queryKey: ['duplication-detail', id], queryFn: () => getDuplicationDetail(id!), enabled: !!id, @@ -242,6 +242,18 @@ const DuplicationDetail: React.FC = () => { ); } + if (isError) { + return ( + <div style={{ padding: 24 }}> + <Empty description="加载查重记录失败"> + <Button onClick={() => navigate('/duplication/results')}> + 返回列表 + </Button> + </Empty> + </div> + ); + } + if (!detail) { return ( <div style={{ padding: 24 }}> diff --git a/apps/web/src/pages/duplication/DuplicationResults.tsx b/apps/web/src/pages/duplication/DuplicationResults.tsx index d0aac3af0..0137182df 100644 --- a/apps/web/src/pages/duplication/DuplicationResults.tsx +++ b/apps/web/src/pages/duplication/DuplicationResults.tsx @@ -105,6 +105,7 @@ const DuplicationResults: React.FC = () => { message.success('已删除'); queryClient.invalidateQueries({ queryKey: ['duplication-records'] }); }, + onError: (err: any) => { if (!err?.__msgShown) message.error('删除失败') }, }); // 重新查重 @@ -114,6 +115,7 @@ const DuplicationResults: React.FC = () => { message.success('已重新提交查重'); queryClient.invalidateQueries({ queryKey: ['duplication-records'] }); }, + onError: (err: any) => { if (!err?.__msgShown) message.error('重新查重失败') }, }); /** 批量删除 */ diff --git a/apps/web/src/pages/duplication/DuplicationUpload.tsx b/apps/web/src/pages/duplication/DuplicationUpload.tsx index 22b8336b4..5b341b899 100644 --- a/apps/web/src/pages/duplication/DuplicationUpload.tsx +++ b/apps/web/src/pages/duplication/DuplicationUpload.tsx @@ -53,9 +53,9 @@ const DuplicationUpload: React.FC = () => { }); message.success('查重任务已提交'); }, - onError: () => { + onError: (err: any) => { setUploading(false); - message.error('上传失败,请重试'); + if (!err?.__msgShown) message.error('上传失败,请重试'); }, }); diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index beb92de75..2002645ae 100644 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -46,31 +46,31 @@ const GeneratePage: React.FC = () => { const [generated, setGenerated] = useState(false); // 获取模板列表 - const { data: templates = [] } = useQuery({ + const { data: templates = [], isLoading: tplLoading, isError: tplError } = useQuery({ queryKey: ['templates'], queryFn: getTemplates, }); // 获取素材库和素材 - const { data: libraries = [] } = useQuery({ + const { data: libraries = [], isLoading: libLoading, isError: libError } = useQuery({ queryKey: ['asset-libraries'], queryFn: getAssetLibraries, }); // 获取标题 - const { data: titles = [] } = useQuery({ + const { data: titles = [], isLoading: titleLoading, isError: titleError } = useQuery({ queryKey: ['titles'], queryFn: getTitles, }); // 获取配音 - const { data: voices = [] } = useQuery({ + const { data: voices = [], isLoading: voiceLoading, isError: voiceError } = useQuery({ queryKey: ['voices'], queryFn: getVoices, }); // 获取所有素材(跨库) - const { data: allAssets = [] } = useQuery({ + const { data: allAssets = [], isLoading: assetsLoading, isError: assetsError } = useQuery({ queryKey: ['all-assets'], queryFn: async () => { const all: AssetItem[] = []; @@ -83,6 +83,9 @@ const GeneratePage: React.FC = () => { enabled: libraries.length > 0, }); + const pageLoading = tplLoading || libLoading || titleLoading || voiceLoading || assetsLoading; + const pageError = tplError || libError || titleError || voiceError || assetsError; + // 创建生成任务 const generateMutation = useMutation({ mutationFn: createGenerationTask, @@ -91,8 +94,8 @@ const GeneratePage: React.FC = () => { setGenerated(true); setGenerating(false); }, - onError: () => { - message.error('生成失败'); + onError: (err: any) => { + if (!err?.__msgShown) message.error('生成失败'); setGenerating(false); }, }); @@ -275,6 +278,28 @@ const GeneratePage: React.FC = () => { }, ]; + if (pageLoading) { + return ( + <div style={{ textAlign: 'center', padding: 80 }}> + <Spin size="large" /> + </div> + ); + } + + if (pageError) { + return ( + <div style={{ padding: '24px', maxWidth: 1200, margin: '0 auto' }}> + <Alert + type="error" + message="加载数据失败" + description="部分数据获取失败,请刷新页面重试。" + showIcon + action={<Button onClick={() => window.location.reload()}>刷新页面</Button>} + /> + </div> + ); + } + return ( <div style={{ padding: '24px', maxWidth: 1200, margin: '0 auto' }}> <Title level={3} style={{ marginBottom: 24 }}> diff --git a/apps/web/src/pages/history/TaskHistory.tsx b/apps/web/src/pages/history/TaskHistory.tsx index 1076e7b66..5ab93014e 100644 --- a/apps/web/src/pages/history/TaskHistory.tsx +++ b/apps/web/src/pages/history/TaskHistory.tsx @@ -61,7 +61,7 @@ const TaskHistory: React.FC = () => { message.success('任务已重新提交'); queryClient.invalidateQueries({ queryKey: ['user-tasks'] }); }, - onError: () => message.error('重试失败'), + onError: (err: any) => { if (!err?.__msgShown) message.error('重试失败') }, }); /** 过滤后的任务 */ diff --git a/apps/web/src/pages/products/ProductLibrary.tsx b/apps/web/src/pages/products/ProductLibrary.tsx index cc21f4075..0d70f8de8 100644 --- a/apps/web/src/pages/products/ProductLibrary.tsx +++ b/apps/web/src/pages/products/ProductLibrary.tsx @@ -64,6 +64,7 @@ const ProductLibrary: React.FC = () => { message.success('已删除'); queryClient.invalidateQueries({ queryKey: ['products'] }); }, + onError: (err: any) => { if (!err?.__msgShown) message.error('删除失败') }, }); // 下载 @@ -71,8 +72,8 @@ const ProductLibrary: React.FC = () => { try { const { url } = await getProductDownloadUrl(productId); window.open(url, '_blank'); - } catch { - message.error('获取下载链接失败'); + } catch (err: any) { + if (!err?.__msgShown) message.error('获取下载链接失败'); } }; diff --git a/apps/web/src/pages/subscription/Billing.tsx b/apps/web/src/pages/subscription/Billing.tsx index 71d98a29b..7dc2a8bbc 100644 --- a/apps/web/src/pages/subscription/Billing.tsx +++ b/apps/web/src/pages/subscription/Billing.tsx @@ -28,8 +28,8 @@ const Billing: React.FC = () => { const data = await getCurrentSubscription(); setSubscription(data); setAutoRenewChecked(data.auto_renew); - } catch { - message.error('加载订阅数据失败'); + } catch (err: any) { + if (!err?.__msgShown) message.error('加载订阅数据失败'); } finally { setLoading(false); } @@ -43,8 +43,8 @@ const Billing: React.FC = () => { if (subscription) { setSubscription({ ...subscription, auto_renew: checked }); } - } catch { - message.error('操作失败'); + } catch (err: any) { + if (!err?.__msgShown) message.error('操作失败'); } }; diff --git a/apps/web/src/pages/subscription/UpgradeSubscription.tsx b/apps/web/src/pages/subscription/UpgradeSubscription.tsx index 0801a32fb..a51329899 100644 --- a/apps/web/src/pages/subscription/UpgradeSubscription.tsx +++ b/apps/web/src/pages/subscription/UpgradeSubscription.tsx @@ -32,8 +32,8 @@ const UpgradeSubscription: React.FC = () => { const data = await getCurrentSubscription(); setSubscription(data); setSelectedPlan(data.plan_id); - } catch { - message.error('获取订阅信息失败'); + } catch (err: any) { + if (!err?.__msgShown) message.error('获取订阅信息失败'); } finally { setLoading(false); } @@ -64,8 +64,8 @@ const UpgradeSubscription: React.FC = () => { } else { message.error(res.message); } - } catch { - message.error('套餐变更失败,请重试'); + } catch (err: any) { + if (!err?.__msgShown) message.error('套餐变更失败,请重试'); } finally { setSubmitting(false); } @@ -80,8 +80,8 @@ const UpgradeSubscription: React.FC = () => { if (subscription) { setSubscription({ ...subscription, auto_renew: enabled }); } - } catch { - message.error('操作失败'); + } catch (err: any) { + if (!err?.__msgShown) message.error('操作失败'); } }; @@ -97,8 +97,8 @@ const UpgradeSubscription: React.FC = () => { const res = await cancelSubscription(); message.success(res.message); navigate('/subscription'); - } catch { - message.error('取消失败'); + } catch (err: any) { + if (!err?.__msgShown) message.error('取消失败'); } }, }); diff --git a/apps/web/src/pages/templates/TemplateLibrary.tsx b/apps/web/src/pages/templates/TemplateLibrary.tsx index cedae704b..d49aa825b 100644 --- a/apps/web/src/pages/templates/TemplateLibrary.tsx +++ b/apps/web/src/pages/templates/TemplateLibrary.tsx @@ -18,6 +18,7 @@ import { Select, Modal, Image, + message, } from 'antd'; import { PlayCircleOutlined, @@ -50,9 +51,11 @@ const TemplateLibrary: React.FC = () => { // 收藏/取消收藏 const favMutation = useMutation({ mutationFn: toggleFavoriteTemplate, - onSuccess: () => { + onSuccess: (data) => { + message.success(data.is_favorite ? '已收藏' : '已取消收藏'); queryClient.invalidateQueries({ queryKey: ['templates'] }); }, + onError: (err: any) => { if (!err?.__msgShown) message.error('操作失败') }, }); /** 提取所有分类 */ diff --git a/apps/web/src/pages/titles/TitleLibrary.tsx b/apps/web/src/pages/titles/TitleLibrary.tsx index fcf982295..92fca2e23 100644 --- a/apps/web/src/pages/titles/TitleLibrary.tsx +++ b/apps/web/src/pages/titles/TitleLibrary.tsx @@ -62,7 +62,7 @@ const TitleLibrary: React.FC = () => { resetForm(); queryClient.invalidateQueries({ queryKey: ['titles'] }); }, - onError: () => message.error('创建失败'), + onError: (err: any) => { if (!err?.__msgShown) message.error('创建失败') }, }); // 更新标题 @@ -75,7 +75,7 @@ const TitleLibrary: React.FC = () => { resetForm(); queryClient.invalidateQueries({ queryKey: ['titles'] }); }, - onError: () => message.error('更新失败'), + onError: (err: any) => { if (!err?.__msgShown) message.error('更新失败') }, }); // 删除标题 @@ -85,6 +85,7 @@ const TitleLibrary: React.FC = () => { message.success('已删除'); queryClient.invalidateQueries({ queryKey: ['titles'] }); }, + onError: (err: any) => { if (!err?.__msgShown) message.error('删除失败') }, }); // 批量导入 @@ -97,7 +98,7 @@ const TitleLibrary: React.FC = () => { setImportText(''); queryClient.invalidateQueries({ queryKey: ['titles'] }); }, - onError: () => message.error('导入失败'), + onError: (err: any) => { if (!err?.__msgShown) message.error('导入失败') }, }); const resetForm = () => { diff --git a/apps/web/src/pages/voices/VoiceLibrary.tsx b/apps/web/src/pages/voices/VoiceLibrary.tsx index ca72148b4..68cfc8196 100644 --- a/apps/web/src/pages/voices/VoiceLibrary.tsx +++ b/apps/web/src/pages/voices/VoiceLibrary.tsx @@ -80,7 +80,7 @@ const VoiceLibrary: React.FC = () => { resetForm(); queryClient.invalidateQueries({ queryKey: ['voices'] }); }, - onError: () => message.error('创建失败'), + onError: (err: any) => { if (!err?.__msgShown) message.error('创建失败') }, }); // 更新配音 @@ -93,7 +93,7 @@ const VoiceLibrary: React.FC = () => { resetForm(); queryClient.invalidateQueries({ queryKey: ['voices'] }); }, - onError: () => message.error('更新失败'), + onError: (err: any) => { if (!err?.__msgShown) message.error('更新失败') }, }); // 删除配音 @@ -103,6 +103,7 @@ const VoiceLibrary: React.FC = () => { message.success('已删除'); queryClient.invalidateQueries({ queryKey: ['voices'] }); }, + onError: (err: any) => { if (!err?.__msgShown) message.error('删除失败') }, }); // AI 生成配音 @@ -114,7 +115,7 @@ const VoiceLibrary: React.FC = () => { setAiText(''); queryClient.invalidateQueries({ queryKey: ['voices'] }); }, - onError: () => message.error('AI 生成失败'), + onError: (err: any) => { if (!err?.__msgShown) message.error('AI 生成失败') }, }); const resetForm = () => {