This commit is contained in:
beabigegg
2025-11-12 22:53:17 +08:00
commit da700721fa
130 changed files with 23393 additions and 0 deletions

View 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>
)
}