diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 7fdc20d..c0f1d06 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -48,7 +48,10 @@ "Bash(npm:*)", "Bash(npx tailwindcss init -p)", "Bash(sqlite3:*)", - "Bash(TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzIiwidXNlcm5hbWUiOiJhZG1pbiIsImV4cCI6MTc2Mjk1ODUzOX0.S1JjFxVVmifdkN5F_dORt5jTRdTFN9MKJ8UJKuYacA8\")" + "Bash(TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzIiwidXNlcm5hbWUiOiJhZG1pbiIsImV4cCI6MTc2Mjk1ODUzOX0.S1JjFxVVmifdkN5F_dORt5jTRdTFN9MKJ8UJKuYacA8\")", + "Bash(tree:*)", + "Bash(done)", + "Bash(git add:*)" ], "deny": [], "ask": [] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8b30f6a..2b56230 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "axios": "^1.13.2", "clsx": "^2.1.1", "i18next": "^25.6.2", + "lucide-react": "^0.553.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-dropzone": "^14.3.8", @@ -3280,7 +3281,6 @@ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -3381,7 +3381,6 @@ "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", "dev": true, "license": "MPL-2.0", - "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -3682,6 +3681,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.553.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.553.0.tgz", + "integrity": "sha512-BRgX5zrWmNy/lkVAe0dXBgd7XQdZ3HTf+Hwe3c9WK6dqgnj9h+hxV+MDncM88xDWlCq27+TKvHGE70ViODNILw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", diff --git a/frontend/package.json b/frontend/package.json index 748e112..9f97148 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "axios": "^1.13.2", "clsx": "^2.1.1", "i18next": "^25.6.2", + "lucide-react": "^0.553.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-dropzone": "^14.3.8", diff --git a/frontend/src/components/FileUpload.tsx b/frontend/src/components/FileUpload.tsx index 178eec0..501f3b1 100644 --- a/frontend/src/components/FileUpload.tsx +++ b/frontend/src/components/FileUpload.tsx @@ -2,7 +2,7 @@ import { useCallback } from 'react' import { useDropzone } from 'react-dropzone' import { useTranslation } from 'react-i18next' import { cn } from '@/lib/utils' -import { Card } from '@/components/ui/card' +import { Upload, Cloud, AlertCircle, FileImage, File } from 'lucide-react' interface FileUploadProps { onFilesSelected: (files: File[]) => void @@ -47,72 +47,122 @@ export default function FileUpload({ return (
- -
+ {/* Gradient overlay on hover */} +
+ +
-
- + {/* Icon */} +
+
+ {isDragActive ? ( + + ) : ( + + )} +
-
+ {/* Text */} +
{isDragActive ? ( -

- {isDragReject ? t('upload.invalidFiles') : t('upload.dropFilesHere')} -

+
+

+ {isDragReject ? t('upload.invalidFiles') : t('upload.dropFilesHere')} +

+
) : ( <> -

+

{t('upload.dragAndDrop')}

-

{t('upload.supportedFormats')}

-

{t('upload.maxFileSize')}

+

+ 或點擊選擇檔案 +

+ + {/* Supported formats */} +
+ {[ + { icon: FileImage, label: 'Images', color: 'text-purple-500' }, + { icon: File, label: 'PDF', color: 'text-red-500' }, + { icon: File, label: 'Word', color: 'text-blue-500' }, + { icon: File, label: 'PPT', color: 'text-orange-500' }, + ].map((format) => ( +
+ + {format.label} +
+ ))} +
+ +

+ 最大檔案大小: 50MB · 最多 {maxFiles} 個檔案 +

)}
- +
+ {/* Error messages */} {fileRejections.length > 0 && ( -
-

- {t('errors.uploadFailed')} -

-
    - {fileRejections.map(({ file, errors }) => ( -
  • - {file.name}:{' '} - {errors.map((e) => { - if (e.code === 'file-too-large') return t('errors.fileTooBig') - if (e.code === 'file-invalid-type') return t('errors.unsupportedFormat') - return e.message - })} -
  • - ))} -
+
+
+ +
+

+ {t('errors.uploadFailed')} +

+
    + {fileRejections.map(({ file, errors }) => ( +
  • + {file.name}:{' '} + {errors.map((e) => { + if (e.code === 'file-too-large') return t('errors.fileTooBig') + if (e.code === 'file-invalid-type') return t('errors.unsupportedFormat') + return e.message + })} +
  • + ))} +
+
+
)}
diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 4758a84..0867e5c 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -1,11 +1,25 @@ -import { Outlet, NavLink } from 'react-router-dom' +import { Outlet, NavLink, useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useAuthStore } from '@/store/authStore' import { apiClient } from '@/services/api' +import { + Upload, + Settings, + FileText, + Download, + Activity, + LogOut, + LayoutDashboard, + ChevronRight, + Bell, + Search +} from 'lucide-react' export default function Layout() { const { t } = useTranslation() + const navigate = useNavigate() const logout = useAuthStore((state) => state.logout) + const user = useAuthStore((state) => state.user) const handleLogout = () => { apiClient.logout() @@ -13,59 +27,113 @@ export default function Layout() { } const navLinks = [ - { to: '/upload', label: t('nav.upload') }, - { to: '/processing', label: t('nav.processing') }, - { to: '/results', label: t('nav.results') }, - { to: '/export', label: t('nav.export') }, - { to: '/settings', label: t('nav.settings') }, + { to: '/upload', label: t('nav.upload'), icon: Upload, description: '上傳檔案' }, + { to: '/processing', label: t('nav.processing'), icon: Activity, description: '處理進度' }, + { to: '/results', label: t('nav.results'), icon: FileText, description: '查看結果' }, + { to: '/export', label: t('nav.export'), icon: Download, description: '導出文件' }, + { to: '/settings', label: t('nav.settings'), icon: Settings, description: '系統設定' }, ] return ( -
- {/* Header */} -
-
-
-

{t('app.title')}

-

{t('app.subtitle')}

+
+ {/* Sidebar */} +
+ - {/* Navigation */} - + {/* Main content area */} +
+ {/* Top bar */} +
+
+ {/* Search bar - placeholder for future use */} +
+
+ + +
+
- {/* Main Content */} -
- -
+ {/* Notifications */} + +
+
+ + {/* Page content */} +
+
+ +
+
+
) } diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index 9e1ad7a..72e46fd 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { cn } from '@/lib/utils' export interface ButtonProps extends React.ButtonHTMLAttributes { - variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' + variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' | 'gradient' size?: 'default' | 'sm' | 'lg' | 'icon' } @@ -11,22 +11,25 @@ const Button = React.forwardRef( return ( @@ -149,172 +174,274 @@ export default function ExportPage() { } return ( -
-
-

{t('export.title')}

-

批次 ID: {batchId}

+
+ {/* Page Header */} +
+

{t('export.title')}

+

+ 批次 ID: {batchId} +

- {/* Format Selection */} - - - {t('export.format')} - - -
- {(['txt', 'json', 'excel', 'markdown', 'pdf'] as ExportFormat[]).map((fmt) => ( - - ))} -
-
-
+
+ {/* Left Column - Configuration */} +
+ {/* Format Selection */} + + + + + {t('export.format')} + + 選擇要匯出的檔案格式 + + +
+ {(['txt', 'json', 'excel', 'markdown', 'pdf'] as ExportFormat[]).map((fmt) => { + const Icon = formatIcons[fmt] + return ( + + ) + })} +
+
+
- {/* Export Rules */} - {exportRules && exportRules.length > 0 && ( - - - {t('export.rules.title')} - - -
- - -
-
-
- )} - - {/* Export Options */} - - - {t('export.options.title')} - - - {/* Confidence Threshold */} -
- - - setOptions((prev) => ({ - ...prev, - confidence_threshold: Number(e.target.value), - })) - } - className="w-full" - /> -
- 0 - 0.5 - 1.0 -
-
- - {/* Include Metadata */} -
- - setOptions((prev) => ({ - ...prev, - include_metadata: e.target.checked, - })) - } - className="w-4 h-4 border border-gray-200 rounded" - /> - -
- - {/* Filename Pattern */} -
- - - setOptions((prev) => ({ - ...prev, - filename_pattern: e.target.value, - })) - } - className="w-full px-3 py-2 border border-gray-200 rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary" - placeholder="{filename}_ocr" - /> -

- 可用變數: {'{filename}'}, {'{batch_id}'}, {'{date}'} -

-
- - {/* CSS Template (PDF only) */} - {format === 'pdf' && cssTemplates && cssTemplates.length > 0 && ( -
- - -
+ {/* Export Rules */} + {exportRules && exportRules.length > 0 && ( + + + + + {t('export.rules.title')} + + 選擇預設的匯出規則 + + + + + )} -
-
- {/* Export Button */} -
- - + {/* Export Options */} + + + + + {t('export.options.title')} + + 自訂匯出參數 + + + {/* Confidence Threshold */} +
+
+ + + {(options.confidence_threshold * 100).toFixed(0)}% + +
+ + setOptions((prev) => ({ + ...prev, + confidence_threshold: Number(e.target.value), + })) + } + className="w-full h-2 bg-muted rounded-lg appearance-none cursor-pointer accent-primary" + /> +
+ 0% + 50% + 100% +
+
+ + {/* Include Metadata */} +
+ + setOptions((prev) => ({ + ...prev, + include_metadata: e.target.checked, + })) + } + className="w-5 h-5 border border-border rounded accent-primary" + /> + +
+ + {/* Filename Pattern */} +
+ + + setOptions((prev) => ({ + ...prev, + filename_pattern: e.target.value, + })) + } + className="w-full px-4 py-3 border border-border rounded-lg bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-colors font-mono text-sm" + placeholder="{filename}_ocr" + /> +

+ 可用變數: {'{filename}'},{' '} + {'{batch_id}'},{' '} + {'{date}'} +

+
+ + {/* CSS Template (PDF only) */} + {format === 'pdf' && cssTemplates && cssTemplates.length > 0 && ( +
+ + +
+ )} +
+
+
+ + {/* Right Column - Preview */} +
+ + + 匯出預覽 + 當前設定概覽 + + +
+
+
格式
+
+ {(() => { + const Icon = formatIcons[format] + return + })()} + {format.toUpperCase()} +
+
+ + {selectedRuleId && exportRules && ( +
+
匯出規則
+
+ {exportRules.find((r) => r.id === selectedRuleId)?.rule_name || '未選擇'} +
+
+ )} + +
+
準確率門檻
+
+ {(options.confidence_threshold * 100).toFixed(0)}% +
+
+ +
+
包含元數據
+
+ {options.include_metadata ? '是' : '否'} +
+
+ +
+
檔名模式
+
+ {options.filename_pattern || '{filename}_ocr'} +
+
+
+ +
+ + + +
+
+
+
) diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 8474b79..844cd06 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useAuthStore } from '@/store/authStore' import { apiClient } from '@/services/api' +import { Lock, User, LayoutDashboard, AlertCircle, Loader2 } from 'lucide-react' export default function LoginPage() { const { t } = useTranslation() @@ -39,57 +40,199 @@ export default function LoginPage() { } return ( -
-
-
-
-

{t('app.title')}

-

{t('app.subtitle')}

+
+ {/* Left side - Branding (hidden on mobile) */} +
+ {/* Subtle background pattern */} +
+
+
+
+ + {/* Content */} +
+ {/* Logo & Title */} +
+
+
+ +
+
+

{t('app.title')}

+

{t('app.subtitle')}

+
+
-
-
- - setUsername(e.target.value)} - className="w-full px-3 py-2 border border-input bg-background rounded-md focus:outline-none focus:ring-2 focus:ring-ring" - required - /> -
+ {/* Feature highlights */} +
+

智能文件識別系統

-
- - setPassword(e.target.value)} - className="w-full px-3 py-2 border border-input bg-background rounded-md focus:outline-none focus:ring-2 focus:ring-ring" - required - /> -
- - {error && ( -
- {error} +
+
+
+ + + +
+
+

高精度識別

+

支援 10+ 種文件格式,識別準確率達 99%

+
- )} - - +
+
+ + + +
+
+

快速處理

+

批量處理大量文件,節省時間成本

+
+
+ +
+
+ + + +
+
+

安全可靠

+

企業級安全保障,數據加密傳輸

+
+
+
+
+ + {/* Statistics */} +
+
+
99%
+
識別準確率
+
+
+
10+
+
支援格式
+
+
+
1M+
+
處理文件
+
+
+
+
+ + {/* Right side - Login form */} +
+
+ {/* Mobile logo (shown only on small screens) */} +
+
+
+ +
+

{t('app.title')}

+
+

{t('app.subtitle')}

+
+ + {/* Form card */} +
+
+

{t('auth.loginButton')}

+

請輸入您的帳號資訊以繼續

+
+ +
+ {/* Username field */} +
+ +
+
+ +
+ setUsername(e.target.value)} + className="w-full pl-10 pr-4 py-2.5 bg-background border border-border rounded-lg + text-foreground placeholder-muted-foreground + focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary + transition-colors" + placeholder="輸入用戶名" + required + /> +
+
+ + {/* Password field */} +
+ +
+
+ +
+ setPassword(e.target.value)} + className="w-full pl-10 pr-4 py-2.5 bg-background border border-border rounded-lg + text-foreground placeholder-muted-foreground + focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary + transition-colors" + placeholder="輸入密碼" + required + /> +
+
+ + {/* Error message */} + {error && ( +
+ +

{error}

+
+ )} + + {/* Submit button */} + +
+ + {/* Footer */} +
+

