- Enable PP-StructureV3's use_doc_orientation_classify feature - Detect rotation angle from doc_preprocessor_res.angle - Swap page dimensions (width <-> height) for 90°/270° rotations - Output PDF now correctly displays landscape-scanned content Also includes: - Archive completed openspec proposals - Add simplify-frontend-ocr-config proposal (pending) - Code cleanup and frontend simplification 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
276 lines
9.6 KiB
TypeScript
276 lines
9.6 KiB
TypeScript
import { useState } from 'react'
|
||
import { useNavigate } from 'react-router-dom'
|
||
import { useTranslation } from 'react-i18next'
|
||
import { useMutation } from '@tanstack/react-query'
|
||
import FileUpload from '@/components/FileUpload'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||
import { useToast } from '@/components/ui/toast'
|
||
import { useUploadStore } from '@/store/uploadStore'
|
||
import { apiClientV2 } from '@/services/apiV2'
|
||
import { FileText, X, Upload, Trash2, CheckCircle2, ArrowRight } from 'lucide-react'
|
||
|
||
export default function UploadPage() {
|
||
const { t } = useTranslation()
|
||
const navigate = useNavigate()
|
||
const { toast } = useToast()
|
||
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
|
||
const { setBatchId, setUploadProgress } = useUploadStore()
|
||
|
||
const uploadMutation = useMutation({
|
||
mutationFn: async (files: File[]) => {
|
||
// Upload files one by one and collect task IDs
|
||
const tasks = []
|
||
for (const file of files) {
|
||
const result = await apiClientV2.uploadFile(file)
|
||
tasks.push(result)
|
||
}
|
||
return tasks
|
||
},
|
||
onSuccess: (tasks) => {
|
||
// Use the first task_id as the current batch identifier
|
||
// Note: Type assertion needed - store expects number but API returns string UUID
|
||
if (tasks.length > 0) {
|
||
setBatchId(tasks[0].task_id as unknown as number)
|
||
}
|
||
toast({
|
||
title: t('upload.uploadSuccess'),
|
||
description: `成功上傳 ${tasks.length} 個檔案`,
|
||
variant: 'success',
|
||
})
|
||
navigate('/processing')
|
||
},
|
||
onError: (error: any) => {
|
||
toast({
|
||
title: t('upload.uploadError'),
|
||
description: error.response?.data?.detail || t('errors.networkError'),
|
||
variant: 'destructive',
|
||
})
|
||
},
|
||
})
|
||
|
||
const handleFilesSelected = (files: File[]) => {
|
||
setSelectedFiles((prev) => [...prev, ...files])
|
||
}
|
||
|
||
const handleRemoveFile = (index: number) => {
|
||
setSelectedFiles((prev) => prev.filter((_, i) => i !== index))
|
||
}
|
||
|
||
const handleClearAll = () => {
|
||
setSelectedFiles([])
|
||
setUploadProgress(0)
|
||
}
|
||
|
||
const handleUpload = () => {
|
||
if (selectedFiles.length === 0) {
|
||
toast({
|
||
title: t('errors.validationError'),
|
||
description: '請選擇至少一個檔案',
|
||
variant: 'destructive',
|
||
})
|
||
return
|
||
}
|
||
|
||
uploadMutation.mutate(selectedFiles)
|
||
}
|
||
|
||
const formatFileSize = (bytes: number) => {
|
||
if (bytes === 0) return '0 Bytes'
|
||
const k = 1024
|
||
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
|
||
}
|
||
|
||
const getFileIcon = (filename: string) => {
|
||
const ext = filename.split('.').pop()?.toLowerCase()
|
||
const colors = {
|
||
pdf: 'text-red-500',
|
||
doc: 'text-blue-500',
|
||
docx: 'text-blue-500',
|
||
ppt: 'text-orange-500',
|
||
pptx: 'text-orange-500',
|
||
jpg: 'text-green-500',
|
||
jpeg: 'text-green-500',
|
||
png: 'text-green-500',
|
||
}
|
||
return colors[ext as keyof typeof colors] || 'text-gray-500'
|
||
}
|
||
|
||
const totalSize = selectedFiles.reduce((acc, file) => acc + file.size, 0)
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Page Header */}
|
||
<div className="page-header">
|
||
<h1 className="page-title">{t('upload.title')}</h1>
|
||
<p className="text-muted-foreground">
|
||
選擇要進行 OCR 處理的檔案,支援圖片、PDF 和 Office 文件
|
||
</p>
|
||
</div>
|
||
|
||
{/* Step Indicator */}
|
||
<div className="flex items-center gap-4">
|
||
<div className="flex items-center gap-3">
|
||
<div className={`flex items-center justify-center w-10 h-10 rounded-full ${
|
||
selectedFiles.length === 0 ? 'bg-primary text-white' : 'bg-success text-white'
|
||
}`}>
|
||
{selectedFiles.length === 0 ? '1' : <CheckCircle2 className="w-5 h-5" />}
|
||
</div>
|
||
<div>
|
||
<div className="text-sm font-medium text-foreground">選擇檔案</div>
|
||
<div className="text-xs text-muted-foreground">上傳要處理的文件</div>
|
||
</div>
|
||
</div>
|
||
|
||
<ArrowRight className="w-5 h-5 text-muted-foreground" />
|
||
|
||
<div className="flex items-center gap-3">
|
||
<div className={`flex items-center justify-center w-10 h-10 rounded-full ${
|
||
selectedFiles.length > 0 ? 'bg-primary text-white' : 'bg-muted text-muted-foreground'
|
||
}`}>
|
||
2
|
||
</div>
|
||
<div>
|
||
<div className={`text-sm font-medium ${
|
||
selectedFiles.length > 0 ? 'text-foreground' : 'text-muted-foreground'
|
||
}`}>確認並上傳</div>
|
||
<div className="text-xs text-muted-foreground">檢查並開始處理</div>
|
||
</div>
|
||
</div>
|
||
|
||
<ArrowRight className="w-5 h-5 text-muted-foreground" />
|
||
|
||
<div className="flex items-center gap-3">
|
||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-muted text-muted-foreground">
|
||
3
|
||
</div>
|
||
<div>
|
||
<div className="text-sm font-medium text-muted-foreground">處理完成</div>
|
||
<div className="text-xs text-muted-foreground">查看結果並導出</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Upload Area */}
|
||
<div className="section">
|
||
<FileUpload
|
||
onFilesSelected={handleFilesSelected}
|
||
disabled={uploadMutation.isPending}
|
||
/>
|
||
</div>
|
||
|
||
{/* Selected Files Section */}
|
||
{selectedFiles.length > 0 && (
|
||
<div className="space-y-4">
|
||
{/* Summary Card */}
|
||
<Card>
|
||
<CardHeader>
|
||
<div className="flex items-center justify-between">
|
||
<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>
|
||
<div>
|
||
<CardTitle className="text-lg">
|
||
{t('upload.selectedFiles')}
|
||
</CardTitle>
|
||
<p className="text-sm text-muted-foreground mt-0.5">
|
||
已選擇 {selectedFiles.length} 個檔案,總大小 {formatFileSize(totalSize)}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={handleClearAll}
|
||
disabled={uploadMutation.isPending}
|
||
className="gap-2"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
清空全部
|
||
</Button>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{/* Files Table */}
|
||
<div className="space-y-2">
|
||
{selectedFiles.map((file, index) => (
|
||
<div
|
||
key={index}
|
||
className="group flex items-center gap-4 p-3 rounded-lg border border-border hover:bg-muted/50 transition-colors"
|
||
>
|
||
{/* File icon */}
|
||
<div className={`p-2 rounded-lg bg-muted/50 ${getFileIcon(file.name)}`}>
|
||
<FileText className="w-5 h-5" />
|
||
</div>
|
||
|
||
{/* File info */}
|
||
<div className="flex-1 min-w-0">
|
||
<p className="text-sm font-medium text-foreground truncate">
|
||
{file.name}
|
||
</p>
|
||
<p className="text-xs text-muted-foreground">
|
||
{formatFileSize(file.size)} · {file.type || '未知類型'}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Status badge */}
|
||
<div className="status-badge-success">
|
||
準備就緒
|
||
</div>
|
||
|
||
{/* Remove button */}
|
||
<button
|
||
onClick={() => handleRemoveFile(index)}
|
||
disabled={uploadMutation.isPending}
|
||
className="p-2 rounded-lg text-muted-foreground hover:bg-destructive/10 hover:text-destructive transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||
title="移除檔案"
|
||
>
|
||
<X className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Action Bar */}
|
||
<div className="flex items-center justify-between p-4 bg-card rounded-xl border border-border">
|
||
<div className="text-sm text-muted-foreground">
|
||
請確認檔案無誤後點擊上傳按鈕開始處理
|
||
</div>
|
||
<div className="flex gap-3">
|
||
<Button
|
||
variant="outline"
|
||
onClick={handleClearAll}
|
||
disabled={uploadMutation.isPending}
|
||
>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
onClick={handleUpload}
|
||
disabled={uploadMutation.isPending}
|
||
className="gap-2"
|
||
>
|
||
{uploadMutation.isPending ? (
|
||
<>
|
||
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
||
{t('upload.uploading')}
|
||
</>
|
||
) : (
|
||
<>
|
||
<Upload className="w-4 h-4" />
|
||
開始上傳並處理
|
||
</>
|
||
)}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|