first
This commit is contained in:
200
frontend/src/pages/ProcessingPage.tsx
Normal file
200
frontend/src/pages/ProcessingPage.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useQuery, useMutation } from '@tanstack/react-query'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
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'
|
||||
|
||||
export default function ProcessingPage() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { toast } = useToast()
|
||||
const { batchId, files } = useUploadStore()
|
||||
|
||||
// Start OCR processing
|
||||
const processOCRMutation = useMutation({
|
||||
mutationFn: () => apiClient.processOCR({ batch_id: batchId! }),
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: '開始處理',
|
||||
description: 'OCR 處理已開始',
|
||||
variant: 'success',
|
||||
})
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast({
|
||||
title: t('errors.processingFailed'),
|
||||
description: error.response?.data?.detail || t('errors.networkError'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
// Poll batch status
|
||||
const { data: batchStatus } = useQuery({
|
||||
queryKey: ['batchStatus', batchId],
|
||||
queryFn: () => apiClient.getBatchStatus(batchId!),
|
||||
enabled: !!batchId,
|
||||
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') {
|
||||
return false
|
||||
}
|
||||
return 2000 // Poll every 2 seconds
|
||||
},
|
||||
})
|
||||
|
||||
// Auto-redirect when completed
|
||||
useEffect(() => {
|
||||
if (batchStatus?.batch.status === 'completed') {
|
||||
setTimeout(() => {
|
||||
navigate('/results')
|
||||
}, 1000)
|
||||
}
|
||||
}, [batchStatus?.batch.status, navigate])
|
||||
|
||||
const handleStartProcessing = () => {
|
||||
processOCRMutation.mutate()
|
||||
}
|
||||
|
||||
const handleViewResults = () => {
|
||||
navigate('/results')
|
||||
}
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return <Badge variant="success">{t('processing.completed')}</Badge>
|
||||
case 'processing':
|
||||
return <Badge variant="default">{t('processing.processing')}</Badge>
|
||||
case 'failed':
|
||||
return <Badge variant="destructive">{t('processing.failed')}</Badge>
|
||||
default:
|
||||
return <Badge variant="secondary">{t('processing.pending')}</Badge>
|
||||
}
|
||||
}
|
||||
|
||||
// Show helpful message when no batch is selected
|
||||
if (!batchId) {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto mt-12">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('processing.title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-center space-y-4">
|
||||
<p className="text-muted-foreground">
|
||||
{t('processing.noBatchMessage', { defaultValue: '尚未選擇任何批次。請先上傳檔案以建立批次。' })}
|
||||
</p>
|
||||
<Button onClick={() => navigate('/upload')}>
|
||||
{t('processing.goToUpload', { defaultValue: '前往上傳頁面' })}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const isProcessing = batchStatus?.batch.status === 'processing'
|
||||
const isCompleted = batchStatus?.batch.status === 'completed'
|
||||
const isPending = !batchStatus || batchStatus.batch.status === 'pending'
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-foreground mb-2">{t('processing.title')}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
批次 ID: {batchId} - 共 {files.length} 個檔案
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Overall Progress */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>{t('processing.progress')}</CardTitle>
|
||||
{batchStatus && getStatusBadge(batchStatus.batch.status)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-2">
|
||||
<span className="text-muted-foreground">{t('processing.status')}</span>
|
||||
<span className="font-medium">
|
||||
{batchStatus?.batch.progress_percentage || 0}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={batchStatus?.batch.progress_percentage || 0} max={100} />
|
||||
</div>
|
||||
|
||||
{batchStatus && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('processing.filesProcessed', {
|
||||
processed: batchStatus.files.filter((f) => f.status === 'completed').length,
|
||||
total: batchStatus.files.length,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3">
|
||||
{isPending && (
|
||||
<Button
|
||||
onClick={handleStartProcessing}
|
||||
disabled={processOCRMutation.isPending}
|
||||
>
|
||||
{processOCRMutation.isPending
|
||||
? t('processing.processing')
|
||||
: t('processing.startProcessing')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isCompleted && (
|
||||
<Button onClick={handleViewResults}>{t('common.next')}</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* File List */}
|
||||
{batchStatus && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>檔案處理狀態</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{batchStatus.files.map((file) => (
|
||||
<div
|
||||
key={file.id}
|
||||
className="flex items-center justify-between p-3 bg-muted rounded-md"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-foreground truncate">
|
||||
{file.filename}
|
||||
</p>
|
||||
{file.processing_time && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
處理時間: {file.processing_time.toFixed(2)}s
|
||||
</p>
|
||||
)}
|
||||
{file.error && (
|
||||
<p className="text-xs text-destructive">{file.error}</p>
|
||||
)}
|
||||
</div>
|
||||
{getStatusBadge(file.status)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user