fix: 前端交互全面审计 — 上传422修复 + 进度反馈 + 防重复提交 #99
@@ -54,11 +54,20 @@ apiClient.interceptors.response.use(
|
||||
handled = true;
|
||||
} else {
|
||||
const status = error.response?.status;
|
||||
if (status && status >= 500) {
|
||||
if (status === 413) {
|
||||
message.error('文件过大,请缩小后重试');
|
||||
handled = true;
|
||||
} else if (status === 415) {
|
||||
message.error('不支持的文件格式');
|
||||
handled = true;
|
||||
} else if (status === 503) {
|
||||
message.error('服务暂不可用,请稍后再试');
|
||||
handled = true;
|
||||
} else if (status && status >= 500) {
|
||||
message.error('服务器繁忙,请稍后再试');
|
||||
handled = true;
|
||||
}
|
||||
// 4xx 且无具体信息时不弹通用提示,由各组件自行处理
|
||||
// 其他 4xx 且无具体信息时不弹通用提示,由各组件自行处理
|
||||
}
|
||||
|
||||
// 标记已展示过提示,组件 onError 可据此跳过重复 toast
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
deleteAsset,
|
||||
uploadAsset,
|
||||
} from '@/api/assets';
|
||||
import { getOrCreateDefaultProject } from '@/api/projects';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { Dragger } = Upload;
|
||||
@@ -71,6 +72,7 @@ const AssetLibrary: React.FC = () => {
|
||||
const [newLibKind, setNewLibKind] = useState<'video' | 'voice' | 'image'>(
|
||||
'video'
|
||||
);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
// 获取素材库列表
|
||||
const { data: libraries = [], isLoading: libsLoading } = useQuery({
|
||||
@@ -125,10 +127,19 @@ const AssetLibrary: React.FC = () => {
|
||||
message.warning('请先选择素材库');
|
||||
return false;
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('library_id', activeLibrary);
|
||||
await uploadMutation.mutateAsync(formData);
|
||||
setUploading(true);
|
||||
try {
|
||||
const project = await getOrCreateDefaultProject();
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('library_id', activeLibrary);
|
||||
formData.append('project_id', project.id);
|
||||
await uploadMutation.mutateAsync(formData);
|
||||
} catch {
|
||||
// uploadMutation.onError 已处理错误提示
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -205,12 +216,15 @@ const AssetLibrary: React.FC = () => {
|
||||
beforeUpload={handleUpload}
|
||||
showUploadList={false}
|
||||
multiple
|
||||
disabled={uploading || uploadMutation.isPending}
|
||||
style={{ marginBottom: 24 }}
|
||||
>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<InboxOutlined />
|
||||
{uploading ? <Spin /> : <InboxOutlined />}
|
||||
</p>
|
||||
<p className="ant-upload-text">
|
||||
{uploading ? '正在上传,请稍候...' : '点击或拖拽文件到此区域上传'}
|
||||
</p>
|
||||
<p className="ant-upload-text">点击或拖拽文件到此区域上传</p>
|
||||
<p className="ant-upload-hint">
|
||||
支持 {kindLabel[currentLib?.kind || 'video']} 格式文件
|
||||
</p>
|
||||
|
||||
@@ -326,14 +326,14 @@ const GeneratePage: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
disabled={currentStep === 0}
|
||||
disabled={currentStep === 0 || generating}
|
||||
onClick={() => setCurrentStep((s) => s - 1)}
|
||||
>
|
||||
上一步
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={currentStep === steps.length - 1}
|
||||
disabled={currentStep === steps.length - 1 || generating}
|
||||
onClick={() => setCurrentStep((s) => s + 1)}
|
||||
>
|
||||
下一步
|
||||
|
||||
@@ -18,6 +18,7 @@ const Billing: React.FC = () => {
|
||||
const [subscription, setSubscription] = useState<SubscriptionInfo | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [autoRenewChecked, setAutoRenewChecked] = useState(false);
|
||||
const [autoRenewLoading, setAutoRenewLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
@@ -36,6 +37,7 @@ const Billing: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleToggleAutoRenew = async (checked: boolean) => {
|
||||
setAutoRenewLoading(true);
|
||||
try {
|
||||
const res = await toggleAutoRenew(checked);
|
||||
message.success(res.message);
|
||||
@@ -45,6 +47,8 @@ const Billing: React.FC = () => {
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (!err?.__msgShown) message.error('操作失败');
|
||||
} finally {
|
||||
setAutoRenewLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -94,6 +98,7 @@ const Billing: React.FC = () => {
|
||||
<Switch
|
||||
checked={autoRenewChecked}
|
||||
onChange={handleToggleAutoRenew}
|
||||
loading={autoRenewLoading}
|
||||
checkedChildren="开"
|
||||
unCheckedChildren="关"
|
||||
/>
|
||||
|
||||
@@ -158,6 +158,7 @@ const TemplateLibrary: React.FC = () => {
|
||||
<StarOutlined />
|
||||
)
|
||||
}
|
||||
disabled={favMutation.isPending}
|
||||
onClick={() => favMutation.mutate(template.id)}
|
||||
/>,
|
||||
]}
|
||||
|
||||
@@ -229,7 +229,21 @@ const TitleLibrary: React.FC = () => {
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : filteredTitles.length === 0 ? (
|
||||
<Empty description={searchText ? '未找到匹配的标题' : '暂无标题'} />
|
||||
<Empty description={searchText ? '未找到匹配的标题' : '暂无标题'}>
|
||||
{!searchText && (
|
||||
<Space>
|
||||
<Button type="primary" onClick={openCreate}>
|
||||
新建标题
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ImportOutlined />}
|
||||
onClick={() => setImportModalOpen(true)}
|
||||
>
|
||||
批量导入
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
</Empty>
|
||||
) : (
|
||||
<Table
|
||||
columns={columns}
|
||||
|
||||
Reference in New Issue
Block a user