refactor: migrate UI to Tailwind CSS for a modernized design system.

This commit is contained in:
CJACK
2026-02-01 05:33:50 +08:00
parent 1d1361616b
commit 1f6043ff70
12 changed files with 1087 additions and 1174 deletions

View File

@@ -1,15 +1,28 @@
import { useState, useEffect } from 'react'
import {
LayoutDashboard,
Key,
Upload,
Cloud,
LogOut,
Menu,
X,
Server,
Users
} from 'lucide-react'
import clsx from 'clsx'
import AccountManager from './components/AccountManager'
import ApiTester from './components/ApiTester'
import BatchImport from './components/BatchImport'
import VercelSync from './components/VercelSync'
import Login from './components/Login'
const TABS = [
{ id: 'accounts', label: '🔑 账号管理' },
{ id: 'test', label: '🧪 API 测试' },
{ id: 'import', label: '📦 批量导入' },
{ id: 'vercel', label: '☁️ Vercel 同步' },
const NAV_ITEMS = [
{ id: 'accounts', label: '账号管理', icon: Users, description: '管理 DeepSeek 账号池' },
{ id: 'test', label: 'API 测试', icon: Server, description: '测试 API 连接与响应' },
{ id: 'import', label: '批量导入', icon: Upload, description: '批量导入账号配置' },
{ id: 'vercel', label: 'Vercel 同步', icon: Cloud, description: '同步配置到 Vercel' },
]
export default function App() {
@@ -19,16 +32,15 @@ export default function App() {
const [message, setMessage] = useState(null)
const [token, setToken] = useState(null)
const [authChecking, setAuthChecking] = useState(true)
const [sidebarOpen, setSidebarOpen] = useState(false)
// 检查已存储的 Token
useEffect(() => {
const checkAuth = async () => {
// 检查 localStorage 或 sessionStorage
const storedToken = localStorage.getItem('ds2api_token') || sessionStorage.getItem('ds2api_token')
const expiresAt = parseInt(localStorage.getItem('ds2api_token_expires') || sessionStorage.getItem('ds2api_token_expires') || '0')
if (storedToken && expiresAt > Date.now()) {
// 验证 token 是否有效
try {
const res = await fetch('/admin/verify', {
headers: { 'Authorization': `Bearer ${storedToken}` }
@@ -36,14 +48,9 @@ export default function App() {
if (res.ok) {
setToken(storedToken)
} else {
// Token 无效,清除
localStorage.removeItem('ds2api_token')
localStorage.removeItem('ds2api_token_expires')
sessionStorage.removeItem('ds2api_token')
sessionStorage.removeItem('ds2api_token_expires')
handleLogout()
}
} catch {
// 网络错误,保留 token 重试
setToken(storedToken)
}
}
@@ -52,7 +59,6 @@ export default function App() {
checkAuth()
}, [])
// 带认证的 fetch
const authFetch = async (url, options = {}) => {
const headers = {
...options.headers,
@@ -60,7 +66,6 @@ export default function App() {
}
const res = await fetch(url, { ...options, headers })
// 401 时自动登出
if (res.status === 401) {
handleLogout()
throw new Error('认证已过期,请重新登录')
@@ -123,27 +128,32 @@ export default function App() {
}
}
// 认证检查中
if (authChecking) {
return (
<div className="app">
<div className="login-container">
<div className="login-card">
<div className="empty-state">
<span className="loading"></span> 检查登录状态...
</div>
</div>
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="flex flex-col items-center gap-4">
<div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
<p className="text-muted-foreground animate-pulse">检查登录状态...</p>
</div>
</div>
)
}
// 未登录
if (!token) {
return (
<div className="app">
<div className="min-h-screen flex flex-col bg-background relative overflow-hidden">
{/* Background decorative elements */}
<div className="absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none z-0">
<div className="absolute top-[-10%] right-[-10%] w-[50%] h-[50%] bg-primary/5 rounded-full blur-[120px]"></div>
<div className="absolute bottom-[-10%] left-[-10%] w-[50%] h-[50%] bg-accent/5 rounded-full blur-[120px]"></div>
</div>
{message && (
<div className={`alert alert-${message.type}`}>
<div className={clsx(
"fixed top-4 right-4 z-50 px-4 py-3 rounded-lg shadow-lg border animate-in slide-in-from-top-2 fade-in",
message.type === 'error' ? "bg-destructive/10 border-destructive/20 text-destructive" :
"bg-primary/10 border-primary/20 text-primary"
)}>
{message.text}
</div>
)}
@@ -152,60 +162,133 @@ export default function App() {
)
}
// 已登录
return (
<div className="app">
<header className="header">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h1>DS2API Admin</h1>
<p>账号管理 · API 测试 · Vercel 部署</p>
</div>
<button className="btn btn-secondary btn-sm" onClick={handleLogout}>
🚪 登出
</button>
</div>
</header>
{message && (
<div className={`alert alert-${message.type}`}>
{message.text}
</div>
<div className="flex h-screen bg-background overflow-hidden text-foreground">
{/* Mobile Sidebar Overlay */}
{sidebarOpen && (
<div
className="fixed inset-0 bg-background/80 backdrop-blur-sm z-40 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
<div className="stats">
<div className="stat">
<div className="stat-value">{config.keys?.length || 0}</div>
<div className="stat-label">API Keys</div>
{/* Sidebar */}
<aside className={clsx(
"fixed lg:static inset-y-0 left-0 z-50 w-64 bg-card border-r border-border transition-transform duration-200 ease-in-out lg:transform-none flex flex-col",
sidebarOpen ? "translate-x-0" : "-translate-x-full"
)}>
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 font-bold text-xl text-primary">
<LayoutDashboard className="w-6 h-6" />
<span>DS2API Admin</span>
</div>
<p className="text-xs text-muted-foreground mt-1 ml-8">V1.0.0 Control Panel</p>
</div>
<div className="stat">
<div className="stat-value">{config.accounts?.length || 0}</div>
<div className="stat-label">账号</div>
</div>
</div>
<div className="tabs">
{TABS.map(tab => (
<nav className="flex-1 p-4 space-y-1 overflow-y-auto">
{NAV_ITEMS.map((item) => {
const Icon = item.icon
const isActive = activeTab === item.id
return (
<button
key={item.id}
onClick={() => {
setActiveTab(item.id)
setSidebarOpen(false)
}}
className={clsx(
"w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 group",
isActive
? "bg-primary text-primary-foreground shadow-md shadow-primary/20"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
)}
>
<Icon className={clsx("w-4 h-4", isActive ? "text-primary-foreground" : "text-muted-foreground group-hover:text-accent-foreground")} />
{item.label}
</button>
)
})}
</nav>
<div className="p-4 border-t border-border bg-card/50">
<div className="space-y-4">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">API Status</span>
<span className="flex items-center gap-1.5 text-xs font-medium text-green-500 bg-green-500/10 px-2 py-0.5 rounded-full">
<span className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse"></span>
Online
</span>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="bg-background rounded p-2 border border-border">
<div className="text-xs text-muted-foreground">Accounts</div>
<div className="text-lg font-bold">{config.accounts?.length || 0}</div>
</div>
<div className="bg-background rounded p-2 border border-border">
<div className="text-xs text-muted-foreground">Api Keys</div>
<div className="text-lg font-bold">{config.keys?.length || 0}</div>
</div>
</div>
<button
onClick={handleLogout}
className="w-full flex items-center justify-center gap-2 px-3 py-2 rounded-md border border-border text-sm font-medium text-muted-foreground hover:bg-destructive/10 hover:text-destructive hover:border-destructive/20 transition-colors"
>
<LogOut className="w-4 h-4" />
Sign Out
</button>
</div>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 flex flex-col min-w-0 overflow-hidden relative">
{/* Mobile Header */}
<header className="lg:hidden flex items-center justify-between p-4 border-b border-border bg-card">
<span className="font-semibold">{NAV_ITEMS.find(n => n.id === activeTab)?.label}</span>
<button
key={tab.id}
className={`tab ${activeTab === tab.id ? 'active' : ''}`}
onClick={() => setActiveTab(tab.id)}
onClick={() => setSidebarOpen(true)}
className="p-2 -mr-2 text-muted-foreground hover:text-foreground"
>
{tab.label}
<Menu className="w-5 h-5" />
</button>
))}
</div>
</header>
{loading ? (
<div className="card">
<div className="empty-state">
<span className="loading"></span> 加载中...
{/* Content Area */}
<div className="flex-1 overflow-auto bg-background/50 p-4 lg:p-8">
<div className="max-w-6xl mx-auto space-y-6">
<div className="hidden lg:block mb-8">
<h1 className="text-3xl font-bold tracking-tight mb-2">
{NAV_ITEMS.find(n => n.id === activeTab)?.label}
</h1>
<p className="text-muted-foreground">
{NAV_ITEMS.find(n => n.id === activeTab)?.description}
</p>
</div>
{message && (
<div className={clsx(
"p-4 rounded-lg border flex items-center gap-3 animate-in fade-in slide-in-from-top-2",
message.type === 'error' ? "bg-destructive/10 border-destructive/20 text-destructive" :
"bg-emerald-500/10 border-emerald-500/20 text-emerald-500"
)}>
{message.type === 'error' ? <X className="w-5 h-5" /> : <div className="w-5 h-5 rounded-full border-2 border-emerald-500 flex items-center justify-center text-[10px]"></div>}
{message.text}
</div>
)}
<div className="animate-in fade-in duration-500">
{loading ? (
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground">
<div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin mb-4"></div>
<p>Please wait while loading data...</p>
</div>
) : (
renderTab()
)}
</div>
</div>
</div>
) : (
renderTab()
)}
</main>
</div>
)
}