fix: 全面补充前端交互状态反馈
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled

- client.ts: 添加全局错误拦截器,自动提取后端 detail/message/msg 字段展示 toast
  - 处理网络异常、超时、5xx 等场景
  - 通过 __msgShown 标记避免与组件 onError 重复弹提示
- 6 个 delete mutation 补充 onError 回退提示(AssetLibrary、TitleLibrary、
  VoiceLibrary、ProductLibrary、DuplicationResults×2)
- GeneratePage: 5 个 query 增加 loading + error 状态展示
- TemplateLibrary: favMutation 增加收藏/取消收藏成功提示及失败回退
- Dashboard: 增加 isError 错误状态展示,避免静默显示全零数据
- DuplicationDetail: 增加 isError 状态,区分加载失败与记录不存在
This commit is contained in:
Audit Bot
2026-06-29 09:35:08 +08:00
parent 6190752bb3
commit 4143018faa
10 changed files with 94 additions and 10 deletions
+32 -2
View File
@@ -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);
}
);
@@ -120,6 +120,7 @@ const AssetLibrary: React.FC = () => {
queryClient.invalidateQueries({ queryKey: ['assets', activeLibrary] });
queryClient.invalidateQueries({ queryKey: ['asset-libraries'] });
},
onError: (err: any) => { if (!err?.__msgShown) message.error('删除失败') },
});
/** 处理上传 */
+10 -1
View File
@@ -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 (
<div style={{ padding: '24px', maxWidth: 1200, margin: '0 auto' }}>
<Alert type="error" message="加载数据失败" description="仪表盘数据获取失败,请刷新页面重试。" showIcon />
</div>
);
}
return (
<div style={{ padding: '24px', maxWidth: 1200, margin: '0 auto' }}>
<Title level={3} style={{ marginBottom: 24 }}>
@@ -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 }}>
@@ -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('重新查重失败') },
});
/** 批量删除 */
+30 -5
View File
@@ -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,
@@ -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 }}>
@@ -64,6 +64,7 @@ const ProductLibrary: React.FC = () => {
message.success('已删除');
queryClient.invalidateQueries({ queryKey: ['products'] });
},
onError: (err: any) => { if (!err?.__msgShown) message.error('删除失败') },
});
// 下载
@@ -50,9 +50,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('操作失败') },
});
/** 提取所有分类 */
@@ -85,6 +85,7 @@ const TitleLibrary: React.FC = () => {
message.success('已删除');
queryClient.invalidateQueries({ queryKey: ['titles'] });
},
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 生成配音