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 { Badge } from '@/components/ui/badge'
|
||||||
import { useToast } from '@/components/ui/toast'
|
import { useToast } from '@/components/ui/toast'
|
||||||
import { useUploadStore } from '@/store/uploadStore'
|
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'
|
import { Play, CheckCircle, FileText, AlertCircle, Clock, Activity, Loader2 } from 'lucide-react'
|
||||||
|
|
||||||
export default function ProcessingPage() {
|
export default function ProcessingPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { toast } = useToast()
|
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
|
// Start OCR processing
|
||||||
const processOCRMutation = useMutation({
|
const processOCRMutation = useMutation({
|
||||||
mutationFn: () => apiClient.processOCR({ batch_id: batchId! }),
|
mutationFn: () => apiClientV2.startTask(taskId!),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast({
|
toast({
|
||||||
title: '開始處理',
|
title: '開始處理',
|
||||||
@@ -36,16 +39,16 @@ export default function ProcessingPage() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Poll batch status
|
// Poll task status
|
||||||
const { data: batchStatus } = useQuery({
|
const { data: taskDetail } = useQuery({
|
||||||
queryKey: ['batchStatus', batchId],
|
queryKey: ['taskDetail', taskId],
|
||||||
queryFn: () => apiClient.getBatchStatus(batchId!),
|
queryFn: () => apiClientV2.getTask(taskId!),
|
||||||
enabled: !!batchId,
|
enabled: !!taskId,
|
||||||
refetchInterval: (query) => {
|
refetchInterval: (query) => {
|
||||||
const data = query.state.data
|
const data = query.state.data
|
||||||
if (!data) return 2000
|
if (!data) return 2000
|
||||||
// Stop polling if completed or failed
|
// 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 false
|
||||||
}
|
}
|
||||||
return 2000 // Poll every 2 seconds
|
return 2000 // Poll every 2 seconds
|
||||||
@@ -54,19 +57,19 @@ export default function ProcessingPage() {
|
|||||||
|
|
||||||
// Auto-redirect when completed
|
// Auto-redirect when completed
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (batchStatus?.batch.status === 'completed') {
|
if (taskDetail?.status === 'completed') {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigate('/results')
|
navigate('/tasks')
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
}, [batchStatus?.batch.status, navigate])
|
}, [taskDetail?.status, navigate])
|
||||||
|
|
||||||
const handleStartProcessing = () => {
|
const handleStartProcessing = () => {
|
||||||
processOCRMutation.mutate()
|
processOCRMutation.mutate()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleViewResults = () => {
|
const handleViewResults = () => {
|
||||||
navigate('/results')
|
navigate('/tasks')
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusBadge = (status: string) => {
|
const getStatusBadge = (status: string) => {
|
||||||
@@ -82,8 +85,21 @@ export default function ProcessingPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show helpful message when no batch is selected
|
const getProgressPercentage = (status: string) => {
|
||||||
if (!batchId) {
|
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 (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-[60vh]">
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
<Card className="max-w-md text-center">
|
<Card className="max-w-md text-center">
|
||||||
@@ -97,7 +113,7 @@ export default function ProcessingPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
{t('processing.noBatchMessage', { defaultValue: '尚未選擇任何批次。請先上傳檔案以建立批次。' })}
|
{t('processing.noBatchMessage', { defaultValue: '尚未選擇任何任務。請先上傳檔案以建立任務。' })}
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => navigate('/upload')}
|
onClick={() => navigate('/upload')}
|
||||||
@@ -111,9 +127,9 @@ export default function ProcessingPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isProcessing = batchStatus?.batch.status === 'processing'
|
const isProcessing = taskDetail?.status === 'processing'
|
||||||
const isCompleted = batchStatus?.batch.status === 'completed'
|
const isCompleted = taskDetail?.status === 'completed'
|
||||||
const isPending = !batchStatus || batchStatus.batch.status === 'pending'
|
const isPending = !taskDetail || taskDetail.status === 'pending'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -123,7 +139,8 @@ export default function ProcessingPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="page-title">{t('processing.title')}</h1>
|
<h1 className="page-title">{t('processing.title')}</h1>
|
||||||
<p className="text-muted-foreground mt-1">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -153,7 +170,7 @@ export default function ProcessingPage() {
|
|||||||
</div>
|
</div>
|
||||||
<CardTitle>{t('processing.progress')}</CardTitle>
|
<CardTitle>{t('processing.progress')}</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
{batchStatus && getStatusBadge(batchStatus.batch.status)}
|
{taskDetail && getStatusBadge(taskDetail.status)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
@@ -162,63 +179,58 @@ export default function ProcessingPage() {
|
|||||||
<div className="flex justify-between text-sm mb-3">
|
<div className="flex justify-between text-sm mb-3">
|
||||||
<span className="text-muted-foreground font-medium">{t('processing.status')}</span>
|
<span className="text-muted-foreground font-medium">{t('processing.status')}</span>
|
||||||
<span className="font-bold text-xl text-primary">
|
<span className="font-bold text-xl text-primary">
|
||||||
{batchStatus?.batch.progress_percentage || 0}%
|
{taskDetail ? getProgressPercentage(taskDetail.status) : 0}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Task Info */}
|
||||||
{batchStatus && (
|
{taskDetail && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
<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-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>
|
|
||||||
<div className="p-4 bg-muted/30 rounded-lg border border-border">
|
<div className="p-4 bg-muted/30 rounded-lg border border-border">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-2 bg-primary/10 rounded-lg">
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-muted-foreground mb-0.5">處理中</p>
|
<p className="text-xs text-muted-foreground mb-0.5">檔案名稱</p>
|
||||||
<p className="text-2xl font-bold text-foreground">
|
<p className="text-sm font-medium text-foreground truncate">
|
||||||
{batchStatus.files.filter((f) => f.status === 'processing').length}
|
{taskDetail.filename || '未知檔案'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{taskDetail.processing_time_ms && (
|
||||||
<div className="p-4 bg-muted/30 rounded-lg border border-border">
|
<div className="p-4 bg-muted/30 rounded-lg border border-border">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-2 bg-destructive/10 rounded-lg">
|
<div className="p-2 bg-success/10 rounded-lg">
|
||||||
<AlertCircle className="w-5 h-5 text-destructive" />
|
<Clock className="w-5 h-5 text-success" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-muted-foreground mb-0.5">失敗</p>
|
<p className="text-xs text-muted-foreground mb-0.5">處理時間</p>
|
||||||
<p className="text-2xl font-bold text-foreground">
|
<p className="text-sm font-medium text-foreground">
|
||||||
{batchStatus.files.filter((f) => f.status === 'failed').length}
|
{(taskDetail.processing_time_ms / 1000).toFixed(2)}s
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</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>
|
<div>
|
||||||
<p className="text-xs text-muted-foreground mb-0.5">總計</p>
|
<p className="text-sm font-medium text-destructive mb-1">處理失敗</p>
|
||||||
<p className="text-2xl font-bold text-foreground">{batchStatus.files.length}</p>
|
<p className="text-sm text-destructive/80">{taskDetail.error_message}</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -255,7 +267,7 @@ export default function ProcessingPage() {
|
|||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
<CheckCircle className="w-4 h-4" />
|
<CheckCircle className="w-4 h-4" />
|
||||||
{t('common.next')}
|
查看任務歷史
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -263,64 +275,45 @@ export default function ProcessingPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* File List */}
|
{/* Task Details Card */}
|
||||||
{batchStatus && (
|
{taskDetail && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-2 bg-primary/10 rounded-lg">
|
<div className="p-2 bg-primary/10 rounded-lg">
|
||||||
<FileText className="w-5 h-5 text-primary" />
|
<FileText className="w-5 h-5 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<CardTitle>檔案處理狀態</CardTitle>
|
<CardTitle>任務詳情</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-2">
|
<div className="space-y-3">
|
||||||
{batchStatus.files.map((file) => (
|
<div className="flex justify-between py-2 border-b border-border">
|
||||||
<div
|
<span className="text-sm text-muted-foreground">任務狀態</span>
|
||||||
key={file.id}
|
{getStatusBadge(taskDetail.status)}
|
||||||
className="flex items-center justify-between p-4 rounded-lg border border-border hover:bg-muted/50 transition-colors"
|
</div>
|
||||||
>
|
<div className="flex justify-between py-2 border-b border-border">
|
||||||
<div className="flex items-center gap-4 flex-1 min-w-0">
|
<span className="text-sm text-muted-foreground">建立時間</span>
|
||||||
<div className={`p-2 rounded-lg ${
|
<span className="text-sm font-medium">
|
||||||
file.status === 'completed' ? 'bg-success/10' :
|
{new Date(taskDetail.created_at).toLocaleString('zh-TW')}
|
||||||
file.status === 'processing' ? 'bg-primary/10' :
|
</span>
|
||||||
file.status === 'failed' ? 'bg-destructive/10' :
|
</div>
|
||||||
'bg-muted'
|
{taskDetail.updated_at && (
|
||||||
}`}>
|
<div className="flex justify-between py-2 border-b border-border">
|
||||||
{file.status === 'completed' ? (
|
<span className="text-sm text-muted-foreground">更新時間</span>
|
||||||
<CheckCircle className="w-5 h-5 text-success" />
|
<span className="text-sm font-medium">
|
||||||
) : file.status === 'processing' ? (
|
{new Date(taskDetail.updated_at).toLocaleString('zh-TW')}
|
||||||
<Loader2 className="w-5 h-5 text-primary animate-spin" />
|
</span>
|
||||||
) : file.status === 'failed' ? (
|
</div>
|
||||||
<AlertCircle className="w-5 h-5 text-destructive" />
|
|
||||||
) : (
|
|
||||||
<FileText className="w-5 h-5 text-muted-foreground" />
|
|
||||||
)}
|
)}
|
||||||
|
{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>
|
||||||
<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>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user