fix: migrate ProcessingPage from V1 batch API to V2 task API
Changes:
- Replace apiClient with apiClientV2 for task queries
- Update from batch status polling to task detail polling
- Change from batch_id to task_id (UUID string)
- Simplify UI to show single task instead of batch with multiple files
- Update redirect from /results to /tasks page
- Add task details card with timestamps
- Add error message display for failed tasks
- Calculate progress based on task status (pending: 0%, processing: 50%, completed/failed: 100%)
Fixes:
- 404 error: GET /api/v2/batch/{id}/status (endpoint no longer exists in V2)
- Continuous polling to non-existent batch endpoint
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -8,18 +8,21 @@ import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { useToast } from '@/components/ui/toast'
|
||||
import { useUploadStore } from '@/store/uploadStore'
|
||||
import { apiClient } from '@/services/api'
|
||||
import { apiClientV2 } from '@/services/apiV2'
|
||||
import { Play, CheckCircle, FileText, AlertCircle, Clock, Activity, Loader2 } from 'lucide-react'
|
||||
|
||||
export default function ProcessingPage() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { toast } = useToast()
|
||||
const { batchId, files } = useUploadStore()
|
||||
const { batchId } = useUploadStore()
|
||||
|
||||
// In V2, batchId is actually a task_id (string)
|
||||
const taskId = batchId ? String(batchId) : null
|
||||
|
||||
// Start OCR processing
|
||||
const processOCRMutation = useMutation({
|
||||
mutationFn: () => apiClient.processOCR({ batch_id: batchId! }),
|
||||
mutationFn: () => apiClientV2.startTask(taskId!),
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: '開始處理',
|
||||
@@ -36,16 +39,16 @@ export default function ProcessingPage() {
|
||||
},
|
||||
})
|
||||
|
||||
// Poll batch status
|
||||
const { data: batchStatus } = useQuery({
|
||||
queryKey: ['batchStatus', batchId],
|
||||
queryFn: () => apiClient.getBatchStatus(batchId!),
|
||||
enabled: !!batchId,
|
||||
// Poll task status
|
||||
const { data: taskDetail } = useQuery({
|
||||
queryKey: ['taskDetail', taskId],
|
||||
queryFn: () => apiClientV2.getTask(taskId!),
|
||||
enabled: !!taskId,
|
||||
refetchInterval: (query) => {
|
||||
const data = query.state.data
|
||||
if (!data) return 2000
|
||||
// Stop polling if completed or failed
|
||||
if (data.batch.status === 'completed' || data.batch.status === 'failed') {
|
||||
if (data.status === 'completed' || data.status === 'failed') {
|
||||
return false
|
||||
}
|
||||
return 2000 // Poll every 2 seconds
|
||||
@@ -54,19 +57,19 @@ export default function ProcessingPage() {
|
||||
|
||||
// Auto-redirect when completed
|
||||
useEffect(() => {
|
||||
if (batchStatus?.batch.status === 'completed') {
|
||||
if (taskDetail?.status === 'completed') {
|
||||
setTimeout(() => {
|
||||
navigate('/results')
|
||||
navigate('/tasks')
|
||||
}, 1000)
|
||||
}
|
||||
}, [batchStatus?.batch.status, navigate])
|
||||
}, [taskDetail?.status, navigate])
|
||||
|
||||
const handleStartProcessing = () => {
|
||||
processOCRMutation.mutate()
|
||||
}
|
||||
|
||||
const handleViewResults = () => {
|
||||
navigate('/results')
|
||||
navigate('/tasks')
|
||||
}
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
@@ -82,8 +85,21 @@ export default function ProcessingPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Show helpful message when no batch is selected
|
||||
if (!batchId) {
|
||||
const getProgressPercentage = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return 100
|
||||
case 'processing':
|
||||
return 50
|
||||
case 'failed':
|
||||
return 100
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Show helpful message when no task is selected
|
||||
if (!taskId) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<Card className="max-w-md text-center">
|
||||
@@ -97,7 +113,7 @@ export default function ProcessingPage() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-muted-foreground">
|
||||
{t('processing.noBatchMessage', { defaultValue: '尚未選擇任何批次。請先上傳檔案以建立批次。' })}
|
||||
{t('processing.noBatchMessage', { defaultValue: '尚未選擇任何任務。請先上傳檔案以建立任務。' })}
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => navigate('/upload')}
|
||||
@@ -111,9 +127,9 @@ export default function ProcessingPage() {
|
||||
)
|
||||
}
|
||||
|
||||
const isProcessing = batchStatus?.batch.status === 'processing'
|
||||
const isCompleted = batchStatus?.batch.status === 'completed'
|
||||
const isPending = !batchStatus || batchStatus.batch.status === 'pending'
|
||||
const isProcessing = taskDetail?.status === 'processing'
|
||||
const isCompleted = taskDetail?.status === 'completed'
|
||||
const isPending = !taskDetail || taskDetail.status === 'pending'
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -123,7 +139,8 @@ export default function ProcessingPage() {
|
||||
<div>
|
||||
<h1 className="page-title">{t('processing.title')}</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
批次 ID: <span className="font-mono text-primary">{batchId}</span> · 共 {files.length} 個檔案
|
||||
任務 ID: <span className="font-mono text-primary">{taskId}</span>
|
||||
{taskDetail?.filename && ` · ${taskDetail.filename}`}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
@@ -153,7 +170,7 @@ export default function ProcessingPage() {
|
||||
</div>
|
||||
<CardTitle>{t('processing.progress')}</CardTitle>
|
||||
</div>
|
||||
{batchStatus && getStatusBadge(batchStatus.batch.status)}
|
||||
{taskDetail && getStatusBadge(taskDetail.status)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
@@ -162,63 +179,58 @@ export default function ProcessingPage() {
|
||||
<div className="flex justify-between text-sm mb-3">
|
||||
<span className="text-muted-foreground font-medium">{t('processing.status')}</span>
|
||||
<span className="font-bold text-xl text-primary">
|
||||
{batchStatus?.batch.progress_percentage || 0}%
|
||||
{taskDetail ? getProgressPercentage(taskDetail.status) : 0}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={batchStatus?.batch.progress_percentage || 0} max={100} className="h-4" />
|
||||
<Progress
|
||||
value={taskDetail ? getProgressPercentage(taskDetail.status) : 0}
|
||||
max={100}
|
||||
className="h-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
{batchStatus && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="p-4 bg-muted/30 rounded-lg border border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-success/10 rounded-lg">
|
||||
<CheckCircle className="w-5 h-5 text-success" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground mb-0.5">已完成</p>
|
||||
<p className="text-2xl font-bold text-foreground">
|
||||
{batchStatus.files.filter((f) => f.status === 'completed').length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Task Info */}
|
||||
{taskDetail && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-muted/30 rounded-lg border border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-primary/10 rounded-lg">
|
||||
<Loader2 className="w-5 h-5 text-primary" />
|
||||
<FileText className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground mb-0.5">處理中</p>
|
||||
<p className="text-2xl font-bold text-foreground">
|
||||
{batchStatus.files.filter((f) => f.status === 'processing').length}
|
||||
<p className="text-xs text-muted-foreground mb-0.5">檔案名稱</p>
|
||||
<p className="text-sm font-medium text-foreground truncate">
|
||||
{taskDetail.filename || '未知檔案'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{taskDetail.processing_time_ms && (
|
||||
<div className="p-4 bg-muted/30 rounded-lg border border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-destructive/10 rounded-lg">
|
||||
<AlertCircle className="w-5 h-5 text-destructive" />
|
||||
<div className="p-2 bg-success/10 rounded-lg">
|
||||
<Clock className="w-5 h-5 text-success" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground mb-0.5">失敗</p>
|
||||
<p className="text-2xl font-bold text-foreground">
|
||||
{batchStatus.files.filter((f) => f.status === 'failed').length}
|
||||
<p className="text-xs text-muted-foreground mb-0.5">處理時間</p>
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{(taskDetail.processing_time_ms / 1000).toFixed(2)}s
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 bg-muted/30 rounded-lg border border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-muted rounded-lg">
|
||||
<FileText className="w-5 h-5 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error message */}
|
||||
{taskDetail?.error_message && (
|
||||
<div className="p-4 bg-destructive/10 rounded-lg border border-destructive/20">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-destructive flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground mb-0.5">總計</p>
|
||||
<p className="text-2xl font-bold text-foreground">{batchStatus.files.length}</p>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-destructive mb-1">處理失敗</p>
|
||||
<p className="text-sm text-destructive/80">{taskDetail.error_message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -255,7 +267,7 @@ export default function ProcessingPage() {
|
||||
size="lg"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
{t('common.next')}
|
||||
查看任務歷史
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -263,64 +275,45 @@ export default function ProcessingPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* File List */}
|
||||
{batchStatus && (
|
||||
{/* Task Details Card */}
|
||||
{taskDetail && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-primary/10 rounded-lg">
|
||||
<FileText className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<CardTitle>檔案處理狀態</CardTitle>
|
||||
<CardTitle>任務詳情</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{batchStatus.files.map((file) => (
|
||||
<div
|
||||
key={file.id}
|
||||
className="flex items-center justify-between p-4 rounded-lg border border-border hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-4 flex-1 min-w-0">
|
||||
<div className={`p-2 rounded-lg ${
|
||||
file.status === 'completed' ? 'bg-success/10' :
|
||||
file.status === 'processing' ? 'bg-primary/10' :
|
||||
file.status === 'failed' ? 'bg-destructive/10' :
|
||||
'bg-muted'
|
||||
}`}>
|
||||
{file.status === 'completed' ? (
|
||||
<CheckCircle className="w-5 h-5 text-success" />
|
||||
) : file.status === 'processing' ? (
|
||||
<Loader2 className="w-5 h-5 text-primary animate-spin" />
|
||||
) : file.status === 'failed' ? (
|
||||
<AlertCircle className="w-5 h-5 text-destructive" />
|
||||
) : (
|
||||
<FileText className="w-5 h-5 text-muted-foreground" />
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between py-2 border-b border-border">
|
||||
<span className="text-sm text-muted-foreground">任務狀態</span>
|
||||
{getStatusBadge(taskDetail.status)}
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b border-border">
|
||||
<span className="text-sm text-muted-foreground">建立時間</span>
|
||||
<span className="text-sm font-medium">
|
||||
{new Date(taskDetail.created_at).toLocaleString('zh-TW')}
|
||||
</span>
|
||||
</div>
|
||||
{taskDetail.updated_at && (
|
||||
<div className="flex justify-between py-2 border-b border-border">
|
||||
<span className="text-sm text-muted-foreground">更新時間</span>
|
||||
<span className="text-sm font-medium">
|
||||
{new Date(taskDetail.updated_at).toLocaleString('zh-TW')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{taskDetail.completed_at && (
|
||||
<div className="flex justify-between py-2 border-b border-border">
|
||||
<span className="text-sm text-muted-foreground">完成時間</span>
|
||||
<span className="text-sm font-medium">
|
||||
{new Date(taskDetail.completed_at).toLocaleString('zh-TW')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-foreground truncate">
|
||||
{file.filename}
|
||||
</p>
|
||||
<div className="flex items-center gap-3 mt-1">
|
||||
{file.processing_time && (
|
||||
<p className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
處理時間: {file.processing_time.toFixed(2)}s
|
||||
</p>
|
||||
)}
|
||||
{file.error && (
|
||||
<p className="text-xs text-destructive flex items-center gap-1">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
{file.error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{getStatusBadge(file.status)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user