From f1d584220340691fbc3fe4ff9ce761367efc9e50 Mon Sep 17 00:00:00 2001 From: Xiaoxia AI Date: Wed, 17 Jun 2026 19:48:19 +0800 Subject: [PATCH] feat(phase7): track batch classification jobs --- .../web/src/pages/workspace/ProjectAssets.tsx | 122 ++++++++++++++++-- 1 file changed, 110 insertions(+), 12 deletions(-) diff --git a/apps/web/src/pages/workspace/ProjectAssets.tsx b/apps/web/src/pages/workspace/ProjectAssets.tsx index 7f73d5d5d..9cdb91467 100644 --- a/apps/web/src/pages/workspace/ProjectAssets.tsx +++ b/apps/web/src/pages/workspace/ProjectAssets.tsx @@ -52,6 +52,26 @@ const batchModeOptions = [ { label: '包含已分类重跑', value: 'include_classified' }, ]; +type BatchProgressState = { + total: number; + submitted: number; + failed: number; + skipped: number; + completed: number; + running: boolean; + tracking: boolean; +}; + +const defaultBatchProgress: BatchProgressState = { + total: 0, + submitted: 0, + failed: 0, + skipped: 0, + completed: 0, + running: false, + tracking: false, +}; + const ProjectAssets: React.FC = () => { const { id } = useParams<{ id: string }>(); const projectId = id || ''; @@ -65,7 +85,10 @@ const ProjectAssets: React.FC = () => { const [classificationFilter, setClassificationFilter] = useState('all'); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [batchMode, setBatchMode] = useState<'unclassified_only' | 'include_classified'>('unclassified_only'); - const [batchProgress, setBatchProgress] = useState({ total: 0, submitted: 0, failed: 0, skipped: 0, running: false }); + const [batchProgress, setBatchProgress] = useState(defaultBatchProgress); + const [batchJobIds, setBatchJobIds] = useState([]); + const [batchCompletedIds, setBatchCompletedIds] = useState([]); + const [batchFailedIds, setBatchFailedIds] = useState([]); const [form] = Form.useForm(); const librariesQuery = useQuery({ @@ -133,6 +156,50 @@ const ProjectAssets: React.FC = () => { } }, [classificationJobQuery.data?.status, classificationJobQuery.data?.classification, assetsQuery]); + useEffect(() => { + if (!batchJobIds.length || !batchProgress.tracking) { + return; + } + + let cancelled = false; + + const pollBatchJobs = async () => { + try { + const results = await Promise.all(batchJobIds.map((jobId) => getClassificationJob(jobId))); + if (cancelled) { + return; + } + + const completedIds = results.filter((job) => job.status === 'completed').map((job) => job.id); + const failedIds = results.filter((job) => job.status === 'failed').map((job) => job.id); + const pendingCount = results.filter((job) => job.status === 'pending' || job.status === 'processing').length; + + setBatchCompletedIds(completedIds); + setBatchFailedIds(failedIds); + setBatchProgress((current) => ({ + ...current, + completed: completedIds.length, + failed: current.failed < failedIds.length ? failedIds.length : current.failed, + tracking: pendingCount > 0, + running: pendingCount > 0, + })); + + if (pendingCount === 0) { + assetsQuery.refetch(); + } + } catch { + } + }; + + pollBatchJobs(); + const timer = setInterval(pollBatchJobs, 2000); + + return () => { + cancelled = true; + clearInterval(timer); + }; + }, [batchJobIds, batchProgress.tracking, assetsQuery]); + const createLibraryMutation = useMutation({ mutationFn: createAssetLibrary, onSuccess: (library) => { @@ -201,31 +268,61 @@ const ProjectAssets: React.FC = () => { return; } - setBatchProgress({ total: selectedAssets.length, submitted: 0, failed: 0, skipped: skippedAssets.length, running: true }); + setBatchProgress({ + total: selectedAssets.length, + submitted: 0, + failed: 0, + skipped: skippedAssets.length, + completed: 0, + running: true, + tracking: false, + }); + setBatchJobIds([]); + setBatchCompletedIds([]); + setBatchFailedIds([]); let submitted = 0; let failed = 0; + const createdJobIds: string[] = []; for (const asset of runnableAssets) { try { - await submitClassificationJob({ + const job = await submitClassificationJob({ workspace_id: asset.workspace_id, project_id: asset.project_id, asset_id: asset.id, }); submitted += 1; + createdJobIds.push(job.id); } catch { failed += 1; } - setBatchProgress({ total: selectedAssets.length, submitted, failed, skipped: skippedAssets.length, running: true }); + setBatchProgress({ + total: selectedAssets.length, + submitted, + failed, + skipped: skippedAssets.length, + completed: 0, + running: true, + tracking: false, + }); } - setBatchProgress({ total: selectedAssets.length, submitted, failed, skipped: skippedAssets.length, running: false }); + setBatchJobIds(createdJobIds); + setBatchProgress({ + total: selectedAssets.length, + submitted, + failed, + skipped: skippedAssets.length, + completed: 0, + running: createdJobIds.length > 0, + tracking: createdJobIds.length > 0, + }); if (failed === 0) { message.success(`批量分类已提交:成功 ${submitted},跳过 ${skippedAssets.length}`); } else { - message.warning(`批量分类已完成:成功 ${submitted},失败 ${failed},跳过 ${skippedAssets.length}`); + message.warning(`批量分类已提交:成功 ${submitted},失败 ${failed},跳过 ${skippedAssets.length}`); } setSelectedRowKeys([]); @@ -309,7 +406,7 @@ const ProjectAssets: React.FC = () => { }; const batchPercent = batchProgress.total - ? Math.round(((batchProgress.submitted + batchProgress.failed + batchProgress.skipped) / batchProgress.total) * 100) + ? Math.round(((batchProgress.completed + batchProgress.failed + batchProgress.skipped) / batchProgress.total) * 100) : 0; return ( @@ -321,7 +418,7 @@ const ProjectAssets: React.FC = () => { extra={ + { )} {batchProgress.total > 0 && ( - - 0 ? 'exception' : 'success'} /> - + + 0 ? 'exception' : 'success'} /> + 总数:{batchProgress.total} - 成功提交:{batchProgress.submitted} + 已提交:{batchProgress.submitted} + 已完成:{batchProgress.completed} 失败:{batchProgress.failed} 跳过:{batchProgress.skipped}