feat: Implement initial full-stack application structure including frontend pages, components, hooks, API integration, and backend services for account pooling and management.

This commit is contained in:
2026-01-30 07:40:35 +08:00
commit f4448bbef2
106 changed files with 19282 additions and 0 deletions

View File

@@ -0,0 +1,241 @@
import { useState } from 'react'
import { TestTube, CheckCircle, XCircle, Loader2, Save, Server, Plus, X } from 'lucide-react'
import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common'
import { useConfig } from '../hooks/useConfig'
export default function S2AConfig() {
const {
config,
updateS2AConfig,
updatePoolingConfig,
testConnection,
isConnected,
} = useConfig()
const [testing, setTesting] = useState(false)
const [testResult, setTestResult] = useState<boolean | null>(null)
const [saved, setSaved] = useState(false)
// Local form state - S2A 连接
const [s2aApiBase, setS2aApiBase] = useState(config.s2a.apiBase)
const [s2aAdminKey, setS2aAdminKey] = useState(config.s2a.adminKey)
// Local form state - 入库设置
const [poolingConcurrency, setPoolingConcurrency] = useState(config.pooling.concurrency)
const [poolingPriority, setPoolingPriority] = useState(config.pooling.priority)
const [groupIds, setGroupIds] = useState<number[]>(config.pooling.groupIds || [])
const [newGroupId, setNewGroupId] = useState('')
const handleTestConnection = async () => {
// Save first
updateS2AConfig({ apiBase: s2aApiBase, adminKey: s2aAdminKey })
setTesting(true)
setTestResult(null)
// Wait a bit for the client to be recreated
await new Promise((resolve) => setTimeout(resolve, 100))
const result = await testConnection()
setTestResult(result)
setTesting(false)
}
const handleSave = () => {
updateS2AConfig({ apiBase: s2aApiBase, adminKey: s2aAdminKey })
updatePoolingConfig({
concurrency: poolingConcurrency,
priority: poolingPriority,
groupIds: groupIds,
})
setSaved(true)
setTimeout(() => setSaved(false), 2000)
}
const handleAddGroupId = () => {
const id = parseInt(newGroupId, 10)
if (!isNaN(id) && !groupIds.includes(id)) {
setGroupIds([...groupIds, id])
setNewGroupId('')
}
}
const handleRemoveGroupId = (id: number) => {
setGroupIds(groupIds.filter(g => g !== id))
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100 flex items-center gap-2">
<Server className="h-7 w-7 text-blue-500" />
S2A
</h1>
<p className="text-sm text-slate-500 dark:text-slate-400"> S2A </p>
</div>
<Button
onClick={handleSave}
icon={saved ? <CheckCircle className="h-4 w-4" /> : <Save className="h-4 w-4" />}
>
{saved ? '已保存' : '保存配置'}
</Button>
</div>
{/* S2A Connection */}
<Card>
<CardHeader>
<CardTitle>S2A </CardTitle>
<div className="flex items-center gap-2">
{isConnected ? (
<span className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400">
<CheckCircle className="h-4 w-4" />
</span>
) : (
<span className="flex items-center gap-1 text-sm text-slate-500 dark:text-slate-400">
<XCircle className="h-4 w-4" />
</span>
)}
</div>
</CardHeader>
<CardContent className="space-y-4">
<Input
label="S2A API 地址"
placeholder="http://localhost:8080"
value={s2aApiBase}
onChange={(e) => setS2aApiBase(e.target.value)}
hint="S2A 服务的 API 地址,例如 http://localhost:8080"
/>
<Input
label="Admin API Key"
type="password"
placeholder="admin-xxxxxxxxxxxxxxxx"
value={s2aAdminKey}
onChange={(e) => setS2aAdminKey(e.target.value)}
hint="S2A 管理密钥,可在 S2A 后台 Settings 页面获取"
/>
<div className="flex items-center gap-4">
<Button
variant="outline"
onClick={handleTestConnection}
disabled={testing || !s2aApiBase || !s2aAdminKey}
icon={
testing ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<TestTube className="h-4 w-4" />
)
}
>
{testing ? '测试中...' : '测试连接'}
</Button>
{testResult !== null && (
<span
className={`text-sm ${testResult
? 'text-green-600 dark:text-green-400'
: 'text-red-600 dark:text-red-400'
}`}
>
{testResult ? '连接成功' : '连接失败'}
</span>
)}
</div>
</CardContent>
</Card>
{/* Pooling Settings */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Input
label="默认并发数"
type="number"
min={1}
max={10}
value={poolingConcurrency}
onChange={(e) => setPoolingConcurrency(Number(e.target.value))}
hint="账号的默认并发请求数"
/>
<Input
label="默认优先级"
type="number"
min={0}
max={100}
value={poolingPriority}
onChange={(e) => setPoolingPriority(Number(e.target.value))}
hint="账号的默认优先级,数值越大优先级越高"
/>
</div>
{/* Group IDs */}
<div className="space-y-2">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300">
ID
</label>
<div className="flex flex-wrap gap-2">
{groupIds.map(id => (
<span
key={id}
className="inline-flex items-center gap-1 px-3 py-1 rounded-full text-sm bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
>
{id}
<button
onClick={() => handleRemoveGroupId(id)}
className="hover:text-red-500 transition-colors"
>
<X className="h-3 w-3" />
</button>
</span>
))}
{groupIds.length === 0 && (
<span className="text-sm text-slate-400"></span>
)}
</div>
<div className="flex gap-2 mt-2">
<Input
placeholder="输入分组 ID"
type="number"
min={1}
value={newGroupId}
onChange={(e) => setNewGroupId(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleAddGroupId()}
/>
<Button
variant="outline"
onClick={handleAddGroupId}
disabled={!newGroupId}
icon={<Plus className="h-4 w-4" />}
>
</Button>
</div>
<p className="text-xs text-slate-500 dark:text-slate-400">
</p>
</div>
</CardContent>
</Card>
{/* Info */}
<Card>
<CardContent>
<div className="text-sm text-slate-500 dark:text-slate-400">
<p className="font-medium mb-2"></p>
<ul className="list-disc list-inside space-y-1">
<li>S2A API S2A URL</li>
<li>Admin API Key </li>
<li></li>
<li> ID </li>
</ul>
</div>
</CardContent>
</Card>
</div>
)
}