mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-04 00:15:28 +08:00
feat: Implement a modern flat theme with updated UI components and styling.
This commit is contained in:
@@ -104,10 +104,11 @@ def release_account(account: dict):
|
||||
# 从使用中移除
|
||||
if acc_id in in_use_accounts:
|
||||
del in_use_accounts[acc_id]
|
||||
|
||||
# 放回队尾
|
||||
account_queue.append(account)
|
||||
logger.debug(f"[release_account] 释放账号: {acc_id} | 队列长度: {len(account_queue)}")
|
||||
# 放回队尾
|
||||
account_queue.append(account)
|
||||
logger.debug(f"[release_account] 释放账号: {acc_id} | 队列长度: {len(account_queue)}")
|
||||
else:
|
||||
logger.warning(f"[release_account] 账号 {acc_id} 不在使用列表中 (可能是因为重置了队列),跳过释放")
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
@@ -174,18 +174,20 @@ export default function App() {
|
||||
|
||||
{/* 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",
|
||||
"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 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 管理面板</span>
|
||||
<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>
|
||||
<p className="text-xs text-muted-foreground mt-1 ml-8">V1.0.0 控制面板</p>
|
||||
<p className="text-[10px] text-muted-foreground mt-2 font-semibold tracking-[0.1em] uppercase opacity-60 px-1">V1.0.0 Admin Panel</p>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-4 space-y-1 overflow-y-auto">
|
||||
<nav className="flex-1 px-3 space-y-1 overflow-y-auto pt-2">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const Icon = item.icon
|
||||
const isActive = activeTab === item.id
|
||||
@@ -197,43 +199,44 @@ export default function App() {
|
||||
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",
|
||||
"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-primary text-primary-foreground shadow-md shadow-primary/20"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
? "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", isActive ? "text-primary-foreground" : "text-muted-foreground group-hover:text-accent-foreground")} />
|
||||
{item.label}
|
||||
<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/50">
|
||||
<div className="p-4 border-t border-border bg-card">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">API 状态</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>
|
||||
在线
|
||||
<div className="flex items-center justify-between text-sm px-1">
|
||||
<span className="text-muted-foreground font-semibold text-[10px] uppercase tracking-wider">System Status</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>
|
||||
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">账号</div>
|
||||
<div className="text-lg font-bold">{config.accounts?.length || 0}</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">Accounts</div>
|
||||
<div className="text-lg font-bold text-foreground leading-tight">{config.accounts?.length || 0}</div>
|
||||
</div>
|
||||
<div className="bg-background rounded p-2 border border-border">
|
||||
<div className="text-xs text-muted-foreground">API 密钥</div>
|
||||
<div className="text-lg font-bold">{config.keys?.length || 0}</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">API Keys</div>
|
||||
<div className="text-lg font-bold text-foreground leading-tight">{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"
|
||||
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-4 h-4" />
|
||||
<LogOut className="w-3.5 h-3.5" />
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
@@ -243,8 +246,13 @@ export default function App() {
|
||||
{/* 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>
|
||||
<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>
|
||||
<button
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
className="p-2 -mr-2 text-muted-foreground hover:text-foreground"
|
||||
@@ -254,7 +262,7 @@ export default function App() {
|
||||
</header>
|
||||
|
||||
{/* Content Area */}
|
||||
<div className="flex-1 overflow-auto bg-background/50 p-2 sm:p-4 lg:p-8">
|
||||
<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">
|
||||
|
||||
@@ -240,44 +240,43 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Queue Status */}
|
||||
{queueStatus && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="bg-card border border-border rounded-xl p-3 lg:p-4 flex items-center justify-between shadow-sm">
|
||||
<div className="flex items-center gap-2 lg:gap-3">
|
||||
<div className="p-1.5 lg:p-2 bg-emerald-500/10 text-emerald-500 rounded-lg">
|
||||
<CheckCircle2 className="w-4 h-4 lg:w-5 h-5" />
|
||||
{/* Queue Status - Flat & Clean */}
|
||||
{
|
||||
queueStatus && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="bg-card border border-border rounded-xl p-4 flex flex-col justify-between shadow-sm relative overflow-hidden group">
|
||||
<div className="absolute right-0 top-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||
<CheckCircle2 className="w-16 h-16" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] lg:text-sm font-medium text-muted-foreground uppercase tracking-wider lg:capitalize lg:tracking-normal">可用账号</p>
|
||||
<p className="text-xl lg:text-2xl font-bold">{queueStatus.available}</p>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-widest">Available</p>
|
||||
<div className="mt-2 flex items-baseline gap-2">
|
||||
<span className="text-3xl font-bold text-foreground">{queueStatus.available}</span>
|
||||
<span className="text-xs text-muted-foreground">accounts</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-card border border-border rounded-xl p-4 flex flex-col justify-between shadow-sm relative overflow-hidden group">
|
||||
<div className="absolute right-0 top-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||
<Server className="w-16 h-16" />
|
||||
</div>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-widest">In Use</p>
|
||||
<div className="mt-2 flex items-baseline gap-2">
|
||||
<span className="text-3xl font-bold text-foreground">{queueStatus.in_use}</span>
|
||||
<span className="text-xs text-muted-foreground">threads</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-card border border-border rounded-xl p-4 flex flex-col justify-between shadow-sm relative overflow-hidden group">
|
||||
<div className="absolute right-0 top-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||
<ShieldCheck className="w-16 h-16" />
|
||||
</div>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-widest">Total Pool</p>
|
||||
<div className="mt-2 flex items-baseline gap-2">
|
||||
<span className="text-3xl font-bold text-foreground">{queueStatus.total}</span>
|
||||
<span className="text-xs text-muted-foreground">accounts</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-card border border-border rounded-xl p-3 lg:p-4 flex items-center justify-between shadow-sm">
|
||||
<div className="flex items-center gap-2 lg:gap-3">
|
||||
<div className="p-1.5 lg:p-2 bg-amber-500/10 text-amber-500 rounded-lg">
|
||||
<Server className="w-4 h-4 lg:w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] lg:text-sm font-medium text-muted-foreground uppercase tracking-wider lg:capitalize lg:tracking-normal">使用中</p>
|
||||
<p className="text-xl lg:text-2xl font-bold">{queueStatus.in_use}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-card border border-border rounded-xl p-3 lg:p-4 flex items-center justify-between shadow-sm">
|
||||
<div className="flex items-center gap-2 lg:gap-3">
|
||||
<div className="p-1.5 lg:p-2 bg-primary/10 text-primary rounded-lg">
|
||||
<ShieldCheck className="w-4 h-4 lg:w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] lg:text-sm font-medium text-muted-foreground uppercase tracking-wider lg:capitalize lg:tracking-normal">账号总数</p>
|
||||
<p className="text-xl lg:text-2xl font-bold">{queueStatus.total}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
{/* API Keys Section */}
|
||||
<div className="bg-card border border-border rounded-xl overflow-hidden shadow-sm">
|
||||
@@ -288,10 +287,10 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAddKey(true)}
|
||||
className="btn btn-primary flex items-center gap-2"
|
||||
className="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors font-medium text-sm shadow-sm"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
新增密钥
|
||||
Add Key
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -327,25 +326,25 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
<button
|
||||
onClick={testAllAccounts}
|
||||
disabled={testingAll || validatingAll || !config.accounts?.length}
|
||||
className="btn btn-secondary text-xs"
|
||||
className="flex items-center px-3 py-2 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors text-xs font-medium border border-border disabled:opacity-50"
|
||||
>
|
||||
{testingAll ? <span className="animate-spin mr-2">⟳</span> : <Play className="w-3 h-3 mr-2" />}
|
||||
测试全部
|
||||
Test All
|
||||
</button>
|
||||
<button
|
||||
onClick={validateAllAccounts}
|
||||
disabled={validatingAll || testingAll || !config.accounts?.length}
|
||||
className="btn btn-secondary text-xs"
|
||||
className="flex items-center px-3 py-2 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors text-xs font-medium border border-border disabled:opacity-50"
|
||||
>
|
||||
{validatingAll ? <span className="animate-spin mr-2">⟳</span> : <CheckCircle2 className="w-3 h-3 mr-2" />}
|
||||
校验全部
|
||||
Validate All
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAddAccount(true)}
|
||||
className="btn btn-primary flex items-center gap-2"
|
||||
className="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors font-medium text-sm shadow-sm"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
添加账号
|
||||
Add Account
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -433,88 +432,92 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
{showAddKey && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in">
|
||||
<div className="bg-card w-full max-w-md rounded-xl border border-border shadow-2xl overflow-hidden animate-in zoom-in-95">
|
||||
<div className="p-4 border-b border-border flex justify-between items-center">
|
||||
<h3 className="font-semibold">添加 API 密钥</h3>
|
||||
<button onClick={() => setShowAddKey(false)} className="text-muted-foreground hover:text-foreground">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1.5">新密钥值</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input-field"
|
||||
placeholder="输入自定义 API 密钥"
|
||||
value={newKey}
|
||||
onChange={e => setNewKey(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<button onClick={() => setShowAddKey(false)} className="btn btn-secondary">取消</button>
|
||||
<button onClick={addKey} disabled={loading} className="btn btn-primary">
|
||||
{loading ? '添加中...' : '添加'}
|
||||
{
|
||||
showAddKey && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in">
|
||||
<div className="bg-card w-full max-w-md rounded-xl border border-border shadow-2xl overflow-hidden animate-in zoom-in-95">
|
||||
<div className="p-4 border-b border-border flex justify-between items-center">
|
||||
<h3 className="font-semibold">添加 API 密钥</h3>
|
||||
<button onClick={() => setShowAddKey(false)} className="text-muted-foreground hover:text-foreground">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1.5">新密钥值</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input-field bg-[#09090b]"
|
||||
placeholder="输入自定义 API 密钥"
|
||||
value={newKey}
|
||||
onChange={e => setNewKey(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<button onClick={() => setShowAddKey(false)} className="px-4 py-2 rounded-lg border border-border hover:bg-secondary transition-colors text-sm font-medium">Cancel</button>
|
||||
<button onClick={addKey} disabled={loading} className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium disabled:opacity-50">
|
||||
{loading ? 'Adding...' : 'Add Key'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
{showAddAccount && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in">
|
||||
<div className="bg-card w-full max-w-md rounded-xl border border-border shadow-2xl overflow-hidden animate-in zoom-in-95">
|
||||
<div className="p-4 border-b border-border flex justify-between items-center">
|
||||
<h3 className="font-semibold">添加 DeepSeek 账号</h3>
|
||||
<button onClick={() => setShowAddAccount(false)} className="text-muted-foreground hover:text-foreground">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1.5">邮箱 (可选)</label>
|
||||
<input
|
||||
type="email"
|
||||
className="input-field"
|
||||
placeholder="user@example.com"
|
||||
value={newAccount.email}
|
||||
onChange={e => setNewAccount({ ...newAccount, email: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1.5">手机号 (可选)</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input-field"
|
||||
placeholder="+86..."
|
||||
value={newAccount.mobile}
|
||||
onChange={e => setNewAccount({ ...newAccount, mobile: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1.5">密码 <span className="text-destructive">*</span></label>
|
||||
<input
|
||||
type="password"
|
||||
className="input-field"
|
||||
placeholder="账号密码"
|
||||
value={newAccount.password}
|
||||
onChange={e => setNewAccount({ ...newAccount, password: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<button onClick={() => setShowAddAccount(false)} className="btn btn-secondary">取消</button>
|
||||
<button onClick={addAccount} disabled={loading} className="btn btn-primary">
|
||||
{loading ? '添加中...' : '添加账号'}
|
||||
{
|
||||
showAddAccount && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in">
|
||||
<div className="bg-card w-full max-w-md rounded-xl border border-border shadow-2xl overflow-hidden animate-in zoom-in-95">
|
||||
<div className="p-4 border-b border-border flex justify-between items-center">
|
||||
<h3 className="font-semibold">添加 DeepSeek 账号</h3>
|
||||
<button onClick={() => setShowAddAccount(false)} className="text-muted-foreground hover:text-foreground">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1.5">邮箱 (可选)</label>
|
||||
<input
|
||||
type="email"
|
||||
className="input-field"
|
||||
placeholder="user@example.com"
|
||||
value={newAccount.email}
|
||||
onChange={e => setNewAccount({ ...newAccount, email: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1.5">手机号 (可选)</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input-field"
|
||||
placeholder="+86..."
|
||||
value={newAccount.mobile}
|
||||
onChange={e => setNewAccount({ ...newAccount, mobile: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1.5">密码 <span className="text-destructive">*</span></label>
|
||||
<input
|
||||
type="password"
|
||||
className="input-field bg-[#09090b]"
|
||||
placeholder="账号密码"
|
||||
value={newAccount.password}
|
||||
onChange={e => setNewAccount({ ...newAccount, password: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<button onClick={() => setShowAddAccount(false)} className="px-4 py-2 rounded-lg border border-border hover:bg-secondary transition-colors text-sm font-medium">Cancel</button>
|
||||
<button onClick={addAccount} disabled={loading} className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium disabled:opacity-50">
|
||||
{loading ? 'Adding...' : 'Add Account'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,15 +12,16 @@ import {
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
ChevronDown,
|
||||
ShieldCheck
|
||||
ShieldCheck,
|
||||
Terminal,
|
||||
Zap
|
||||
} from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
const MODELS = [
|
||||
{ id: 'deepseek-chat', name: 'DeepSeek Chat', icon: MessageSquare, desc: '通用聊天模型' },
|
||||
{ id: 'deepseek-reasoner', name: 'DeepSeek Reasoner', icon: Cpu, desc: '推理能力增强模型' },
|
||||
// Removed search models as they might be deprecated or identical to chat with search tool
|
||||
]
|
||||
{ id: "deepseek-chat", name: "deepseek-chat", icon: MessageSquare, desc: "通用高智商模型 (V3)", color: "text-amber-500" },
|
||||
{ id: "deepseek-reasoner", name: "deepseek-reasoner", icon: Cpu, desc: "深度推理/思维链 (R1)", color: "text-amber-600" },
|
||||
];
|
||||
|
||||
export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
const [model, setModel] = useState('deepseek-chat')
|
||||
@@ -183,39 +184,36 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:grid lg:grid-cols-3 gap-4 lg:gap-6 h-[calc(100vh-140px)] lg:h-[calc(100vh-140px)]">
|
||||
<div className="flex flex-col lg:grid lg:grid-cols-12 gap-6 h-[calc(100vh-140px)]">
|
||||
{/* Configuration Panel */}
|
||||
<div className={clsx(
|
||||
"lg:col-span-1 flex flex-col transition-all duration-300 ease-in-out",
|
||||
"lg:col-span-3 flex flex-col transition-all duration-300 ease-in-out z-20",
|
||||
configExpanded ? "h-auto" : "h-14 lg:h-full"
|
||||
)}>
|
||||
<div className="bg-card border border-border rounded-xl shadow-sm overflow-hidden flex flex-col h-full">
|
||||
<div className="bg-card border border-border rounded-xl flex flex-col h-full shadow-sm">
|
||||
{/* Mobile Toggle Header */}
|
||||
<button
|
||||
onClick={() => setConfigExpanded(!configExpanded)}
|
||||
className="lg:hidden flex items-center justify-between p-4 w-full hover:bg-muted/50 transition-colors"
|
||||
className="lg:hidden flex items-center justify-between p-4 w-full bg-muted/20 hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
<Sparkles className="w-4 h-4 text-primary" />
|
||||
<span>模型与配置</span>
|
||||
<div className="flex items-center gap-2.5 font-medium text-sm text-foreground">
|
||||
<div className="p-1.5 rounded-md bg-transparent text-foreground">
|
||||
<Terminal className="w-4 h-4" />
|
||||
</div>
|
||||
<span>配置</span>
|
||||
</div>
|
||||
<div className={clsx("transition-transform duration-200", configExpanded ? "rotate-180" : "")}>
|
||||
<div className={clsx("transition-transform duration-300 text-muted-foreground", configExpanded ? "rotate-180" : "")}>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className={clsx(
|
||||
"p-5 space-y-5 overflow-y-auto custom-scrollbar flex-1",
|
||||
"p-4 space-y-6 overflow-y-auto custom-scrollbar flex-1",
|
||||
!configExpanded && "hidden lg:block"
|
||||
)}>
|
||||
<h3 className="hidden lg:flex font-semibold items-center gap-2 mb-2">
|
||||
<Sparkles className="w-4 h-4 text-primary" />
|
||||
模型选项
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium text-muted-foreground">模型</label>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-1 gap-2">
|
||||
<label className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider ml-0.5">模型</label>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{MODELS.map(m => {
|
||||
const Icon = m.icon
|
||||
return (
|
||||
@@ -223,19 +221,29 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
key={m.id}
|
||||
onClick={() => setModel(m.id)}
|
||||
className={clsx(
|
||||
"flex items-center lg:items-center gap-2 lg:gap-3 p-2 lg:p-3 rounded-lg border text-left transition-all",
|
||||
"group relative flex items-start gap-3 p-3 rounded-lg border text-left transition-all duration-200",
|
||||
model === m.id
|
||||
? "border-primary bg-primary/5 ring-1 ring-primary/20"
|
||||
: "border-border hover:bg-secondary/50"
|
||||
? "bg-secondary border-primary/50 shadow-sm"
|
||||
: "bg-transparent border-transparent hover:bg-muted"
|
||||
)}
|
||||
>
|
||||
<div className={clsx("p-1.5 lg:p-2 rounded-md shrink-0", model === m.id ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground")}>
|
||||
<Icon className="w-3.5 h-3.5 lg:w-4 h-4" />
|
||||
<div className={clsx(
|
||||
"p-1.5 rounded-md shrink-0 transition-colors",
|
||||
model === m.id ? "text-primary" : "text-muted-foreground group-hover:text-foreground"
|
||||
)}>
|
||||
<Icon className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="font-medium text-xs lg:text-sm truncate">{m.name}</div>
|
||||
<div className="text-[10px] lg:text-xs text-muted-foreground truncate hidden sm:block lg:block">{m.desc}</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className={clsx("font-medium text-sm", model === m.id ? "text-foreground" : "text-foreground/80")}>
|
||||
{m.name}
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground mt-0.5">{m.desc}</div>
|
||||
</div>
|
||||
{model === m.id && (
|
||||
<div className="absolute top-3 right-3 text-primary">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-current" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
@@ -243,27 +251,30 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-muted-foreground">账号策略</label>
|
||||
<select
|
||||
className="input-field"
|
||||
value={selectedAccount}
|
||||
onChange={e => setSelectedAccount(e.target.value)}
|
||||
>
|
||||
<option value="">🎲 随机切换 (支持流式预览)</option>
|
||||
{accounts.map((acc, i) => (
|
||||
<option key={i} value={acc.email || acc.mobile}>
|
||||
👤 {acc.email || acc.mobile}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<label className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider ml-0.5">账号策略</label>
|
||||
<div className="relative">
|
||||
<select
|
||||
className="w-full h-10 pl-3 pr-8 bg-secondary border border-border rounded-lg text-sm appearance-none focus:outline-none focus:ring-1 focus:ring-ring focus:border-ring transition-all cursor-pointer hover:bg-muted"
|
||||
value={selectedAccount}
|
||||
onChange={e => setSelectedAccount(e.target.value)}
|
||||
>
|
||||
<option value="" className="bg-popover text-popover-foreground">🎲 随机切换 (支持流式预览)</option>
|
||||
{accounts.map((acc, i) => (
|
||||
<option key={i} value={acc.email || acc.mobile} className="bg-popover text-popover-foreground">
|
||||
👤 {acc.email || acc.mobile}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown className="absolute right-2.5 top-3 w-4 h-4 text-muted-foreground pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-muted-foreground">API 密钥 (可选)</label>
|
||||
<label className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider ml-0.5">API 密钥 (可选)</label>
|
||||
<input
|
||||
type="password"
|
||||
className="input-field font-mono text-xs"
|
||||
placeholder={config.keys?.[0] ? `默认: ${config.keys[0].slice(0, 8)}...` : '输入自定义 API 密钥'}
|
||||
className="w-full h-10 px-3 bg-muted/30 border border-border rounded-lg text-sm font-mono placeholder:text-muted-foreground/40 focus:outline-none focus:ring-1 focus:ring-ring focus:border-ring transition-all"
|
||||
placeholder={config.keys?.[0] ? `默认: ...${config.keys[0].slice(-6)}` : '输入自定义 Key'}
|
||||
value={apiKey}
|
||||
onChange={e => setApiKey(e.target.value)}
|
||||
/>
|
||||
@@ -273,19 +284,17 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
</div>
|
||||
|
||||
{/* Chat Interface */}
|
||||
<div className="lg:col-span-2 flex flex-col bg-card border border-border rounded-xl shadow-sm overflow-hidden min-h-0 flex-1">
|
||||
<div className="lg:col-span-9 flex flex-col bg-card border border-border rounded-xl shadow-sm overflow-hidden min-h-0 flex-1 relative">
|
||||
|
||||
{/* Messages Area */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-6 custom-scrollbar">
|
||||
<div className="flex-1 overflow-y-auto p-4 lg:p-6 space-y-8 custom-scrollbar scroll-smooth">
|
||||
{/* User Message */}
|
||||
<div className="flex gap-4 max-w-3xl mx-auto">
|
||||
<div className="w-8 h-8 rounded-full bg-secondary flex items-center justify-center shrink-0">
|
||||
<div className="flex gap-4 max-w-4xl mx-auto flex-row-reverse group">
|
||||
<div className="w-8 h-8 rounded-lg bg-secondary flex items-center justify-center shrink-0 border border-border">
|
||||
<User className="w-4 h-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="space-y-1 flew-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">用户</span>
|
||||
</div>
|
||||
<div className="bg-secondary/50 rounded-2xl rounded-tl-none px-4 py-3 text-sm border border-border">
|
||||
<div className="space-y-1 max-w-[85%] lg:max-w-[75%]">
|
||||
<div className="bg-primary text-primary-foreground rounded-2xl rounded-tr-sm px-5 py-3 text-sm leading-relaxed shadow-sm">
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
@@ -293,43 +302,45 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
|
||||
{/* AI Response */}
|
||||
{(response || isStreaming) && (
|
||||
<div className="flex gap-4 max-w-3xl mx-auto animate-in fade-in slide-in-from-bottom-2">
|
||||
<div className="flex gap-4 max-w-4xl mx-auto animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||
<div className={clsx(
|
||||
"w-8 h-8 rounded-full flex items-center justify-center shrink-0",
|
||||
response?.success !== false ? "bg-primary text-primary-foreground" : "bg-destructive text-destructive-foreground"
|
||||
"w-8 h-8 rounded-lg flex items-center justify-center shrink-0 border border-border",
|
||||
response?.success !== false ? "bg-muted" : "bg-destructive/10 border-destructive/20"
|
||||
)}>
|
||||
<Bot className="w-4 h-4" />
|
||||
<Bot className={clsx("w-4 h-4", response?.success !== false ? "text-foreground" : "text-destructive")} />
|
||||
</div>
|
||||
<div className="space-y-2 flex-1 min-w-0">
|
||||
<div className="space-y-3 flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">DeepSeek</span>
|
||||
<span className="font-semibold text-sm text-foreground">
|
||||
DeepSeek
|
||||
</span>
|
||||
{response && (
|
||||
<span className={clsx(
|
||||
"text-[10px] px-1.5 py-0.5 rounded border uppercase font-bold tracking-wider",
|
||||
response.success ? "border-emerald-500/20 text-emerald-500 bg-emerald-500/5" : "border-destructive/20 text-destructive bg-destructive/5"
|
||||
"text-[10px] px-1.5 py-0.5 rounded-sm border uppercase font-medium tracking-wider",
|
||||
response.success ? "border-emerald-500/20 text-emerald-500 bg-emerald-500/10" : "border-destructive/20 text-destructive bg-destructive/10"
|
||||
)}>
|
||||
{response.status_code || 'ERROR'}
|
||||
{response.status_code || 'ERR'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(streamingThinking || response?.response?.thinking) && (
|
||||
<div className="text-xs text-muted-foreground bg-muted/30 border border-border rounded-lg p-3 space-y-1">
|
||||
<div className="flex items-center gap-1.5 opacity-70 mb-1">
|
||||
<Sparkles className="w-3 h-3" />
|
||||
<span>思维链/推理过程</span>
|
||||
<div className="text-xs bg-secondary/50 border border-border rounded-lg p-3 space-y-1.5">
|
||||
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||
<Zap className="w-3.5 h-3.5" />
|
||||
<span className="font-medium">Thinking Process</span>
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap leading-relaxed opacity-90 font-mono">
|
||||
<div className="whitespace-pre-wrap leading-relaxed text-muted-foreground font-mono text-[11px] max-h-60 overflow-y-auto custom-scrollbar pl-5 border-l-2 border-border/50">
|
||||
{streamingThinking || response?.response?.thinking}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-sm leading-relaxed whitespace-pre-wrap">
|
||||
<div className="text-sm leading-7 text-foreground whitespace-pre-wrap">
|
||||
{!selectedAccount ? (
|
||||
streamingContent || (response?.error && <span className="text-destructive">{response.error}</span>)
|
||||
streamingContent || (response?.error && <span className="text-destructive font-medium">{response.error}</span>)
|
||||
) : (
|
||||
response?.response?.message || <span className="text-muted-foreground italic">...</span>
|
||||
response?.response?.message || <span className="text-muted-foreground italic">Generating response...</span>
|
||||
)}
|
||||
{isStreaming && <span className="inline-block w-1.5 h-4 bg-primary ml-1 align-middle animate-pulse" />}
|
||||
</div>
|
||||
@@ -339,12 +350,13 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
</div>
|
||||
|
||||
{/* Input Area */}
|
||||
<div className="p-4 border-t border-border bg-card">
|
||||
<div className="max-w-3xl mx-auto relative">
|
||||
<div className="p-4 lg:p-6 border-t border-border bg-card">
|
||||
<div className="max-w-4xl mx-auto relative group">
|
||||
<textarea
|
||||
className="w-full bg-secondary/30 border border-border rounded-xl pl-4 pr-14 py-3 text-sm focus:bg-background focus:ring-1 focus:ring-primary focus:border-primary transition-all resize-none custom-scrollbar"
|
||||
placeholder="输入消息..."
|
||||
rows={3}
|
||||
className="w-full bg-[#09090b] border border-border rounded-xl pl-4 pr-12 py-3 text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all resize-none custom-scrollbar placeholder:text-muted-foreground/50 text-foreground shadow-inner"
|
||||
placeholder="Type a message..."
|
||||
rows={1}
|
||||
style={{ minHeight: '52px' }}
|
||||
value={message}
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
@@ -358,7 +370,7 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
{loading && isStreaming ? (
|
||||
<button
|
||||
onClick={stopGeneration}
|
||||
className="p-2 bg-destructive text-destructive-foreground rounded-lg hover:opacity-90 transition-opacity"
|
||||
className="p-2 text-muted-foreground hover:text-destructive transition-colors"
|
||||
>
|
||||
<Square className="w-4 h-4 fill-current" />
|
||||
</button>
|
||||
@@ -366,13 +378,16 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
<button
|
||||
onClick={sendTest}
|
||||
disabled={loading || !message.trim()}
|
||||
className="p-2 bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="p-2 text-primary hover:text-primary/80 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-w-4xl mx-auto mt-3 flex justify-center">
|
||||
<span className="text-[10px] text-muted-foreground/40 font-medium">DeepSeek Admin Interface</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -134,7 +134,7 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 h-[calc(100vh-140px)]">
|
||||
<div className="flex flex-col lg:grid lg:grid-cols-3 gap-6 lg:h-[calc(100vh-140px)]">
|
||||
{/* Templates Panel */}
|
||||
<div className="md:col-span-1 space-y-4">
|
||||
<div className="bg-card border border-border rounded-xl p-5 shadow-sm">
|
||||
@@ -166,10 +166,10 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
||||
</p>
|
||||
<button
|
||||
onClick={copyBase64}
|
||||
className="w-full btn btn-primary bg-primary/90 hover:bg-primary shadow-lg shadow-primary/20"
|
||||
className="w-full flex items-center justify-center gap-2 py-2.5 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-all font-medium text-sm shadow-sm"
|
||||
>
|
||||
{copied ? <Check className="w-4 h-4 mr-2" /> : <Copy className="w-4 h-4 mr-2" />}
|
||||
{copied ? '已复制!' : '复制 Base64 配置'}
|
||||
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
||||
{copied ? 'Copied' : 'Copy Base64 Config'}
|
||||
</button>
|
||||
<p className="text-[10px] text-muted-foreground mt-2 text-center">
|
||||
变量名: <code className="bg-background px-1 py-0.5 rounded border border-border">DS2API_CONFIG_JSON</code>
|
||||
@@ -178,25 +178,25 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
||||
</div>
|
||||
|
||||
{/* Editor Panel */}
|
||||
<div className="md:col-span-2 flex flex-col bg-card border border-border rounded-xl shadow-sm overflow-hidden h-full">
|
||||
<div className="lg:col-span-2 flex flex-col bg-card border border-border rounded-xl shadow-sm overflow-hidden min-h-[400px] lg:h-full">
|
||||
<div className="p-4 border-b border-border flex items-center justify-between bg-muted/20">
|
||||
<h3 className="font-semibold flex items-center gap-2">
|
||||
<Upload className="w-4 h-4 text-primary" />
|
||||
JSON 编辑器
|
||||
</h3>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={handleExport} className="btn btn-secondary text-xs h-8">
|
||||
加载当前配置
|
||||
<button onClick={handleExport} className="px-3 py-1.5 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors text-xs font-medium border border-border">
|
||||
Load Current
|
||||
</button>
|
||||
<button onClick={handleImport} disabled={loading} className="btn btn-primary text-xs h-8">
|
||||
{loading ? '导入中...' : '模拟导入/应用'}
|
||||
<button onClick={handleImport} disabled={loading} className="px-3 py-1.5 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors text-xs font-medium disabled:opacity-50">
|
||||
{loading ? 'Importing...' : 'Apply Config'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 relative">
|
||||
<div className="flex-1 relative min-h-[400px]">
|
||||
<textarea
|
||||
className="absolute inset-0 w-full h-full p-4 font-mono text-sm bg-secondary/10 resize-none focus:outline-none custom-scrollbar"
|
||||
className="absolute inset-0 w-full h-full p-4 font-mono text-sm bg-[#09090b] text-foreground resize-none focus:outline-none custom-scrollbar"
|
||||
value={jsonInput}
|
||||
onChange={e => setJsonInput(e.target.value)}
|
||||
placeholder={'{\n "keys": ["your-api-key"],\n "accounts": [\n {"email": "...", "password": "...", "token": ""}\n ]\n}'}
|
||||
|
||||
@@ -42,84 +42,92 @@ export default function Login({ onLogin, onMessage }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-4 w-full max-w-md relative z-10">
|
||||
<div className="w-full bg-card/50 backdrop-blur-xl border border-white/10 shadow-2xl rounded-2xl p-8 space-y-8 animate-in fade-in zoom-in-95 duration-300">
|
||||
<div className="text-center space-y-2">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary/20 text-primary mb-4 ring-1 ring-white/10 shadow-inner">
|
||||
<Lock className="w-8 h-8" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-white">
|
||||
欢迎回来
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
请输入管理员密钥访问控制面板
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-h-screen w-full flex flex-col items-center justify-center p-4 bg-background text-foreground">
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-muted-foreground ml-1 uppercase tracking-wider">
|
||||
管理员密钥
|
||||
</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-muted-foreground group-focus-within:text-primary transition-colors">
|
||||
<Key className="w-4 h-4" />
|
||||
<div className="w-full max-w-[400px] relative z-10 animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="w-full bg-card border border-border rounded-xl p-8 shadow-sm">
|
||||
<div className="text-center space-y-2 mb-8">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10 text-primary mb-2">
|
||||
<Lock className="w-6 h-6" />
|
||||
</div>
|
||||
<h1 className="text-xl font-semibold tracking-tight text-foreground">
|
||||
欢迎回来
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
请输入管理员密钥继续
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-muted-foreground ml-0.5">
|
||||
管理员密钥
|
||||
</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-muted-foreground/50 transition-colors">
|
||||
<Key className="w-4 h-4" />
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
id="admin_key"
|
||||
className="w-full h-12 px-4 bg-[#09090b] border border-border rounded-lg text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all text-foreground font-mono"
|
||||
placeholder="••••••••••••••••"
|
||||
value={adminKey}
|
||||
onChange={(e) => setAdminKey(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
className="block w-full pl-10 pr-3 py-2.5 bg-secondary/50 border border-border rounded-lg text-sm placeholder:text-muted-foreground/50 focus:border-primary/50 focus:ring-2 focus:ring-primary/20 focus:bg-secondary focus:outline-none transition-all duration-200"
|
||||
placeholder="输入您的 DS2API_ADMIN_KEY"
|
||||
value={adminKey}
|
||||
onChange={e => setAdminKey(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<button
|
||||
type="button"
|
||||
role="checkbox"
|
||||
aria-checked={remember}
|
||||
onClick={() => setRemember(!remember)}
|
||||
className={clsx(
|
||||
"w-4 h-4 rounded border flex items-center justify-center transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-ring/40",
|
||||
remember ? "bg-primary border-primary text-primary-foreground" : "border-muted-foreground/40 bg-transparent hover:border-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{remember && <div className="w-2 h-2 rounded-[1px] bg-current" />}
|
||||
</button>
|
||||
<span
|
||||
onClick={() => setRemember(!remember)}
|
||||
className="text-xs text-muted-foreground cursor-pointer select-none hover:text-foreground transition-colors"
|
||||
>
|
||||
记住登录状态
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
role="checkbox"
|
||||
aria-checked={remember}
|
||||
onClick={() => setRemember(!remember)}
|
||||
className={clsx(
|
||||
"w-4 h-4 rounded border flex items-center justify-center transition-all duration-200",
|
||||
remember ? "bg-primary border-primary text-primary-foreground" : "border-muted-foreground/50 bg-transparent"
|
||||
)}
|
||||
>
|
||||
{remember && <div className="w-2 h-2 rounded-[1px] bg-current" />}
|
||||
</button>
|
||||
<span
|
||||
onClick={() => setRemember(!remember)}
|
||||
className="text-sm text-muted-foreground cursor-pointer select-none hover:text-foreground transition-colors"
|
||||
>
|
||||
记住登录状态
|
||||
</span>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full flex items-center justify-center py-2.5 px-4 rounded-lg bg-primary hover:bg-primary/90 text-primary-foreground font-medium text-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-ring disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? (
|
||||
<div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>登录</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 pt-6 border-t border-border flex justify-center">
|
||||
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground/60 font-medium tracking-wide uppercase">
|
||||
<ShieldCheck className="w-3 h-3" />
|
||||
<span>Secured Connection</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full flex items-center justify-center py-2.5 px-4 rounded-lg bg-primary hover:bg-primary/90 text-primary-foreground font-medium text-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary/20 disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-primary/25 hover:shadow-primary/40 hover:-translate-y-0.5 active:translate-y-0"
|
||||
>
|
||||
{loading ? (
|
||||
<div className="w-5 h-5 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<span>进入控制面板</span>
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="pt-6 border-t border-border/50 text-center">
|
||||
<div className="flex items-center justify-center gap-2 text-xs text-muted-foreground/70">
|
||||
<ShieldCheck className="w-3 h-3" />
|
||||
<span>安全会话 • 24小时有效</span>
|
||||
</div>
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-[10px] text-muted-foreground/30 font-mono">DS2API Admin Portal</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -92,8 +92,8 @@ export default function VercelSync({ onMessage, authFetch }) {
|
||||
<div className="relative">
|
||||
<input
|
||||
type="password"
|
||||
className="input-field pr-10"
|
||||
placeholder={preconfig?.has_token ? "正在使用预配置令牌" : "输入 Vercel 访问令牌"}
|
||||
className="w-full h-10 px-3 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-all pr-10"
|
||||
placeholder={preconfig?.has_token ? "Using pre-configured token" : "Enter Vercel Access Token"}
|
||||
value={vercelToken}
|
||||
onChange={e => setVercelToken(e.target.value)}
|
||||
/>
|
||||
@@ -109,8 +109,8 @@ export default function VercelSync({ onMessage, authFetch }) {
|
||||
<label className="text-sm font-medium">项目 ID</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input-field"
|
||||
placeholder="prj_xxxxxxxxxxxx 或项目名称"
|
||||
className="w-full h-10 px-3 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-all"
|
||||
placeholder="prj_xxxxxxxxxxxx or Project Name"
|
||||
value={projectId}
|
||||
onChange={e => setProjectId(e.target.value)}
|
||||
/>
|
||||
@@ -123,7 +123,7 @@ export default function VercelSync({ onMessage, authFetch }) {
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input-field"
|
||||
className="w-full h-10 px-3 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-all"
|
||||
placeholder="team_xxxxxxxxxxxx"
|
||||
value={teamId}
|
||||
onChange={e => setTeamId(e.target.value)}
|
||||
@@ -135,16 +135,16 @@ export default function VercelSync({ onMessage, authFetch }) {
|
||||
<button
|
||||
onClick={handleSync}
|
||||
disabled={loading}
|
||||
className="w-full btn btn-primary flex justify-center items-center py-3 text-base shadow-lg shadow-primary/20 hover:shadow-primary/30"
|
||||
className="w-full flex items-center justify-center gap-2 py-3 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-all font-medium text-sm shadow-sm hover:shadow-md disabled:opacity-50 disabled:shadow-none"
|
||||
>
|
||||
{loading ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
同步中...
|
||||
<span className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
||||
Syncing...
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-2">
|
||||
同步并重新部署 <ArrowRight className="w-5 h-5" />
|
||||
Sync & Redeploy <ArrowRight className="w-4 h-4" />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -2,34 +2,40 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Custom CSS Variables for Design Tokens */
|
||||
/* Modern Flat Theme (Big Tech Style) - Zinc & Amber */
|
||||
:root {
|
||||
--color-border: hsl(217.2 32.6% 17.5%);
|
||||
--color-input: hsl(217.2 32.6% 17.5%);
|
||||
--color-ring: hsl(212.7 26.8% 83.9%);
|
||||
--color-background: hsl(222.2 84% 4.9%);
|
||||
--color-foreground: hsl(210 40% 98%);
|
||||
/* Base - Deep Matte Zinc */
|
||||
--color-background: hsl(240 10% 3.9%);
|
||||
--color-foreground: hsl(0 0% 98%);
|
||||
|
||||
--color-primary: hsl(210 40% 98%);
|
||||
--color-primary-foreground: hsl(222.2 47.4% 11.2%);
|
||||
/* Surface */
|
||||
--color-card: hsl(240 10% 6.5%);
|
||||
--color-card-foreground: hsl(0 0% 98%);
|
||||
|
||||
--color-secondary: hsl(217.2 32.6% 17.5%);
|
||||
--color-secondary-foreground: hsl(210 40% 98%);
|
||||
--color-popover: hsl(240 10% 3.9%);
|
||||
--color-popover-foreground: hsl(0 0% 98%);
|
||||
|
||||
--color-destructive: hsl(0 62.8% 30.6%);
|
||||
--color-destructive-foreground: hsl(210 40% 98%);
|
||||
/* Primary Accent - Warm Amber/Gold */
|
||||
--color-primary: hsl(45 93% 47%);
|
||||
--color-primary-foreground: hsl(240 10% 3.9%);
|
||||
|
||||
--color-muted: hsl(217.2 32.6% 17.5%);
|
||||
--color-muted-foreground: hsl(215 20.2% 65.1%);
|
||||
/* Secondary */
|
||||
--color-secondary: hsl(240 5.9% 10%);
|
||||
--color-secondary-foreground: hsl(0 0% 98%);
|
||||
|
||||
--color-accent: hsl(217.2 32.6% 17.5%);
|
||||
--color-accent-foreground: hsl(210 40% 98%);
|
||||
/* UI Elements */
|
||||
--color-muted: hsl(240 3.7% 15.9%);
|
||||
--color-muted-foreground: hsl(240 5% 64.9%);
|
||||
|
||||
--color-popover: hsl(222.2 84% 4.9%);
|
||||
--color-popover-foreground: hsl(210 40% 98%);
|
||||
--color-accent: hsl(240 3.7% 15.9%);
|
||||
--color-accent-foreground: hsl(0 0% 98%);
|
||||
|
||||
--color-card: hsl(222.2 84% 4.9%);
|
||||
--color-card-foreground: hsl(210 40% 98%);
|
||||
--color-destructive: hsl(0 84.2% 60.2%);
|
||||
--color-destructive-foreground: hsl(0 0% 98%);
|
||||
|
||||
--color-border: hsl(240 3.7% 15.9%);
|
||||
--color-input: hsl(240 3.7% 15.9%);
|
||||
--color-ring: hsl(45 93% 47%);
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
@@ -47,10 +53,10 @@ body {
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
/* Custom scrollbar - Slim & Modern */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@@ -58,12 +64,12 @@ body {
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-muted);
|
||||
border-radius: 4px;
|
||||
background: hsla(215, 27.9%, 26.9%, 0.6);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-muted-foreground);
|
||||
background: hsla(215, 27.9%, 40%, 0.8);
|
||||
}
|
||||
|
||||
/* Custom scrollbar utility */
|
||||
@@ -141,7 +147,8 @@ body {
|
||||
width: 100%;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--color-input);
|
||||
background-color: rgba(30, 41, 59, 0.3);
|
||||
background-color: #09090b;
|
||||
color: hsl(var(--color-foreground));
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.15s;
|
||||
|
||||
Reference in New Issue
Block a user