fix: 前端交互全面审计 — 上传422修复 + 进度反馈 + 防重复提交 #99

Merged
xiaoxia merged 1 commits from fix/frontend-interaction-audit into develop 2026-06-29 12:32:27 +08:00
6 changed files with 54 additions and 11 deletions
+11 -2
View File
@@ -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
+20 -6
View File
@@ -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>
+2 -2
View File
@@ -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)}
/>,
]}
+15 -1
View File
@@ -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}