Files
OCR/frontend/src/components/Layout.tsx
egg 65abd51d60 feat: add translation billing stats and remove Export/Settings pages
- Add TranslationLog model to track translation API usage per task
- Integrate Dify API actual price (total_price) into translation stats
- Display translation statistics in admin dashboard with per-task costs
- Remove unused Export and Settings pages to simplify frontend
- Add GET /api/v2/admin/translation-stats endpoint

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 17:38:12 +08:00

128 lines
4.8 KiB
TypeScript

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,
FileText,
Activity,
LogOut,
LayoutDashboard,
ChevronRight,
History,
Shield
} 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)
// Check if user is admin
const isAdmin = user?.email === 'ymirliu@panjit.com.tw'
const handleLogout = async () => {
try {
await apiClientV2.logout()
} catch (error) {
console.error('Logout error:', error)
} finally {
logout()
navigate('/login')
}
}
const navLinks = [
{ to: '/upload', label: t('nav.upload'), icon: Upload, description: '上傳檔案', adminOnly: false },
{ to: '/processing', label: t('nav.processing'), icon: Activity, description: '處理進度', adminOnly: false },
{ to: '/results', label: t('nav.results'), icon: FileText, description: '查看結果', adminOnly: false },
{ to: '/tasks', label: '任務歷史', icon: History, description: '查看任務記錄', adminOnly: false },
{ to: '/admin', label: '管理員儀表板', icon: Shield, description: '系統管理', adminOnly: true },
]
// Filter nav links based on admin status
const visibleNavLinks = navLinks.filter(link => !link.adminOnly || isAdmin)
return (
<div className="flex h-screen bg-background overflow-hidden">
{/* Sidebar */}
<aside className="w-64 bg-sidebar text-sidebar-foreground flex flex-col border-r border-border/20">
{/* Logo */}
<div className="px-6 py-5 border-b border-border/20">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-primary/20 flex items-center justify-center">
<LayoutDashboard className="w-5 h-5 text-primary" />
</div>
<div>
<h1 className="font-semibold text-lg">{t('app.title')}</h1>
<p className="text-xs text-sidebar-foreground/60">{t('app.subtitle')}</p>
</div>
</div>
</div>
{/* Navigation */}
<nav className="flex-1 px-3 py-6 space-y-1 overflow-y-auto scrollbar-thin">
{visibleNavLinks.map((link) => (
<NavLink
key={link.to}
to={link.to}
className={({ isActive }) =>
`group flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors ${
isActive
? 'bg-primary text-white'
: 'text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-white/5'
}`
}
>
{({ isActive }) => (
<>
<link.icon className={`w-5 h-5 flex-shrink-0 ${isActive ? 'text-white' : ''}`} />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium">{link.label}</div>
<div className="text-xs opacity-60 truncate">{link.description}</div>
</div>
{isActive && <ChevronRight className="w-4 h-4 flex-shrink-0" />}
</>
)}
</NavLink>
))}
</nav>
{/* User section */}
<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">
<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>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium truncate">{user.displayName || user.username}</div>
<div className="text-xs text-sidebar-foreground/60 truncate">{user.email || user.username}</div>
</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"
>
<LogOut className="w-5 h-5" />
<span>{t('nav.logout')}</span>
</button>
</div>
</aside>
{/* Main content area */}
<main className="flex-1 overflow-y-auto bg-background p-6 scrollbar-thin">
<div className="max-w-7xl mx-auto">
<Outlet />
</div>
</main>
</div>
)
}