- 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>
128 lines
4.8 KiB
TypeScript
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>
|
|
)
|
|
}
|