chore: sync develop → main (PR#97 前端交互反馈优化) #98
@@ -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);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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('删除失败') },
|
||||
});
|
||||
|
||||
/** 处理上传 */
|
||||
|
||||
@@ -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 || '发送失败,请重试');
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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 || '登录失败,请检查邮箱和密码');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 || '注册失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 || '重置失败,请重试');
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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('重新查重失败') },
|
||||
});
|
||||
|
||||
/** 批量删除 */
|
||||
|
||||
@@ -53,9 +53,9 @@ const DuplicationUpload: React.FC = () => {
|
||||
});
|
||||
message.success('查重任务已提交');
|
||||
},
|
||||
onError: () => {
|
||||
onError: (err: any) => {
|
||||
setUploading(false);
|
||||
message.error('上传失败,请重试');
|
||||
if (!err?.__msgShown) message.error('上传失败,请重试');
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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('重试失败') },
|
||||
});
|
||||
|
||||
/** 过滤后的任务 */
|
||||
|
||||
@@ -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('获取下载链接失败');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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('操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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('取消失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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('操作失败') },
|
||||
});
|
||||
|
||||
/** 提取所有分类 */
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
Reference in New Issue
Block a user