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 */}
+
+
+ {/* 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 (