feat: implement responsive collapsible configuration panel in API Tester and refine account manager component styling.

This commit is contained in:
CJACK
2026-02-01 05:44:55 +08:00
parent 880ab34739
commit 842a942a55
3 changed files with 113 additions and 86 deletions

View File

@@ -254,8 +254,8 @@ export default function App() {
</header>
{/* 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="flex-1 overflow-auto bg-background/50 p-2 sm:p-4 lg:p-8">
<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">
{NAV_ITEMS.find(n => n.id === activeTab)?.label}

View File

@@ -243,36 +243,36 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
{/* 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-4 flex items-center justify-between shadow-sm">
<div className="flex items-center gap-3">
<div className="p-2 bg-emerald-500/10 text-emerald-500 rounded-lg">
<CheckCircle2 className="w-5 h-5" />
<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" />
</div>
<div>
<p className="text-sm font-medium text-muted-foreground">可用账号</p>
<p className="text-2xl font-bold">{queueStatus.available}</p>
<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>
</div>
</div>
</div>
<div className="bg-card border border-border rounded-xl p-4 flex items-center justify-between shadow-sm">
<div className="flex items-center gap-3">
<div className="p-2 bg-amber-500/10 text-amber-500 rounded-lg">
<Server className="w-5 h-5" />
<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-sm font-medium text-muted-foreground">使用中</p>
<p className="text-2xl font-bold">{queueStatus.in_use}</p>
<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-4 flex items-center justify-between shadow-sm">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 text-primary rounded-lg">
<ShieldCheck className="w-5 h-5" />
<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-sm font-medium text-muted-foreground">账号总数</p>
<p className="text-2xl font-bold">{queueStatus.total}</p>
<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>
@@ -401,26 +401,26 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
</div>
</div>
</div>
<div className="flex items-center gap-2 self-end md:self-auto">
<div className="flex items-center gap-2 self-start lg:self-auto ml-5 lg:ml-0">
<button
onClick={() => testAccount(id)}
disabled={testing[id]}
className="px-3 py-1.5 text-xs font-medium border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50"
className="px-2 lg:px-3 py-1 lg:py-1.5 text-[10px] lg:text-xs font-medium border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50"
>
{testing[id] ? '正在测试...' : '测试'}
</button>
<button
onClick={() => validateAccount(id)}
disabled={validating[id]}
className="px-3 py-1.5 text-xs font-medium border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50"
className="px-2 lg:px-3 py-1 lg:py-1.5 text-[10px] lg:text-xs font-medium border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50"
>
{validating[id] ? '正在校验...' : '校验'}
</button>
<button
onClick={() => deleteAccount(id)}
className="p-1.5 text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded-md transition-colors"
className="p-1 lg:p-1.5 text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded-md transition-colors"
>
<Trash2 className="w-4 h-4" />
<Trash2 className="w-3.5 h-3.5 lg:w-4 h-4" />
</button>
</div>
</div>

View File

@@ -10,7 +10,9 @@ import {
User,
Loader2,
CheckCircle2,
AlertCircle
AlertCircle,
ChevronDown,
ShieldCheck
} from 'lucide-react'
import clsx from 'clsx'
@@ -32,6 +34,9 @@ export default function ApiTester({ config, onMessage, authFetch }) {
const [isStreaming, setIsStreaming] = useState(false)
const abortControllerRef = useRef(null)
const [sidebarOpen, setSidebarOpen] = useState(false)
const [configExpanded, setConfigExpanded] = useState(false)
const apiFetch = authFetch || fetch
const accounts = config.accounts || []
@@ -178,75 +183,97 @@ export default function ApiTester({ config, onMessage, authFetch }) {
}
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 h-[calc(100vh-140px)]">
<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)]">
{/* Configuration Panel */}
<div className="lg:col-span-1 space-y-4 overflow-y-auto pr-2">
<div className="bg-card border border-border rounded-xl p-5 shadow-sm space-y-5">
<h3 className="font-semibold flex items-center gap-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-1 gap-2">
{MODELS.map(m => {
const Icon = m.icon
return (
<button
key={m.id}
onClick={() => setModel(m.id)}
className={clsx(
"flex items-center gap-3 p-3 rounded-lg border text-left transition-all",
model === m.id
? "border-primary bg-primary/5 ring-1 ring-primary/20"
: "border-border hover:bg-secondary/50"
)}
>
<div className={clsx("p-2 rounded-md", model === m.id ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground")}>
<Icon className="w-4 h-4" />
</div>
<div>
<div className="font-medium text-sm">{m.name}</div>
<div className="text-xs text-muted-foreground">{m.desc}</div>
</div>
</button>
)
})}
<div className={clsx(
"lg:col-span-1 flex flex-col transition-all duration-300 ease-in-out",
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">
{/* 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"
>
<div className="flex items-center gap-2 font-semibold">
<Sparkles className="w-4 h-4 text-primary" />
<span>模型与配置</span>
</div>
</div>
<div className={clsx("transition-transform duration-200", configExpanded ? "rotate-180" : "")}>
<ChevronDown className="w-4 h-4" />
</div>
</button>
<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>
</div>
<div className={clsx(
"p-5 space-y-5 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-2">
<label className="text-sm font-medium text-muted-foreground">API 密钥 (可选)</label>
<input
type="password"
className="input-field font-mono text-xs"
placeholder={config.keys?.[0] ? `默认: ${config.keys[0].slice(0, 8)}...` : '输入自定义 API 密钥'}
value={apiKey}
onChange={e => setApiKey(e.target.value)}
/>
<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">
{MODELS.map(m => {
const Icon = m.icon
return (
<button
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",
model === m.id
? "border-primary bg-primary/5 ring-1 ring-primary/20"
: "border-border hover:bg-secondary/50"
)}
>
<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>
<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>
</button>
)
})}
</div>
</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>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-muted-foreground">API 密钥 (可选)</label>
<input
type="password"
className="input-field font-mono text-xs"
placeholder={config.keys?.[0] ? `默认: ${config.keys[0].slice(0, 8)}...` : '输入自定义 API 密钥'}
value={apiKey}
onChange={e => setApiKey(e.target.value)}
/>
</div>
</div>
</div>
</div>
{/* Chat Interface */}
<div className="lg: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-0 flex-1">
{/* Messages Area */}
<div className="flex-1 overflow-y-auto p-4 space-y-6 custom-scrollbar">
{/* User Message */}