From af487c5f6f2d74e033a7a3c8f3c97e91be144886 Mon Sep 17 00:00:00 2001 From: Xiaoxia AI Date: Wed, 17 Jun 2026 19:30:30 +0800 Subject: [PATCH] feat(phase7): add batch classification progress feedback --- .../web/src/pages/workspace/ProjectAssets.tsx | 186 +++++++++--------- 1 file changed, 90 insertions(+), 96 deletions(-) diff --git a/apps/web/src/pages/workspace/ProjectAssets.tsx b/apps/web/src/pages/workspace/ProjectAssets.tsx index 389433b0b..335fbd5d1 100644 --- a/apps/web/src/pages/workspace/ProjectAssets.tsx +++ b/apps/web/src/pages/workspace/ProjectAssets.tsx @@ -9,6 +9,7 @@ import { Form, Input, Modal, + Progress, Row, Select, Space, @@ -58,6 +59,7 @@ const ProjectAssets: React.FC = () => { const [classifyingAssetId, setClassifyingAssetId] = useState(''); const [classificationFilter, setClassificationFilter] = useState('all'); const [selectedRowKeys, setSelectedRowKeys] = useState([]); + const [batchProgress, setBatchProgress] = useState({ total: 0, submitted: 0, failed: 0, skipped: 0, running: false }); const [form] = Form.useForm(); const librariesQuery = useQuery({ @@ -150,20 +152,14 @@ const ProjectAssets: React.FC = () => { }); const libraryOptions = useMemo( - () => - (librariesQuery.data || []).map((item) => ({ - label: `${item.name} (${item.kind})`, - value: item.id, - })), + () => (librariesQuery.data || []).map((item) => ({ label: `${item.name} (${item.kind})`, value: item.id })), [librariesQuery.data] ); const filteredAssets = useMemo(() => { const items = assetsQuery.data || []; if (classificationFilter === 'all') return items; - if (classificationFilter === 'unclassified') { - return items.filter((item) => !item.metadata?.classification); - } + if (classificationFilter === 'unclassified') return items.filter((item) => !item.metadata?.classification); return items.filter((item) => item.metadata?.classification === classificationFilter); }, [assetsQuery.data, classificationFilter]); @@ -172,33 +168,71 @@ const ProjectAssets: React.FC = () => { [filteredAssets, selectedRowKeys] ); - const selectableAssets = useMemo( - () => - selectedAssets.filter( - (item) => item.metadata?.auto_classification !== 'queued' && classifyingAssetId !== item.id - ), - [selectedAssets, classifyingAssetId] - ); - const handleBatchClassification = async () => { - if (!selectableAssets.length) { - message.warning('没有可批量分类的素材'); + if (!selectedAssets.length) { + message.warning('请先选择素材'); return; } - try { - for (const asset of selectableAssets) { + + const skippedAssets = selectedAssets.filter( + (item) => item.metadata?.auto_classification === 'queued' || classifyingAssetId === item.id + ); + const runnableAssets = selectedAssets.filter( + (item) => item.metadata?.auto_classification !== 'queued' && classifyingAssetId !== item.id + ); + + if (!runnableAssets.length) { + message.warning('选中的素材都处于分类中,无法重复提交'); + return; + } + + setBatchProgress({ + total: selectedAssets.length, + submitted: 0, + failed: 0, + skipped: skippedAssets.length, + running: true, + }); + + let submitted = 0; + let failed = 0; + + for (const asset of runnableAssets) { + try { await submitClassificationJob({ workspace_id: asset.workspace_id, project_id: asset.project_id, asset_id: asset.id, }); + submitted += 1; + } catch { + failed += 1; } - message.success(`已发起 ${selectableAssets.length} 个分类任务`); - setSelectedRowKeys([]); - assetsQuery.refetch(); - } catch { - message.error('批量发起分类失败'); + setBatchProgress({ + total: selectedAssets.length, + submitted, + failed, + skipped: skippedAssets.length, + running: true, + }); } + + setBatchProgress({ + total: selectedAssets.length, + submitted, + failed, + skipped: skippedAssets.length, + running: false, + }); + + if (failed === 0) { + message.success(`批量分类已提交:成功 ${submitted},跳过 ${skippedAssets.length}`); + } else { + message.warning(`批量分类已完成:成功 ${submitted},失败 ${failed},跳过 ${skippedAssets.length}`); + } + + setSelectedRowKeys([]); + assetsQuery.refetch(); }; const renderClassification = (asset: AssetItem) => { @@ -222,23 +256,9 @@ const ProjectAssets: React.FC = () => { const columns = [ { title: '名称', dataIndex: 'name', key: 'name' }, - { - title: '类型', - dataIndex: 'mime_type', - key: 'mime_type', - render: (value: string) => {value}, - }, - { - title: '分类结果', - key: 'classification', - render: (_: unknown, record: AssetItem) => renderClassification(record), - }, - { - title: '存储键', - dataIndex: 'storage_key', - key: 'storage_key', - ellipsis: true, - }, + { title: '类型', dataIndex: 'mime_type', key: 'mime_type', render: (value: string) => {value} }, + { title: '分类结果', key: 'classification', render: (_: unknown, record: AssetItem) => renderClassification(record) }, + { title: '存储键', dataIndex: 'storage_key', key: 'storage_key', ellipsis: true }, { title: '操作', key: 'actions', @@ -250,7 +270,7 @@ const ProjectAssets: React.FC = () => { ); }, @@ -297,6 +311,10 @@ const ProjectAssets: React.FC = () => { } }; + const batchPercent = batchProgress.total + ? Math.round(((batchProgress.submitted + batchProgress.failed + batchProgress.skipped) / batchProgress.total) * 100) + : 0; + return (
@@ -305,12 +323,7 @@ const ProjectAssets: React.FC = () => { title="项目素材管理" extra={ - setWorkspaceId(e.target.value)} /> - - - - + setWorkspaceId(e.target.value)} /> +