+ Powered by AI Technology +

+
+
diff --git a/frontend/src/pages/ProcessingPage.tsx b/frontend/src/pages/ProcessingPage.tsx index 4f93aa7..b28d982 100644 --- a/frontend/src/pages/ProcessingPage.tsx +++ b/frontend/src/pages/ProcessingPage.tsx @@ -9,6 +9,7 @@ import { Badge } from '@/components/ui/badge' import { useToast } from '@/components/ui/toast' import { useUploadStore } from '@/store/uploadStore' import { apiClient } from '@/services/api' +import { Play, CheckCircle, FileText, AlertCircle, Clock, Activity, Loader2, TrendingUp } from 'lucide-react' export default function ProcessingPage() { const { t } = useTranslation() @@ -84,16 +85,24 @@ export default function ProcessingPage() { // Show helpful message when no batch is selected if (!batchId) { return ( -
- +
+ - {t('processing.title')} +
+
+ +
+
+ {t('processing.title')}
- +

{t('processing.noBatchMessage', { defaultValue: '尚未選擇任何批次。請先上傳檔案以建立批次。' })}

-
@@ -107,58 +116,150 @@ export default function ProcessingPage() { const isPending = !batchStatus || batchStatus.batch.status === 'pending' return ( -
-
-

{t('processing.title')}

-

- 批次 ID: {batchId} - 共 {files.length} 個檔案 -

+
+ {/* Page Header */} +
+
+
+

{t('processing.title')}

+

+ 批次 ID: {batchId} · 共 {files.length} 個檔案 +

+
+
+ {isCompleted && ( +
+ + 處理完成 +
+ )} + {isProcessing && ( +
+ + 處理中 +
+ )} +
+
{/* Overall Progress */}
- {t('processing.progress')} +
+
+ +
+ {t('processing.progress')} +
{batchStatus && getStatusBadge(batchStatus.batch.status)}
- + + {/* Progress bar */}
-
- {t('processing.status')} - +
+ {t('processing.status')} + {batchStatus?.batch.progress_percentage || 0}%
- +
+ {/* Stats */} {batchStatus && ( -
- {t('processing.filesProcessed', { - processed: batchStatus.files.filter((f) => f.status === 'completed').length, - total: batchStatus.files.length, - })} +
+
+
+
+ +
+
+

已完成

+

+ {batchStatus.files.filter((f) => f.status === 'completed').length} +

+
+
+
+
+
+
+ +
+
+

處理中

+

+ {batchStatus.files.filter((f) => f.status === 'processing').length} +

+
+
+
+
+
+
+ +
+
+

失敗

+

+ {batchStatus.files.filter((f) => f.status === 'failed').length} +

+
+
+
+
+
+
+ +
+
+

總計

+

{batchStatus.files.length}

+
+
+
)} -
- {isPending && ( - - )} + {/* Action buttons */} + {(isPending || isCompleted) && ( +
+ {isPending && ( + + )} - {isCompleted && ( - - )} -
+ {isCompleted && ( + + )} +
+ )} @@ -166,27 +267,56 @@ export default function ProcessingPage() { {batchStatus && ( - 檔案處理狀態 +
+
+ +
+ 檔案處理狀態 +
{batchStatus.files.map((file) => (
-
-

- {file.filename} -

- {file.processing_time && ( -

- 處理時間: {file.processing_time.toFixed(2)}s +

+
+ {file.status === 'completed' ? ( + + ) : file.status === 'processing' ? ( + + ) : file.status === 'failed' ? ( + + ) : ( + + )} +
+
+

+ {file.filename}

- )} - {file.error && ( -

{file.error}

- )} +
+ {file.processing_time && ( +

+ + 處理時間: {file.processing_time.toFixed(2)}s +

+ )} + {file.error && ( +

+ + {file.error} +

+ )} +
+
{getStatusBadge(file.status)}
diff --git a/frontend/src/pages/ResultsPage.tsx b/frontend/src/pages/ResultsPage.tsx index 652ce86..feb1271 100644 --- a/frontend/src/pages/ResultsPage.tsx +++ b/frontend/src/pages/ResultsPage.tsx @@ -9,6 +9,7 @@ import MarkdownPreview from '@/components/MarkdownPreview' import { useToast } from '@/components/ui/toast' import { useUploadStore } from '@/store/uploadStore' import { apiClient } from '@/services/api' +import { FileText, Download, Languages, AlertCircle, TrendingUp, Clock, Layers } from 'lucide-react' export default function ResultsPage() { const { t } = useTranslation() @@ -68,16 +69,21 @@ export default function ResultsPage() { // Show helpful message when no batch is selected if (!batchId) { return ( -
- +
+ - {t('results.title')} +
+
+ +
+
+ {t('results.title')}
- +

{t('results.noBatchMessage', { defaultValue: '尚未選擇任何批次。請先上傳並處理檔案。' })}

-
@@ -89,33 +95,40 @@ export default function ResultsPage() { const completedFiles = batchStatus?.files.filter((f) => f.status === 'completed') || [] return ( -
-
-
-

{t('results.title')}

-

- 批次 ID: {batchId} - 已完成 {completedFiles.length} 個檔案 -

-
-
- - +
+ {/* Page Header */} +
+
+
+

{t('results.title')}

+

+ 批次 ID: {batchId} · 已完成 {completedFiles.length} 個檔案 +

+
+
+ + +
-
- {/* Results Table */} -
+
+ {/* Results Table - Takes 2 columns */} +
- {/* Preview Panel */} -
+ {/* Preview Panel - Takes 3 columns */} +
{selectedFileId && ocrResult ? (
+ {/* Preview Card */} -
-

- {t('results.confidence')}: {((ocrResult.confidence || 0) * 100).toFixed(2)}% -

-

- {t('results.processingTime')}: {(ocrResult.processing_time || 0).toFixed(2)}s -

-

- {t('results.textBlocks')}: {ocrResult.json_data?.total_text_regions || 0} -

+ + {/* Stats Grid */} +
+ + +
+
+ +
+
+

準確率

+

+ {((ocrResult.confidence || 0) * 100).toFixed(1)}% +

+
+
+
+
+ + + +
+
+ +
+
+

處理時間

+

+ {(ocrResult.processing_time || 0).toFixed(2)}s +

+
+
+
+
+ + + +
+
+ +
+
+

文字區塊

+

+ {ocrResult.json_data?.total_text_regions || 0} +

+
+
+
+
) : ( -
-

- {isLoadingResult ? t('common.loading') : '選擇檔案以查看結果'} -

-
+ + +
+ +
+

+ {isLoadingResult ? t('common.loading') : '選擇左側檔案以查看詳細結果'} +

+
+
)}
diff --git a/frontend/src/pages/UploadPage.tsx b/frontend/src/pages/UploadPage.tsx index abaed47..eb33149 100644 --- a/frontend/src/pages/UploadPage.tsx +++ b/frontend/src/pages/UploadPage.tsx @@ -8,6 +8,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { useToast } from '@/components/ui/toast' import { useUploadStore } from '@/store/uploadStore' import { apiClient } from '@/services/api' +import { FileText, X, Upload, Trash2, CheckCircle2, ArrowRight } from 'lucide-react' export default function UploadPage() { const { t } = useTranslation() @@ -71,69 +72,192 @@ export default function UploadPage() { return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i] } + const getFileIcon = (filename: string) => { + const ext = filename.split('.').pop()?.toLowerCase() + const colors = { + pdf: 'text-red-500', + doc: 'text-blue-500', + docx: 'text-blue-500', + ppt: 'text-orange-500', + pptx: 'text-orange-500', + jpg: 'text-green-500', + jpeg: 'text-green-500', + png: 'text-green-500', + } + return colors[ext as keyof typeof colors] || 'text-gray-500' + } + + const totalSize = selectedFiles.reduce((acc, file) => acc + file.size, 0) + return ( -
-
-

{t('upload.title')}

+
+ {/* Page Header */} +
+

{t('upload.title')}

選擇要進行 OCR 處理的檔案,支援圖片、PDF 和 Office 文件

- + {/* Step Indicator */} +
+
+
+ {selectedFiles.length === 0 ? '1' : } +
+
+
選擇檔案
+
上傳要處理的文件
+
+
+ + +
+
0 ? 'bg-primary text-white' : 'bg-muted text-muted-foreground' + }`}> + 2 +
+
+
0 ? 'text-foreground' : 'text-muted-foreground' + }`}>確認並上傳
+
檢查並開始處理
+
+
+ + + +
+
+ 3 +
+
+
處理完成
+
查看結果並導出
+
+
+
+ + {/* Upload Area */} +
+ +
+ + {/* Selected Files Section */} {selectedFiles.length > 0 && ( - - -
- - {t('upload.selectedFiles')} ({selectedFiles.length}) - - -
-
- -
- {selectedFiles.map((file, index) => ( -
-
-

{file.name}

-

{formatFileSize(file.size)}

+
+ {/* Summary Card */} + + +
+
+
+ +
+
+ + {t('upload.selectedFiles')} + +

+ 已選擇 {selectedFiles.length} 個檔案,總大小 {formatFileSize(totalSize)} +

-
- ))} -
+ +
+ + + {/* Files Table */} +
+ {selectedFiles.map((file, index) => ( +
+ {/* File icon */} +
+ +
-
+ {/* File info */} +
+

+ {file.name} +

+

+ {formatFileSize(file.size)} · {file.type || '未知類型'} +

+
+ + {/* Status badge */} +
+ 準備就緒 +
+ + {/* Remove button */} + +
+ ))} +
+ + + + {/* Action Bar */} +
+
+ 請確認檔案無誤後點擊上傳按鈕開始處理 +
+
-
- - +
+
)}
)