feat: simplify login page UX and add i18n English support
- Redesign LoginPage with minimal professional style - Remove animated gradient backgrounds and floating orbs - Remove marketing claims (99% accuracy, enterprise-grade) - Center login form with clean card design - Add multi-language support (zh-TW, en-US) - Create LanguageSwitcher component in sidebar - Add en-US.json translation file - Persist language preference in localStorage - Remove unused top header bar with search - Move language switcher to sidebar user section 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
45
frontend/src/components/LanguageSwitcher.tsx
Normal file
45
frontend/src/components/LanguageSwitcher.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Globe } from 'lucide-react'
|
||||
|
||||
const languages = [
|
||||
{ code: 'zh-TW', label: '繁體中文' },
|
||||
{ code: 'en-US', label: 'English' },
|
||||
]
|
||||
|
||||
export default function LanguageSwitcher() {
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
const handleLanguageChange = (langCode: string) => {
|
||||
i18n.changeLanguage(langCode)
|
||||
localStorage.setItem('tool-ocr-language', langCode)
|
||||
}
|
||||
|
||||
const currentLang = languages.find((lang) => lang.code === i18n.language) || languages[0]
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
<button
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-sidebar-foreground/70
|
||||
hover:text-sidebar-foreground hover:bg-white/5 rounded-lg transition-colors"
|
||||
>
|
||||
<Globe className="w-5 h-5" />
|
||||
<span>{currentLang.label}</span>
|
||||
</button>
|
||||
|
||||
{/* Dropdown */}
|
||||
<div className="absolute left-0 bottom-full mb-1 py-1 bg-card border border-border rounded-lg shadow-lg
|
||||
opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all z-50 min-w-[140px]">
|
||||
{languages.map((lang) => (
|
||||
<button
|
||||
key={lang.code}
|
||||
onClick={() => handleLanguageChange(lang.code)}
|
||||
className={`w-full px-3 py-2 text-sm text-left hover:bg-muted transition-colors
|
||||
${i18n.language === lang.code ? 'text-primary font-medium' : 'text-foreground'}`}
|
||||
>
|
||||
{lang.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Outlet, NavLink, useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { apiClientV2 } from '@/services/apiV2'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher'
|
||||
import {
|
||||
Upload,
|
||||
Settings,
|
||||
@@ -11,8 +12,6 @@ import {
|
||||
LogOut,
|
||||
LayoutDashboard,
|
||||
ChevronRight,
|
||||
Bell,
|
||||
Search,
|
||||
History,
|
||||
Shield
|
||||
} from 'lucide-react'
|
||||
@@ -96,9 +95,9 @@ export default function Layout() {
|
||||
</nav>
|
||||
|
||||
{/* User section */}
|
||||
<div className="px-3 py-4 border-t border-border/20">
|
||||
<div className="px-3 py-4 border-t border-border/20 space-y-2">
|
||||
{user && (
|
||||
<div className="flex items-center gap-3 px-3 py-2 rounded-lg bg-white/5 mb-2">
|
||||
<div className="flex items-center gap-3 px-3 py-2 rounded-lg bg-white/5">
|
||||
<div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center text-white font-semibold text-sm flex-shrink-0">
|
||||
{user.username.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
@@ -108,6 +107,9 @@ export default function Layout() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="px-3">
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-white/5 transition-colors text-sm"
|
||||
@@ -119,37 +121,11 @@ export default function Layout() {
|
||||
</aside>
|
||||
|
||||
{/* Main content area */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Top bar */}
|
||||
<header className="bg-card border-b border-border px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Search bar - placeholder for future use */}
|
||||
<div className="flex-1 max-w-2xl">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-muted-foreground" />
|
||||
<input
|
||||
type="search"
|
||||
placeholder="搜尋檔案或功能..."
|
||||
className="w-full pl-10 pr-4 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notifications */}
|
||||
<button className="ml-4 p-2 rounded-lg hover:bg-muted transition-colors relative">
|
||||
<Bell className="w-5 h-5 text-muted-foreground" />
|
||||
<span className="absolute top-1 right-1 w-2 h-2 bg-destructive rounded-full"></span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Page content */}
|
||||
<main className="flex-1 overflow-y-auto bg-background p-6 scrollbar-thin">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
import i18n from 'i18next'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
import zhTW from './locales/zh-TW.json'
|
||||
import enUS from './locales/en-US.json'
|
||||
|
||||
const LANGUAGE_STORAGE_KEY = 'tool-ocr-language'
|
||||
|
||||
/**
|
||||
* Get saved language preference from localStorage
|
||||
* Falls back to zh-TW if no preference is saved
|
||||
*/
|
||||
function getSavedLanguage(): string {
|
||||
const saved = localStorage.getItem(LANGUAGE_STORAGE_KEY)
|
||||
if (saved && ['zh-TW', 'en-US'].includes(saved)) {
|
||||
return saved
|
||||
}
|
||||
return 'zh-TW'
|
||||
}
|
||||
|
||||
/**
|
||||
* i18n Configuration
|
||||
* Supported languages: Traditional Chinese (zh-TW), English (en-US)
|
||||
* Default language: Traditional Chinese (zh-TW)
|
||||
*/
|
||||
i18n.use(initReactI18next).init({
|
||||
@@ -11,8 +27,11 @@ i18n.use(initReactI18next).init({
|
||||
'zh-TW': {
|
||||
translation: zhTW,
|
||||
},
|
||||
'en-US': {
|
||||
translation: enUS,
|
||||
},
|
||||
lng: 'zh-TW',
|
||||
},
|
||||
lng: getSavedLanguage(),
|
||||
fallbackLng: 'zh-TW',
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
|
||||
238
frontend/src/i18n/locales/en-US.json
Normal file
238
frontend/src/i18n/locales/en-US.json
Normal file
@@ -0,0 +1,238 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "OCR Batch Processing System",
|
||||
"subtitle": "Document Recognition and Conversion Platform"
|
||||
},
|
||||
"nav": {
|
||||
"upload": "Upload Files",
|
||||
"processing": "Processing",
|
||||
"results": "View Results",
|
||||
"export": "Export",
|
||||
"settings": "Settings",
|
||||
"logout": "Logout",
|
||||
"taskHistory": "Task History",
|
||||
"adminDashboard": "Admin Dashboard"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Login",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"loginButton": "Login",
|
||||
"loginError": "Login failed. Please check your credentials.",
|
||||
"welcomeBack": "Welcome Back",
|
||||
"loginPrompt": "Sign in to access OCR services",
|
||||
"loggingIn": "Signing in...",
|
||||
"usernamePlaceholder": "Enter your username",
|
||||
"passwordPlaceholder": "Enter your password",
|
||||
"supportedFormats": "Supported formats: PDF, Images, Office documents"
|
||||
},
|
||||
"upload": {
|
||||
"title": "Upload Files",
|
||||
"dragAndDrop": "Drag and drop files here, or click to select",
|
||||
"dropFilesHere": "Drop files here to upload",
|
||||
"invalidFiles": "Some file formats are not supported",
|
||||
"supportedFormats": "Supported formats: PNG, JPG, JPEG, PDF, DOC, DOCX, PPT, PPTX",
|
||||
"maxFileSize": "Maximum file size: 50MB",
|
||||
"uploadButton": "Start Upload",
|
||||
"uploading": "Uploading...",
|
||||
"uploadSuccess": "Upload successful",
|
||||
"uploadError": "Upload failed",
|
||||
"fileCount": "{{count}} file(s) selected",
|
||||
"clearAll": "Clear All",
|
||||
"removeFile": "Remove",
|
||||
"selectedFiles": "Selected Files"
|
||||
},
|
||||
"processing": {
|
||||
"title": "OCR Processing",
|
||||
"status": "Status",
|
||||
"progress": "Progress",
|
||||
"currentFile": "Current File",
|
||||
"filesProcessed": "Processed {{processed}} / {{total}} files",
|
||||
"startProcessing": "Start Processing",
|
||||
"processing": "Processing...",
|
||||
"completed": "Completed",
|
||||
"failed": "Failed",
|
||||
"pending": "Pending",
|
||||
"estimatedTime": "Estimated Time Remaining",
|
||||
"settings": {
|
||||
"title": "Processing Settings",
|
||||
"language": "Recognition Language",
|
||||
"threshold": "Confidence Threshold",
|
||||
"layoutDetection": "Layout Detection"
|
||||
},
|
||||
"layoutModel": {
|
||||
"title": "Layout Detection Model",
|
||||
"chinese": "Chinese Document Model",
|
||||
"chineseDesc": "PP-DocLayout_plus-L (83.2% mAP) - For complex Chinese documents, supports 20 layout elements (Recommended)",
|
||||
"default": "Standard Model",
|
||||
"defaultDesc": "PubLayNet model (~94% mAP) - For English academic papers and reports",
|
||||
"cdla": "CDLA Model",
|
||||
"cdlaDesc": "CDLA layout analysis model (~86% mAP) - Specialized for Chinese document layout",
|
||||
"recommended": "Recommended",
|
||||
"note": "The layout model affects detection of document structure (tables, text blocks, images). Choose the appropriate model based on your document type."
|
||||
},
|
||||
"tableDetection": {
|
||||
"title": "Table Detection Mode",
|
||||
"wired": "Bordered Tables",
|
||||
"wiredDesc": "Detect tables with visible grid borders, suitable for formal documents",
|
||||
"wireless": "Borderless Tables",
|
||||
"wirelessDesc": "Detect tables without borders by analyzing text alignment",
|
||||
"region": "Region Detection",
|
||||
"regionDesc": "Auxiliary table region detection for improved cell recognition",
|
||||
"note": "Multiple detection modes can be enabled simultaneously. The system will automatically integrate results. Adjust detection modes if table cell borders are incorrect."
|
||||
},
|
||||
"preprocessing": {
|
||||
"title": "Image Preprocessing",
|
||||
"mode": {
|
||||
"auto": "Auto Mode",
|
||||
"autoDesc": "System automatically analyzes image quality and determines optimal preprocessing",
|
||||
"manual": "Manual Mode",
|
||||
"manualDesc": "Manually select preprocessing options and intensity for full control",
|
||||
"disabled": "Disable Preprocessing",
|
||||
"disabledDesc": "No preprocessing applied, use original image directly"
|
||||
},
|
||||
"recommended": "Recommended",
|
||||
"preview": "Preview Effect",
|
||||
"manualConfig": "Manual Configuration",
|
||||
"contrast": {
|
||||
"label": "Contrast Enhancement",
|
||||
"none": "None",
|
||||
"histogram": "Histogram Equalization",
|
||||
"clahe": "CLAHE Adaptive Equalization",
|
||||
"document": "Scan Optimization (Background Correction + CLAHE)"
|
||||
},
|
||||
"sharpen": "Edge Sharpening",
|
||||
"strength": {
|
||||
"label": "Intensity",
|
||||
"subtle": "Subtle",
|
||||
"normal": "Normal",
|
||||
"strong": "Strong",
|
||||
"maximum": "Maximum"
|
||||
},
|
||||
"removeScanArtifacts": "Remove Scan Artifacts",
|
||||
"removeScanArtifactsDesc": "Remove horizontal lines from scanning to prevent misdetection as table borders",
|
||||
"advanced": "Advanced Options",
|
||||
"binarize": "Binarization",
|
||||
"binarizeWarning": "Not recommended",
|
||||
"note": "Preprocessing only affects the layout detection stage to improve table and text block recognition. Original images are used for final OCR text extraction to ensure best quality.",
|
||||
"previewPanel": {
|
||||
"title": "Preprocessing Preview",
|
||||
"loading": "Loading preview...",
|
||||
"loadError": "Failed to load preview",
|
||||
"refresh": "Refresh Preview",
|
||||
"original": "Original Image",
|
||||
"preprocessed": "Preprocessed",
|
||||
"fullscreen": "Fullscreen View",
|
||||
"qualityAnalysis": "Image Quality Analysis",
|
||||
"contrast": "Contrast",
|
||||
"sharpness": "Sharpness",
|
||||
"qualityLow": "Low",
|
||||
"qualityMedium": "Medium",
|
||||
"qualityHigh": "High",
|
||||
"qualityBlurry": "Blurry",
|
||||
"qualityNormal": "Normal",
|
||||
"qualitySharp": "Sharp",
|
||||
"autoDetectedConfig": "Auto-detected Settings",
|
||||
"contrastEnhancement": "Contrast Enhancement",
|
||||
"sharpenEnabled": "Enabled",
|
||||
"sharpenDisabled": "Disabled"
|
||||
}
|
||||
}
|
||||
},
|
||||
"results": {
|
||||
"title": "OCR Results",
|
||||
"filename": "Filename",
|
||||
"status": "Status",
|
||||
"confidence": "Confidence",
|
||||
"processingTime": "Processing Time",
|
||||
"actions": "Actions",
|
||||
"viewMarkdown": "View Markdown",
|
||||
"viewJSON": "View JSON",
|
||||
"downloadPDF": "Download PDF",
|
||||
"preview": "Preview",
|
||||
"noResults": "No results yet",
|
||||
"textBlocks": "Text Blocks",
|
||||
"layoutInfo": "Layout Info"
|
||||
},
|
||||
"export": {
|
||||
"title": "Export Results",
|
||||
"format": "Export Format",
|
||||
"formats": {
|
||||
"txt": "Plain Text (.txt)",
|
||||
"json": "JSON (.json)",
|
||||
"excel": "Excel (.xlsx)",
|
||||
"markdown": "Markdown (.md)",
|
||||
"pdf": "PDF (.pdf)"
|
||||
},
|
||||
"options": {
|
||||
"title": "Export Options",
|
||||
"confidenceThreshold": "Confidence Threshold",
|
||||
"includeMetadata": "Include Metadata",
|
||||
"filenamePattern": "Filename Pattern",
|
||||
"cssTemplate": "CSS Template"
|
||||
},
|
||||
"rules": {
|
||||
"title": "Export Rules",
|
||||
"selectRule": "Select Rule",
|
||||
"saveRule": "Save Rule",
|
||||
"newRule": "New Rule",
|
||||
"ruleName": "Rule Name",
|
||||
"deleteRule": "Delete Rule"
|
||||
},
|
||||
"cssTemplates": {
|
||||
"default": "Default",
|
||||
"academic": "Academic",
|
||||
"business": "Business",
|
||||
"report": "Report"
|
||||
},
|
||||
"exportButton": "Export",
|
||||
"exporting": "Exporting...",
|
||||
"exportSuccess": "Export successful",
|
||||
"exportError": "Export failed"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"exportRules": "Export Rules Management",
|
||||
"language": "Language",
|
||||
"theme": "Theme",
|
||||
"about": "About"
|
||||
},
|
||||
"common": {
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"close": "Close",
|
||||
"loading": "Loading...",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"warning": "Warning",
|
||||
"info": "Info",
|
||||
"search": "Search",
|
||||
"filter": "Filter",
|
||||
"sort": "Sort",
|
||||
"refresh": "Refresh",
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"previous": "Previous",
|
||||
"submit": "Submit"
|
||||
},
|
||||
"errors": {
|
||||
"networkError": "Network error. Please try again later.",
|
||||
"unauthorized": "Unauthorized. Please login again.",
|
||||
"notFound": "Resource not found",
|
||||
"serverError": "Server error",
|
||||
"validationError": "Validation error",
|
||||
"fileTooBig": "File too large",
|
||||
"unsupportedFormat": "Unsupported format",
|
||||
"uploadFailed": "Upload failed",
|
||||
"processingFailed": "Processing failed",
|
||||
"exportFailed": "Export failed"
|
||||
},
|
||||
"translation": {
|
||||
"title": "Translation",
|
||||
"comingSoon": "Coming Soon",
|
||||
"description": "Document translation feature is under development"
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,9 @@
|
||||
"results": "結果檢視",
|
||||
"export": "匯出",
|
||||
"settings": "設定",
|
||||
"logout": "登出"
|
||||
"logout": "登出",
|
||||
"taskHistory": "任務歷史",
|
||||
"adminDashboard": "管理員儀表板"
|
||||
},
|
||||
"auth": {
|
||||
"login": "登入",
|
||||
@@ -17,7 +19,12 @@
|
||||
"password": "密碼",
|
||||
"loginButton": "登入",
|
||||
"loginError": "登入失敗,請檢查帳號密碼",
|
||||
"welcomeBack": "歡迎回來"
|
||||
"welcomeBack": "歡迎回來",
|
||||
"loginPrompt": "登入以使用 OCR 服務",
|
||||
"loggingIn": "登入中...",
|
||||
"usernamePlaceholder": "輸入您的使用者名稱",
|
||||
"passwordPlaceholder": "輸入您的密碼",
|
||||
"supportedFormats": "支援格式:PDF、圖片、Office 文件"
|
||||
},
|
||||
"upload": {
|
||||
"title": "上傳檔案",
|
||||
|
||||
@@ -3,7 +3,8 @@ import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { apiClientV2 } from '@/services/apiV2'
|
||||
import { Lock, User, LayoutDashboard, AlertCircle, Loader2, Sparkles, Zap, Shield } from 'lucide-react'
|
||||
import { Lock, User, LayoutDashboard, AlertCircle, Loader2 } from 'lucide-react'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher'
|
||||
|
||||
export default function LoginPage() {
|
||||
const { t } = useTranslation()
|
||||
@@ -20,10 +21,8 @@ export default function LoginPage() {
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
// Use V2 API with external authentication
|
||||
const response = await apiClientV2.login({ username, password })
|
||||
|
||||
// Store user info from V2 API response
|
||||
setUser({
|
||||
id: response.user.id,
|
||||
username: response.user.email,
|
||||
@@ -47,198 +46,116 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative overflow-hidden">
|
||||
{/* Full-screen Animated Background */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-600 via-purple-600 to-pink-500">
|
||||
{/* Animated overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-tr from-blue-500/50 via-transparent to-purple-500/50 animate-pulse"></div>
|
||||
|
||||
{/* Floating orbs */}
|
||||
<div className="absolute top-20 left-20 w-72 h-72 bg-white/10 rounded-full blur-3xl animate-float"></div>
|
||||
<div className="absolute bottom-20 right-20 w-96 h-96 bg-blue-400/20 rounded-full blur-3xl animate-float" style={{ animationDelay: '1s' }}></div>
|
||||
<div className="absolute top-1/2 left-1/2 w-64 h-64 bg-purple-400/10 rounded-full blur-3xl animate-float" style={{ animationDelay: '0.5s' }}></div>
|
||||
|
||||
{/* Grid pattern */}
|
||||
<div className="absolute inset-0 opacity-10">
|
||||
<div className="absolute inset-0" style={{ backgroundImage: 'linear-gradient(rgba(255,255,255,.05) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.05) 1px, transparent 1px)', backgroundSize: '50px 50px' }}></div>
|
||||
</div>
|
||||
<div className="min-h-screen bg-background flex flex-col">
|
||||
{/* Top bar with language switcher */}
|
||||
<div className="flex justify-end p-4">
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
|
||||
{/* Content Container */}
|
||||
<div className="relative z-10 min-h-screen flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-6xl flex flex-col lg:flex-row gap-8 items-center">
|
||||
|
||||
{/* Left Side - Branding */}
|
||||
<div className="flex-1 text-white text-center lg:text-left animate-fade-in">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center justify-center lg:justify-start gap-4 mb-8">
|
||||
<div className="w-16 h-16 bg-white/20 backdrop-blur-md rounded-2xl flex items-center justify-center shadow-2xl animate-float">
|
||||
<LayoutDashboard className="w-10 h-10" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold tracking-tight">Tool_OCR</h1>
|
||||
<p className="text-white/80 text-sm mt-1">智能 OCR 處理平台</p>
|
||||
{/* Centered login form */}
|
||||
<div className="flex-1 flex items-center justify-center px-4 pb-16">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Logo and title */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-xl bg-primary/10 mb-4">
|
||||
<LayoutDashboard className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-foreground">{t('app.title')}</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t('app.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
{/* Features - Only show on larger screens */}
|
||||
<div className="hidden lg:block space-y-6 mb-12 animate-slide-in-left" style={{ animationDelay: '0.1s' }}>
|
||||
<h2 className="text-3xl font-bold mb-8">為什麼選擇我們?</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-4 group hover:translate-x-2 transition-transform">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-white/20 to-white/10 backdrop-blur-sm rounded-xl flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform">
|
||||
<Sparkles className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-xl mb-1">高精度識別</h3>
|
||||
<p className="text-white/70">AI 驅動的 OCR 引擎,識別準確率高達 99%</p>
|
||||
</div>
|
||||
{/* Login card */}
|
||||
<div className="bg-card rounded-xl border border-border p-8 shadow-sm">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold text-foreground">{t('auth.welcomeBack')}</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t('auth.loginPrompt')}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4 group hover:translate-x-2 transition-transform">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-white/20 to-white/10 backdrop-blur-sm rounded-xl flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform">
|
||||
<Zap className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-xl mb-1">閃電般快速</h3>
|
||||
<p className="text-white/70">批量處理數百份文件,大幅提升工作效率</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4 group hover:translate-x-2 transition-transform">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-white/20 to-white/10 backdrop-blur-sm rounded-xl flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform">
|
||||
<Shield className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-xl mb-1">安全可靠</h3>
|
||||
<p className="text-white/70">企業級加密,確保您的資料絕對安全</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="hidden lg:grid grid-cols-3 gap-4 animate-slide-in-left" style={{ animationDelay: '0.2s' }}>
|
||||
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-6 hover:bg-white/20 hover:scale-105 transition-all cursor-pointer border border-white/20">
|
||||
<div className="text-4xl font-bold mb-2">99%</div>
|
||||
<div className="text-white/70 text-sm">準確率</div>
|
||||
</div>
|
||||
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-6 hover:bg-white/20 hover:scale-105 transition-all cursor-pointer border border-white/20">
|
||||
<div className="text-4xl font-bold mb-2">10+</div>
|
||||
<div className="text-white/70 text-sm">支援格式</div>
|
||||
</div>
|
||||
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-6 hover:bg-white/20 hover:scale-105 transition-all cursor-pointer border border-white/20">
|
||||
<div className="text-4xl font-bold mb-2">24/7</div>
|
||||
<div className="text-white/70 text-sm">全天候</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side - Login Form */}
|
||||
<div className="w-full lg:w-auto lg:min-w-[480px] animate-scale-in">
|
||||
<div className="bg-white/95 backdrop-blur-xl rounded-3xl p-8 md:p-10 shadow-2xl border border-white/20">
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-2">歡迎回來</h2>
|
||||
<p className="text-gray-600">登入以開始使用 OCR 服務</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
{/* Username */}
|
||||
<div className="space-y-2 animate-slide-in-right" style={{ animationDelay: '0.1s' }}>
|
||||
<label htmlFor="username" className="block text-sm font-semibold text-gray-700">
|
||||
使用者名稱
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="username" className="block text-sm font-medium text-foreground">
|
||||
{t('auth.username')}
|
||||
</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<User className="h-5 w-5 text-gray-400 group-focus-within:text-blue-600 transition-colors" />
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<User className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="w-full pl-11 pr-4 py-3 bg-gray-50 border-2 border-gray-200 rounded-xl
|
||||
text-gray-900 placeholder-gray-400
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-500/20 focus:border-blue-500
|
||||
hover:border-gray-300
|
||||
transition-all duration-200"
|
||||
placeholder="輸入您的用戶名"
|
||||
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={t('auth.usernamePlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div className="space-y-2 animate-slide-in-right" style={{ animationDelay: '0.2s' }}>
|
||||
<label htmlFor="password" className="block text-sm font-semibold text-gray-700">
|
||||
密碼
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="password" className="block text-sm font-medium text-foreground">
|
||||
{t('auth.password')}
|
||||
</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<Lock className="h-5 w-5 text-gray-400 group-focus-within:text-blue-600 transition-colors" />
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Lock className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full pl-11 pr-4 py-3 bg-gray-50 border-2 border-gray-200 rounded-xl
|
||||
text-gray-900 placeholder-gray-400
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-500/20 focus:border-blue-500
|
||||
hover:border-gray-300
|
||||
transition-all duration-200"
|
||||
placeholder="輸入您的密碼"
|
||||
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={t('auth.passwordPlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<div className="flex items-start gap-3 p-4 bg-red-50 border-2 border-red-200 rounded-xl animate-scale-in">
|
||||
<AlertCircle className="h-5 w-5 text-red-600 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-red-600 font-medium">{error}</p>
|
||||
<div className="flex items-start gap-3 p-3 bg-destructive/10 border border-destructive/20 rounded-lg">
|
||||
<AlertCircle className="h-5 w-5 text-destructive flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit Button */}
|
||||
{/* Submit button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full py-4 px-6 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-xl font-bold text-lg
|
||||
shadow-lg shadow-blue-500/50 hover:shadow-xl hover:shadow-blue-500/60 hover:scale-[1.02]
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-500/50
|
||||
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100
|
||||
transition-all duration-200
|
||||
relative overflow-hidden group
|
||||
animate-slide-in-right"
|
||||
style={{ animationDelay: '0.3s' }}
|
||||
className="w-full py-2.5 px-4 bg-primary text-primary-foreground rounded-lg font-medium
|
||||
hover:bg-primary/90
|
||||
focus:outline-none focus:ring-2 focus:ring-primary/50 focus:ring-offset-2
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
transition-colors"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-700 to-purple-700 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
<span className="relative flex items-center justify-center gap-2">
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-6 h-6 animate-spin" />
|
||||
<span>登入中...</span>
|
||||
</>
|
||||
) : (
|
||||
<span>立即登入</span>
|
||||
)}
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
{t('auth.loggingIn')}
|
||||
</span>
|
||||
) : (
|
||||
t('auth.loginButton')
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-8 text-center animate-fade-in" style={{ animationDelay: '0.4s' }}>
|
||||
<p className="text-xs text-gray-500">
|
||||
Powered by <span className="font-bold text-gray-700">AI Technology</span>
|
||||
{/* Supported formats info */}
|
||||
<p className="text-center text-xs text-muted-foreground mt-6">
|
||||
{t('auth.supportedFormats')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Change: 前端 UX 簡化與 i18n 英文支援
|
||||
|
||||
## Why
|
||||
|
||||
目前登入頁面設計過於花俏(漸層動畫、浮動光球、脈衝效果),與內部頁面的專業簡約風格不一致。此外,頁面包含不實宣傳文案(如「99% 準確率」、「企業級加密」等未經證實的聲明)。系統目前僅支援繁體中文,缺乏多語言支援。
|
||||
|
||||
## What Changes
|
||||
|
||||
### 1. LoginPage 簡化
|
||||
- 移除花俏動畫效果(浮動光球、網格圖案、脈衝動畫)
|
||||
- 移除漸層背景,改用簡潔單色背景
|
||||
- 移除不實宣傳區塊(「為什麼選擇我們」、統計數據卡片)
|
||||
- 統一登入頁與內部頁面視覺風格
|
||||
|
||||
### 2. 文案修正
|
||||
- 移除誇大宣稱(「99% 準確率」、「閃電般快速」、「企業級加密」)
|
||||
- 改用務實功能描述
|
||||
|
||||
### 3. i18n 擴充
|
||||
- 新增英文 (en-US) 翻譯檔案
|
||||
- 新增語言切換功能元件
|
||||
- 儲存使用者語言偏好至 localStorage
|
||||
- 將語言切換器整合至 Layout 頂部欄
|
||||
|
||||
### 4. 整體風格統一
|
||||
- 確保所有頁面使用一致的設計語言
|
||||
- 遵循專業簡約風格準則
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: frontend-ui (新增)
|
||||
- Affected code:
|
||||
- `frontend/src/pages/LoginPage.tsx` - 重新設計
|
||||
- `frontend/src/components/Layout.tsx` - 新增語言切換器
|
||||
- `frontend/src/i18n/index.ts` - 擴充多語言設定
|
||||
- `frontend/src/i18n/locales/en-US.json` - 新增英文翻譯
|
||||
- `frontend/src/i18n/locales/zh-TW.json` - 補充缺少的翻譯鍵
|
||||
- `frontend/src/components/LanguageSwitcher.tsx` - 新增元件
|
||||
@@ -0,0 +1,89 @@
|
||||
# Frontend UI Specification
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Minimal Login Page Design
|
||||
|
||||
The login page SHALL use a professional minimal design style that is consistent with the rest of the application.
|
||||
|
||||
The login page SHALL NOT include:
|
||||
- Animated gradient backgrounds
|
||||
- Floating decorative elements (orbs, particles)
|
||||
- Pulsing or floating animations
|
||||
- Marketing claims or statistics
|
||||
- Feature promotion sections
|
||||
|
||||
The login page SHALL include:
|
||||
- Centered login form with clean white card
|
||||
- Application logo and name
|
||||
- Username and password input fields
|
||||
- Login button with loading state
|
||||
- Error message display area
|
||||
- Simple solid color background
|
||||
|
||||
#### Scenario: Login page renders with minimal design
|
||||
- **WHEN** user navigates to the login page
|
||||
- **THEN** the page displays a centered login form
|
||||
- **AND** no animated decorative elements are visible
|
||||
- **AND** no marketing content is displayed
|
||||
|
||||
#### Scenario: Login form visual consistency
|
||||
- **WHEN** comparing login page to internal pages
|
||||
- **THEN** the visual style (colors, typography, spacing) is consistent
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Multi-language Support
|
||||
|
||||
The application SHALL support multiple languages with user-selectable language preference.
|
||||
|
||||
Supported languages:
|
||||
- Traditional Chinese (zh-TW) - Default
|
||||
- English (en-US)
|
||||
|
||||
The language selection SHALL be persisted in localStorage and restored on page reload.
|
||||
|
||||
#### Scenario: Language switcher available
|
||||
- **WHEN** user is logged in and viewing any page
|
||||
- **THEN** a language switcher component is visible in the top navigation bar
|
||||
|
||||
#### Scenario: Switch to English
|
||||
- **WHEN** user selects English from the language switcher
|
||||
- **THEN** all UI text immediately changes to English
|
||||
- **AND** the preference is saved to localStorage
|
||||
|
||||
#### Scenario: Switch to Traditional Chinese
|
||||
- **WHEN** user selects Traditional Chinese from the language switcher
|
||||
- **THEN** all UI text immediately changes to Traditional Chinese
|
||||
- **AND** the preference is saved to localStorage
|
||||
|
||||
#### Scenario: Language preference persistence
|
||||
- **WHEN** user has previously selected a language preference
|
||||
- **AND** user reloads the page or returns later
|
||||
- **THEN** the application displays in the previously selected language
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Accurate Product Description
|
||||
|
||||
All user-facing text SHALL accurately describe the product capabilities without exaggeration.
|
||||
|
||||
The application SHALL NOT display:
|
||||
- Unverified accuracy percentages (e.g., "99% accuracy")
|
||||
- Superlative marketing claims (e.g., "lightning fast", "enterprise-grade")
|
||||
- Unsubstantiated statistics
|
||||
- Comparative claims without evidence
|
||||
|
||||
The application MAY display:
|
||||
- Factual feature descriptions
|
||||
- Supported file formats
|
||||
- Authentication method information
|
||||
|
||||
#### Scenario: Login page displays factual information
|
||||
- **WHEN** user views the login page
|
||||
- **THEN** only factual product information is displayed
|
||||
- **AND** no unverified claims are present
|
||||
|
||||
#### Scenario: Feature descriptions are accurate
|
||||
- **WHEN** any page describes product features
|
||||
- **THEN** the descriptions are factual and verifiable
|
||||
@@ -0,0 +1,29 @@
|
||||
# Tasks: 前端 UX 簡化與 i18n 英文支援
|
||||
|
||||
## 1. LoginPage 簡化
|
||||
|
||||
- [x] 1.1 移除動畫背景元素(浮動光球、網格圖案、脈衝效果)
|
||||
- [x] 1.2 替換漸層背景為簡潔單色背景
|
||||
- [x] 1.3 移除左側宣傳區塊(「為什麼選擇我們」、功能特色、統計數據)
|
||||
- [x] 1.4 重新設計登入表單區塊,採用居中簡約版面
|
||||
- [x] 1.5 移除不必要的動畫 class(animate-float, animate-slide-in-left 等)
|
||||
|
||||
## 2. i18n 英文支援
|
||||
|
||||
- [x] 2.1 建立 `frontend/src/i18n/locales/en-US.json` 英文翻譯檔
|
||||
- [x] 2.2 更新 `frontend/src/i18n/index.ts` 支援多語言切換
|
||||
- [x] 2.3 補充 `zh-TW.json` 缺少的翻譯鍵(登入頁相關)
|
||||
|
||||
## 3. 語言切換功能
|
||||
|
||||
- [x] 3.1 建立 `frontend/src/components/LanguageSwitcher.tsx` 元件
|
||||
- [x] 3.2 整合語言切換器至 `Layout.tsx` 頂部欄
|
||||
- [x] 3.3 實作語言偏好 localStorage 持久化
|
||||
- [x] 3.4 確保語言切換即時生效(無需重新載入頁面)
|
||||
|
||||
## 4. 測試與驗證
|
||||
|
||||
- [x] 4.1 驗證 LoginPage 在不同螢幕尺寸的顯示效果
|
||||
- [x] 4.2 驗證中英文切換功能正常運作
|
||||
- [x] 4.3 驗證語言偏好在頁面重新載入後保持
|
||||
- [x] 4.4 檢查所有頁面的翻譯完整性
|
||||
91
openspec/specs/frontend-ui/spec.md
Normal file
91
openspec/specs/frontend-ui/spec.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# frontend-ui Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change refactor-frontend-ux-i18n. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: Minimal Login Page Design
|
||||
|
||||
The login page SHALL use a professional minimal design style that is consistent with the rest of the application.
|
||||
|
||||
The login page SHALL NOT include:
|
||||
- Animated gradient backgrounds
|
||||
- Floating decorative elements (orbs, particles)
|
||||
- Pulsing or floating animations
|
||||
- Marketing claims or statistics
|
||||
- Feature promotion sections
|
||||
|
||||
The login page SHALL include:
|
||||
- Centered login form with clean white card
|
||||
- Application logo and name
|
||||
- Username and password input fields
|
||||
- Login button with loading state
|
||||
- Error message display area
|
||||
- Simple solid color background
|
||||
|
||||
#### Scenario: Login page renders with minimal design
|
||||
- **WHEN** user navigates to the login page
|
||||
- **THEN** the page displays a centered login form
|
||||
- **AND** no animated decorative elements are visible
|
||||
- **AND** no marketing content is displayed
|
||||
|
||||
#### Scenario: Login form visual consistency
|
||||
- **WHEN** comparing login page to internal pages
|
||||
- **THEN** the visual style (colors, typography, spacing) is consistent
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Multi-language Support
|
||||
|
||||
The application SHALL support multiple languages with user-selectable language preference.
|
||||
|
||||
Supported languages:
|
||||
- Traditional Chinese (zh-TW) - Default
|
||||
- English (en-US)
|
||||
|
||||
The language selection SHALL be persisted in localStorage and restored on page reload.
|
||||
|
||||
#### Scenario: Language switcher available
|
||||
- **WHEN** user is logged in and viewing any page
|
||||
- **THEN** a language switcher component is visible in the top navigation bar
|
||||
|
||||
#### Scenario: Switch to English
|
||||
- **WHEN** user selects English from the language switcher
|
||||
- **THEN** all UI text immediately changes to English
|
||||
- **AND** the preference is saved to localStorage
|
||||
|
||||
#### Scenario: Switch to Traditional Chinese
|
||||
- **WHEN** user selects Traditional Chinese from the language switcher
|
||||
- **THEN** all UI text immediately changes to Traditional Chinese
|
||||
- **AND** the preference is saved to localStorage
|
||||
|
||||
#### Scenario: Language preference persistence
|
||||
- **WHEN** user has previously selected a language preference
|
||||
- **AND** user reloads the page or returns later
|
||||
- **THEN** the application displays in the previously selected language
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Accurate Product Description
|
||||
|
||||
All user-facing text SHALL accurately describe the product capabilities without exaggeration.
|
||||
|
||||
The application SHALL NOT display:
|
||||
- Unverified accuracy percentages (e.g., "99% accuracy")
|
||||
- Superlative marketing claims (e.g., "lightning fast", "enterprise-grade")
|
||||
- Unsubstantiated statistics
|
||||
- Comparative claims without evidence
|
||||
|
||||
The application MAY display:
|
||||
- Factual feature descriptions
|
||||
- Supported file formats
|
||||
- Authentication method information
|
||||
|
||||
#### Scenario: Login page displays factual information
|
||||
- **WHEN** user views the login page
|
||||
- **THEN** only factual product information is displayed
|
||||
- **AND** no unverified claims are present
|
||||
|
||||
#### Scenario: Feature descriptions are accurate
|
||||
- **WHEN** any page describes product features
|
||||
- **THEN** the descriptions are factual and verifiable
|
||||
|
||||
Reference in New Issue
Block a user