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 { useTranslation } from 'react-i18next'
|
||||||
import { useAuthStore } from '@/store/authStore'
|
import { useAuthStore } from '@/store/authStore'
|
||||||
import { apiClientV2 } from '@/services/apiV2'
|
import { apiClientV2 } from '@/services/apiV2'
|
||||||
|
import LanguageSwitcher from '@/components/LanguageSwitcher'
|
||||||
import {
|
import {
|
||||||
Upload,
|
Upload,
|
||||||
Settings,
|
Settings,
|
||||||
@@ -11,8 +12,6 @@ import {
|
|||||||
LogOut,
|
LogOut,
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Bell,
|
|
||||||
Search,
|
|
||||||
History,
|
History,
|
||||||
Shield
|
Shield
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
@@ -96,9 +95,9 @@ export default function Layout() {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* User section */}
|
{/* 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 && (
|
{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">
|
<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()}
|
{user.username.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
@@ -108,6 +107,9 @@ export default function Layout() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="px-3">
|
||||||
|
<LanguageSwitcher />
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
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"
|
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>
|
</aside>
|
||||||
|
|
||||||
{/* Main content area */}
|
{/* Main content area */}
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<main className="flex-1 overflow-y-auto bg-background p-6 scrollbar-thin">
|
||||||
{/* Top bar */}
|
<div className="max-w-7xl mx-auto">
|
||||||
<header className="bg-card border-b border-border px-6 py-4">
|
<Outlet />
|
||||||
<div className="flex items-center justify-between">
|
</div>
|
||||||
{/* Search bar - placeholder for future use */}
|
</main>
|
||||||
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,25 @@
|
|||||||
import i18n from 'i18next'
|
import i18n from 'i18next'
|
||||||
import { initReactI18next } from 'react-i18next'
|
import { initReactI18next } from 'react-i18next'
|
||||||
import zhTW from './locales/zh-TW.json'
|
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
|
* i18n Configuration
|
||||||
|
* Supported languages: Traditional Chinese (zh-TW), English (en-US)
|
||||||
* Default language: Traditional Chinese (zh-TW)
|
* Default language: Traditional Chinese (zh-TW)
|
||||||
*/
|
*/
|
||||||
i18n.use(initReactI18next).init({
|
i18n.use(initReactI18next).init({
|
||||||
@@ -11,8 +27,11 @@ i18n.use(initReactI18next).init({
|
|||||||
'zh-TW': {
|
'zh-TW': {
|
||||||
translation: zhTW,
|
translation: zhTW,
|
||||||
},
|
},
|
||||||
|
'en-US': {
|
||||||
|
translation: enUS,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
lng: 'zh-TW',
|
lng: getSavedLanguage(),
|
||||||
fallbackLng: 'zh-TW',
|
fallbackLng: 'zh-TW',
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false,
|
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": "結果檢視",
|
"results": "結果檢視",
|
||||||
"export": "匯出",
|
"export": "匯出",
|
||||||
"settings": "設定",
|
"settings": "設定",
|
||||||
"logout": "登出"
|
"logout": "登出",
|
||||||
|
"taskHistory": "任務歷史",
|
||||||
|
"adminDashboard": "管理員儀表板"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"login": "登入",
|
"login": "登入",
|
||||||
@@ -17,7 +19,12 @@
|
|||||||
"password": "密碼",
|
"password": "密碼",
|
||||||
"loginButton": "登入",
|
"loginButton": "登入",
|
||||||
"loginError": "登入失敗,請檢查帳號密碼",
|
"loginError": "登入失敗,請檢查帳號密碼",
|
||||||
"welcomeBack": "歡迎回來"
|
"welcomeBack": "歡迎回來",
|
||||||
|
"loginPrompt": "登入以使用 OCR 服務",
|
||||||
|
"loggingIn": "登入中...",
|
||||||
|
"usernamePlaceholder": "輸入您的使用者名稱",
|
||||||
|
"passwordPlaceholder": "輸入您的密碼",
|
||||||
|
"supportedFormats": "支援格式:PDF、圖片、Office 文件"
|
||||||
},
|
},
|
||||||
"upload": {
|
"upload": {
|
||||||
"title": "上傳檔案",
|
"title": "上傳檔案",
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { useNavigate } from 'react-router-dom'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useAuthStore } from '@/store/authStore'
|
import { useAuthStore } from '@/store/authStore'
|
||||||
import { apiClientV2 } from '@/services/apiV2'
|
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() {
|
export default function LoginPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -20,10 +21,8 @@ export default function LoginPage() {
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use V2 API with external authentication
|
|
||||||
const response = await apiClientV2.login({ username, password })
|
const response = await apiClientV2.login({ username, password })
|
||||||
|
|
||||||
// Store user info from V2 API response
|
|
||||||
setUser({
|
setUser({
|
||||||
id: response.user.id,
|
id: response.user.id,
|
||||||
username: response.user.email,
|
username: response.user.email,
|
||||||
@@ -47,196 +46,114 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen relative overflow-hidden">
|
<div className="min-h-screen bg-background flex flex-col">
|
||||||
{/* Full-screen Animated Background */}
|
{/* Top bar with language switcher */}
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-600 via-purple-600 to-pink-500">
|
<div className="flex justify-end p-4">
|
||||||
{/* Animated overlay */}
|
<LanguageSwitcher />
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
{/* Content Container */}
|
{/* Centered login form */}
|
||||||
<div className="relative z-10 min-h-screen flex items-center justify-center p-4">
|
<div className="flex-1 flex items-center justify-center px-4 pb-16">
|
||||||
<div className="w-full max-w-6xl flex flex-col lg:flex-row gap-8 items-center">
|
<div className="w-full max-w-md">
|
||||||
|
{/* Logo and title */}
|
||||||
{/* Left Side - Branding */}
|
<div className="text-center mb-8">
|
||||||
<div className="flex-1 text-white text-center lg:text-left animate-fade-in">
|
<div className="inline-flex items-center justify-center w-16 h-16 rounded-xl bg-primary/10 mb-4">
|
||||||
{/* Logo */}
|
<LayoutDashboard className="w-8 h-8 text-primary" />
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</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>
|
|
||||||
</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>
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* Right Side - Login Form */}
|
{/* Login card */}
|
||||||
<div className="w-full lg:w-auto lg:min-w-[480px] animate-scale-in">
|
<div className="bg-card rounded-xl border border-border p-8 shadow-sm">
|
||||||
<div className="bg-white/95 backdrop-blur-xl rounded-3xl p-8 md:p-10 shadow-2xl border border-white/20">
|
<div className="mb-6">
|
||||||
<div className="mb-8">
|
<h2 className="text-xl font-semibold text-foreground">{t('auth.welcomeBack')}</h2>
|
||||||
<h2 className="text-3xl font-bold text-gray-900 mb-2">歡迎回來</h2>
|
<p className="text-sm text-muted-foreground mt-1">{t('auth.loginPrompt')}</p>
|
||||||
<p className="text-gray-600">登入以開始使用 OCR 服務</p>
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
{/* Username */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="username" className="block text-sm font-medium text-foreground">
|
||||||
|
{t('auth.username')}
|
||||||
|
</label>
|
||||||
|
<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-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>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
{/* Password */}
|
||||||
{/* Username */}
|
<div className="space-y-2">
|
||||||
<div className="space-y-2 animate-slide-in-right" style={{ animationDelay: '0.1s' }}>
|
<label htmlFor="password" className="block text-sm font-medium text-foreground">
|
||||||
<label htmlFor="username" className="block text-sm font-semibold text-gray-700">
|
{t('auth.password')}
|
||||||
使用者名稱
|
</label>
|
||||||
</label>
|
<div className="relative">
|
||||||
<div className="relative group">
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
<Lock className="h-5 w-5 text-muted-foreground" />
|
||||||
<User className="h-5 w-5 text-gray-400 group-focus-within:text-blue-600 transition-colors" />
|
|
||||||
</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="輸入您的用戶名"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => 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={t('auth.passwordPlaceholder')}
|
||||||
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Password */}
|
{/* Error message */}
|
||||||
<div className="space-y-2 animate-slide-in-right" style={{ animationDelay: '0.2s' }}>
|
{error && (
|
||||||
<label htmlFor="password" className="block text-sm font-semibold text-gray-700">
|
<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" />
|
||||||
</label>
|
<p className="text-sm text-destructive">{error}</p>
|
||||||
<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>
|
|
||||||
<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="輸入您的密碼"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Error */}
|
{/* Submit button */}
|
||||||
{error && (
|
<button
|
||||||
<div className="flex items-start gap-3 p-4 bg-red-50 border-2 border-red-200 rounded-xl animate-scale-in">
|
type="submit"
|
||||||
<AlertCircle className="h-5 w-5 text-red-600 flex-shrink-0 mt-0.5" />
|
disabled={loading}
|
||||||
<p className="text-sm text-red-600 font-medium">{error}</p>
|
className="w-full py-2.5 px-4 bg-primary text-primary-foreground rounded-lg font-medium
|
||||||
</div>
|
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
|
||||||
{/* Submit Button */}
|
transition-colors"
|
||||||
<button
|
>
|
||||||
type="submit"
|
{loading ? (
|
||||||
disabled={loading}
|
<span className="flex items-center justify-center gap-2">
|
||||||
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
|
<Loader2 className="w-5 h-5 animate-spin" />
|
||||||
shadow-lg shadow-blue-500/50 hover:shadow-xl hover:shadow-blue-500/60 hover:scale-[1.02]
|
{t('auth.loggingIn')}
|
||||||
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' }}
|
|
||||||
>
|
|
||||||
<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>
|
</span>
|
||||||
</button>
|
) : (
|
||||||
</form>
|
t('auth.loginButton')
|
||||||
|
)}
|
||||||
{/* Footer */}
|
</button>
|
||||||
<div className="mt-8 text-center animate-fade-in" style={{ animationDelay: '0.4s' }}>
|
</form>
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
Powered by <span className="font-bold text-gray-700">AI Technology</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 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