feat: complete i18n support for all frontend pages and components
Add comprehensive bilingual (zh-TW/en-US) support across the entire frontend: Pages updated: - AdminDashboardPage: All 63+ strings translated - TaskHistoryPage: All 80+ strings translated - TaskDetailPage: All 90+ strings translated - AuditLogsPage: All audit log UI translated - ResultsPage/ProcessingPage: Fixed i18n integration - UploadPage: Step indicators and file list UI translated Components updated: - TaskNotFound: Task deletion messages - FileUpload: Prompts and file size limits - ProcessingTrackSelector: Processing mode options with analysis info - Layout: Navigation descriptions - ProtectedRoute: Loading and access denied messages - PDFViewer: Page navigation and error messages Locale files: Added ~200 new translation keys to both zh-TW.json and en-US.json 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -110,7 +110,7 @@ export default function FileUpload({
|
|||||||
{t('upload.dragAndDrop')}
|
{t('upload.dragAndDrop')}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-base text-muted-foreground">
|
<p className="text-base text-muted-foreground">
|
||||||
或點擊選擇檔案
|
{t('upload.orClickToSelect')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Supported formats */}
|
{/* Supported formats */}
|
||||||
@@ -132,7 +132,7 @@ export default function FileUpload({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-sm text-muted-foreground mt-4">
|
<p className="text-sm text-muted-foreground mt-4">
|
||||||
最大檔案大小: 50MB · 最多 {maxFiles} 個檔案
|
{t('upload.maxFileSizeWithCount', { maxFiles })}
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -35,11 +35,11 @@ export default function Layout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const navLinks = [
|
const navLinks = [
|
||||||
{ to: '/upload', label: t('nav.upload'), icon: Upload, description: '上傳檔案', adminOnly: false },
|
{ to: '/upload', label: t('nav.upload'), icon: Upload, description: t('upload.title'), adminOnly: false },
|
||||||
{ to: '/processing', label: t('nav.processing'), icon: Activity, description: '處理進度', adminOnly: false },
|
{ to: '/processing', label: t('nav.processing'), icon: Activity, description: t('processing.title'), adminOnly: false },
|
||||||
{ to: '/results', label: t('nav.results'), icon: FileText, description: '查看結果', adminOnly: false },
|
{ to: '/results', label: t('nav.results'), icon: FileText, description: t('results.title'), adminOnly: false },
|
||||||
{ to: '/tasks', label: '任務歷史', icon: History, description: '查看任務記錄', adminOnly: false },
|
{ to: '/tasks', label: t('nav.taskHistory'), icon: History, description: t('taskHistory.subtitle'), adminOnly: false },
|
||||||
{ to: '/admin', label: '管理員儀表板', icon: Shield, description: '系統管理', adminOnly: true },
|
{ to: '/admin', label: t('nav.adminDashboard'), icon: Shield, description: t('admin.subtitle'), adminOnly: true },
|
||||||
]
|
]
|
||||||
|
|
||||||
// Filter nav links based on admin status
|
// Filter nav links based on admin status
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useCallback, useMemo, useRef, useEffect } from 'react'
|
import { useState, useCallback, useMemo, useRef, useEffect } from 'react'
|
||||||
import { Document, Page, pdfjs } from 'react-pdf'
|
import { Document, Page, pdfjs } from 'react-pdf'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
// Type alias for PDFDocumentProxy to avoid direct pdfjs-dist import issues
|
// Type alias for PDFDocumentProxy to avoid direct pdfjs-dist import issues
|
||||||
type PDFDocumentProxy = ReturnType<typeof pdfjs.getDocument> extends Promise<infer T> ? T : never
|
type PDFDocumentProxy = ReturnType<typeof pdfjs.getDocument> extends Promise<infer T> ? T : never
|
||||||
@@ -22,6 +23,7 @@ interface PDFViewerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function PDFViewer({ title, pdfUrl, className, httpHeaders }: PDFViewerProps) {
|
export default function PDFViewer({ title, pdfUrl, className, httpHeaders }: PDFViewerProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
const [numPages, setNumPages] = useState<number>(0)
|
const [numPages, setNumPages] = useState<number>(0)
|
||||||
const [pageNumber, setPageNumber] = useState<number>(1)
|
const [pageNumber, setPageNumber] = useState<number>(1)
|
||||||
const [scale, setScale] = useState<number>(1.0)
|
const [scale, setScale] = useState<number>(1.0)
|
||||||
@@ -55,10 +57,10 @@ export default function PDFViewer({ title, pdfUrl, className, httpHeaders }: PDF
|
|||||||
|
|
||||||
const onDocumentLoadError = useCallback((err: Error) => {
|
const onDocumentLoadError = useCallback((err: Error) => {
|
||||||
console.error('Error loading PDF:', err)
|
console.error('Error loading PDF:', err)
|
||||||
setError('無法載入 PDF 檔案。請稍後再試。')
|
setError(t('pdfViewer.loadError'))
|
||||||
setDocumentLoaded(false)
|
setDocumentLoaded(false)
|
||||||
pdfDocRef.current = null
|
pdfDocRef.current = null
|
||||||
}, [])
|
}, [t])
|
||||||
|
|
||||||
const goToPreviousPage = useCallback(() => {
|
const goToPreviousPage = useCallback(() => {
|
||||||
setPageNumber((prev) => Math.max(prev - 1, 1))
|
setPageNumber((prev) => Math.max(prev - 1, 1))
|
||||||
@@ -97,7 +99,7 @@ export default function PDFViewer({ title, pdfUrl, className, httpHeaders }: PDF
|
|||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeft className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<span className="text-sm whitespace-nowrap">
|
<span className="text-sm whitespace-nowrap">
|
||||||
第 {pageNumber} 頁 / 共 {numPages || '...'} 頁
|
{t('pdfViewer.pageInfo', { current: pageNumber, total: numPages || '...' })}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -139,7 +141,7 @@ export default function PDFViewer({ title, pdfUrl, className, httpHeaders }: PDF
|
|||||||
{error ? (
|
{error ? (
|
||||||
<div className="flex items-center justify-center min-h-[400px]">
|
<div className="flex items-center justify-center min-h-[400px]">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-destructive font-semibold mb-2">錯誤</p>
|
<p className="text-destructive font-semibold mb-2">{t('pdfViewer.error')}</p>
|
||||||
<p className="text-sm text-muted-foreground">{error}</p>
|
<p className="text-sm text-muted-foreground">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,7 +156,7 @@ export default function PDFViewer({ title, pdfUrl, className, httpHeaders }: PDF
|
|||||||
<div className="flex items-center justify-center min-h-[400px]">
|
<div className="flex items-center justify-center min-h-[400px]">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Loader2 className="w-12 h-12 animate-spin text-primary mx-auto mb-4" />
|
<Loader2 className="w-12 h-12 animate-spin text-primary mx-auto mb-4" />
|
||||||
<p className="text-muted-foreground">載入 PDF 中...</p>
|
<p className="text-muted-foreground">{t('pdfViewer.loading')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -173,7 +175,7 @@ export default function PDFViewer({ title, pdfUrl, className, httpHeaders }: PDF
|
|||||||
}
|
}
|
||||||
error={
|
error={
|
||||||
<div className="text-center p-4 text-destructive">
|
<div className="text-center p-4 text-destructive">
|
||||||
無法載入第 {pageNumber} 頁
|
{t('pdfViewer.pageLoadError', { page: pageNumber })}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Cpu, FileText, Sparkles, Info } from 'lucide-react'
|
import { Cpu, FileText, Sparkles, Info } from 'lucide-react'
|
||||||
@@ -16,13 +17,14 @@ export default function ProcessingTrackSelector({
|
|||||||
documentAnalysis,
|
documentAnalysis,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}: ProcessingTrackSelectorProps) {
|
}: ProcessingTrackSelectorProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
const recommendedTrack = documentAnalysis?.recommended_track
|
const recommendedTrack = documentAnalysis?.recommended_track
|
||||||
|
|
||||||
const tracks = [
|
const tracks = [
|
||||||
{
|
{
|
||||||
id: null as ProcessingTrack | null,
|
id: null as ProcessingTrack | null,
|
||||||
name: '自動選擇',
|
name: t('processingTrack.auto.label'),
|
||||||
description: '根據文件類型自動選擇最佳處理方式',
|
description: t('processingTrack.auto.description'),
|
||||||
icon: Sparkles,
|
icon: Sparkles,
|
||||||
color: 'text-purple-600',
|
color: 'text-purple-600',
|
||||||
bgColor: 'bg-purple-50',
|
bgColor: 'bg-purple-50',
|
||||||
@@ -31,8 +33,8 @@ export default function ProcessingTrackSelector({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'direct' as ProcessingTrack,
|
id: 'direct' as ProcessingTrack,
|
||||||
name: '直接提取 (DIRECT)',
|
name: t('processingTrack.direct.label'),
|
||||||
description: '從 PDF 中直接提取文字圖層,適用於可編輯 PDF',
|
description: t('processingTrack.direct.description'),
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
color: 'text-blue-600',
|
color: 'text-blue-600',
|
||||||
bgColor: 'bg-blue-50',
|
bgColor: 'bg-blue-50',
|
||||||
@@ -41,8 +43,8 @@ export default function ProcessingTrackSelector({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ocr' as ProcessingTrack,
|
id: 'ocr' as ProcessingTrack,
|
||||||
name: 'OCR 識別',
|
name: t('processingTrack.ocr.label'),
|
||||||
description: '使用光學字元識別處理圖片或掃描文件',
|
description: t('processingTrack.ocr.description'),
|
||||||
icon: Cpu,
|
icon: Cpu,
|
||||||
color: 'text-green-600',
|
color: 'text-green-600',
|
||||||
bgColor: 'bg-green-50',
|
bgColor: 'bg-green-50',
|
||||||
@@ -51,6 +53,10 @@ export default function ProcessingTrackSelector({
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const getTrackLabel = (track: string) => {
|
||||||
|
return track === 'direct' ? t('processingTrack.direct.label') : t('processingTrack.ocr.label')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -59,9 +65,9 @@ export default function ProcessingTrackSelector({
|
|||||||
<Sparkles className="w-5 h-5 text-primary" />
|
<Sparkles className="w-5 h-5 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>處理方式選擇</CardTitle>
|
<CardTitle>{t('processingTrack.title')}</CardTitle>
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
選擇文件的處理方式,或讓系統自動判斷
|
{t('processingTrack.subtitle')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,7 +78,7 @@ export default function ProcessingTrackSelector({
|
|||||||
<div className="flex items-start gap-2 p-3 bg-amber-50 border border-amber-200 rounded-lg">
|
<div className="flex items-start gap-2 p-3 bg-amber-50 border border-amber-200 rounded-lg">
|
||||||
<Info className="w-4 h-4 text-amber-600 flex-shrink-0 mt-0.5" />
|
<Info className="w-4 h-4 text-amber-600 flex-shrink-0 mt-0.5" />
|
||||||
<p className="text-sm text-amber-800">
|
<p className="text-sm text-amber-800">
|
||||||
您已覆蓋系統建議。系統原本建議使用「{recommendedTrack === 'direct' ? '直接提取' : 'OCR 識別'}」方式處理此文件。
|
{t('processingTrack.overrideWarning', { track: getTrackLabel(recommendedTrack) })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -109,12 +115,12 @@ export default function ProcessingTrackSelector({
|
|||||||
</span>
|
</span>
|
||||||
{track.recommended && (
|
{track.recommended && (
|
||||||
<Badge variant="outline" className="text-xs bg-white">
|
<Badge variant="outline" className="text-xs bg-white">
|
||||||
系統建議
|
{t('processingTrack.recommended')}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<Badge variant="default" className="text-xs">
|
<Badge variant="default" className="text-xs">
|
||||||
已選擇
|
{t('processingTrack.selected')}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -132,12 +138,12 @@ export default function ProcessingTrackSelector({
|
|||||||
{documentAnalysis && (
|
{documentAnalysis && (
|
||||||
<div className="pt-3 border-t border-border">
|
<div className="pt-3 border-t border-border">
|
||||||
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
||||||
<span>文件分析信心度: {(documentAnalysis.confidence * 100).toFixed(0)}%</span>
|
<span>{t('processingTrack.analysisConfidence', { value: (documentAnalysis.confidence * 100).toFixed(0) })}</span>
|
||||||
{documentAnalysis.page_count && (
|
{documentAnalysis.page_count && (
|
||||||
<span>頁數: {documentAnalysis.page_count}</span>
|
<span>{t('processingTrack.pageCount', { value: documentAnalysis.page_count })}</span>
|
||||||
)}
|
)}
|
||||||
{documentAnalysis.text_coverage !== null && (
|
{documentAnalysis.text_coverage !== null && (
|
||||||
<span>文字覆蓋率: {(documentAnalysis.text_coverage * 100).toFixed(1)}%</span>
|
<span>{t('processingTrack.textCoverage', { value: (documentAnalysis.text_coverage * 100).toFixed(1) })}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Navigate, useLocation } from 'react-router-dom'
|
import { Navigate, useLocation } from 'react-router-dom'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { apiClientV2 } from '@/services/apiV2'
|
import { apiClientV2 } from '@/services/apiV2'
|
||||||
|
|
||||||
interface ProtectedRouteProps {
|
interface ProtectedRouteProps {
|
||||||
@@ -13,6 +14,7 @@ interface ProtectedRouteProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ProtectedRoute({ children, requireAdmin = false }: ProtectedRouteProps) {
|
export default function ProtectedRoute({ children, requireAdmin = false }: ProtectedRouteProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const [isChecking, setIsChecking] = useState(true)
|
const [isChecking, setIsChecking] = useState(true)
|
||||||
const [isValid, setIsValid] = useState(false)
|
const [isValid, setIsValid] = useState(false)
|
||||||
@@ -65,7 +67,7 @@ export default function ProtectedRoute({ children, requireAdmin = false }: Prote
|
|||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||||
<p className="text-gray-600">驗證中...</p>
|
<p className="text-gray-600">{t('common.verifying')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -78,9 +80,9 @@ export default function ProtectedRoute({ children, requireAdmin = false }: Prote
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-2xl font-bold text-red-600 mb-4">訪問被拒絕</h1>
|
<h1 className="text-2xl font-bold text-red-600 mb-4">{t('common.accessDenied')}</h1>
|
||||||
<p className="text-gray-600 mb-4">您沒有權限訪問此頁面</p>
|
<p className="text-gray-600 mb-4">{t('common.accessDeniedDesc')}</p>
|
||||||
<a href="/" className="text-blue-600 hover:underline">返回首頁</a>
|
<a href="/" className="text-blue-600 hover:underline">{t('common.backToHome')}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Trash2 } from 'lucide-react'
|
import { Trash2 } from 'lucide-react'
|
||||||
@@ -9,6 +10,7 @@ interface TaskNotFoundProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function TaskNotFound({ taskId, onClearAndUpload }: TaskNotFoundProps) {
|
export default function TaskNotFound({ taskId, onClearAndUpload }: TaskNotFoundProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
@@ -25,19 +27,19 @@ export default function TaskNotFound({ taskId, onClearAndUpload }: TaskNotFoundP
|
|||||||
<Trash2 className="w-8 h-8 text-destructive" />
|
<Trash2 className="w-8 h-8 text-destructive" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-xl">任務已刪除</CardTitle>
|
<CardTitle className="text-xl">{t('common.taskDeleted')}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
此任務已被刪除或不存在。請上傳新檔案以建立新任務。
|
{t('common.taskDeletedDesc')}
|
||||||
</p>
|
</p>
|
||||||
{taskId && (
|
{taskId && (
|
||||||
<p className="text-xs text-muted-foreground font-mono">
|
<p className="text-xs text-muted-foreground font-mono">
|
||||||
任務 ID: {taskId}
|
{t('common.taskIdLabel', { id: taskId })}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<Button onClick={handleClick} size="lg">
|
<Button onClick={handleClick} size="lg">
|
||||||
前往上傳頁面
|
{t('common.goToUpload')}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
"taskHistory": "Task History",
|
"taskHistory": "Task History",
|
||||||
"adminDashboard": "Admin Dashboard"
|
"adminDashboard": "Admin Dashboard",
|
||||||
|
"auditLogs": "Audit Logs"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
@@ -24,15 +25,19 @@
|
|||||||
"loggingIn": "Signing in...",
|
"loggingIn": "Signing in...",
|
||||||
"usernamePlaceholder": "Enter your username",
|
"usernamePlaceholder": "Enter your username",
|
||||||
"passwordPlaceholder": "Enter your password",
|
"passwordPlaceholder": "Enter your password",
|
||||||
"supportedFormats": "Supported formats: PDF, Images, Office documents"
|
"supportedFormats": "Supported formats: PDF, Images, Office documents",
|
||||||
|
"sessionExpired": "Session expired. Please login again.",
|
||||||
|
"redirecting": "Redirecting to login page..."
|
||||||
},
|
},
|
||||||
"upload": {
|
"upload": {
|
||||||
"title": "Upload Files",
|
"title": "Upload Files",
|
||||||
|
"subtitle": "Select files for OCR processing. Supports images, PDFs and Office documents",
|
||||||
"dragAndDrop": "Drag and drop files here, or click to select",
|
"dragAndDrop": "Drag and drop files here, or click to select",
|
||||||
"dropFilesHere": "Drop files here to upload",
|
"dropFilesHere": "Drop files here to upload",
|
||||||
"invalidFiles": "Some file formats are not supported",
|
"invalidFiles": "Some file formats are not supported",
|
||||||
"supportedFormats": "Supported formats: PNG, JPG, JPEG, PDF, DOC, DOCX, PPT, PPTX",
|
"supportedFormats": "Supported formats: PNG, JPG, JPEG, PDF, DOC, DOCX, PPT, PPTX",
|
||||||
"maxFileSize": "Maximum file size: 50MB",
|
"maxFileSize": "Maximum file size: 50MB",
|
||||||
|
"maxFileSizeWithCount": "Maximum file size: 50MB · Up to {{maxFiles}} files",
|
||||||
"uploadButton": "Start Upload",
|
"uploadButton": "Start Upload",
|
||||||
"uploading": "Uploading...",
|
"uploading": "Uploading...",
|
||||||
"uploadSuccess": "Upload successful",
|
"uploadSuccess": "Upload successful",
|
||||||
@@ -41,7 +46,30 @@
|
|||||||
"clearAll": "Clear All",
|
"clearAll": "Clear All",
|
||||||
"removeFile": "Remove",
|
"removeFile": "Remove",
|
||||||
"selectedFiles": "Selected Files",
|
"selectedFiles": "Selected Files",
|
||||||
"filesUploaded": "Successfully uploaded {{count}} file(s)"
|
"filesUploaded": "Successfully uploaded {{count}} file(s)",
|
||||||
|
"orClickToSelect": "or click to select files",
|
||||||
|
"selectFilesButton": "Select Files",
|
||||||
|
"continueToProcess": "Continue to Process",
|
||||||
|
"goToProcessing": "Go to Processing",
|
||||||
|
"uploadMoreFiles": "Upload More Files",
|
||||||
|
"selectAtLeastOne": "Please select at least one file",
|
||||||
|
"steps": {
|
||||||
|
"selectFiles": "Select Files",
|
||||||
|
"selectFilesDesc": "Upload files to process",
|
||||||
|
"confirmUpload": "Confirm Upload",
|
||||||
|
"confirmUploadDesc": "Review and start processing",
|
||||||
|
"processingComplete": "Processing Complete",
|
||||||
|
"processingCompleteDesc": "View results and export"
|
||||||
|
},
|
||||||
|
"fileList": {
|
||||||
|
"summary": "{{count}} file(s) selected, total size {{size}}",
|
||||||
|
"unknownType": "Unknown type",
|
||||||
|
"ready": "Ready",
|
||||||
|
"removeFile": "Remove file",
|
||||||
|
"confirmPrompt": "Please confirm files are correct before clicking upload",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"startUpload": "Start Upload"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"processing": {
|
"processing": {
|
||||||
"title": "OCR Processing",
|
"title": "OCR Processing",
|
||||||
@@ -55,6 +83,11 @@
|
|||||||
"failed": "Failed",
|
"failed": "Failed",
|
||||||
"pending": "Pending",
|
"pending": "Pending",
|
||||||
"estimatedTime": "Estimated Time Remaining",
|
"estimatedTime": "Estimated Time Remaining",
|
||||||
|
"ocrStarted": "OCR processing started",
|
||||||
|
"loadingTask": "Loading task information...",
|
||||||
|
"analyzingDocument": "Analyzing document type...",
|
||||||
|
"noBatchMessage": "No task selected. Please upload files first.",
|
||||||
|
"goToUpload": "Go to Upload Page",
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Processing Settings",
|
"title": "Processing Settings",
|
||||||
"language": "Recognition Language",
|
"language": "Recognition Language",
|
||||||
@@ -151,9 +184,23 @@
|
|||||||
"viewJSON": "View JSON",
|
"viewJSON": "View JSON",
|
||||||
"downloadPDF": "Download PDF",
|
"downloadPDF": "Download PDF",
|
||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
|
"noBatchMessage": "No task selected. Please upload and process files first.",
|
||||||
|
"goToUpload": "Go to Upload Page",
|
||||||
"noResults": "No results yet",
|
"noResults": "No results yet",
|
||||||
"textBlocks": "Text Blocks",
|
"textBlocks": "Text Blocks",
|
||||||
"layoutInfo": "Layout Info"
|
"layoutInfo": "Layout Info",
|
||||||
|
"pdfDownloaded": "PDF downloaded",
|
||||||
|
"markdownDownloaded": "Markdown downloaded",
|
||||||
|
"jsonDownloaded": "JSON downloaded",
|
||||||
|
"loadingResults": "Loading task results...",
|
||||||
|
"processingStatus": "Processing Status",
|
||||||
|
"taskType": "Task Type",
|
||||||
|
"processingInProgress": "Processing in progress...",
|
||||||
|
"processingInProgressDesc": "Please wait, OCR processing takes some time",
|
||||||
|
"waitingToProcess": "Waiting to process",
|
||||||
|
"waitingToProcessDesc": "Go to the processing page to start OCR",
|
||||||
|
"goToProcessing": "Go to Processing",
|
||||||
|
"viewTaskHistory": "View Task History"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "Export Results",
|
"title": "Export Results",
|
||||||
@@ -217,7 +264,37 @@
|
|||||||
"back": "Back",
|
"back": "Back",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"previous": "Previous",
|
"previous": "Previous",
|
||||||
"submit": "Submit"
|
"submit": "Submit",
|
||||||
|
"retry": "Retry",
|
||||||
|
"viewDetails": "View Details",
|
||||||
|
"unknownFile": "Unknown file",
|
||||||
|
"unknownError": "Unknown error",
|
||||||
|
"seconds": "seconds",
|
||||||
|
"total": "Total",
|
||||||
|
"active": "Active",
|
||||||
|
"inactive": "Inactive",
|
||||||
|
"all": "All",
|
||||||
|
"none": "None",
|
||||||
|
"enabled": "Enabled",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No",
|
||||||
|
"or": "or",
|
||||||
|
"and": "and",
|
||||||
|
"show": "Show",
|
||||||
|
"hide": "Hide",
|
||||||
|
"more": "More",
|
||||||
|
"less": "Less",
|
||||||
|
"downloadFailed": "Download failed",
|
||||||
|
"downloadSuccess": "Download successful",
|
||||||
|
"taskDeleted": "Task Deleted",
|
||||||
|
"taskDeletedDesc": "This task has been deleted or does not exist. Please upload a new file to create a new task.",
|
||||||
|
"taskIdLabel": "Task ID: {{id}}",
|
||||||
|
"goToUpload": "Go to Upload Page",
|
||||||
|
"verifying": "Verifying...",
|
||||||
|
"accessDenied": "Access Denied",
|
||||||
|
"accessDeniedDesc": "You do not have permission to access this page",
|
||||||
|
"backToHome": "Back to Home"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"networkError": "Network error. Please try again later.",
|
"networkError": "Network error. Please try again later.",
|
||||||
@@ -229,12 +306,54 @@
|
|||||||
"unsupportedFormat": "Unsupported format",
|
"unsupportedFormat": "Unsupported format",
|
||||||
"uploadFailed": "Upload failed",
|
"uploadFailed": "Upload failed",
|
||||||
"processingFailed": "Processing failed",
|
"processingFailed": "Processing failed",
|
||||||
"exportFailed": "Export failed"
|
"exportFailed": "Export failed",
|
||||||
|
"loadFailed": "Load failed",
|
||||||
|
"deleteFailed": "Delete failed",
|
||||||
|
"startFailed": "Start failed",
|
||||||
|
"cancelFailed": "Cancel failed",
|
||||||
|
"retryFailed": "Retry failed"
|
||||||
},
|
},
|
||||||
"translation": {
|
"translation": {
|
||||||
"title": "Translation",
|
"title": "Document Translation",
|
||||||
"comingSoon": "Coming Soon",
|
"comingSoon": "Coming Soon",
|
||||||
"description": "Document translation feature is under development"
|
"description": "Translate documents using cloud translation services, supporting multiple target languages.",
|
||||||
|
"targetLanguage": "Target Language",
|
||||||
|
"selectLanguage": "Select Language",
|
||||||
|
"startTranslation": "Start Translation",
|
||||||
|
"translating": "Translating...",
|
||||||
|
"translationComplete": "Translation complete",
|
||||||
|
"translationFailed": "Translation failed",
|
||||||
|
"translationExists": "Translation already exists",
|
||||||
|
"translationStarted": "Translation started",
|
||||||
|
"translationStartedDesc": "Translation task started, please wait...",
|
||||||
|
"completedTranslations": "Completed Translations",
|
||||||
|
"deleteTranslation": "Delete Translation",
|
||||||
|
"deleteSuccess": "Delete successful",
|
||||||
|
"translationDeleted": "Translation ({{lang}}) deleted",
|
||||||
|
"downloadTranslatedPdf": "Translated {{format}} PDF ({{lang}}) downloaded",
|
||||||
|
"status": {
|
||||||
|
"preparing": "Preparing...",
|
||||||
|
"loadingModel": "Loading translation model...",
|
||||||
|
"translating": "Translating...",
|
||||||
|
"complete": "Complete",
|
||||||
|
"failed": "Failed"
|
||||||
|
},
|
||||||
|
"stats": "{{elements}} elements, {{time}}s",
|
||||||
|
"languages": {
|
||||||
|
"en": "English",
|
||||||
|
"ja": "日本語",
|
||||||
|
"ko": "한국어",
|
||||||
|
"zh-TW": "繁體中文",
|
||||||
|
"zh-CN": "简体中文",
|
||||||
|
"de": "Deutsch",
|
||||||
|
"fr": "Français",
|
||||||
|
"es": "Español",
|
||||||
|
"pt": "Português",
|
||||||
|
"it": "Italiano",
|
||||||
|
"ru": "Русский",
|
||||||
|
"vi": "Tiếng Việt",
|
||||||
|
"th": "ภาษาไทย"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"batch": {
|
"batch": {
|
||||||
"title": "Batch Processing",
|
"title": "Batch Processing",
|
||||||
@@ -267,5 +386,255 @@
|
|||||||
"processingError": "Batch Processing Error",
|
"processingError": "Batch Processing Error",
|
||||||
"processingCancelled": "Batch Processing Cancelled",
|
"processingCancelled": "Batch Processing Cancelled",
|
||||||
"concurrencyInfo": "Direct Track: max 5 parallel, OCR Track: sequential (GPU limitation)"
|
"concurrencyInfo": "Direct Track: max 5 parallel, OCR Track: sequential (GPU limitation)"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"title": "Admin Dashboard",
|
||||||
|
"subtitle": "System Statistics and User Management",
|
||||||
|
"loadingDashboard": "Loading admin dashboard...",
|
||||||
|
"loadFailed": "Failed to load admin data",
|
||||||
|
"auditLogs": "Audit Logs",
|
||||||
|
"totalUsers": "Total Users",
|
||||||
|
"totalTasks": "Total Tasks",
|
||||||
|
"pendingTasks": "Pending",
|
||||||
|
"processingTasks": "Processing",
|
||||||
|
"completedTasks": "Completed",
|
||||||
|
"failedTasks": "Failed",
|
||||||
|
"activeUsers": "Active",
|
||||||
|
"translationStats": {
|
||||||
|
"title": "Translation Statistics",
|
||||||
|
"description": "Translation API usage and billing tracking",
|
||||||
|
"totalTranslations": "Total Translations",
|
||||||
|
"totalTokens": "Total Tokens",
|
||||||
|
"totalCharacters": "Total Characters",
|
||||||
|
"estimatedCost": "Estimated Cost",
|
||||||
|
"last30Days": "Last 30 days",
|
||||||
|
"languageBreakdown": "Language Breakdown",
|
||||||
|
"recentTranslations": "Recent Translations",
|
||||||
|
"count": "times",
|
||||||
|
"tokens": "tokens"
|
||||||
|
},
|
||||||
|
"topUsers": {
|
||||||
|
"title": "Top Users",
|
||||||
|
"description": "Users with most tasks",
|
||||||
|
"displayName": "Display Name",
|
||||||
|
"totalTasks": "Total Tasks",
|
||||||
|
"completedTasks": "Completed"
|
||||||
|
},
|
||||||
|
"recentUsers": {
|
||||||
|
"title": "Recent Users",
|
||||||
|
"description": "Recently registered users",
|
||||||
|
"noUsers": "No users",
|
||||||
|
"displayName": "Display Name",
|
||||||
|
"registeredAt": "Registered At",
|
||||||
|
"lastLogin": "Last Login",
|
||||||
|
"status": "Status",
|
||||||
|
"taskCount": "Tasks",
|
||||||
|
"completedCount": "Completed",
|
||||||
|
"failedCount": "Failed"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"taskId": "Task ID",
|
||||||
|
"targetLang": "Target Language",
|
||||||
|
"tokenCount": "Tokens",
|
||||||
|
"charCount": "Characters",
|
||||||
|
"cost": "Cost",
|
||||||
|
"processingTime": "Processing Time",
|
||||||
|
"time": "Time"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"taskHistory": {
|
||||||
|
"title": "Task History",
|
||||||
|
"subtitle": "View and manage your OCR tasks",
|
||||||
|
"loadFailed": "Failed to load tasks",
|
||||||
|
"deleteConfirm": "Are you sure you want to delete this task?",
|
||||||
|
"deleteFailed": "Failed to delete task",
|
||||||
|
"noTasksToDelete": "No tasks to delete",
|
||||||
|
"deleteAllConfirm": "Are you sure you want to delete all {{count}} tasks? This action cannot be undone!",
|
||||||
|
"allTasksDeleted": "All tasks deleted",
|
||||||
|
"downloadPdfFailed": "Failed to download PDF",
|
||||||
|
"startTaskFailed": "Failed to start task",
|
||||||
|
"cancelConfirm": "Are you sure you want to cancel this task?",
|
||||||
|
"cancelFailed": "Failed to cancel task",
|
||||||
|
"retryFailed": "Failed to retry task",
|
||||||
|
"deleteAll": "Delete All",
|
||||||
|
"filterConditions": "Filter",
|
||||||
|
"statusFilter": "Status",
|
||||||
|
"filenameFilter": "Filename",
|
||||||
|
"searchFilename": "Search filename",
|
||||||
|
"startDate": "Start Date",
|
||||||
|
"endDate": "End Date",
|
||||||
|
"clearFilter": "Clear Filter",
|
||||||
|
"taskList": "Task List",
|
||||||
|
"taskCountInfo": "{{total}} tasks (Page {{page}})",
|
||||||
|
"noTasks": "No tasks",
|
||||||
|
"table": {
|
||||||
|
"filename": "Filename",
|
||||||
|
"status": "Status",
|
||||||
|
"createdAt": "Created At",
|
||||||
|
"completedAt": "Completed At",
|
||||||
|
"processingTime": "Processing Time",
|
||||||
|
"actions": "Actions"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"startProcessing": "Start Processing",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"retry": "Retry",
|
||||||
|
"downloadLayoutPdf": "Download Layout PDF",
|
||||||
|
"layoutPdf": "Layout",
|
||||||
|
"downloadReflowPdf": "Download Reflow PDF",
|
||||||
|
"reflowPdf": "Reflow",
|
||||||
|
"viewDetails": "View Details",
|
||||||
|
"delete": "Delete"
|
||||||
|
},
|
||||||
|
"pagination": {
|
||||||
|
"showing": "Showing {{start}} - {{end}} of {{total}}",
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"all": "All",
|
||||||
|
"pending": "Pending",
|
||||||
|
"processing": "Processing",
|
||||||
|
"completed": "Completed",
|
||||||
|
"failed": "Failed"
|
||||||
|
},
|
||||||
|
"unnamed": "Unnamed file"
|
||||||
|
},
|
||||||
|
"taskDetail": {
|
||||||
|
"title": "Task Details",
|
||||||
|
"taskId": "Task ID: {{id}}",
|
||||||
|
"loadingTask": "Loading task details...",
|
||||||
|
"taskNotFound": "Task not found",
|
||||||
|
"taskNotFoundDesc": "Task ID not found: {{id}}",
|
||||||
|
"returnToHistory": "Return to Task History",
|
||||||
|
"taskInfo": "Task Information",
|
||||||
|
"filename": "Filename",
|
||||||
|
"createdAt": "Created At",
|
||||||
|
"completedAt": "Completed At",
|
||||||
|
"taskStatus": "Task Status",
|
||||||
|
"processingTrack": "Processing Track",
|
||||||
|
"processingTime": "Processing Time",
|
||||||
|
"lastUpdated": "Last Updated",
|
||||||
|
"downloadResults": "Download Results",
|
||||||
|
"layoutPdf": "Layout PDF",
|
||||||
|
"reflowPdf": "Reflow PDF",
|
||||||
|
"downloadVisualization": "Download Recognition Images (ZIP)",
|
||||||
|
"visualizationDownloaded": "Recognition images downloaded",
|
||||||
|
"errorMessage": "Error Message",
|
||||||
|
"processingInProgress": "Processing in progress...",
|
||||||
|
"processingInProgressDesc": "Please wait, OCR processing takes some time",
|
||||||
|
"ocrPreview": "OCR Result Preview - {{filename}}",
|
||||||
|
"stats": {
|
||||||
|
"processingTime": "Processing Time",
|
||||||
|
"pageCount": "Pages",
|
||||||
|
"textRegions": "Text Regions",
|
||||||
|
"tables": "Tables",
|
||||||
|
"images": "Images",
|
||||||
|
"avgConfidence": "Avg Confidence"
|
||||||
|
},
|
||||||
|
"track": {
|
||||||
|
"ocr": "OCR Scan",
|
||||||
|
"direct": "Direct Extract",
|
||||||
|
"hybrid": "Hybrid",
|
||||||
|
"auto": "Auto",
|
||||||
|
"ocrDesc": "PaddleOCR Text Recognition",
|
||||||
|
"directDesc": "PyMuPDF Direct Extract",
|
||||||
|
"hybridDesc": "Hybrid Processing"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"completed": "Completed",
|
||||||
|
"processing": "Processing",
|
||||||
|
"failed": "Failed",
|
||||||
|
"pending": "Pending"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auditLogs": {
|
||||||
|
"title": "Audit Logs",
|
||||||
|
"subtitle": "System operation records and security audit",
|
||||||
|
"loadFailed": "Failed to load audit logs",
|
||||||
|
"loadingLogs": "Loading audit logs...",
|
||||||
|
"filterConditions": "Filter",
|
||||||
|
"actionFilter": "Action Type",
|
||||||
|
"userFilter": "User",
|
||||||
|
"allActions": "All Actions",
|
||||||
|
"allUsers": "All Users",
|
||||||
|
"categoryFilter": "Category",
|
||||||
|
"allCategories": "All",
|
||||||
|
"statusFilter": "Status",
|
||||||
|
"allStatuses": "All",
|
||||||
|
"startDate": "Start Date",
|
||||||
|
"endDate": "End Date",
|
||||||
|
"clearFilter": "Clear Filter",
|
||||||
|
"logList": "Log List",
|
||||||
|
"logCountInfo": "{{total}} records (Page {{page}})",
|
||||||
|
"noLogs": "No logs",
|
||||||
|
"category": {
|
||||||
|
"auth": "Authentication",
|
||||||
|
"task": "Task",
|
||||||
|
"file": "File",
|
||||||
|
"admin": "Admin",
|
||||||
|
"system": "System"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"time": "Time",
|
||||||
|
"user": "User",
|
||||||
|
"action": "Action",
|
||||||
|
"target": "Target",
|
||||||
|
"ip": "IP Address",
|
||||||
|
"status": "Status",
|
||||||
|
"category": "Category",
|
||||||
|
"resource": "Resource",
|
||||||
|
"errorMessage": "Error Message"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"success": "Success",
|
||||||
|
"failed": "Failed"
|
||||||
|
},
|
||||||
|
"pagination": {
|
||||||
|
"showing": "Showing {{start}} - {{end}} of {{total}}",
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"processingTrack": {
|
||||||
|
"title": "Processing Mode",
|
||||||
|
"subtitle": "Select processing method or let the system decide",
|
||||||
|
"auto": {
|
||||||
|
"label": "Auto Detect",
|
||||||
|
"description": "System automatically analyzes document type and chooses the best processing method"
|
||||||
|
},
|
||||||
|
"direct": {
|
||||||
|
"label": "Direct Extract (DIRECT)",
|
||||||
|
"description": "Extract text directly from PDF text layer, suitable for editable PDFs"
|
||||||
|
},
|
||||||
|
"ocr": {
|
||||||
|
"label": "OCR Recognition",
|
||||||
|
"description": "Use optical character recognition for scanned documents or images"
|
||||||
|
},
|
||||||
|
"recommended": "Recommended",
|
||||||
|
"selected": "Selected",
|
||||||
|
"overrideWarning": "You have overridden system recommendation. System originally recommended using \"{{track}}\" for this document.",
|
||||||
|
"analysisConfidence": "Analysis confidence: {{value}}%",
|
||||||
|
"pageCount": "Pages: {{value}}",
|
||||||
|
"textCoverage": "Text coverage: {{value}}%",
|
||||||
|
"note": "Auto mode analyzes PDF content: uses OCR for scanned documents, direct extraction for digital PDFs."
|
||||||
|
},
|
||||||
|
"pdfViewer": {
|
||||||
|
"loading": "Loading PDF...",
|
||||||
|
"loadError": "Failed to load PDF",
|
||||||
|
"noPreview": "Cannot preview",
|
||||||
|
"page": "Page",
|
||||||
|
"of": "/",
|
||||||
|
"zoomIn": "Zoom In",
|
||||||
|
"zoomOut": "Zoom Out",
|
||||||
|
"fitWidth": "Fit Width",
|
||||||
|
"fitPage": "Fit Page",
|
||||||
|
"pageInfo": "Page {{current}} / {{total}}",
|
||||||
|
"error": "Error",
|
||||||
|
"pageLoadError": "Failed to load page {{page}}"
|
||||||
|
},
|
||||||
|
"languageSwitcher": {
|
||||||
|
"zhTW": "繁體中文",
|
||||||
|
"enUS": "English"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
"settings": "設定",
|
"settings": "設定",
|
||||||
"logout": "登出",
|
"logout": "登出",
|
||||||
"taskHistory": "任務歷史",
|
"taskHistory": "任務歷史",
|
||||||
"adminDashboard": "管理員儀表板"
|
"adminDashboard": "管理員儀表板",
|
||||||
|
"auditLogs": "審計日誌"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"login": "登入",
|
"login": "登入",
|
||||||
@@ -24,15 +25,19 @@
|
|||||||
"loggingIn": "登入中...",
|
"loggingIn": "登入中...",
|
||||||
"usernamePlaceholder": "輸入您的使用者名稱",
|
"usernamePlaceholder": "輸入您的使用者名稱",
|
||||||
"passwordPlaceholder": "輸入您的密碼",
|
"passwordPlaceholder": "輸入您的密碼",
|
||||||
"supportedFormats": "支援格式:PDF、圖片、Office 文件"
|
"supportedFormats": "支援格式:PDF、圖片、Office 文件",
|
||||||
|
"sessionExpired": "登入已過期,請重新登入",
|
||||||
|
"redirecting": "正在跳轉至登入頁面..."
|
||||||
},
|
},
|
||||||
"upload": {
|
"upload": {
|
||||||
"title": "上傳檔案",
|
"title": "上傳檔案",
|
||||||
|
"subtitle": "選擇要進行 OCR 處理的檔案,支援圖片、PDF 和 Office 文件",
|
||||||
"dragAndDrop": "拖曳檔案至此,或點擊選擇檔案",
|
"dragAndDrop": "拖曳檔案至此,或點擊選擇檔案",
|
||||||
"dropFilesHere": "放開以上傳檔案",
|
"dropFilesHere": "放開以上傳檔案",
|
||||||
"invalidFiles": "部分檔案格式不支援",
|
"invalidFiles": "部分檔案格式不支援",
|
||||||
"supportedFormats": "支援格式:PNG, JPG, JPEG, PDF, DOC, DOCX, PPT, PPTX",
|
"supportedFormats": "支援格式:PNG, JPG, JPEG, PDF, DOC, DOCX, PPT, PPTX",
|
||||||
"maxFileSize": "單檔最大 50MB",
|
"maxFileSize": "單檔最大 50MB",
|
||||||
|
"maxFileSizeWithCount": "最大檔案大小: 50MB · 最多 {{maxFiles}} 個檔案",
|
||||||
"uploadButton": "開始上傳",
|
"uploadButton": "開始上傳",
|
||||||
"uploading": "上傳中...",
|
"uploading": "上傳中...",
|
||||||
"uploadSuccess": "上傳成功",
|
"uploadSuccess": "上傳成功",
|
||||||
@@ -41,7 +46,30 @@
|
|||||||
"clearAll": "清除全部",
|
"clearAll": "清除全部",
|
||||||
"removeFile": "移除",
|
"removeFile": "移除",
|
||||||
"selectedFiles": "已選擇的檔案",
|
"selectedFiles": "已選擇的檔案",
|
||||||
"filesUploaded": "成功上傳 {{count}} 個檔案"
|
"filesUploaded": "成功上傳 {{count}} 個檔案",
|
||||||
|
"orClickToSelect": "或點擊選擇檔案",
|
||||||
|
"selectFilesButton": "選擇檔案",
|
||||||
|
"continueToProcess": "繼續處理",
|
||||||
|
"goToProcessing": "前往處理頁面",
|
||||||
|
"uploadMoreFiles": "上傳更多檔案",
|
||||||
|
"selectAtLeastOne": "請選擇至少一個檔案",
|
||||||
|
"steps": {
|
||||||
|
"selectFiles": "選擇檔案",
|
||||||
|
"selectFilesDesc": "上傳要處理的文件",
|
||||||
|
"confirmUpload": "確認並上傳",
|
||||||
|
"confirmUploadDesc": "檢查並開始處理",
|
||||||
|
"processingComplete": "處理完成",
|
||||||
|
"processingCompleteDesc": "查看結果並導出"
|
||||||
|
},
|
||||||
|
"fileList": {
|
||||||
|
"summary": "已選擇 {{count}} 個檔案,總大小 {{size}}",
|
||||||
|
"unknownType": "未知類型",
|
||||||
|
"ready": "準備就緒",
|
||||||
|
"removeFile": "移除檔案",
|
||||||
|
"confirmPrompt": "請確認檔案無誤後點擊上傳按鈕開始處理",
|
||||||
|
"cancel": "取消",
|
||||||
|
"startUpload": "開始上傳並處理"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"processing": {
|
"processing": {
|
||||||
"title": "OCR 處理中",
|
"title": "OCR 處理中",
|
||||||
@@ -55,6 +83,11 @@
|
|||||||
"failed": "處理失敗",
|
"failed": "處理失敗",
|
||||||
"pending": "等待中",
|
"pending": "等待中",
|
||||||
"estimatedTime": "預計剩餘時間",
|
"estimatedTime": "預計剩餘時間",
|
||||||
|
"ocrStarted": "OCR 處理已開始",
|
||||||
|
"loadingTask": "載入任務資訊...",
|
||||||
|
"analyzingDocument": "分析文件類型中...",
|
||||||
|
"noBatchMessage": "尚未選擇任何任務。請先上傳檔案以建立任務。",
|
||||||
|
"goToUpload": "前往上傳頁面",
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "處理設定",
|
"title": "處理設定",
|
||||||
"language": "識別語言",
|
"language": "識別語言",
|
||||||
@@ -151,9 +184,23 @@
|
|||||||
"viewJSON": "檢視 JSON",
|
"viewJSON": "檢視 JSON",
|
||||||
"downloadPDF": "下載 PDF",
|
"downloadPDF": "下載 PDF",
|
||||||
"preview": "預覽",
|
"preview": "預覽",
|
||||||
|
"noBatchMessage": "尚未選擇任何任務。請先上傳並處理檔案。",
|
||||||
|
"goToUpload": "前往上傳頁面",
|
||||||
"noResults": "尚無處理結果",
|
"noResults": "尚無處理結果",
|
||||||
"textBlocks": "文字區塊",
|
"textBlocks": "文字區塊",
|
||||||
"layoutInfo": "版面資訊"
|
"layoutInfo": "版面資訊",
|
||||||
|
"pdfDownloaded": "PDF 已下載",
|
||||||
|
"markdownDownloaded": "Markdown 已下載",
|
||||||
|
"jsonDownloaded": "JSON 已下載",
|
||||||
|
"loadingResults": "載入任務結果...",
|
||||||
|
"processingStatus": "處理狀態",
|
||||||
|
"taskType": "任務類型",
|
||||||
|
"processingInProgress": "正在處理中...",
|
||||||
|
"processingInProgressDesc": "請稍候,OCR 處理需要一些時間",
|
||||||
|
"waitingToProcess": "等待處理",
|
||||||
|
"waitingToProcessDesc": "請前往處理頁面啟動 OCR 處理",
|
||||||
|
"goToProcessing": "前往處理頁面",
|
||||||
|
"viewTaskHistory": "查看任務歷史"
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"title": "匯出結果",
|
"title": "匯出結果",
|
||||||
@@ -217,7 +264,37 @@
|
|||||||
"back": "返回",
|
"back": "返回",
|
||||||
"next": "下一步",
|
"next": "下一步",
|
||||||
"previous": "上一步",
|
"previous": "上一步",
|
||||||
"submit": "提交"
|
"submit": "提交",
|
||||||
|
"retry": "重試",
|
||||||
|
"viewDetails": "查看詳情",
|
||||||
|
"unknownFile": "未知檔案",
|
||||||
|
"unknownError": "未知錯誤",
|
||||||
|
"seconds": "秒",
|
||||||
|
"total": "總計",
|
||||||
|
"active": "活躍",
|
||||||
|
"inactive": "停用",
|
||||||
|
"all": "全部",
|
||||||
|
"none": "無",
|
||||||
|
"enabled": "啟用",
|
||||||
|
"disabled": "停用",
|
||||||
|
"yes": "是",
|
||||||
|
"no": "否",
|
||||||
|
"or": "或",
|
||||||
|
"and": "和",
|
||||||
|
"show": "顯示",
|
||||||
|
"hide": "隱藏",
|
||||||
|
"more": "更多",
|
||||||
|
"less": "更少",
|
||||||
|
"downloadFailed": "下載失敗",
|
||||||
|
"downloadSuccess": "下載成功",
|
||||||
|
"taskDeleted": "任務已刪除",
|
||||||
|
"taskDeletedDesc": "此任務已被刪除或不存在。請上傳新檔案以建立新任務。",
|
||||||
|
"taskIdLabel": "任務 ID: {{id}}",
|
||||||
|
"goToUpload": "前往上傳頁面",
|
||||||
|
"verifying": "驗證中...",
|
||||||
|
"accessDenied": "訪問被拒絕",
|
||||||
|
"accessDeniedDesc": "您沒有權限訪問此頁面",
|
||||||
|
"backToHome": "返回首頁"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"networkError": "網路錯誤,請稍後再試",
|
"networkError": "網路錯誤,請稍後再試",
|
||||||
@@ -229,12 +306,54 @@
|
|||||||
"unsupportedFormat": "不支援的格式",
|
"unsupportedFormat": "不支援的格式",
|
||||||
"uploadFailed": "上傳失敗",
|
"uploadFailed": "上傳失敗",
|
||||||
"processingFailed": "處理失敗",
|
"processingFailed": "處理失敗",
|
||||||
"exportFailed": "匯出失敗"
|
"exportFailed": "匯出失敗",
|
||||||
|
"loadFailed": "載入失敗",
|
||||||
|
"deleteFailed": "刪除失敗",
|
||||||
|
"startFailed": "啟動失敗",
|
||||||
|
"cancelFailed": "取消失敗",
|
||||||
|
"retryFailed": "重試失敗"
|
||||||
},
|
},
|
||||||
"translation": {
|
"translation": {
|
||||||
"title": "翻譯功能",
|
"title": "文件翻譯",
|
||||||
"comingSoon": "即將推出",
|
"comingSoon": "即將推出",
|
||||||
"description": "文件翻譯功能正在開發中,敬請期待"
|
"description": "使用雲端翻譯服務進行多語言翻譯,支援多種目標語言。",
|
||||||
|
"targetLanguage": "目標語言",
|
||||||
|
"selectLanguage": "選擇語言",
|
||||||
|
"startTranslation": "開始翻譯",
|
||||||
|
"translating": "翻譯中...",
|
||||||
|
"translationComplete": "翻譯完成",
|
||||||
|
"translationFailed": "翻譯失敗",
|
||||||
|
"translationExists": "翻譯已存在",
|
||||||
|
"translationStarted": "開始翻譯",
|
||||||
|
"translationStartedDesc": "翻譯任務已啟動,請稍候...",
|
||||||
|
"completedTranslations": "已完成的翻譯",
|
||||||
|
"deleteTranslation": "刪除翻譯",
|
||||||
|
"deleteSuccess": "刪除成功",
|
||||||
|
"translationDeleted": "翻譯結果 ({{lang}}) 已刪除",
|
||||||
|
"downloadTranslatedPdf": "翻譯 {{format}} PDF ({{lang}}) 已下載",
|
||||||
|
"status": {
|
||||||
|
"preparing": "準備中...",
|
||||||
|
"loadingModel": "載入翻譯模型...",
|
||||||
|
"translating": "翻譯中...",
|
||||||
|
"complete": "完成",
|
||||||
|
"failed": "失敗"
|
||||||
|
},
|
||||||
|
"stats": "{{elements}} 元素, {{time}}s",
|
||||||
|
"languages": {
|
||||||
|
"en": "English",
|
||||||
|
"ja": "日本語",
|
||||||
|
"ko": "한국어",
|
||||||
|
"zh-TW": "繁體中文",
|
||||||
|
"zh-CN": "简体中文",
|
||||||
|
"de": "Deutsch",
|
||||||
|
"fr": "Français",
|
||||||
|
"es": "Español",
|
||||||
|
"pt": "Português",
|
||||||
|
"it": "Italiano",
|
||||||
|
"ru": "Русский",
|
||||||
|
"vi": "Tiếng Việt",
|
||||||
|
"th": "ภาษาไทย"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"batch": {
|
"batch": {
|
||||||
"title": "批次處理",
|
"title": "批次處理",
|
||||||
@@ -267,5 +386,255 @@
|
|||||||
"processingError": "批次處理錯誤",
|
"processingError": "批次處理錯誤",
|
||||||
"processingCancelled": "批次處理已取消",
|
"processingCancelled": "批次處理已取消",
|
||||||
"concurrencyInfo": "Direct Track 最多 5 並行處理,OCR Track 依序處理 (GPU 限制)"
|
"concurrencyInfo": "Direct Track 最多 5 並行處理,OCR Track 依序處理 (GPU 限制)"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"title": "管理員儀表板",
|
||||||
|
"subtitle": "系統統計與用戶管理",
|
||||||
|
"loadingDashboard": "載入管理員儀表板...",
|
||||||
|
"loadFailed": "載入管理員資料失敗",
|
||||||
|
"auditLogs": "審計日誌",
|
||||||
|
"totalUsers": "總用戶數",
|
||||||
|
"totalTasks": "總任務數",
|
||||||
|
"pendingTasks": "待處理",
|
||||||
|
"processingTasks": "處理中",
|
||||||
|
"completedTasks": "已完成",
|
||||||
|
"failedTasks": "失敗",
|
||||||
|
"activeUsers": "活躍",
|
||||||
|
"translationStats": {
|
||||||
|
"title": "翻譯統計",
|
||||||
|
"description": "翻譯 API 使用量與計費追蹤",
|
||||||
|
"totalTranslations": "總翻譯次數",
|
||||||
|
"totalTokens": "總 Token 數",
|
||||||
|
"totalCharacters": "總字元數",
|
||||||
|
"estimatedCost": "預估成本",
|
||||||
|
"last30Days": "近30天",
|
||||||
|
"languageBreakdown": "語言分佈",
|
||||||
|
"recentTranslations": "最近翻譯記錄",
|
||||||
|
"count": "次",
|
||||||
|
"tokens": "tokens"
|
||||||
|
},
|
||||||
|
"topUsers": {
|
||||||
|
"title": "活躍用戶排行",
|
||||||
|
"description": "任務數量最多的用戶",
|
||||||
|
"displayName": "顯示名稱",
|
||||||
|
"totalTasks": "總任務",
|
||||||
|
"completedTasks": "已完成"
|
||||||
|
},
|
||||||
|
"recentUsers": {
|
||||||
|
"title": "最近用戶",
|
||||||
|
"description": "最新註冊的用戶列表",
|
||||||
|
"noUsers": "暫無用戶",
|
||||||
|
"displayName": "顯示名稱",
|
||||||
|
"registeredAt": "註冊時間",
|
||||||
|
"lastLogin": "最後登入",
|
||||||
|
"status": "狀態",
|
||||||
|
"taskCount": "任務數",
|
||||||
|
"completedCount": "完成",
|
||||||
|
"failedCount": "失敗"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"taskId": "任務 ID",
|
||||||
|
"targetLang": "目標語言",
|
||||||
|
"tokenCount": "Token 數",
|
||||||
|
"charCount": "字元數",
|
||||||
|
"cost": "成本",
|
||||||
|
"processingTime": "處理時間",
|
||||||
|
"time": "時間"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"taskHistory": {
|
||||||
|
"title": "任務歷史",
|
||||||
|
"subtitle": "查看和管理您的 OCR 任務",
|
||||||
|
"loadFailed": "載入任務失敗",
|
||||||
|
"deleteConfirm": "確定要刪除此任務嗎?",
|
||||||
|
"deleteFailed": "刪除任務失敗",
|
||||||
|
"noTasksToDelete": "沒有可刪除的任務",
|
||||||
|
"deleteAllConfirm": "確定要刪除所有 {{count}} 個任務嗎?此操作無法復原!",
|
||||||
|
"allTasksDeleted": "所有任務已刪除",
|
||||||
|
"downloadPdfFailed": "下載 PDF 檔案失敗",
|
||||||
|
"startTaskFailed": "啟動任務失敗",
|
||||||
|
"cancelConfirm": "確定要取消此任務嗎?",
|
||||||
|
"cancelFailed": "取消任務失敗",
|
||||||
|
"retryFailed": "重試任務失敗",
|
||||||
|
"deleteAll": "刪除全部",
|
||||||
|
"filterConditions": "篩選條件",
|
||||||
|
"statusFilter": "狀態",
|
||||||
|
"filenameFilter": "檔案名稱",
|
||||||
|
"searchFilename": "搜尋檔案名稱",
|
||||||
|
"startDate": "開始日期",
|
||||||
|
"endDate": "結束日期",
|
||||||
|
"clearFilter": "清除篩選",
|
||||||
|
"taskList": "任務列表",
|
||||||
|
"taskCountInfo": "共 {{total}} 個任務 (顯示第 {{page}} 頁)",
|
||||||
|
"noTasks": "暫無任務",
|
||||||
|
"table": {
|
||||||
|
"filename": "檔案名稱",
|
||||||
|
"status": "狀態",
|
||||||
|
"createdAt": "建立時間",
|
||||||
|
"completedAt": "完成時間",
|
||||||
|
"processingTime": "處理時間",
|
||||||
|
"actions": "操作"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"startProcessing": "開始處理",
|
||||||
|
"cancel": "取消",
|
||||||
|
"retry": "重試",
|
||||||
|
"downloadLayoutPdf": "下載版面 PDF",
|
||||||
|
"layoutPdf": "版面",
|
||||||
|
"downloadReflowPdf": "下載流式 PDF",
|
||||||
|
"reflowPdf": "流式",
|
||||||
|
"viewDetails": "查看詳情",
|
||||||
|
"delete": "刪除"
|
||||||
|
},
|
||||||
|
"pagination": {
|
||||||
|
"showing": "顯示 {{start}} - {{end}} / 共 {{total}} 個",
|
||||||
|
"previous": "上一頁",
|
||||||
|
"next": "下一頁"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"all": "全部",
|
||||||
|
"pending": "待處理",
|
||||||
|
"processing": "處理中",
|
||||||
|
"completed": "已完成",
|
||||||
|
"failed": "失敗"
|
||||||
|
},
|
||||||
|
"unnamed": "未命名檔案"
|
||||||
|
},
|
||||||
|
"taskDetail": {
|
||||||
|
"title": "任務詳情",
|
||||||
|
"taskId": "任務 ID: {{id}}",
|
||||||
|
"loadingTask": "載入任務詳情...",
|
||||||
|
"taskNotFound": "任務不存在",
|
||||||
|
"taskNotFoundDesc": "找不到任務 ID: {{id}}",
|
||||||
|
"returnToHistory": "返回任務歷史",
|
||||||
|
"taskInfo": "任務資訊",
|
||||||
|
"filename": "檔案名稱",
|
||||||
|
"createdAt": "建立時間",
|
||||||
|
"completedAt": "完成時間",
|
||||||
|
"taskStatus": "任務狀態",
|
||||||
|
"processingTrack": "處理軌道",
|
||||||
|
"processingTime": "處理時間",
|
||||||
|
"lastUpdated": "最後更新",
|
||||||
|
"downloadResults": "下載結果",
|
||||||
|
"layoutPdf": "版面 PDF",
|
||||||
|
"reflowPdf": "流式 PDF",
|
||||||
|
"downloadVisualization": "下載辨識結果圖片 (ZIP)",
|
||||||
|
"visualizationDownloaded": "辨識結果圖片已下載",
|
||||||
|
"errorMessage": "錯誤訊息",
|
||||||
|
"processingInProgress": "正在處理中...",
|
||||||
|
"processingInProgressDesc": "請稍候,OCR 處理需要一些時間",
|
||||||
|
"ocrPreview": "OCR 結果預覽 - {{filename}}",
|
||||||
|
"stats": {
|
||||||
|
"processingTime": "處理時間",
|
||||||
|
"pageCount": "頁數",
|
||||||
|
"textRegions": "文本區域",
|
||||||
|
"tables": "表格",
|
||||||
|
"images": "圖片",
|
||||||
|
"avgConfidence": "平均置信度"
|
||||||
|
},
|
||||||
|
"track": {
|
||||||
|
"ocr": "OCR 掃描",
|
||||||
|
"direct": "直接提取",
|
||||||
|
"hybrid": "混合",
|
||||||
|
"auto": "自動",
|
||||||
|
"ocrDesc": "PaddleOCR 文字識別",
|
||||||
|
"directDesc": "PyMuPDF 直接提取",
|
||||||
|
"hybridDesc": "混合處理"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"completed": "已完成",
|
||||||
|
"processing": "處理中",
|
||||||
|
"failed": "失敗",
|
||||||
|
"pending": "等待中"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auditLogs": {
|
||||||
|
"title": "審計日誌",
|
||||||
|
"subtitle": "系統操作記錄與安全審計",
|
||||||
|
"loadFailed": "載入審計日誌失敗",
|
||||||
|
"loadingLogs": "載入審計日誌...",
|
||||||
|
"filterConditions": "篩選條件",
|
||||||
|
"actionFilter": "操作類型",
|
||||||
|
"userFilter": "用戶",
|
||||||
|
"allActions": "所有操作",
|
||||||
|
"allUsers": "所有用戶",
|
||||||
|
"categoryFilter": "類別",
|
||||||
|
"allCategories": "全部",
|
||||||
|
"statusFilter": "狀態",
|
||||||
|
"allStatuses": "全部",
|
||||||
|
"startDate": "開始日期",
|
||||||
|
"endDate": "結束日期",
|
||||||
|
"clearFilter": "清除篩選",
|
||||||
|
"logList": "日誌列表",
|
||||||
|
"logCountInfo": "共 {{total}} 筆記錄 (顯示第 {{page}} 頁)",
|
||||||
|
"noLogs": "暫無日誌記錄",
|
||||||
|
"category": {
|
||||||
|
"auth": "認證",
|
||||||
|
"task": "任務",
|
||||||
|
"file": "檔案",
|
||||||
|
"admin": "管理",
|
||||||
|
"system": "系統"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"time": "時間",
|
||||||
|
"user": "用戶",
|
||||||
|
"action": "操作",
|
||||||
|
"target": "目標",
|
||||||
|
"ip": "IP 位址",
|
||||||
|
"status": "狀態",
|
||||||
|
"category": "類別",
|
||||||
|
"resource": "資源",
|
||||||
|
"errorMessage": "錯誤訊息"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"success": "成功",
|
||||||
|
"failed": "失敗"
|
||||||
|
},
|
||||||
|
"pagination": {
|
||||||
|
"showing": "顯示 {{start}} - {{end}} / 共 {{total}} 筆",
|
||||||
|
"previous": "上一頁",
|
||||||
|
"next": "下一頁"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"processingTrack": {
|
||||||
|
"title": "處理方式選擇",
|
||||||
|
"subtitle": "選擇文件的處理方式,或讓系統自動判斷",
|
||||||
|
"auto": {
|
||||||
|
"label": "自動選擇",
|
||||||
|
"description": "根據文件類型自動選擇最佳處理方式"
|
||||||
|
},
|
||||||
|
"direct": {
|
||||||
|
"label": "直接提取 (DIRECT)",
|
||||||
|
"description": "從 PDF 中直接提取文字圖層,適用於可編輯 PDF"
|
||||||
|
},
|
||||||
|
"ocr": {
|
||||||
|
"label": "OCR 識別",
|
||||||
|
"description": "使用光學字元識別處理圖片或掃描文件"
|
||||||
|
},
|
||||||
|
"recommended": "系統建議",
|
||||||
|
"selected": "已選擇",
|
||||||
|
"overrideWarning": "您已覆蓋系統建議。系統原本建議使用「{{track}}」方式處理此文件。",
|
||||||
|
"analysisConfidence": "文件分析信心度: {{value}}%",
|
||||||
|
"pageCount": "頁數: {{value}}",
|
||||||
|
"textCoverage": "文字覆蓋率: {{value}}%",
|
||||||
|
"note": "自動模式會根據 PDF 內容判斷:若為掃描件則使用 OCR,若為數位 PDF 則直接擷取。"
|
||||||
|
},
|
||||||
|
"pdfViewer": {
|
||||||
|
"loading": "載入 PDF 中...",
|
||||||
|
"loadError": "載入 PDF 失敗",
|
||||||
|
"noPreview": "無法預覽",
|
||||||
|
"page": "頁",
|
||||||
|
"of": "/",
|
||||||
|
"zoomIn": "放大",
|
||||||
|
"zoomOut": "縮小",
|
||||||
|
"fitWidth": "符合寬度",
|
||||||
|
"fitPage": "符合頁面",
|
||||||
|
"pageInfo": "第 {{current}} 頁 / 共 {{total}} 頁",
|
||||||
|
"error": "錯誤",
|
||||||
|
"pageLoadError": "無法載入第 {{page}} 頁"
|
||||||
|
},
|
||||||
|
"languageSwitcher": {
|
||||||
|
"zhTW": "繁體中文",
|
||||||
|
"enUS": "English"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { apiClientV2 } from '@/services/apiV2'
|
import { apiClientV2 } from '@/services/apiV2'
|
||||||
import type { SystemStats, UserWithStats, TopUser, TranslationStats } from '@/types/apiV2'
|
import type { SystemStats, UserWithStats, TopUser, TranslationStats } from '@/types/apiV2'
|
||||||
import {
|
import {
|
||||||
@@ -34,6 +35,7 @@ import {
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
|
||||||
export default function AdminDashboardPage() {
|
export default function AdminDashboardPage() {
|
||||||
|
const { t, i18n } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [stats, setStats] = useState<SystemStats | null>(null)
|
const [stats, setStats] = useState<SystemStats | null>(null)
|
||||||
const [users, setUsers] = useState<UserWithStats[]>([])
|
const [users, setUsers] = useState<UserWithStats[]>([])
|
||||||
@@ -61,7 +63,7 @@ export default function AdminDashboardPage() {
|
|||||||
setTranslationStats(translationStatsData)
|
setTranslationStats(translationStatsData)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to fetch admin data:', err)
|
console.error('Failed to fetch admin data:', err)
|
||||||
setError(err.response?.data?.detail || '載入管理員資料失敗')
|
setError(err.response?.data?.detail || t('admin.loadFailed'))
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -71,11 +73,11 @@ export default function AdminDashboardPage() {
|
|||||||
fetchData()
|
fetchData()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Format date
|
// Format date based on current locale
|
||||||
const formatDate = (dateStr: string | null) => {
|
const formatDate = (dateStr: string | null) => {
|
||||||
if (!dateStr) return '-'
|
if (!dateStr) return '-'
|
||||||
const date = new Date(dateStr)
|
const date = new Date(dateStr)
|
||||||
return date.toLocaleString('zh-TW')
|
return date.toLocaleString(i18n.language === 'zh-TW' ? 'zh-TW' : 'en-US')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -83,7 +85,7 @@ export default function AdminDashboardPage() {
|
|||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Loader2 className="w-12 h-12 animate-spin text-blue-600 mx-auto mb-4" />
|
<Loader2 className="w-12 h-12 animate-spin text-blue-600 mx-auto mb-4" />
|
||||||
<p className="text-gray-600">載入管理員儀表板...</p>
|
<p className="text-gray-600">{t('admin.loadingDashboard')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -95,7 +97,7 @@ export default function AdminDashboardPage() {
|
|||||||
<div className="flex items-center gap-3 p-4 bg-red-50 border border-red-200 rounded-lg">
|
<div className="flex items-center gap-3 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||||
<XCircle className="w-5 h-5 text-red-600" />
|
<XCircle className="w-5 h-5 text-red-600" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-red-600 font-semibold">載入失敗</p>
|
<p className="text-red-600 font-semibold">{t('errors.loadFailed')}</p>
|
||||||
<p className="text-red-500 text-sm">{error}</p>
|
<p className="text-red-500 text-sm">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,18 +112,18 @@ export default function AdminDashboardPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Shield className="w-8 h-8 text-blue-600" />
|
<Shield className="w-8 h-8 text-blue-600" />
|
||||||
<h1 className="text-3xl font-bold text-gray-900">管理員儀表板</h1>
|
<h1 className="text-3xl font-bold text-gray-900">{t('admin.title')}</h1>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-600 mt-1">系統統計與用戶管理</p>
|
<p className="text-gray-600 mt-1">{t('admin.subtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button onClick={() => navigate('/admin/audit-logs')} variant="outline">
|
<Button onClick={() => navigate('/admin/audit-logs')} variant="outline">
|
||||||
<Activity className="w-4 h-4 mr-2" />
|
<Activity className="w-4 h-4 mr-2" />
|
||||||
審計日誌
|
{t('admin.auditLogs')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={fetchData} variant="outline">
|
<Button onClick={fetchData} variant="outline">
|
||||||
<RefreshCw className="w-4 h-4 mr-2" />
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
刷新
|
{t('common.refresh')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,13 +135,13 @@ export default function AdminDashboardPage() {
|
|||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-gray-600 flex items-center gap-2">
|
<CardTitle className="text-sm font-medium text-gray-600 flex items-center gap-2">
|
||||||
<Users className="w-4 h-4" />
|
<Users className="w-4 h-4" />
|
||||||
總用戶數
|
{t('admin.totalUsers')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">{stats.total_users}</div>
|
<div className="text-2xl font-bold">{stats.total_users}</div>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
活躍: {stats.active_users}
|
{t('admin.activeUsers')}: {stats.active_users}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -148,7 +150,7 @@ export default function AdminDashboardPage() {
|
|||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-gray-600 flex items-center gap-2">
|
<CardTitle className="text-sm font-medium text-gray-600 flex items-center gap-2">
|
||||||
<ClipboardList className="w-4 h-4" />
|
<ClipboardList className="w-4 h-4" />
|
||||||
總任務數
|
{t('admin.totalTasks')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -160,7 +162,7 @@ export default function AdminDashboardPage() {
|
|||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-gray-600 flex items-center gap-2">
|
<CardTitle className="text-sm font-medium text-gray-600 flex items-center gap-2">
|
||||||
<Clock className="w-4 h-4" />
|
<Clock className="w-4 h-4" />
|
||||||
待處理
|
{t('admin.pendingTasks')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -174,7 +176,7 @@ export default function AdminDashboardPage() {
|
|||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-gray-600 flex items-center gap-2">
|
<CardTitle className="text-sm font-medium text-gray-600 flex items-center gap-2">
|
||||||
<Loader2 className="w-4 h-4" />
|
<Loader2 className="w-4 h-4" />
|
||||||
處理中
|
{t('admin.processingTasks')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -188,7 +190,7 @@ export default function AdminDashboardPage() {
|
|||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-gray-600 flex items-center gap-2">
|
<CardTitle className="text-sm font-medium text-gray-600 flex items-center gap-2">
|
||||||
<CheckCircle2 className="w-4 h-4" />
|
<CheckCircle2 className="w-4 h-4" />
|
||||||
已完成
|
{t('admin.completedTasks')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -196,7 +198,7 @@ export default function AdminDashboardPage() {
|
|||||||
{stats.task_stats.completed}
|
{stats.task_stats.completed}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-red-600 mt-1">
|
<p className="text-xs text-red-600 mt-1">
|
||||||
失敗: {stats.task_stats.failed}
|
{t('admin.failedTasks')}: {stats.task_stats.failed}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -209,42 +211,42 @@ export default function AdminDashboardPage() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Languages className="w-5 h-5" />
|
<Languages className="w-5 h-5" />
|
||||||
翻譯統計
|
{t('admin.translationStats.title')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>翻譯 API 使用量與計費追蹤</CardDescription>
|
<CardDescription>{t('admin.translationStats.description')}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||||
<div className="p-4 bg-blue-50 rounded-lg">
|
<div className="p-4 bg-blue-50 rounded-lg">
|
||||||
<div className="flex items-center gap-2 text-blue-600 mb-1">
|
<div className="flex items-center gap-2 text-blue-600 mb-1">
|
||||||
<Languages className="w-4 h-4" />
|
<Languages className="w-4 h-4" />
|
||||||
<span className="text-sm font-medium">總翻譯次數</span>
|
<span className="text-sm font-medium">{t('admin.translationStats.totalTranslations')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-bold text-blue-700">
|
<div className="text-2xl font-bold text-blue-700">
|
||||||
{translationStats.total_translations.toLocaleString()}
|
{translationStats.total_translations.toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-blue-500 mt-1">
|
<p className="text-xs text-blue-500 mt-1">
|
||||||
近30天: {translationStats.last_30_days.count}
|
{t('admin.translationStats.last30Days')}: {translationStats.last_30_days.count}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 bg-purple-50 rounded-lg">
|
<div className="p-4 bg-purple-50 rounded-lg">
|
||||||
<div className="flex items-center gap-2 text-purple-600 mb-1">
|
<div className="flex items-center gap-2 text-purple-600 mb-1">
|
||||||
<Activity className="w-4 h-4" />
|
<Activity className="w-4 h-4" />
|
||||||
<span className="text-sm font-medium">總 Token 數</span>
|
<span className="text-sm font-medium">{t('admin.translationStats.totalTokens')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-bold text-purple-700">
|
<div className="text-2xl font-bold text-purple-700">
|
||||||
{translationStats.total_tokens.toLocaleString()}
|
{translationStats.total_tokens.toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-purple-500 mt-1">
|
<p className="text-xs text-purple-500 mt-1">
|
||||||
近30天: {translationStats.last_30_days.tokens.toLocaleString()}
|
{t('admin.translationStats.last30Days')}: {translationStats.last_30_days.tokens.toLocaleString()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 bg-green-50 rounded-lg">
|
<div className="p-4 bg-green-50 rounded-lg">
|
||||||
<div className="flex items-center gap-2 text-green-600 mb-1">
|
<div className="flex items-center gap-2 text-green-600 mb-1">
|
||||||
<ClipboardList className="w-4 h-4" />
|
<ClipboardList className="w-4 h-4" />
|
||||||
<span className="text-sm font-medium">總字元數</span>
|
<span className="text-sm font-medium">{t('admin.translationStats.totalCharacters')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-bold text-green-700">
|
<div className="text-2xl font-bold text-green-700">
|
||||||
{translationStats.total_characters.toLocaleString()}
|
{translationStats.total_characters.toLocaleString()}
|
||||||
@@ -254,7 +256,7 @@ export default function AdminDashboardPage() {
|
|||||||
<div className="p-4 bg-amber-50 rounded-lg">
|
<div className="p-4 bg-amber-50 rounded-lg">
|
||||||
<div className="flex items-center gap-2 text-amber-600 mb-1">
|
<div className="flex items-center gap-2 text-amber-600 mb-1">
|
||||||
<Coins className="w-4 h-4" />
|
<Coins className="w-4 h-4" />
|
||||||
<span className="text-sm font-medium">預估成本</span>
|
<span className="text-sm font-medium">{t('admin.translationStats.estimatedCost')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-bold text-amber-700">
|
<div className="text-2xl font-bold text-amber-700">
|
||||||
${translationStats.estimated_cost.toFixed(2)}
|
${translationStats.estimated_cost.toFixed(2)}
|
||||||
@@ -266,11 +268,11 @@ export default function AdminDashboardPage() {
|
|||||||
{/* Language Breakdown */}
|
{/* Language Breakdown */}
|
||||||
{translationStats.by_language.length > 0 && (
|
{translationStats.by_language.length > 0 && (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<h4 className="text-sm font-medium text-gray-700 mb-2">語言分佈</h4>
|
<h4 className="text-sm font-medium text-gray-700 mb-2">{t('admin.translationStats.languageBreakdown')}</h4>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{translationStats.by_language.map((lang) => (
|
{translationStats.by_language.map((lang) => (
|
||||||
<Badge key={lang.language} variant="outline" className="px-3 py-1">
|
<Badge key={lang.language} variant="outline" className="px-3 py-1">
|
||||||
{lang.language}: {lang.count} 次 ({lang.tokens.toLocaleString()} tokens)
|
{lang.language}: {lang.count} {t('admin.translationStats.count')} ({lang.tokens.toLocaleString()} {t('admin.translationStats.tokens')})
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -280,42 +282,42 @@ export default function AdminDashboardPage() {
|
|||||||
{/* Recent Translations */}
|
{/* Recent Translations */}
|
||||||
{translationStats.recent_translations.length > 0 && (
|
{translationStats.recent_translations.length > 0 && (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<h4 className="text-sm font-medium text-gray-700 mb-2">最近翻譯記錄</h4>
|
<h4 className="text-sm font-medium text-gray-700 mb-2">{t('admin.translationStats.recentTranslations')}</h4>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>任務 ID</TableHead>
|
<TableHead>{t('admin.table.taskId')}</TableHead>
|
||||||
<TableHead>目標語言</TableHead>
|
<TableHead>{t('admin.table.targetLang')}</TableHead>
|
||||||
<TableHead className="text-right">Token 數</TableHead>
|
<TableHead className="text-right">{t('admin.table.tokenCount')}</TableHead>
|
||||||
<TableHead className="text-right">字元數</TableHead>
|
<TableHead className="text-right">{t('admin.table.charCount')}</TableHead>
|
||||||
<TableHead className="text-right">成本</TableHead>
|
<TableHead className="text-right">{t('admin.table.cost')}</TableHead>
|
||||||
<TableHead className="text-right">處理時間</TableHead>
|
<TableHead className="text-right">{t('admin.table.processingTime')}</TableHead>
|
||||||
<TableHead>時間</TableHead>
|
<TableHead>{t('admin.table.time')}</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{translationStats.recent_translations.slice(0, 10).map((t) => (
|
{translationStats.recent_translations.slice(0, 10).map((tr) => (
|
||||||
<TableRow key={t.id}>
|
<TableRow key={tr.id}>
|
||||||
<TableCell className="font-mono text-xs">
|
<TableCell className="font-mono text-xs">
|
||||||
{t.task_id.substring(0, 8)}...
|
{tr.task_id.substring(0, 8)}...
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant="secondary">{t.target_lang}</Badge>
|
<Badge variant="secondary">{tr.target_lang}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
{t.total_tokens.toLocaleString()}
|
{tr.total_tokens.toLocaleString()}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
{t.total_characters.toLocaleString()}
|
{tr.total_characters.toLocaleString()}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right font-medium text-amber-600">
|
<TableCell className="text-right font-medium text-amber-600">
|
||||||
${t.estimated_cost.toFixed(4)}
|
${tr.estimated_cost.toFixed(4)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
{t.processing_time_seconds.toFixed(1)}s
|
{tr.processing_time_seconds.toFixed(1)}s
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-sm text-gray-600">
|
<TableCell className="text-sm text-gray-600">
|
||||||
{new Date(t.created_at).toLocaleString('zh-TW')}
|
{formatDate(tr.created_at)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
@@ -333,9 +335,9 @@ export default function AdminDashboardPage() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<TrendingUp className="w-5 h-5" />
|
<TrendingUp className="w-5 h-5" />
|
||||||
活躍用戶排行
|
{t('admin.topUsers.title')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>任務數量最多的用戶</CardDescription>
|
<CardDescription>{t('admin.topUsers.description')}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Table>
|
<Table>
|
||||||
@@ -343,9 +345,9 @@ export default function AdminDashboardPage() {
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-12">#</TableHead>
|
<TableHead className="w-12">#</TableHead>
|
||||||
<TableHead>Email</TableHead>
|
<TableHead>Email</TableHead>
|
||||||
<TableHead>顯示名稱</TableHead>
|
<TableHead>{t('admin.topUsers.displayName')}</TableHead>
|
||||||
<TableHead className="text-right">總任務</TableHead>
|
<TableHead className="text-right">{t('admin.topUsers.totalTasks')}</TableHead>
|
||||||
<TableHead className="text-right">已完成</TableHead>
|
<TableHead className="text-right">{t('admin.topUsers.completedTasks')}</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -377,26 +379,26 @@ export default function AdminDashboardPage() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Users className="w-5 h-5" />
|
<Users className="w-5 h-5" />
|
||||||
最近用戶
|
{t('admin.recentUsers.title')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>最新註冊的用戶列表</CardDescription>
|
<CardDescription>{t('admin.recentUsers.description')}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{users.length === 0 ? (
|
{users.length === 0 ? (
|
||||||
<div className="text-center py-8 text-gray-500">
|
<div className="text-center py-8 text-gray-500">
|
||||||
<Users className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
<Users className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
||||||
<p>暫無用戶</p>
|
<p>{t('admin.recentUsers.noUsers')}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Email</TableHead>
|
<TableHead>Email</TableHead>
|
||||||
<TableHead>顯示名稱</TableHead>
|
<TableHead>{t('admin.recentUsers.displayName')}</TableHead>
|
||||||
<TableHead>註冊時間</TableHead>
|
<TableHead>{t('admin.recentUsers.registeredAt')}</TableHead>
|
||||||
<TableHead>最後登入</TableHead>
|
<TableHead>{t('admin.recentUsers.lastLogin')}</TableHead>
|
||||||
<TableHead>狀態</TableHead>
|
<TableHead>{t('admin.recentUsers.status')}</TableHead>
|
||||||
<TableHead className="text-right">任務數</TableHead>
|
<TableHead className="text-right">{t('admin.recentUsers.taskCount')}</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -412,14 +414,14 @@ export default function AdminDashboardPage() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={user.is_active ? 'default' : 'secondary'}>
|
<Badge variant={user.is_active ? 'default' : 'secondary'}>
|
||||||
{user.is_active ? '活躍' : '停用'}
|
{user.is_active ? t('common.active') : t('common.inactive')}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-semibold">{user.task_count}</div>
|
<div className="font-semibold">{user.task_count}</div>
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-gray-500">
|
||||||
完成: {user.completed_tasks} | 失敗: {user.failed_tasks}
|
{t('admin.recentUsers.completedCount')}: {user.completed_tasks} | {t('admin.recentUsers.failedCount')}: {user.failed_tasks}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { apiClientV2 } from '@/services/apiV2'
|
import { apiClientV2 } from '@/services/apiV2'
|
||||||
import type { AuditLog } from '@/types/apiV2'
|
import type { AuditLog } from '@/types/apiV2'
|
||||||
import {
|
import {
|
||||||
@@ -33,6 +34,7 @@ import { NativeSelect } from '@/components/ui/select'
|
|||||||
|
|
||||||
export default function AuditLogsPage() {
|
export default function AuditLogsPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { t, i18n } = useTranslation()
|
||||||
const [logs, setLogs] = useState<AuditLog[]>([])
|
const [logs, setLogs] = useState<AuditLog[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
@@ -71,7 +73,7 @@ export default function AuditLogsPage() {
|
|||||||
setHasMore(response.has_more)
|
setHasMore(response.has_more)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to fetch audit logs:', err)
|
console.error('Failed to fetch audit logs:', err)
|
||||||
setError(err.response?.data?.detail || '載入審計日誌失敗')
|
setError(err.response?.data?.detail || t('auditLogs.loadFailed'))
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -89,22 +91,24 @@ export default function AuditLogsPage() {
|
|||||||
// Format date
|
// Format date
|
||||||
const formatDate = (dateStr: string) => {
|
const formatDate = (dateStr: string) => {
|
||||||
const date = new Date(dateStr)
|
const date = new Date(dateStr)
|
||||||
return date.toLocaleString('zh-TW')
|
return date.toLocaleString(i18n.language === 'en' ? 'en-US' : 'zh-TW')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get category badge
|
// Get category badge
|
||||||
const getCategoryBadge = (category: string) => {
|
const getCategoryBadge = (category: string) => {
|
||||||
const variants: Record<string, { variant: any; label: string }> = {
|
const variants: Record<string, { variant: any; labelKey: string }> = {
|
||||||
auth: { variant: 'default', label: '認證' },
|
auth: { variant: 'default', labelKey: 'auditLogs.category.auth' },
|
||||||
task: { variant: 'secondary', label: '任務' },
|
task: { variant: 'secondary', labelKey: 'auditLogs.category.task' },
|
||||||
file: { variant: 'secondary', label: '檔案' },
|
file: { variant: 'secondary', labelKey: 'auditLogs.category.file' },
|
||||||
admin: { variant: 'destructive', label: '管理' },
|
admin: { variant: 'destructive', labelKey: 'auditLogs.category.admin' },
|
||||||
system: { variant: 'secondary', label: '系統' },
|
system: { variant: 'secondary', labelKey: 'auditLogs.category.system' },
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = variants[category] || { variant: 'secondary', label: category }
|
const config = variants[category]
|
||||||
|
if (config) {
|
||||||
return <Badge variant={config.variant}>{config.label}</Badge>
|
return <Badge variant={config.variant}>{t(config.labelKey)}</Badge>
|
||||||
|
}
|
||||||
|
return <Badge variant="secondary">{category}</Badge>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -120,16 +124,16 @@ export default function AuditLogsPage() {
|
|||||||
className="mr-2"
|
className="mr-2"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-4 h-4 mr-1" />
|
<ChevronLeft className="w-4 h-4 mr-1" />
|
||||||
返回
|
{t('common.back')}
|
||||||
</Button>
|
</Button>
|
||||||
<Shield className="w-8 h-8 text-blue-600" />
|
<Shield className="w-8 h-8 text-blue-600" />
|
||||||
<h1 className="text-3xl font-bold text-gray-900">審計日誌</h1>
|
<h1 className="text-3xl font-bold text-gray-900">{t('auditLogs.title')}</h1>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-600 mt-1">系統操作記錄與審計追蹤</p>
|
<p className="text-gray-600 mt-1">{t('auditLogs.subtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => fetchLogs()} variant="outline">
|
<Button onClick={() => fetchLogs()} variant="outline">
|
||||||
<RefreshCw className="w-4 h-4 mr-2" />
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
刷新
|
{t('common.refresh')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -138,13 +142,13 @@ export default function AuditLogsPage() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg flex items-center gap-2">
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
<Filter className="w-5 h-5" />
|
<Filter className="w-5 h-5" />
|
||||||
篩選條件
|
{t('auditLogs.filterConditions')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">類別</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t('auditLogs.categoryFilter')}</label>
|
||||||
<NativeSelect
|
<NativeSelect
|
||||||
value={categoryFilter}
|
value={categoryFilter}
|
||||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
|
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
@@ -152,18 +156,18 @@ export default function AuditLogsPage() {
|
|||||||
handleFilterChange()
|
handleFilterChange()
|
||||||
}}
|
}}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'all', label: '全部' },
|
{ value: 'all', label: t('auditLogs.allCategories') },
|
||||||
{ value: 'auth', label: '認證' },
|
{ value: 'auth', label: t('auditLogs.category.auth') },
|
||||||
{ value: 'task', label: '任務' },
|
{ value: 'task', label: t('auditLogs.category.task') },
|
||||||
{ value: 'file', label: '檔案' },
|
{ value: 'file', label: t('auditLogs.category.file') },
|
||||||
{ value: 'admin', label: '管理' },
|
{ value: 'admin', label: t('auditLogs.category.admin') },
|
||||||
{ value: 'system', label: '系統' },
|
{ value: 'system', label: t('auditLogs.category.system') },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">狀態</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t('auditLogs.statusFilter')}</label>
|
||||||
<NativeSelect
|
<NativeSelect
|
||||||
value={successFilter}
|
value={successFilter}
|
||||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
|
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
@@ -171,9 +175,9 @@ export default function AuditLogsPage() {
|
|||||||
handleFilterChange()
|
handleFilterChange()
|
||||||
}}
|
}}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'all', label: '全部' },
|
{ value: 'all', label: t('auditLogs.allStatuses') },
|
||||||
{ value: 'true', label: '成功' },
|
{ value: 'true', label: t('auditLogs.status.success') },
|
||||||
{ value: 'false', label: '失敗' },
|
{ value: 'false', label: t('auditLogs.status.failed') },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,7 +194,7 @@ export default function AuditLogsPage() {
|
|||||||
handleFilterChange()
|
handleFilterChange()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
清除篩選
|
{t('auditLogs.clearFilter')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -208,9 +212,9 @@ export default function AuditLogsPage() {
|
|||||||
{/* Audit Logs List */}
|
{/* Audit Logs List */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg">審計日誌記錄</CardTitle>
|
<CardTitle className="text-lg">{t('auditLogs.logList')}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
共 {total} 筆記錄 {hasMore && `(顯示第 ${page} 頁)`}
|
{t('auditLogs.logCountInfo', { total, page })}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -221,20 +225,20 @@ export default function AuditLogsPage() {
|
|||||||
) : logs.length === 0 ? (
|
) : logs.length === 0 ? (
|
||||||
<div className="text-center py-12 text-gray-500">
|
<div className="text-center py-12 text-gray-500">
|
||||||
<FileText className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
<FileText className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
||||||
<p>暫無審計日誌</p>
|
<p>{t('auditLogs.noLogs')}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>時間</TableHead>
|
<TableHead>{t('auditLogs.table.time')}</TableHead>
|
||||||
<TableHead>用戶</TableHead>
|
<TableHead>{t('auditLogs.table.user')}</TableHead>
|
||||||
<TableHead>類別</TableHead>
|
<TableHead>{t('auditLogs.table.category')}</TableHead>
|
||||||
<TableHead>操作</TableHead>
|
<TableHead>{t('auditLogs.table.action')}</TableHead>
|
||||||
<TableHead>資源</TableHead>
|
<TableHead>{t('auditLogs.table.resource')}</TableHead>
|
||||||
<TableHead>狀態</TableHead>
|
<TableHead>{t('auditLogs.table.status')}</TableHead>
|
||||||
<TableHead>錯誤訊息</TableHead>
|
<TableHead>{t('auditLogs.table.errorMessage')}</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -269,12 +273,12 @@ export default function AuditLogsPage() {
|
|||||||
{log.success ? (
|
{log.success ? (
|
||||||
<Badge variant="default" className="flex items-center gap-1 w-fit">
|
<Badge variant="default" className="flex items-center gap-1 w-fit">
|
||||||
<CheckCircle2 className="w-3 h-3" />
|
<CheckCircle2 className="w-3 h-3" />
|
||||||
成功
|
{t('auditLogs.status.success')}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge variant="destructive" className="flex items-center gap-1 w-fit">
|
<Badge variant="destructive" className="flex items-center gap-1 w-fit">
|
||||||
<XCircle className="w-3 h-3" />
|
<XCircle className="w-3 h-3" />
|
||||||
失敗
|
{t('auditLogs.status.failed')}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -293,8 +297,11 @@ export default function AuditLogsPage() {
|
|||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
<div className="flex items-center justify-between mt-4">
|
<div className="flex items-center justify-between mt-4">
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
顯示 {(page - 1) * pageSize + 1} - {Math.min(page * pageSize, total)} / 共{' '}
|
{t('auditLogs.pagination.showing', {
|
||||||
{total} 筆
|
start: (page - 1) * pageSize + 1,
|
||||||
|
end: Math.min(page * pageSize, total),
|
||||||
|
total,
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -303,7 +310,7 @@ export default function AuditLogsPage() {
|
|||||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||||
disabled={page === 1}
|
disabled={page === 1}
|
||||||
>
|
>
|
||||||
上一頁
|
{t('auditLogs.pagination.previous')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -311,7 +318,7 @@ export default function AuditLogsPage() {
|
|||||||
onClick={() => setPage((p) => p + 1)}
|
onClick={() => setPage((p) => p + 1)}
|
||||||
disabled={!hasMore}
|
disabled={!hasMore}
|
||||||
>
|
>
|
||||||
下一頁
|
{t('auditLogs.pagination.next')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -124,8 +124,8 @@ function SingleTaskProcessing() {
|
|||||||
updateTaskStatus(taskId, 'processing', forceTrack || undefined)
|
updateTaskStatus(taskId, 'processing', forceTrack || undefined)
|
||||||
}
|
}
|
||||||
toast({
|
toast({
|
||||||
title: '開始處理',
|
title: t('processing.startProcessing'),
|
||||||
description: 'OCR 處理已開始',
|
description: t('processing.ocrStarted'),
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -200,7 +200,7 @@ function SingleTaskProcessing() {
|
|||||||
<div className="flex items-center justify-center min-h-[60vh]">
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Loader2 className="w-12 h-12 animate-spin text-primary mx-auto mb-4" />
|
<Loader2 className="w-12 h-12 animate-spin text-primary mx-auto mb-4" />
|
||||||
<p className="text-muted-foreground">載入任務資訊...</p>
|
<p className="text-muted-foreground">{t('processing.loadingTask')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -226,13 +226,13 @@ function SingleTaskProcessing() {
|
|||||||
</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')}
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => navigate('/upload')}
|
onClick={() => navigate('/upload')}
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
{t('processing.goToUpload', { defaultValue: '前往上傳頁面' })}
|
{t('processing.goToUpload')}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -252,7 +252,7 @@ function SingleTaskProcessing() {
|
|||||||
<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">{taskId}</span>
|
{t('taskDetail.taskId', { id: taskId })}
|
||||||
{taskDetail?.filename && ` · ${taskDetail.filename}`}
|
{taskDetail?.filename && ` · ${taskDetail.filename}`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -260,13 +260,13 @@ function SingleTaskProcessing() {
|
|||||||
{isCompleted && (
|
{isCompleted && (
|
||||||
<div className="flex items-center gap-2 text-success">
|
<div className="flex items-center gap-2 text-success">
|
||||||
<CheckCircle className="w-6 h-6" />
|
<CheckCircle className="w-6 h-6" />
|
||||||
<span className="font-semibold">處理完成</span>
|
<span className="font-semibold">{t('processing.completed')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isProcessing && (
|
{isProcessing && (
|
||||||
<div className="flex items-center gap-2 text-primary">
|
<div className="flex items-center gap-2 text-primary">
|
||||||
<Loader2 className="w-6 h-6 animate-spin" />
|
<Loader2 className="w-6 h-6 animate-spin" />
|
||||||
<span className="font-semibold">處理中</span>
|
<span className="font-semibold">{t('processing.processing')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -311,9 +311,9 @@ function SingleTaskProcessing() {
|
|||||||
<FileText 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">{t('taskDetail.filename')}</p>
|
||||||
<p className="text-sm font-medium text-foreground truncate">
|
<p className="text-sm font-medium text-foreground truncate">
|
||||||
{taskDetail.filename || '未知檔案'}
|
{taskDetail.filename || t('common.unknownFile')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -325,7 +325,7 @@ function SingleTaskProcessing() {
|
|||||||
<Clock className="w-5 h-5 text-success" />
|
<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">{t('taskDetail.processingTime')}</p>
|
||||||
<p className="text-sm font-medium text-foreground">
|
<p className="text-sm font-medium text-foreground">
|
||||||
{(taskDetail.processing_time_ms / 1000).toFixed(2)}s
|
{(taskDetail.processing_time_ms / 1000).toFixed(2)}s
|
||||||
</p>
|
</p>
|
||||||
@@ -342,7 +342,7 @@ function SingleTaskProcessing() {
|
|||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<AlertCircle className="w-5 h-5 text-destructive flex-shrink-0 mt-0.5" />
|
<AlertCircle className="w-5 h-5 text-destructive flex-shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-destructive mb-1">處理失敗</p>
|
<p className="text-sm font-medium text-destructive mb-1">{t('processing.failed')}</p>
|
||||||
<p className="text-sm text-destructive/80">{taskDetail.error_message}</p>
|
<p className="text-sm text-destructive/80">{taskDetail.error_message}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -380,7 +380,7 @@ function SingleTaskProcessing() {
|
|||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
<CheckCircle className="w-4 h-4" />
|
<CheckCircle className="w-4 h-4" />
|
||||||
查看任務歷史
|
{t('results.viewTaskHistory')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -396,24 +396,24 @@ function SingleTaskProcessing() {
|
|||||||
<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>{t('taskDetail.title')}</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex justify-between py-2 border-b border-border">
|
<div className="flex justify-between py-2 border-b border-border">
|
||||||
<span className="text-sm text-muted-foreground">任務狀態</span>
|
<span className="text-sm text-muted-foreground">{t('taskDetail.taskStatus')}</span>
|
||||||
{getStatusBadge(taskDetail.status)}
|
{getStatusBadge(taskDetail.status)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between py-2 border-b border-border">
|
<div className="flex justify-between py-2 border-b border-border">
|
||||||
<span className="text-sm text-muted-foreground">建立時間</span>
|
<span className="text-sm text-muted-foreground">{t('taskDetail.createdAt')}</span>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{new Date(taskDetail.created_at).toLocaleString('zh-TW')}
|
{new Date(taskDetail.created_at).toLocaleString('zh-TW')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{taskDetail.updated_at && (
|
{taskDetail.updated_at && (
|
||||||
<div className="flex justify-between py-2 border-b border-border">
|
<div className="flex justify-between py-2 border-b border-border">
|
||||||
<span className="text-sm text-muted-foreground">更新時間</span>
|
<span className="text-sm text-muted-foreground">{t('taskDetail.lastUpdated')}</span>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{new Date(taskDetail.updated_at).toLocaleString('zh-TW')}
|
{new Date(taskDetail.updated_at).toLocaleString('zh-TW')}
|
||||||
</span>
|
</span>
|
||||||
@@ -421,7 +421,7 @@ function SingleTaskProcessing() {
|
|||||||
)}
|
)}
|
||||||
{taskDetail.completed_at && (
|
{taskDetail.completed_at && (
|
||||||
<div className="flex justify-between py-2 border-b border-border">
|
<div className="flex justify-between py-2 border-b border-border">
|
||||||
<span className="text-sm text-muted-foreground">完成時間</span>
|
<span className="text-sm text-muted-foreground">{t('taskDetail.completedAt')}</span>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{new Date(taskDetail.completed_at).toLocaleString('zh-TW')}
|
{new Date(taskDetail.completed_at).toLocaleString('zh-TW')}
|
||||||
</span>
|
</span>
|
||||||
@@ -439,7 +439,7 @@ function SingleTaskProcessing() {
|
|||||||
{isAnalyzing && (
|
{isAnalyzing && (
|
||||||
<div className="flex items-center gap-2 p-4 bg-muted/30 rounded-lg border">
|
<div className="flex items-center gap-2 p-4 bg-muted/30 rounded-lg border">
|
||||||
<Loader2 className="w-4 h-4 animate-spin text-primary" />
|
<Loader2 className="w-4 h-4 animate-spin text-primary" />
|
||||||
<span className="text-sm text-muted-foreground">分析文件類型中...</span>
|
<span className="text-sm text-muted-foreground">{t('processing.analyzingDocument')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export default function ResultsPage() {
|
|||||||
await apiClientV2.downloadPDF(taskId)
|
await apiClientV2.downloadPDF(taskId)
|
||||||
toast({
|
toast({
|
||||||
title: t('export.exportSuccess'),
|
title: t('export.exportSuccess'),
|
||||||
description: 'PDF 已下載',
|
description: t('results.pdfDownloaded'),
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
})
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -65,7 +65,7 @@ export default function ResultsPage() {
|
|||||||
await apiClientV2.downloadMarkdown(taskId)
|
await apiClientV2.downloadMarkdown(taskId)
|
||||||
toast({
|
toast({
|
||||||
title: t('export.exportSuccess'),
|
title: t('export.exportSuccess'),
|
||||||
description: 'Markdown 已下載',
|
description: t('results.markdownDownloaded'),
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
})
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -83,7 +83,7 @@ export default function ResultsPage() {
|
|||||||
await apiClientV2.downloadJSON(taskId)
|
await apiClientV2.downloadJSON(taskId)
|
||||||
toast({
|
toast({
|
||||||
title: t('export.exportSuccess'),
|
title: t('export.exportSuccess'),
|
||||||
description: 'JSON 已下載',
|
description: t('results.jsonDownloaded'),
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
})
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -98,13 +98,13 @@ export default function ResultsPage() {
|
|||||||
const getStatusBadge = (status: string) => {
|
const getStatusBadge = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'completed':
|
case 'completed':
|
||||||
return <Badge variant="default" className="bg-green-600">已完成</Badge>
|
return <Badge variant="default" className="bg-green-600">{t('taskDetail.status.completed')}</Badge>
|
||||||
case 'processing':
|
case 'processing':
|
||||||
return <Badge variant="default">處理中</Badge>
|
return <Badge variant="default">{t('taskDetail.status.processing')}</Badge>
|
||||||
case 'failed':
|
case 'failed':
|
||||||
return <Badge variant="destructive">失敗</Badge>
|
return <Badge variant="destructive">{t('taskDetail.status.failed')}</Badge>
|
||||||
default:
|
default:
|
||||||
return <Badge variant="secondary">待處理</Badge>
|
return <Badge variant="secondary">{t('taskDetail.status.pending')}</Badge>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ export default function ResultsPage() {
|
|||||||
<div className="flex items-center justify-center min-h-[60vh]">
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Loader2 className="w-12 h-12 animate-spin text-primary mx-auto mb-4" />
|
<Loader2 className="w-12 h-12 animate-spin text-primary mx-auto mb-4" />
|
||||||
<p className="text-muted-foreground">載入任務結果...</p>
|
<p className="text-muted-foreground">{t('results.loadingResults')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -140,10 +140,10 @@ export default function ResultsPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
{t('results.noBatchMessage', { defaultValue: '尚未選擇任何任務。請先上傳並處理檔案。' })}
|
{t('results.noBatchMessage')}
|
||||||
</p>
|
</p>
|
||||||
<Button onClick={() => navigate('/upload')} size="lg">
|
<Button onClick={() => navigate('/upload')} size="lg">
|
||||||
{t('results.goToUpload', { defaultValue: '前往上傳頁面' })}
|
{t('results.goToUpload')}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -157,11 +157,11 @@ export default function ResultsPage() {
|
|||||||
<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">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>任務不存在</CardTitle>
|
<CardTitle>{t('taskDetail.taskNotFound')}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Button onClick={() => navigate('/tasks')}>
|
<Button onClick={() => navigate('/tasks')}>
|
||||||
查看任務歷史
|
{t('results.viewTaskHistory')}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -179,7 +179,7 @@ export default function ResultsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="page-title">{t('results.title')}</h1>
|
<h1 className="page-title">{t('results.title')}</h1>
|
||||||
<p className="text-muted-foreground mt-1">
|
<p className="text-muted-foreground mt-1">
|
||||||
任務 ID: <span className="font-mono text-primary">{taskId}</span>
|
{t('taskDetail.taskId', { id: taskId })}
|
||||||
{taskDetail.filename && ` · ${taskDetail.filename}`}
|
{taskDetail.filename && ` · ${taskDetail.filename}`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -215,7 +215,7 @@ export default function ResultsPage() {
|
|||||||
<Clock className="w-6 h-6 text-primary" />
|
<Clock className="w-6 h-6 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">處理時間</p>
|
<p className="text-sm text-muted-foreground">{t('results.processingTime')}</p>
|
||||||
<p className="text-2xl font-bold">
|
<p className="text-2xl font-bold">
|
||||||
{taskDetail.processing_time_ms ? (taskDetail.processing_time_ms / 1000).toFixed(2) : '0'}s
|
{taskDetail.processing_time_ms ? (taskDetail.processing_time_ms / 1000).toFixed(2) : '0'}s
|
||||||
</p>
|
</p>
|
||||||
@@ -231,8 +231,8 @@ export default function ResultsPage() {
|
|||||||
<TrendingUp className="w-6 h-6 text-success" />
|
<TrendingUp className="w-6 h-6 text-success" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">處理狀態</p>
|
<p className="text-sm text-muted-foreground">{t('results.processingStatus')}</p>
|
||||||
<p className="text-2xl font-bold text-success">成功</p>
|
<p className="text-2xl font-bold text-success">{t('common.success')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -245,7 +245,7 @@ export default function ResultsPage() {
|
|||||||
<Layers className="w-6 h-6 text-accent" />
|
<Layers className="w-6 h-6 text-accent" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">任務類型</p>
|
<p className="text-sm text-muted-foreground">{t('results.taskType')}</p>
|
||||||
<p className="text-2xl font-bold">OCR</p>
|
<p className="text-2xl font-bold">OCR</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -257,7 +257,7 @@ export default function ResultsPage() {
|
|||||||
{/* Results Preview */}
|
{/* Results Preview */}
|
||||||
{isCompleted ? (
|
{isCompleted ? (
|
||||||
<PDFViewer
|
<PDFViewer
|
||||||
title={`OCR 結果預覽 - ${taskDetail.filename || '未知檔案'}`}
|
title={t('taskDetail.ocrPreview', { filename: taskDetail.filename || t('common.unknownFile') })}
|
||||||
pdfUrl={pdfUrl}
|
pdfUrl={pdfUrl}
|
||||||
httpHeaders={pdfHttpHeaders}
|
httpHeaders={pdfHttpHeaders}
|
||||||
/>
|
/>
|
||||||
@@ -265,15 +265,15 @@ export default function ResultsPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-12 text-center">
|
<CardContent className="p-12 text-center">
|
||||||
<Loader2 className="w-16 h-16 animate-spin text-primary mx-auto mb-4" />
|
<Loader2 className="w-16 h-16 animate-spin text-primary mx-auto mb-4" />
|
||||||
<p className="text-lg font-semibold">正在處理中...</p>
|
<p className="text-lg font-semibold">{t('results.processingInProgress')}</p>
|
||||||
<p className="text-muted-foreground mt-2">請稍候,OCR 處理需要一些時間</p>
|
<p className="text-muted-foreground mt-2">{t('results.processingInProgressDesc')}</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : taskDetail.status === 'failed' ? (
|
) : taskDetail.status === 'failed' ? (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-12 text-center">
|
<CardContent className="p-12 text-center">
|
||||||
<AlertCircle className="w-16 h-16 text-destructive mx-auto mb-4" />
|
<AlertCircle className="w-16 h-16 text-destructive mx-auto mb-4" />
|
||||||
<p className="text-lg font-semibold text-destructive">處理失敗</p>
|
<p className="text-lg font-semibold text-destructive">{t('processing.failed')}</p>
|
||||||
{taskDetail.error_message && (
|
{taskDetail.error_message && (
|
||||||
<p className="text-muted-foreground mt-2">{taskDetail.error_message}</p>
|
<p className="text-muted-foreground mt-2">{taskDetail.error_message}</p>
|
||||||
)}
|
)}
|
||||||
@@ -283,10 +283,10 @@ export default function ResultsPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-12 text-center">
|
<CardContent className="p-12 text-center">
|
||||||
<Clock className="w-16 h-16 text-muted-foreground mx-auto mb-4" />
|
<Clock className="w-16 h-16 text-muted-foreground mx-auto mb-4" />
|
||||||
<p className="text-lg font-semibold">等待處理</p>
|
<p className="text-lg font-semibold">{t('results.waitingToProcess')}</p>
|
||||||
<p className="text-muted-foreground mt-2">請前往處理頁面啟動 OCR 處理</p>
|
<p className="text-muted-foreground mt-2">{t('results.waitingToProcessDesc')}</p>
|
||||||
<Button onClick={() => navigate('/processing')} className="mt-4">
|
<Button onClick={() => navigate('/processing')} className="mt-4">
|
||||||
前往處理頁面
|
{t('results.goToProcessing')}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const LANGUAGE_OPTIONS = [
|
|||||||
|
|
||||||
export default function TaskDetailPage() {
|
export default function TaskDetailPage() {
|
||||||
const { taskId } = useParams<{ taskId: string }>()
|
const { taskId } = useParams<{ taskId: string }>()
|
||||||
const { t } = useTranslation()
|
const { t, i18n } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
|
|
||||||
@@ -124,16 +124,16 @@ export default function TaskDetailPage() {
|
|||||||
setIsTranslating(false)
|
setIsTranslating(false)
|
||||||
setTranslationProgress(100)
|
setTranslationProgress(100)
|
||||||
toast({
|
toast({
|
||||||
title: '翻譯完成',
|
title: t('translation.translationComplete'),
|
||||||
description: `文件已翻譯為 ${LANGUAGE_OPTIONS.find(l => l.value === targetLang)?.label || targetLang}`,
|
description: `${LANGUAGE_OPTIONS.find(l => l.value === targetLang)?.label || targetLang}`,
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
})
|
})
|
||||||
refetchTranslations()
|
refetchTranslations()
|
||||||
} else if (status.status === 'failed') {
|
} else if (status.status === 'failed') {
|
||||||
setIsTranslating(false)
|
setIsTranslating(false)
|
||||||
toast({
|
toast({
|
||||||
title: '翻譯失敗',
|
title: t('translation.translationFailed'),
|
||||||
description: status.error_message || '未知錯誤',
|
description: status.error_message || t('common.unknownError'),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -143,7 +143,7 @@ export default function TaskDetailPage() {
|
|||||||
}, 2000)
|
}, 2000)
|
||||||
|
|
||||||
return () => clearInterval(pollInterval)
|
return () => clearInterval(pollInterval)
|
||||||
}, [isTranslating, taskId, targetLang, toast, refetchTranslations])
|
}, [isTranslating, taskId, targetLang, toast, refetchTranslations, t])
|
||||||
|
|
||||||
// Construct PDF URL for preview - memoize to prevent unnecessary reloads
|
// Construct PDF URL for preview - memoize to prevent unnecessary reloads
|
||||||
// Must be called unconditionally before any early returns (React hooks rule)
|
// Must be called unconditionally before any early returns (React hooks rule)
|
||||||
@@ -162,24 +162,24 @@ export default function TaskDetailPage() {
|
|||||||
if (!track) return null
|
if (!track) return null
|
||||||
switch (track) {
|
switch (track) {
|
||||||
case 'direct':
|
case 'direct':
|
||||||
return <Badge variant="default" className="bg-blue-600">直接提取</Badge>
|
return <Badge variant="default" className="bg-blue-600">{t('taskDetail.track.direct')}</Badge>
|
||||||
case 'ocr':
|
case 'ocr':
|
||||||
return <Badge variant="default" className="bg-purple-600">OCR</Badge>
|
return <Badge variant="default" className="bg-purple-600">OCR</Badge>
|
||||||
case 'hybrid':
|
case 'hybrid':
|
||||||
return <Badge variant="default" className="bg-orange-600">混合</Badge>
|
return <Badge variant="default" className="bg-orange-600">{t('taskDetail.track.hybrid')}</Badge>
|
||||||
default:
|
default:
|
||||||
return <Badge variant="secondary">自動</Badge>
|
return <Badge variant="secondary">{t('taskDetail.track.auto')}</Badge>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTrackDescription = (track?: ProcessingTrack) => {
|
const getTrackDescription = (track?: ProcessingTrack) => {
|
||||||
switch (track) {
|
switch (track) {
|
||||||
case 'direct':
|
case 'direct':
|
||||||
return 'PyMuPDF 直接提取'
|
return t('taskDetail.track.directDesc')
|
||||||
case 'ocr':
|
case 'ocr':
|
||||||
return 'PP-StructureV3 OCR'
|
return 'PP-StructureV3 OCR'
|
||||||
case 'hybrid':
|
case 'hybrid':
|
||||||
return '混合處理'
|
return t('taskDetail.track.hybridDesc')
|
||||||
default:
|
default:
|
||||||
return 'OCR'
|
return 'OCR'
|
||||||
}
|
}
|
||||||
@@ -191,7 +191,7 @@ export default function TaskDetailPage() {
|
|||||||
await apiClientV2.downloadPDF(taskId, 'layout')
|
await apiClientV2.downloadPDF(taskId, 'layout')
|
||||||
toast({
|
toast({
|
||||||
title: t('export.exportSuccess'),
|
title: t('export.exportSuccess'),
|
||||||
description: '版面 PDF 已下載',
|
description: t('taskDetail.layoutPdf'),
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
})
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -209,7 +209,7 @@ export default function TaskDetailPage() {
|
|||||||
await apiClientV2.downloadPDF(taskId, 'reflow')
|
await apiClientV2.downloadPDF(taskId, 'reflow')
|
||||||
toast({
|
toast({
|
||||||
title: t('export.exportSuccess'),
|
title: t('export.exportSuccess'),
|
||||||
description: '流式 PDF 已下載',
|
description: t('taskDetail.reflowPdf'),
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
})
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -239,7 +239,7 @@ export default function TaskDetailPage() {
|
|||||||
setIsTranslating(false)
|
setIsTranslating(false)
|
||||||
setTranslationProgress(100)
|
setTranslationProgress(100)
|
||||||
toast({
|
toast({
|
||||||
title: '翻譯已存在',
|
title: t('translation.translationExists'),
|
||||||
description: response.message,
|
description: response.message,
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
})
|
})
|
||||||
@@ -247,15 +247,15 @@ export default function TaskDetailPage() {
|
|||||||
} else {
|
} else {
|
||||||
setTranslationStatus(response.status)
|
setTranslationStatus(response.status)
|
||||||
toast({
|
toast({
|
||||||
title: '開始翻譯',
|
title: t('translation.translationStarted'),
|
||||||
description: '翻譯任務已啟動,請稍候...',
|
description: t('translation.translationStartedDesc'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setIsTranslating(false)
|
setIsTranslating(false)
|
||||||
setTranslationStatus(null)
|
setTranslationStatus(null)
|
||||||
toast({
|
toast({
|
||||||
title: '啟動翻譯失敗',
|
title: t('errors.startFailed'),
|
||||||
description: error.response?.data?.detail || t('errors.networkError'),
|
description: error.response?.data?.detail || t('errors.networkError'),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
})
|
})
|
||||||
@@ -267,14 +267,14 @@ export default function TaskDetailPage() {
|
|||||||
try {
|
try {
|
||||||
await apiClientV2.deleteTranslation(taskId, lang)
|
await apiClientV2.deleteTranslation(taskId, lang)
|
||||||
toast({
|
toast({
|
||||||
title: '刪除成功',
|
title: t('translation.deleteSuccess'),
|
||||||
description: `翻譯結果 (${lang}) 已刪除`,
|
description: t('translation.translationDeleted', { lang }),
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
})
|
})
|
||||||
refetchTranslations()
|
refetchTranslations()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast({
|
toast({
|
||||||
title: '刪除失敗',
|
title: t('errors.deleteFailed'),
|
||||||
description: error.response?.data?.detail || t('errors.networkError'),
|
description: error.response?.data?.detail || t('errors.networkError'),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
})
|
})
|
||||||
@@ -285,15 +285,15 @@ export default function TaskDetailPage() {
|
|||||||
if (!taskId) return
|
if (!taskId) return
|
||||||
try {
|
try {
|
||||||
await apiClientV2.downloadTranslatedPdf(taskId, lang, format)
|
await apiClientV2.downloadTranslatedPdf(taskId, lang, format)
|
||||||
const formatLabel = format === 'layout' ? '版面' : '流式'
|
const formatLabel = format === 'layout' ? t('taskDetail.layoutPdf') : t('taskDetail.reflowPdf')
|
||||||
toast({
|
toast({
|
||||||
title: '下載成功',
|
title: t('common.downloadSuccess'),
|
||||||
description: `翻譯 ${formatLabel} PDF (${lang}) 已下載`,
|
description: `${formatLabel} (${lang})`,
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
})
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast({
|
toast({
|
||||||
title: '下載失敗',
|
title: t('common.downloadFailed'),
|
||||||
description: error.response?.data?.detail || t('errors.networkError'),
|
description: error.response?.data?.detail || t('errors.networkError'),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
})
|
})
|
||||||
@@ -305,13 +305,13 @@ export default function TaskDetailPage() {
|
|||||||
try {
|
try {
|
||||||
await apiClientV2.downloadVisualization(taskId)
|
await apiClientV2.downloadVisualization(taskId)
|
||||||
toast({
|
toast({
|
||||||
title: '下載成功',
|
title: t('common.downloadSuccess'),
|
||||||
description: '辨識結果圖片已下載',
|
description: t('taskDetail.visualizationDownloaded'),
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
})
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast({
|
toast({
|
||||||
title: '下載失敗',
|
title: t('common.downloadFailed'),
|
||||||
description: error.response?.data?.detail || t('errors.networkError'),
|
description: error.response?.data?.detail || t('errors.networkError'),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
})
|
})
|
||||||
@@ -321,28 +321,28 @@ export default function TaskDetailPage() {
|
|||||||
const getStatusBadge = (status: string) => {
|
const getStatusBadge = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'completed':
|
case 'completed':
|
||||||
return <Badge variant="default" className="bg-green-600">已完成</Badge>
|
return <Badge variant="default" className="bg-green-600">{t('taskDetail.status.completed')}</Badge>
|
||||||
case 'processing':
|
case 'processing':
|
||||||
return <Badge variant="default">處理中</Badge>
|
return <Badge variant="default">{t('taskDetail.status.processing')}</Badge>
|
||||||
case 'failed':
|
case 'failed':
|
||||||
return <Badge variant="destructive">失敗</Badge>
|
return <Badge variant="destructive">{t('taskDetail.status.failed')}</Badge>
|
||||||
default:
|
default:
|
||||||
return <Badge variant="secondary">待處理</Badge>
|
return <Badge variant="secondary">{t('taskDetail.status.pending')}</Badge>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTranslationStatusText = (status: TranslationStatus | null) => {
|
const getTranslationStatusText = (status: TranslationStatus | null) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'pending':
|
case 'pending':
|
||||||
return '準備中...'
|
return t('translation.status.preparing')
|
||||||
case 'loading_model':
|
case 'loading_model':
|
||||||
return '載入翻譯模型...'
|
return t('translation.status.loadingModel')
|
||||||
case 'translating':
|
case 'translating':
|
||||||
return '翻譯中...'
|
return t('translation.status.translating')
|
||||||
case 'completed':
|
case 'completed':
|
||||||
return '完成'
|
return t('translation.status.complete')
|
||||||
case 'failed':
|
case 'failed':
|
||||||
return '失敗'
|
return t('taskDetail.status.failed')
|
||||||
default:
|
default:
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -350,7 +350,7 @@ export default function TaskDetailPage() {
|
|||||||
|
|
||||||
const formatDate = (dateStr: string) => {
|
const formatDate = (dateStr: string) => {
|
||||||
const date = new Date(dateStr)
|
const date = new Date(dateStr)
|
||||||
return date.toLocaleString('zh-TW')
|
return date.toLocaleString(i18n.language === 'en' ? 'en-US' : 'zh-TW')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -358,7 +358,7 @@ export default function TaskDetailPage() {
|
|||||||
<div className="flex items-center justify-center min-h-[60vh]">
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Loader2 className="w-12 h-12 animate-spin text-primary mx-auto mb-4" />
|
<Loader2 className="w-12 h-12 animate-spin text-primary mx-auto mb-4" />
|
||||||
<p className="text-muted-foreground">載入任務詳情...</p>
|
<p className="text-muted-foreground">{t('taskDetail.loadingTask')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -372,12 +372,12 @@ export default function TaskDetailPage() {
|
|||||||
<div className="flex justify-center mb-4">
|
<div className="flex justify-center mb-4">
|
||||||
<AlertCircle className="w-16 h-16 text-destructive" />
|
<AlertCircle className="w-16 h-16 text-destructive" />
|
||||||
</div>
|
</div>
|
||||||
<CardTitle>任務不存在</CardTitle>
|
<CardTitle>{t('taskDetail.taskNotFound')}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<p className="text-muted-foreground">找不到任務 ID: {taskId}</p>
|
<p className="text-muted-foreground">{t('taskDetail.taskNotFoundDesc', { id: taskId })}</p>
|
||||||
<Button onClick={() => navigate('/tasks')}>
|
<Button onClick={() => navigate('/tasks')}>
|
||||||
返回任務歷史
|
{t('taskDetail.returnToHistory')}
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -397,19 +397,19 @@ export default function TaskDetailPage() {
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Button variant="outline" onClick={() => navigate('/tasks')} className="gap-2">
|
<Button variant="outline" onClick={() => navigate('/tasks')} className="gap-2">
|
||||||
<ArrowLeft className="w-4 h-4" />
|
<ArrowLeft className="w-4 h-4" />
|
||||||
返回
|
{t('common.back')}
|
||||||
</Button>
|
</Button>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="page-title">任務詳情</h1>
|
<h1 className="page-title">{t('taskDetail.title')}</h1>
|
||||||
<p className="text-muted-foreground mt-1">
|
<p className="text-muted-foreground mt-1">
|
||||||
任務 ID: <span className="font-mono text-primary">{taskId}</span>
|
{t('taskDetail.taskId', { id: taskId })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3 items-center">
|
<div className="flex gap-3 items-center">
|
||||||
<Button onClick={() => refetch()} variant="outline" size="sm" className="gap-2">
|
<Button onClick={() => refetch()} variant="outline" size="sm" className="gap-2">
|
||||||
<RefreshCw className="w-4 h-4" />
|
<RefreshCw className="w-4 h-4" />
|
||||||
刷新
|
{t('common.refresh')}
|
||||||
</Button>
|
</Button>
|
||||||
{getStatusBadge(taskDetail.status)}
|
{getStatusBadge(taskDetail.status)}
|
||||||
</div>
|
</div>
|
||||||
@@ -421,35 +421,35 @@ export default function TaskDetailPage() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<FileText className="w-5 h-5" />
|
<FileText className="w-5 h-5" />
|
||||||
任務資訊
|
{t('taskDetail.taskInfo')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground mb-1">檔案名稱</p>
|
<p className="text-sm text-muted-foreground mb-1">{t('taskDetail.filename')}</p>
|
||||||
<p className="font-medium">{taskDetail.filename || '未知檔案'}</p>
|
<p className="font-medium">{taskDetail.filename || t('common.unknownFile')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground mb-1">建立時間</p>
|
<p className="text-sm text-muted-foreground mb-1">{t('taskDetail.createdAt')}</p>
|
||||||
<p className="font-medium">{formatDate(taskDetail.created_at)}</p>
|
<p className="font-medium">{formatDate(taskDetail.created_at)}</p>
|
||||||
</div>
|
</div>
|
||||||
{taskDetail.completed_at && (
|
{taskDetail.completed_at && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground mb-1">完成時間</p>
|
<p className="text-sm text-muted-foreground mb-1">{t('taskDetail.completedAt')}</p>
|
||||||
<p className="font-medium">{formatDate(taskDetail.completed_at)}</p>
|
<p className="font-medium">{formatDate(taskDetail.completed_at)}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground mb-1">任務狀態</p>
|
<p className="text-sm text-muted-foreground mb-1">{t('taskDetail.taskStatus')}</p>
|
||||||
{getStatusBadge(taskDetail.status)}
|
{getStatusBadge(taskDetail.status)}
|
||||||
</div>
|
</div>
|
||||||
{(taskDetail.processing_track || processingMetadata?.processing_track) && (
|
{(taskDetail.processing_track || processingMetadata?.processing_track) && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground mb-1">處理軌道</p>
|
<p className="text-sm text-muted-foreground mb-1">{t('taskDetail.processingTrack')}</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{getTrackBadge(taskDetail.processing_track || processingMetadata?.processing_track)}
|
{getTrackBadge(taskDetail.processing_track || processingMetadata?.processing_track)}
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
@@ -460,13 +460,13 @@ export default function TaskDetailPage() {
|
|||||||
)}
|
)}
|
||||||
{taskDetail.processing_time_ms && (
|
{taskDetail.processing_time_ms && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground mb-1">處理時間</p>
|
<p className="text-sm text-muted-foreground mb-1">{t('taskDetail.processingTime')}</p>
|
||||||
<p className="font-medium">{(taskDetail.processing_time_ms / 1000).toFixed(2)} 秒</p>
|
<p className="font-medium">{(taskDetail.processing_time_ms / 1000).toFixed(2)} {t('common.seconds')}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{taskDetail.updated_at && (
|
{taskDetail.updated_at && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground mb-1">最後更新</p>
|
<p className="text-sm text-muted-foreground mb-1">{t('taskDetail.lastUpdated')}</p>
|
||||||
<p className="font-medium">{formatDate(taskDetail.updated_at)}</p>
|
<p className="font-medium">{formatDate(taskDetail.updated_at)}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -481,18 +481,18 @@ export default function TaskDetailPage() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Download className="w-5 h-5" />
|
<Download className="w-5 h-5" />
|
||||||
下載結果
|
{t('taskDetail.downloadResults')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<Button onClick={handleDownloadLayoutPDF} className="gap-2 h-20 flex-col">
|
<Button onClick={handleDownloadLayoutPDF} className="gap-2 h-20 flex-col">
|
||||||
<Download className="w-8 h-8" />
|
<Download className="w-8 h-8" />
|
||||||
<span>版面 PDF</span>
|
<span>{t('taskDetail.layoutPdf')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleDownloadReflowPDF} variant="outline" className="gap-2 h-20 flex-col">
|
<Button onClick={handleDownloadReflowPDF} variant="outline" className="gap-2 h-20 flex-col">
|
||||||
<Download className="w-8 h-8" />
|
<Download className="w-8 h-8" />
|
||||||
<span>流式 PDF</span>
|
<span>{t('taskDetail.reflowPdf')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/* Visualization download for OCR Track */}
|
{/* Visualization download for OCR Track */}
|
||||||
@@ -504,7 +504,7 @@ export default function TaskDetailPage() {
|
|||||||
className="w-full gap-2"
|
className="w-full gap-2"
|
||||||
>
|
>
|
||||||
<Image className="w-4 h-4" />
|
<Image className="w-4 h-4" />
|
||||||
下載辨識結果圖片 (ZIP)
|
{t('taskDetail.downloadVisualization')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -518,7 +518,7 @@ export default function TaskDetailPage() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Languages className="w-5 h-5" />
|
<Languages className="w-5 h-5" />
|
||||||
文件翻譯
|
{t('translation.title')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
@@ -526,14 +526,14 @@ export default function TaskDetailPage() {
|
|||||||
<div className="flex flex-col md:flex-row items-start md:items-center gap-4">
|
<div className="flex flex-col md:flex-row items-start md:items-center gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Globe className="w-4 h-4 text-muted-foreground" />
|
<Globe className="w-4 h-4 text-muted-foreground" />
|
||||||
<span className="text-sm text-muted-foreground">目標語言:</span>
|
<span className="text-sm text-muted-foreground">{t('translation.targetLanguage')}</span>
|
||||||
<Select
|
<Select
|
||||||
value={targetLang}
|
value={targetLang}
|
||||||
onValueChange={setTargetLang}
|
onValueChange={setTargetLang}
|
||||||
disabled={isTranslating}
|
disabled={isTranslating}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-40">
|
<SelectTrigger className="w-40">
|
||||||
<SelectValue placeholder="選擇語言" />
|
<SelectValue placeholder={t('translation.selectLanguage')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{LANGUAGE_OPTIONS.map(lang => (
|
{LANGUAGE_OPTIONS.map(lang => (
|
||||||
@@ -554,7 +554,7 @@ export default function TaskDetailPage() {
|
|||||||
) : (
|
) : (
|
||||||
<Languages className="w-4 h-4" />
|
<Languages className="w-4 h-4" />
|
||||||
)}
|
)}
|
||||||
{isTranslating ? getTranslationStatusText(translationStatus) : '開始翻譯'}
|
{isTranslating ? getTranslationStatusText(translationStatus) : t('translation.startTranslation')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -572,7 +572,7 @@ export default function TaskDetailPage() {
|
|||||||
{/* Existing Translations */}
|
{/* Existing Translations */}
|
||||||
{translationList && translationList.translations.length > 0 && (
|
{translationList && translationList.translations.length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-medium text-muted-foreground">已完成的翻譯:</p>
|
<p className="text-sm font-medium text-muted-foreground">{t('translation.completedTranslations')}</p>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{translationList.translations.map((item: TranslationListItem) => (
|
{translationList.translations.map((item: TranslationListItem) => (
|
||||||
<div
|
<div
|
||||||
@@ -586,7 +586,7 @@ export default function TaskDetailPage() {
|
|||||||
{LANGUAGE_OPTIONS.find(l => l.value === item.target_lang)?.label || item.target_lang}
|
{LANGUAGE_OPTIONS.find(l => l.value === item.target_lang)?.label || item.target_lang}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-muted-foreground ml-2">
|
<span className="text-sm text-muted-foreground ml-2">
|
||||||
({item.statistics.translated_elements} 元素, {item.statistics.processing_time_seconds.toFixed(1)}s)
|
({item.statistics.translated_elements} elements, {item.statistics.processing_time_seconds.toFixed(1)}s)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -598,7 +598,7 @@ export default function TaskDetailPage() {
|
|||||||
className="gap-1"
|
className="gap-1"
|
||||||
>
|
>
|
||||||
<Download className="w-3 h-3" />
|
<Download className="w-3 h-3" />
|
||||||
流式 PDF
|
{t('taskDetail.reflowPdf')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -616,7 +616,7 @@ export default function TaskDetailPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
使用雲端翻譯服務進行多語言翻譯,支援多種目標語言。
|
{t('translation.description')}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -628,7 +628,7 @@ export default function TaskDetailPage() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-destructive">
|
<CardTitle className="flex items-center gap-2 text-destructive">
|
||||||
<AlertCircle className="w-5 h-5" />
|
<AlertCircle className="w-5 h-5" />
|
||||||
錯誤訊息
|
{t('taskDetail.errorMessage')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -642,8 +642,8 @@ export default function TaskDetailPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-12 text-center">
|
<CardContent className="p-12 text-center">
|
||||||
<Loader2 className="w-16 h-16 animate-spin text-primary mx-auto mb-4" />
|
<Loader2 className="w-16 h-16 animate-spin text-primary mx-auto mb-4" />
|
||||||
<p className="text-lg font-semibold">正在處理中...</p>
|
<p className="text-lg font-semibold">{t('taskDetail.processingInProgress')}</p>
|
||||||
<p className="text-muted-foreground mt-2">請稍候,OCR 處理需要一些時間</p>
|
<p className="text-muted-foreground mt-2">{t('taskDetail.processingInProgressDesc')}</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
@@ -658,7 +658,7 @@ export default function TaskDetailPage() {
|
|||||||
<Clock className="w-5 h-5 text-primary" />
|
<Clock className="w-5 h-5 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-muted-foreground">處理時間</p>
|
<p className="text-xs text-muted-foreground">{t('taskDetail.processingTime')}</p>
|
||||||
<p className="text-lg font-bold">
|
<p className="text-lg font-bold">
|
||||||
{processingMetadata?.processing_time_seconds?.toFixed(2) ||
|
{processingMetadata?.processing_time_seconds?.toFixed(2) ||
|
||||||
(taskDetail.processing_time_ms ? (taskDetail.processing_time_ms / 1000).toFixed(2) : '0')}s
|
(taskDetail.processing_time_ms ? (taskDetail.processing_time_ms / 1000).toFixed(2) : '0')}s
|
||||||
@@ -675,7 +675,7 @@ export default function TaskDetailPage() {
|
|||||||
<Layers className="w-5 h-5 text-blue-500" />
|
<Layers className="w-5 h-5 text-blue-500" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-muted-foreground">頁數</p>
|
<p className="text-xs text-muted-foreground">{t('taskDetail.stats.pageCount')}</p>
|
||||||
<p className="text-lg font-bold">
|
<p className="text-lg font-bold">
|
||||||
{processingMetadata?.page_count || '-'}
|
{processingMetadata?.page_count || '-'}
|
||||||
</p>
|
</p>
|
||||||
@@ -691,7 +691,7 @@ export default function TaskDetailPage() {
|
|||||||
<FileSearch className="w-5 h-5 text-purple-500" />
|
<FileSearch className="w-5 h-5 text-purple-500" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-muted-foreground">文本區域</p>
|
<p className="text-xs text-muted-foreground">{t('taskDetail.stats.textRegions')}</p>
|
||||||
<p className="text-lg font-bold">
|
<p className="text-lg font-bold">
|
||||||
{processingMetadata?.total_text_regions || '-'}
|
{processingMetadata?.total_text_regions || '-'}
|
||||||
</p>
|
</p>
|
||||||
@@ -707,7 +707,7 @@ export default function TaskDetailPage() {
|
|||||||
<Table2 className="w-5 h-5 text-green-500" />
|
<Table2 className="w-5 h-5 text-green-500" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-muted-foreground">表格</p>
|
<p className="text-xs text-muted-foreground">{t('taskDetail.stats.tables')}</p>
|
||||||
<p className="text-lg font-bold">
|
<p className="text-lg font-bold">
|
||||||
{processingMetadata?.total_tables || '-'}
|
{processingMetadata?.total_tables || '-'}
|
||||||
</p>
|
</p>
|
||||||
@@ -723,7 +723,7 @@ export default function TaskDetailPage() {
|
|||||||
<Image className="w-5 h-5 text-orange-500" />
|
<Image className="w-5 h-5 text-orange-500" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-muted-foreground">圖片</p>
|
<p className="text-xs text-muted-foreground">{t('taskDetail.stats.images')}</p>
|
||||||
<p className="text-lg font-bold">
|
<p className="text-lg font-bold">
|
||||||
{processingMetadata?.total_images || '-'}
|
{processingMetadata?.total_images || '-'}
|
||||||
</p>
|
</p>
|
||||||
@@ -739,7 +739,7 @@ export default function TaskDetailPage() {
|
|||||||
<BarChart3 className="w-5 h-5 text-cyan-500" />
|
<BarChart3 className="w-5 h-5 text-cyan-500" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-muted-foreground">平均置信度</p>
|
<p className="text-xs text-muted-foreground">{t('taskDetail.stats.avgConfidence')}</p>
|
||||||
<p className="text-lg font-bold">
|
<p className="text-lg font-bold">
|
||||||
{processingMetadata?.average_confidence
|
{processingMetadata?.average_confidence
|
||||||
? `${(processingMetadata.average_confidence * 100).toFixed(0)}%`
|
? `${(processingMetadata.average_confidence * 100).toFixed(0)}%`
|
||||||
@@ -755,7 +755,7 @@ export default function TaskDetailPage() {
|
|||||||
{/* Result Preview */}
|
{/* Result Preview */}
|
||||||
{isCompleted && (
|
{isCompleted && (
|
||||||
<PDFViewer
|
<PDFViewer
|
||||||
title={`OCR 結果預覽 - ${taskDetail.filename || '未知檔案'}`}
|
title={t('taskDetail.ocrPreview', { filename: taskDetail.filename || t('common.unknownFile') })}
|
||||||
pdfUrl={pdfUrl}
|
pdfUrl={pdfUrl}
|
||||||
httpHeaders={pdfHttpHeaders}
|
httpHeaders={pdfHttpHeaders}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { apiClientV2 } from '@/services/apiV2'
|
import { apiClientV2 } from '@/services/apiV2'
|
||||||
import type { Task, TaskStats, TaskStatus } from '@/types/apiV2'
|
import type { Task, TaskStats, TaskStatus } from '@/types/apiV2'
|
||||||
import {
|
import {
|
||||||
@@ -33,6 +34,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
|||||||
|
|
||||||
export default function TaskHistoryPage() {
|
export default function TaskHistoryPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { t, i18n } = useTranslation()
|
||||||
const [tasks, setTasks] = useState<Task[]>([])
|
const [tasks, setTasks] = useState<Task[]>([])
|
||||||
const [stats, setStats] = useState<TaskStats | null>(null)
|
const [stats, setStats] = useState<TaskStats | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
@@ -69,7 +71,7 @@ export default function TaskHistoryPage() {
|
|||||||
setTotal(response.total)
|
setTotal(response.total)
|
||||||
setHasMore(response.has_more)
|
setHasMore(response.has_more)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.response?.data?.detail || '載入任務失敗')
|
setError(err.response?.data?.detail || t('taskHistory.loadFailed'))
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -101,25 +103,25 @@ export default function TaskHistoryPage() {
|
|||||||
|
|
||||||
// Delete task
|
// Delete task
|
||||||
const handleDelete = async (taskId: string) => {
|
const handleDelete = async (taskId: string) => {
|
||||||
if (!confirm('確定要刪除此任務嗎?')) return
|
if (!confirm(t('taskHistory.deleteConfirm'))) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiClientV2.deleteTask(taskId)
|
await apiClientV2.deleteTask(taskId)
|
||||||
fetchTasks()
|
fetchTasks()
|
||||||
fetchStats()
|
fetchStats()
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert(err.response?.data?.detail || '刪除任務失敗')
|
alert(err.response?.data?.detail || t('taskHistory.deleteFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete all tasks
|
// Delete all tasks
|
||||||
const handleDeleteAll = async () => {
|
const handleDeleteAll = async () => {
|
||||||
if (tasks.length === 0) {
|
if (tasks.length === 0) {
|
||||||
alert('沒有可刪除的任務')
|
alert(t('taskHistory.noTasksToDelete'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!confirm(`確定要刪除所有 ${total} 個任務嗎?此操作無法復原!`)) return
|
if (!confirm(t('taskHistory.deleteAllConfirm', { count: total }))) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -139,9 +141,9 @@ export default function TaskHistoryPage() {
|
|||||||
}
|
}
|
||||||
fetchTasks()
|
fetchTasks()
|
||||||
fetchStats()
|
fetchStats()
|
||||||
alert('所有任務已刪除')
|
alert(t('taskHistory.allTasksDeleted'))
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert(err.response?.data?.detail || '刪除任務失敗')
|
alert(err.response?.data?.detail || t('taskHistory.deleteFailed'))
|
||||||
fetchTasks()
|
fetchTasks()
|
||||||
fetchStats()
|
fetchStats()
|
||||||
} finally {
|
} finally {
|
||||||
@@ -159,7 +161,7 @@ export default function TaskHistoryPage() {
|
|||||||
try {
|
try {
|
||||||
await apiClientV2.downloadPDF(taskId, format)
|
await apiClientV2.downloadPDF(taskId, format)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert(err.response?.data?.detail || `下載 PDF 檔案失敗`)
|
alert(err.response?.data?.detail || t('taskHistory.downloadPdfFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,18 +171,18 @@ export default function TaskHistoryPage() {
|
|||||||
await apiClientV2.startTask(taskId)
|
await apiClientV2.startTask(taskId)
|
||||||
fetchTasks()
|
fetchTasks()
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert(err.response?.data?.detail || '啟動任務失敗')
|
alert(err.response?.data?.detail || t('taskHistory.startTaskFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCancelTask = async (taskId: string) => {
|
const handleCancelTask = async (taskId: string) => {
|
||||||
if (!confirm('確定要取消此任務嗎?')) return
|
if (!confirm(t('taskHistory.cancelConfirm'))) return
|
||||||
try {
|
try {
|
||||||
await apiClientV2.cancelTask(taskId)
|
await apiClientV2.cancelTask(taskId)
|
||||||
fetchTasks()
|
fetchTasks()
|
||||||
fetchStats()
|
fetchStats()
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert(err.response?.data?.detail || '取消任務失敗')
|
alert(err.response?.data?.detail || t('taskHistory.cancelFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,14 +192,14 @@ export default function TaskHistoryPage() {
|
|||||||
fetchTasks()
|
fetchTasks()
|
||||||
fetchStats()
|
fetchStats()
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert(err.response?.data?.detail || '重試任務失敗')
|
alert(err.response?.data?.detail || t('taskHistory.retryFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format date
|
// Format date
|
||||||
const formatDate = (dateStr: string) => {
|
const formatDate = (dateStr: string) => {
|
||||||
const date = new Date(dateStr)
|
const date = new Date(dateStr)
|
||||||
return date.toLocaleString('zh-TW')
|
return date.toLocaleString(i18n.language)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format processing time
|
// Format processing time
|
||||||
@@ -213,22 +215,22 @@ export default function TaskHistoryPage() {
|
|||||||
pending: {
|
pending: {
|
||||||
variant: 'secondary',
|
variant: 'secondary',
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
label: '待處理',
|
label: t('taskHistory.status.pending'),
|
||||||
},
|
},
|
||||||
processing: {
|
processing: {
|
||||||
variant: 'default',
|
variant: 'default',
|
||||||
icon: Loader2,
|
icon: Loader2,
|
||||||
label: '處理中',
|
label: t('taskHistory.status.processing'),
|
||||||
},
|
},
|
||||||
completed: {
|
completed: {
|
||||||
variant: 'default',
|
variant: 'default',
|
||||||
icon: CheckCircle2,
|
icon: CheckCircle2,
|
||||||
label: '已完成',
|
label: t('taskHistory.status.completed'),
|
||||||
},
|
},
|
||||||
failed: {
|
failed: {
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
icon: XCircle,
|
icon: XCircle,
|
||||||
label: '失敗',
|
label: t('taskHistory.status.failed'),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,17 +250,17 @@ export default function TaskHistoryPage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900">任務歷史</h1>
|
<h1 className="text-3xl font-bold text-gray-900">{t('taskHistory.title')}</h1>
|
||||||
<p className="text-gray-600 mt-1">查看和管理您的 OCR 任務</p>
|
<p className="text-gray-600 mt-1">{t('taskHistory.subtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button onClick={() => fetchTasks()} variant="outline">
|
<Button onClick={() => fetchTasks()} variant="outline">
|
||||||
<RefreshCw className="w-4 h-4 mr-2" />
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
刷新
|
{t('common.refresh')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleDeleteAll} variant="destructive" disabled={loading || tasks.length === 0}>
|
<Button onClick={handleDeleteAll} variant="destructive" disabled={loading || tasks.length === 0}>
|
||||||
<Trash2 className="w-4 h-4 mr-2" />
|
<Trash2 className="w-4 h-4 mr-2" />
|
||||||
刪除全部
|
{t('taskHistory.deleteAll')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -268,7 +270,7 @@ export default function TaskHistoryPage() {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-gray-600">總計</CardTitle>
|
<CardTitle className="text-sm font-medium text-gray-600">{t('common.total')}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">{stats.total}</div>
|
<div className="text-2xl font-bold">{stats.total}</div>
|
||||||
@@ -277,7 +279,7 @@ export default function TaskHistoryPage() {
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-gray-600">待處理</CardTitle>
|
<CardTitle className="text-sm font-medium text-gray-600">{t('taskHistory.status.pending')}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold text-gray-600">{stats.pending}</div>
|
<div className="text-2xl font-bold text-gray-600">{stats.pending}</div>
|
||||||
@@ -286,7 +288,7 @@ export default function TaskHistoryPage() {
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-gray-600">處理中</CardTitle>
|
<CardTitle className="text-sm font-medium text-gray-600">{t('taskHistory.status.processing')}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold text-blue-600">{stats.processing}</div>
|
<div className="text-2xl font-bold text-blue-600">{stats.processing}</div>
|
||||||
@@ -295,7 +297,7 @@ export default function TaskHistoryPage() {
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-gray-600">已完成</CardTitle>
|
<CardTitle className="text-sm font-medium text-gray-600">{t('taskHistory.status.completed')}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold text-green-600">{stats.completed}</div>
|
<div className="text-2xl font-bold text-green-600">{stats.completed}</div>
|
||||||
@@ -304,7 +306,7 @@ export default function TaskHistoryPage() {
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-gray-600">失敗</CardTitle>
|
<CardTitle className="text-sm font-medium text-gray-600">{t('taskHistory.status.failed')}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold text-red-600">{stats.failed}</div>
|
<div className="text-2xl font-bold text-red-600">{stats.failed}</div>
|
||||||
@@ -318,13 +320,13 @@ export default function TaskHistoryPage() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg flex items-center gap-2">
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
<Filter className="w-5 h-5" />
|
<Filter className="w-5 h-5" />
|
||||||
篩選條件
|
{t('taskHistory.filterConditions')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">狀態</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t('taskHistory.statusFilter')}</label>
|
||||||
<NativeSelect
|
<NativeSelect
|
||||||
value={statusFilter}
|
value={statusFilter}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -332,17 +334,17 @@ export default function TaskHistoryPage() {
|
|||||||
handleFilterChange()
|
handleFilterChange()
|
||||||
}}
|
}}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'all', label: '全部' },
|
{ value: 'all', label: t('taskHistory.status.all') },
|
||||||
{ value: 'pending', label: '待處理' },
|
{ value: 'pending', label: t('taskHistory.status.pending') },
|
||||||
{ value: 'processing', label: '處理中' },
|
{ value: 'processing', label: t('taskHistory.status.processing') },
|
||||||
{ value: 'completed', label: '已完成' },
|
{ value: 'completed', label: t('taskHistory.status.completed') },
|
||||||
{ value: 'failed', label: '失敗' },
|
{ value: 'failed', label: t('taskHistory.status.failed') },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">檔案名稱</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t('taskHistory.filenameFilter')}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={filenameSearch}
|
value={filenameSearch}
|
||||||
@@ -350,13 +352,13 @@ export default function TaskHistoryPage() {
|
|||||||
setFilenameSearch(e.target.value)
|
setFilenameSearch(e.target.value)
|
||||||
handleFilterChange()
|
handleFilterChange()
|
||||||
}}
|
}}
|
||||||
placeholder="搜尋檔案名稱"
|
placeholder={t('taskHistory.searchFilename')}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">開始日期</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t('taskHistory.startDate')}</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={dateFrom}
|
value={dateFrom}
|
||||||
@@ -369,7 +371,7 @@ export default function TaskHistoryPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">結束日期</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">{t('taskHistory.endDate')}</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={dateTo}
|
value={dateTo}
|
||||||
@@ -395,7 +397,7 @@ export default function TaskHistoryPage() {
|
|||||||
handleFilterChange()
|
handleFilterChange()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
清除篩選
|
{t('taskHistory.clearFilter')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -413,9 +415,9 @@ export default function TaskHistoryPage() {
|
|||||||
{/* Task List */}
|
{/* Task List */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg">任務列表</CardTitle>
|
<CardTitle className="text-lg">{t('taskHistory.taskList')}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
共 {total} 個任務 {hasMore && `(顯示第 ${page} 頁)`}
|
{t('common.total')} {total} {hasMore && `(${t('taskHistory.pagination.showing', { start: (page - 1) * pageSize + 1, end: Math.min(page * pageSize, total), total })})`}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -426,26 +428,26 @@ export default function TaskHistoryPage() {
|
|||||||
) : tasks.length === 0 ? (
|
) : tasks.length === 0 ? (
|
||||||
<div className="text-center py-12 text-gray-500">
|
<div className="text-center py-12 text-gray-500">
|
||||||
<FileText className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
<FileText className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
||||||
<p>暫無任務</p>
|
<p>{t('taskHistory.noTasks')}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>檔案名稱</TableHead>
|
<TableHead>{t('taskHistory.filenameFilter')}</TableHead>
|
||||||
<TableHead>狀態</TableHead>
|
<TableHead>{t('taskHistory.statusFilter')}</TableHead>
|
||||||
<TableHead>建立時間</TableHead>
|
<TableHead>{t('taskHistory.table.createdAt')}</TableHead>
|
||||||
<TableHead>完成時間</TableHead>
|
<TableHead>{t('taskHistory.table.completedAt')}</TableHead>
|
||||||
<TableHead>處理時間</TableHead>
|
<TableHead>{t('taskHistory.table.processingTime')}</TableHead>
|
||||||
<TableHead className="text-right">操作</TableHead>
|
<TableHead className="text-right">{t('taskHistory.table.actions')}</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{tasks.map((task) => (
|
{tasks.map((task) => (
|
||||||
<TableRow key={task.id}>
|
<TableRow key={task.id}>
|
||||||
<TableCell className="font-medium">
|
<TableCell className="font-medium">
|
||||||
{task.filename || '未命名檔案'}
|
{task.filename || t('taskHistory.unnamed')}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{getStatusBadge(task.status)}</TableCell>
|
<TableCell>{getStatusBadge(task.status)}</TableCell>
|
||||||
<TableCell className="text-sm text-gray-600">
|
<TableCell className="text-sm text-gray-600">
|
||||||
@@ -466,7 +468,7 @@ export default function TaskHistoryPage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleStartTask(task.task_id)}
|
onClick={() => handleStartTask(task.task_id)}
|
||||||
title="開始處理"
|
title={t('taskHistory.actions.startProcessing')}
|
||||||
>
|
>
|
||||||
<Play className="w-4 h-4" />
|
<Play className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -474,7 +476,7 @@ export default function TaskHistoryPage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleCancelTask(task.task_id)}
|
onClick={() => handleCancelTask(task.task_id)}
|
||||||
title="取消"
|
title={t('taskHistory.actions.cancel')}
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -485,7 +487,7 @@ export default function TaskHistoryPage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleCancelTask(task.task_id)}
|
onClick={() => handleCancelTask(task.task_id)}
|
||||||
title="取消"
|
title={t('taskHistory.actions.cancel')}
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -495,7 +497,7 @@ export default function TaskHistoryPage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleRetryTask(task.task_id)}
|
onClick={() => handleRetryTask(task.task_id)}
|
||||||
title="重試"
|
title={t('taskHistory.actions.retry')}
|
||||||
>
|
>
|
||||||
<RotateCcw className="w-4 h-4" />
|
<RotateCcw className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -507,25 +509,25 @@ export default function TaskHistoryPage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleDownloadPDF(task.task_id, 'layout')}
|
onClick={() => handleDownloadPDF(task.task_id, 'layout')}
|
||||||
title="下載版面 PDF"
|
title={t('taskHistory.actions.downloadLayoutPdf')}
|
||||||
>
|
>
|
||||||
<Download className="w-3 h-3 mr-1" />
|
<Download className="w-3 h-3 mr-1" />
|
||||||
版面
|
{t('taskHistory.actions.layoutPdf')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleDownloadPDF(task.task_id, 'reflow')}
|
onClick={() => handleDownloadPDF(task.task_id, 'reflow')}
|
||||||
title="下載流式 PDF"
|
title={t('taskHistory.actions.downloadReflowPdf')}
|
||||||
>
|
>
|
||||||
<Download className="w-3 h-3 mr-1" />
|
<Download className="w-3 h-3 mr-1" />
|
||||||
流式
|
{t('taskHistory.actions.reflowPdf')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleViewDetails(task.task_id)}
|
onClick={() => handleViewDetails(task.task_id)}
|
||||||
title="查看詳情"
|
title={t('taskHistory.actions.viewDetails')}
|
||||||
>
|
>
|
||||||
<Eye className="w-4 h-4" />
|
<Eye className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -536,7 +538,7 @@ export default function TaskHistoryPage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleDelete(task.task_id)}
|
onClick={() => handleDelete(task.task_id)}
|
||||||
title="刪除"
|
title={t('taskHistory.actions.delete')}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4 text-red-600" />
|
<Trash2 className="w-4 h-4 text-red-600" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -550,8 +552,7 @@ export default function TaskHistoryPage() {
|
|||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
<div className="flex items-center justify-between mt-4">
|
<div className="flex items-center justify-between mt-4">
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
顯示 {(page - 1) * pageSize + 1} - {Math.min(page * pageSize, total)} / 共{' '}
|
{t('taskHistory.pagination.showing', { start: (page - 1) * pageSize + 1, end: Math.min(page * pageSize, total), total })}
|
||||||
{total} 個
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -560,7 +561,7 @@ export default function TaskHistoryPage() {
|
|||||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||||
disabled={page === 1}
|
disabled={page === 1}
|
||||||
>
|
>
|
||||||
上一頁
|
{t('taskHistory.pagination.previous')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -568,7 +569,7 @@ export default function TaskHistoryPage() {
|
|||||||
onClick={() => setPage((p) => p + 1)}
|
onClick={() => setPage((p) => p + 1)}
|
||||||
disabled={!hasMore}
|
disabled={!hasMore}
|
||||||
>
|
>
|
||||||
下一頁
|
{t('taskHistory.pagination.next')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export default function UploadPage() {
|
|||||||
if (selectedFiles.length === 0) {
|
if (selectedFiles.length === 0) {
|
||||||
toast({
|
toast({
|
||||||
title: t('errors.validationError'),
|
title: t('errors.validationError'),
|
||||||
description: '請選擇至少一個檔案',
|
description: t('upload.selectAtLeastOne'),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -122,7 +122,7 @@ export default function UploadPage() {
|
|||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<h1 className="page-title">{t('upload.title')}</h1>
|
<h1 className="page-title">{t('upload.title')}</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
選擇要進行 OCR 處理的檔案,支援圖片、PDF 和 Office 文件
|
{t('upload.subtitle')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -135,8 +135,8 @@ export default function UploadPage() {
|
|||||||
{selectedFiles.length === 0 ? '1' : <CheckCircle2 className="w-5 h-5" />}
|
{selectedFiles.length === 0 ? '1' : <CheckCircle2 className="w-5 h-5" />}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-foreground">選擇檔案</div>
|
<div className="text-sm font-medium text-foreground">{t('upload.steps.selectFiles')}</div>
|
||||||
<div className="text-xs text-muted-foreground">上傳要處理的文件</div>
|
<div className="text-xs text-muted-foreground">{t('upload.steps.selectFilesDesc')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -151,8 +151,8 @@ export default function UploadPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div className={`text-sm font-medium ${
|
<div className={`text-sm font-medium ${
|
||||||
selectedFiles.length > 0 ? 'text-foreground' : 'text-muted-foreground'
|
selectedFiles.length > 0 ? 'text-foreground' : 'text-muted-foreground'
|
||||||
}`}>確認並上傳</div>
|
}`}>{t('upload.steps.confirmUpload')}</div>
|
||||||
<div className="text-xs text-muted-foreground">檢查並開始處理</div>
|
<div className="text-xs text-muted-foreground">{t('upload.steps.confirmUploadDesc')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -163,8 +163,8 @@ export default function UploadPage() {
|
|||||||
3
|
3
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-muted-foreground">處理完成</div>
|
<div className="text-sm font-medium text-muted-foreground">{t('upload.steps.processingComplete')}</div>
|
||||||
<div className="text-xs text-muted-foreground">查看結果並導出</div>
|
<div className="text-xs text-muted-foreground">{t('upload.steps.processingCompleteDesc')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -193,7 +193,7 @@ export default function UploadPage() {
|
|||||||
{t('upload.selectedFiles')}
|
{t('upload.selectedFiles')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<p className="text-sm text-muted-foreground mt-0.5">
|
<p className="text-sm text-muted-foreground mt-0.5">
|
||||||
已選擇 {selectedFiles.length} 個檔案,總大小 {formatFileSize(totalSize)}
|
{t('upload.fileList.summary', { count: selectedFiles.length, size: formatFileSize(totalSize) })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -205,7 +205,7 @@ export default function UploadPage() {
|
|||||||
className="gap-2"
|
className="gap-2"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
清空全部
|
{t('upload.clearAll')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -228,13 +228,13 @@ export default function UploadPage() {
|
|||||||
{file.name}
|
{file.name}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{formatFileSize(file.size)} · {file.type || '未知類型'}
|
{formatFileSize(file.size)} · {file.type || t('upload.fileList.unknownType')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status badge */}
|
{/* Status badge */}
|
||||||
<div className="status-badge-success">
|
<div className="status-badge-success">
|
||||||
準備就緒
|
{t('upload.fileList.ready')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Remove button */}
|
{/* Remove button */}
|
||||||
@@ -242,7 +242,7 @@ export default function UploadPage() {
|
|||||||
onClick={() => handleRemoveFile(index)}
|
onClick={() => handleRemoveFile(index)}
|
||||||
disabled={uploadMutation.isPending}
|
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"
|
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="移除檔案"
|
title={t('upload.fileList.removeFile')}
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -255,7 +255,7 @@ export default function UploadPage() {
|
|||||||
{/* Action Bar */}
|
{/* Action Bar */}
|
||||||
<div className="flex items-center justify-between p-4 bg-card rounded-xl border border-border">
|
<div className="flex items-center justify-between p-4 bg-card rounded-xl border border-border">
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
請確認檔案無誤後點擊上傳按鈕開始處理
|
{t('upload.fileList.confirmPrompt')}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Button
|
<Button
|
||||||
@@ -263,7 +263,7 @@ export default function UploadPage() {
|
|||||||
onClick={handleClearAll}
|
onClick={handleClearAll}
|
||||||
disabled={uploadMutation.isPending}
|
disabled={uploadMutation.isPending}
|
||||||
>
|
>
|
||||||
取消
|
{t('upload.fileList.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
@@ -278,7 +278,7 @@ export default function UploadPage() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Upload className="w-4 h-4" />
|
<Upload className="w-4 h-4" />
|
||||||
開始上傳並處理
|
{t('upload.fileList.startUpload')}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user