mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-13 20:57:41 +08:00
refactor: migrate UI to Tailwind CSS for a modernized design system.
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user