feat:重构UI

This commit is contained in:
2025-12-26 16:03:12 +08:00
parent 1429e0e66a
commit abcbe3cddc
67 changed files with 12170 additions and 515 deletions

View File

@@ -1,7 +1,15 @@
'use client'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { usePathname, useRouter } from 'next/navigation'
import { useEffect, useMemo, useState } from 'react'
import { BarChart3, Bot, LayoutDashboard, LogOut, Settings } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { ADMIN_UNAUTHORIZED_EVENT } from '@/lib/admin-api'
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'
export default function AdminLayout({
children,
@@ -9,43 +17,166 @@ export default function AdminLayout({
children: React.ReactNode
}) {
const router = useRouter()
const pathname = usePathname()
const [authChecked, setAuthChecked] = useState(false)
const [adminName, setAdminName] = useState<string>('')
const navItems = useMemo(
() => [
{ href: '/admin', label: '概览', icon: LayoutDashboard },
{ href: '/admin/providers', label: 'AI 配置', icon: Bot },
{ href: '/admin/stats', label: '统计', icon: BarChart3 },
{ href: '/admin/settings', label: '设置', icon: Settings },
],
[]
)
const getToken = () => localStorage.getItem('admin_token') || ''
const fetchMe = async () => {
const token = getToken()
if (!token) return
try {
const res = await fetch(`${API_BASE}/api/v1/admin/me`, {
headers: { Authorization: `Bearer ${token}` },
})
if (res.status === 401) throw new Error('unauthorized')
if (!res.ok) return
const data = await res.json()
setAdminName(data.username || data.sub || '')
} catch {
localStorage.removeItem('admin_token')
router.replace('/login')
}
}
useEffect(() => {
const token = getToken()
if (!token) {
router.replace('/login')
return
}
setAuthChecked(true)
fetchMe()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
const onUnauthorized = () => {
localStorage.removeItem('admin_token')
router.replace('/login')
}
window.addEventListener(ADMIN_UNAUTHORIZED_EVENT, onUnauthorized)
return () => window.removeEventListener(ADMIN_UNAUTHORIZED_EVENT, onUnauthorized)
}, [router])
const handleLogout = () => {
localStorage.removeItem('admin_token')
router.push('/login')
router.replace('/login')
}
if (!authChecked) {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="text-sm text-muted-foreground">...</div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-100">
<nav className="bg-white shadow">
<div className="max-w-7xl mx-auto px-4">
<div className="flex justify-between h-16">
<div className="flex items-center space-x-8">
<span className="text-xl font-bold"></span>
<Link href="/admin" className="text-gray-600 hover:text-gray-900">
</Link>
<Link href="/admin/providers" className="text-gray-600 hover:text-gray-900">
AI
</Link>
<Link href="/admin/stats" className="text-gray-600 hover:text-gray-900">
</Link>
</div>
<div className="flex items-center">
<button
onClick={handleLogout}
className="text-gray-600 hover:text-gray-900"
>
退
</button>
</div>
<div className="min-h-screen bg-background">
<div className="flex min-h-screen">
{/* Sidebar */}
<aside className="hidden md:flex w-64 flex-col border-r bg-card">
<div className="h-14 px-4 flex items-center border-b">
<Link href="/" className="font-semibold tracking-tight">
AI Translator
</Link>
<span className="ml-2 text-xs text-muted-foreground">Admin</span>
</div>
<nav className="flex-1 p-2 space-y-1">
{navItems.map((item) => {
const Icon = item.icon
const active =
pathname === item.href ||
(item.href !== '/admin' && pathname.startsWith(item.href))
return (
<Link
key={item.href}
href={item.href}
className={cn(
'flex items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors',
active
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
)}
>
<Icon className="h-4 w-4" />
{item.label}
</Link>
)
})}
</nav>
<div className="p-2 border-t">
<Button variant="ghost" className="w-full justify-start" onClick={handleLogout}>
<LogOut className="mr-2 h-4 w-4" />
退
</Button>
</div>
</aside>
{/* Main */}
<div className="flex-1 flex flex-col">
<header className="h-14 border-b bg-card/50 backdrop-blur-sm">
<div className="h-full container flex items-center justify-between">
<div className="md:hidden flex items-center gap-3 overflow-x-auto">
<div className="font-semibold shrink-0"></div>
<div className="flex items-center gap-1">
{navItems.map((item) => {
const active =
pathname === item.href ||
(item.href !== '/admin' && pathname.startsWith(item.href))
return (
<Link
key={item.href}
href={item.href}
className={cn(
'rounded-md px-2 py-1 text-sm transition-colors',
active
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
)}
>
{item.label}
</Link>
)
})}
</div>
</div>
<div className="flex items-center gap-3">
{adminName && (
<div className="text-sm text-muted-foreground">
{adminName}
</div>
)}
<div className="md:hidden">
<Button variant="ghost" size="sm" onClick={handleLogout}>
<LogOut className="mr-2 h-4 w-4" />
退
</Button>
</div>
</div>
</div>
</header>
<main className="flex-1">
<div className="container py-6">{children}</div>
</main>
</div>
</nav>
<main className="max-w-7xl mx-auto py-6 px-4">
{children}
</main>
</div>
</div>
)
}