mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-21 16:37:47 +08:00
feat: Implement DeepSeek integration, refactor model adapters for streaming and tool calls, enhance admin and account management, and introduce new UI features for settings, API testing, and Vercel sync.
This commit is contained in:
198
webui/src/layout/DashboardShell.jsx
Normal file
198
webui/src/layout/DashboardShell.jsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Upload,
|
||||
Cloud,
|
||||
Settings as SettingsIcon,
|
||||
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 Settings from '../components/Settings'
|
||||
import LanguageToggle from '../components/LanguageToggle'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
export default function DashboardShell({ token, onLogout, config, fetchConfig, showMessage, message, onForceLogout, isVercel }) {
|
||||
const { t } = useI18n()
|
||||
const [activeTab, setActiveTab] = useState('accounts')
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
|
||||
const navItems = [
|
||||
{ id: 'accounts', label: t('nav.accounts.label'), icon: Users, description: t('nav.accounts.desc') },
|
||||
{ id: 'test', label: t('nav.test.label'), icon: Server, description: t('nav.test.desc') },
|
||||
{ id: 'import', label: t('nav.import.label'), icon: Upload, description: t('nav.import.desc') },
|
||||
{ id: 'vercel', label: t('nav.vercel.label'), icon: Cloud, description: t('nav.vercel.desc') },
|
||||
{ id: 'settings', label: t('nav.settings.label'), icon: SettingsIcon, description: t('nav.settings.desc') },
|
||||
]
|
||||
|
||||
const authFetch = useCallback(async (url, options = {}) => {
|
||||
const headers = {
|
||||
...options.headers,
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
const res = await fetch(url, { ...options, headers })
|
||||
|
||||
if (res.status === 401) {
|
||||
onLogout()
|
||||
throw new Error(t('auth.expired'))
|
||||
}
|
||||
return res
|
||||
}, [onLogout, t, token])
|
||||
|
||||
const renderTab = () => {
|
||||
switch (activeTab) {
|
||||
case 'accounts':
|
||||
return <AccountManager config={config} onRefresh={fetchConfig} onMessage={showMessage} authFetch={authFetch} />
|
||||
case 'test':
|
||||
return <ApiTester config={config} onMessage={showMessage} authFetch={authFetch} />
|
||||
case 'import':
|
||||
return <BatchImport onRefresh={fetchConfig} onMessage={showMessage} authFetch={authFetch} />
|
||||
case 'vercel':
|
||||
return <VercelSync onMessage={showMessage} authFetch={authFetch} isVercel={isVercel} />
|
||||
case 'settings':
|
||||
return <Settings onRefresh={fetchConfig} onMessage={showMessage} authFetch={authFetch} onForceLogout={onForceLogout} isVercel={isVercel} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-background overflow-hidden text-foreground">
|
||||
{sidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-background/80 backdrop-blur-sm z-40 lg:hidden"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<aside className={clsx(
|
||||
"fixed lg:static inset-y-0 left-0 z-50 w-64 bg-card border-r border-border transition-transform duration-300 ease-in-out lg:transform-none flex flex-col shadow-2xl lg:shadow-none",
|
||||
sidebarOpen ? "translate-x-0" : "-translate-x-full"
|
||||
)}>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-2.5 font-bold text-xl text-foreground tracking-tight">
|
||||
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center text-primary-foreground shadow-lg shadow-primary/20">
|
||||
<LayoutDashboard className="w-5 h-5" />
|
||||
</div>
|
||||
<span>DS2API</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<p className="text-[10px] text-muted-foreground font-semibold tracking-[0.1em] uppercase opacity-60 px-1">{t('sidebar.onlineAdminConsole')}</p>
|
||||
<LanguageToggle />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 px-3 space-y-1 overflow-y-auto pt-2">
|
||||
{navItems.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 border",
|
||||
isActive
|
||||
? "bg-secondary text-primary border-border shadow-sm"
|
||||
: "text-muted-foreground border-transparent hover:bg-secondary/80 hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<Icon className={clsx("w-4 h-4 transition-colors", isActive ? "text-primary" : "text-muted-foreground group-hover:text-foreground")} />
|
||||
<span className="flex-1 text-left">{item.label}</span>
|
||||
{isActive && <div className="w-1.5 h-1.5 rounded-full bg-primary" />}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-border bg-card">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between text-sm px-1">
|
||||
<span className="text-muted-foreground font-semibold text-[10px] uppercase tracking-wider">{t('sidebar.systemStatus')}</span>
|
||||
<span className="flex items-center gap-1.5 text-[10px] font-bold text-emerald-500 bg-emerald-500/10 px-2 py-0.5 rounded-full border border-emerald-500/20">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
|
||||
{t('sidebar.statusOnline')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="bg-background rounded-lg p-3 border border-border shadow-sm">
|
||||
<div className="text-[9px] text-muted-foreground font-bold uppercase tracking-wider mb-0.5 opacity-70">{t('sidebar.accounts')}</div>
|
||||
<div className="text-lg font-bold text-foreground leading-tight">{config.accounts?.length || 0}</div>
|
||||
</div>
|
||||
<div className="bg-background rounded-lg p-3 border border-border shadow-sm">
|
||||
<div className="text-[9px] text-muted-foreground font-bold uppercase tracking-wider mb-0.5 opacity-70">{t('sidebar.keys')}</div>
|
||||
<div className="text-lg font-bold text-foreground">{config.keys?.length || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className="w-full h-10 flex items-center justify-center gap-2 rounded-lg border border-border text-xs font-medium text-muted-foreground hover:bg-destructive/10 hover:text-destructive hover:border-destructive/20 transition-all"
|
||||
>
|
||||
<LogOut className="w-3.5 h-3.5" />
|
||||
{t('sidebar.signOut')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="flex-1 flex flex-col min-w-0 overflow-hidden relative">
|
||||
<header className="lg:hidden h-14 flex items-center justify-between px-4 border-b border-border bg-card">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-6 h-6 rounded bg-primary flex items-center justify-center text-primary-foreground text-[10px]">
|
||||
<LayoutDashboard className="w-3.5 h-3.5" />
|
||||
</div>
|
||||
<span className="font-semibold text-sm">DS2API</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<LanguageToggle />
|
||||
<button
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
className="p-2 -mr-2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Menu className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-auto bg-background p-4 lg:p-10">
|
||||
<div className="max-w-6xl mx-auto space-y-4 lg:space-y-6">
|
||||
<div className="hidden lg:block mb-8">
|
||||
<h1 className="text-3xl font-bold tracking-tight mb-2">
|
||||
{navItems.find(n => n.id === activeTab)?.label}
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{navItems.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">
|
||||
{renderTab()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user