feat: introduce a new configuration system with dedicated pages for S2A and email settings.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react'
|
||||
import type { AppConfig } from '../types'
|
||||
import { defaultConfig } from '../types'
|
||||
import { loadConfig, saveConfig } from '../utils/storage'
|
||||
import { saveConfig } from '../utils/storage'
|
||||
import { S2AClient } from '../api/s2a'
|
||||
|
||||
interface ConfigContextValue {
|
||||
@@ -14,6 +14,7 @@ interface ConfigContextValue {
|
||||
isConnected: boolean
|
||||
testConnection: () => Promise<boolean>
|
||||
s2aClient: S2AClient | null
|
||||
refreshConfig: () => Promise<void>
|
||||
}
|
||||
|
||||
const ConfigContext = createContext<ConfigContextValue | null>(null)
|
||||
@@ -23,24 +24,37 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||
const [isConnected, setIsConnected] = useState(false)
|
||||
const [s2aClient, setS2aClient] = useState<S2AClient | null>(null)
|
||||
|
||||
// Load config from localStorage on mount
|
||||
useEffect(() => {
|
||||
const savedConfig = loadConfig()
|
||||
setConfig(savedConfig)
|
||||
|
||||
// Create S2A client if config is available
|
||||
if (savedConfig.s2a.apiBase && savedConfig.s2a.adminKey) {
|
||||
const client = new S2AClient({
|
||||
baseUrl: savedConfig.s2a.apiBase,
|
||||
apiKey: savedConfig.s2a.adminKey,
|
||||
})
|
||||
setS2aClient(client)
|
||||
|
||||
// Test connection on load
|
||||
client.testConnection().then(setIsConnected)
|
||||
// Load config from server on mount
|
||||
const refreshConfig = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/config')
|
||||
const data = await res.json()
|
||||
if (data.code === 0 && data.data) {
|
||||
const serverConfig = data.data
|
||||
setConfig(prev => ({
|
||||
...prev,
|
||||
s2a: {
|
||||
...prev.s2a,
|
||||
apiBase: serverConfig.s2a_api_base || '',
|
||||
adminKey: serverConfig.s2a_admin_key || '',
|
||||
},
|
||||
pooling: {
|
||||
...prev.pooling,
|
||||
concurrency: serverConfig.concurrency || 2,
|
||||
priority: serverConfig.priority || 0,
|
||||
groupIds: serverConfig.group_ids || [],
|
||||
},
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load config from server:', error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
refreshConfig()
|
||||
}, [refreshConfig])
|
||||
|
||||
// Update S2A client when config changes
|
||||
useEffect(() => {
|
||||
if (config.s2a.apiBase && config.s2a.adminKey) {
|
||||
@@ -110,8 +124,9 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||
const testConnection = useCallback(async (): Promise<boolean> => {
|
||||
try {
|
||||
// 使用后端代理 API 来测试 S2A 连接(避免 CORS 问题)
|
||||
const res = await fetch('http://localhost:8088/api/s2a/test')
|
||||
const connected = res.ok
|
||||
const res = await fetch('/api/s2a/test')
|
||||
const data = await res.json()
|
||||
const connected = data.code === 0
|
||||
setIsConnected(connected)
|
||||
return connected
|
||||
} catch {
|
||||
@@ -132,6 +147,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||
isConnected,
|
||||
testConnection,
|
||||
s2aClient,
|
||||
refreshConfig,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,105 +1,27 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import {
|
||||
Server,
|
||||
Mail,
|
||||
ChevronRight,
|
||||
Settings,
|
||||
Save,
|
||||
RefreshCw,
|
||||
Globe,
|
||||
ToggleLeft,
|
||||
ToggleRight
|
||||
CheckCircle,
|
||||
XCircle
|
||||
} from 'lucide-react'
|
||||
import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common'
|
||||
import { Card, CardContent, Button } from '../components/common'
|
||||
import { useConfig } from '../hooks/useConfig'
|
||||
|
||||
export default function Config() {
|
||||
const { config, isConnected } = useConfig()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null)
|
||||
|
||||
// 编辑状态
|
||||
const [editS2ABase, setEditS2ABase] = useState('')
|
||||
const [editS2AKey, setEditS2AKey] = useState('')
|
||||
const [editConcurrency, setEditConcurrency] = useState(2)
|
||||
const [editPriority, setEditPriority] = useState(0)
|
||||
const [editGroupIds, setEditGroupIds] = useState('')
|
||||
const [proxyEnabled, setProxyEnabled] = useState(false)
|
||||
const [proxyAddress, setProxyAddress] = useState('')
|
||||
|
||||
// 获取服务器配置
|
||||
const fetchServerConfig = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch('/api/config')
|
||||
const data = await res.json()
|
||||
if (data.code === 0 && data.data) {
|
||||
setEditS2ABase(data.data.s2a_api_base || '')
|
||||
setEditS2AKey(data.data.s2a_admin_key || '')
|
||||
setEditConcurrency(data.data.concurrency || 2)
|
||||
setEditPriority(data.data.priority || 0)
|
||||
setEditGroupIds(data.data.group_ids?.join(', ') || '')
|
||||
setProxyEnabled(data.data.proxy_enabled || false)
|
||||
setProxyAddress(data.data.default_proxy || '')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch config:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchServerConfig()
|
||||
}, [])
|
||||
|
||||
// 保存配置
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
setMessage(null)
|
||||
try {
|
||||
// 解析 group_ids
|
||||
const groupIds = editGroupIds
|
||||
.split(',')
|
||||
.map(s => parseInt(s.trim()))
|
||||
.filter(n => !isNaN(n))
|
||||
|
||||
const res = await fetch('/api/config', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
s2a_api_base: editS2ABase,
|
||||
s2a_admin_key: editS2AKey,
|
||||
concurrency: editConcurrency,
|
||||
priority: editPriority,
|
||||
group_ids: groupIds,
|
||||
proxy_enabled: proxyEnabled,
|
||||
default_proxy: proxyAddress,
|
||||
}),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.code === 0) {
|
||||
setMessage({ type: 'success', text: '配置已保存' })
|
||||
fetchServerConfig()
|
||||
} else {
|
||||
setMessage({ type: 'error', text: data.message || '保存失败' })
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: 'error', text: '网络错误' })
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
const { config, isConnected, refreshConfig } = useConfig()
|
||||
|
||||
const configItems = [
|
||||
{
|
||||
to: '/config/s2a',
|
||||
icon: Server,
|
||||
title: 'S2A 高级配置',
|
||||
description: 'S2A 号池详细设置和测试',
|
||||
title: 'S2A 号池配置',
|
||||
description: 'S2A 连接、入库参数和代理设置',
|
||||
status: isConnected ? '已连接' : '未连接',
|
||||
statusIcon: isConnected ? CheckCircle : XCircle,
|
||||
statusColor: isConnected ? 'text-green-600 dark:text-green-400' : 'text-red-500',
|
||||
},
|
||||
{
|
||||
@@ -108,6 +30,7 @@ export default function Config() {
|
||||
title: '邮箱服务配置',
|
||||
description: '配置邮箱服务用于自动注册',
|
||||
status: (config.email?.services?.length ?? 0) > 0 ? '已配置' : '未配置',
|
||||
statusIcon: (config.email?.services?.length ?? 0) > 0 ? CheckCircle : XCircle,
|
||||
statusColor: (config.email?.services?.length ?? 0) > 0 ? 'text-green-600 dark:text-green-400' : 'text-orange-500',
|
||||
},
|
||||
]
|
||||
@@ -126,151 +49,19 @@ export default function Config() {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={fetchServerConfig}
|
||||
disabled={loading}
|
||||
icon={<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />}
|
||||
onClick={() => refreshConfig()}
|
||||
icon={<RefreshCw className="h-4 w-4" />}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
{message && (
|
||||
<div className={`p-3 rounded-lg text-sm ${message.type === 'success'
|
||||
? 'bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
||||
: 'bg-red-50 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
||||
}`}>
|
||||
{message.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Config Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Server className="h-5 w-5 text-blue-500" />
|
||||
核心配置
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* S2A Config */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||
S2A API 地址
|
||||
</label>
|
||||
<Input
|
||||
value={editS2ABase}
|
||||
onChange={(e) => setEditS2ABase(e.target.value)}
|
||||
placeholder="https://your-s2a-server.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||
S2A Admin Key
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={editS2AKey}
|
||||
onChange={(e) => setEditS2AKey(e.target.value)}
|
||||
placeholder="admin-xxxxxx"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pooling Config */}
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||
入库并发
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={editConcurrency}
|
||||
onChange={(e) => setEditConcurrency(parseInt(e.target.value) || 1)}
|
||||
min={1}
|
||||
max={100}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||
优先级
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={editPriority}
|
||||
onChange={(e) => setEditPriority(parseInt(e.target.value) || 0)}
|
||||
min={0}
|
||||
max={100}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||
分组 ID (逗号分隔)
|
||||
</label>
|
||||
<Input
|
||||
value={editGroupIds}
|
||||
onChange={(e) => setEditGroupIds(e.target.value)}
|
||||
placeholder="1, 2, 3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Proxy Config */}
|
||||
<div className="border-t border-slate-200 dark:border-slate-700 pt-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="h-5 w-5 text-orange-500" />
|
||||
<span className="font-medium text-slate-700 dark:text-slate-300">代理设置</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setProxyEnabled(!proxyEnabled)}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
>
|
||||
{proxyEnabled ? (
|
||||
<>
|
||||
<ToggleRight className="h-6 w-6 text-green-500" />
|
||||
<span className="text-green-600 dark:text-green-400">已启用</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ToggleLeft className="h-6 w-6 text-slate-400" />
|
||||
<span className="text-slate-500">已禁用</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<Input
|
||||
value={proxyAddress}
|
||||
onChange={(e) => setProxyAddress(e.target.value)}
|
||||
placeholder="http://127.0.0.1:7890"
|
||||
disabled={!proxyEnabled}
|
||||
className={!proxyEnabled ? 'opacity-50' : ''}
|
||||
/>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
服务器部署时通常不需要代理,在本地开发或特殊网络环境下可启用
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex justify-end pt-2">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
icon={<Save className="h-4 w-4" />}
|
||||
>
|
||||
{saving ? '保存中...' : '保存配置'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Sub Config Cards */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{/* Config Cards */}
|
||||
<div className="grid gap-4">
|
||||
{configItems.map((item) => (
|
||||
<Link key={item.to} to={item.to} className="block group">
|
||||
<Card className="h-full transition-all duration-200 hover:shadow-lg hover:border-blue-300 dark:hover:border-blue-600">
|
||||
<CardContent className="flex items-center gap-4 py-4">
|
||||
<Card className="transition-all duration-200 hover:shadow-lg hover:border-blue-300 dark:hover:border-blue-600">
|
||||
<CardContent className="flex items-center gap-4 py-5">
|
||||
<div className="p-3 rounded-xl bg-blue-50 dark:bg-blue-900/30 group-hover:bg-blue-100 dark:group-hover:bg-blue-900/50 transition-colors">
|
||||
<item.icon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
@@ -278,9 +69,12 @@ export default function Config() {
|
||||
<h3 className="font-semibold text-slate-900 dark:text-slate-100">{item.title}</h3>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">{item.description}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<item.statusIcon className={`h-4 w-4 ${item.statusColor}`} />
|
||||
<span className={`text-sm font-medium ${item.statusColor}`}>
|
||||
{item.status}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronRight className="h-5 w-5 text-slate-400 group-hover:text-blue-500 transition-colors" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,55 +1,102 @@
|
||||
import { useState } from 'react'
|
||||
import { TestTube, CheckCircle, XCircle, Loader2, Save, Server, Plus, X } from 'lucide-react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { TestTube, CheckCircle, XCircle, Loader2, Save, Server, Plus, X, Globe, ToggleLeft, ToggleRight } 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,
|
||||
refreshConfig,
|
||||
} = useConfig()
|
||||
|
||||
const [testing, setTesting] = useState(false)
|
||||
const [testResult, setTestResult] = useState<boolean | null>(null)
|
||||
const [saved, setSaved] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null)
|
||||
|
||||
// Local form state - S2A 连接
|
||||
const [s2aApiBase, setS2aApiBase] = useState(config.s2a.apiBase)
|
||||
const [s2aAdminKey, setS2aAdminKey] = useState(config.s2a.adminKey)
|
||||
// S2A 连接配置
|
||||
const [s2aApiBase, setS2aApiBase] = useState('')
|
||||
const [s2aAdminKey, setS2aAdminKey] = useState('')
|
||||
|
||||
// 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 [concurrency, setConcurrency] = useState(2)
|
||||
const [priority, setPriority] = useState(0)
|
||||
const [groupIds, setGroupIds] = useState<number[]>([])
|
||||
const [newGroupId, setNewGroupId] = useState('')
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
// Save first
|
||||
updateS2AConfig({ apiBase: s2aApiBase, adminKey: s2aAdminKey })
|
||||
// 代理设置
|
||||
const [proxyEnabled, setProxyEnabled] = useState(false)
|
||||
const [proxyAddress, setProxyAddress] = useState('')
|
||||
|
||||
// 从服务器加载配置
|
||||
const fetchConfig = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch('/api/config')
|
||||
const data = await res.json()
|
||||
if (data.code === 0 && data.data) {
|
||||
setS2aApiBase(data.data.s2a_api_base || '')
|
||||
setS2aAdminKey(data.data.s2a_admin_key || '')
|
||||
setConcurrency(data.data.concurrency || 2)
|
||||
setPriority(data.data.priority || 0)
|
||||
setGroupIds(data.data.group_ids || [])
|
||||
setProxyEnabled(data.data.proxy_enabled || false)
|
||||
setProxyAddress(data.data.default_proxy || '')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch config:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig()
|
||||
}, [])
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
setTesting(true)
|
||||
setTestResult(null)
|
||||
|
||||
// Wait a bit for the client to be recreated
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
// 先保存配置
|
||||
await handleSave()
|
||||
|
||||
const result = await testConnection()
|
||||
setTestResult(result)
|
||||
setTesting(false)
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
updateS2AConfig({ apiBase: s2aApiBase, adminKey: s2aAdminKey })
|
||||
updatePoolingConfig({
|
||||
concurrency: poolingConcurrency,
|
||||
priority: poolingPriority,
|
||||
groupIds: groupIds,
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
setMessage(null)
|
||||
try {
|
||||
const res = await fetch('/api/config', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
s2a_api_base: s2aApiBase,
|
||||
s2a_admin_key: s2aAdminKey,
|
||||
concurrency: concurrency,
|
||||
priority: priority,
|
||||
group_ids: groupIds,
|
||||
proxy_enabled: proxyEnabled,
|
||||
default_proxy: proxyAddress,
|
||||
}),
|
||||
})
|
||||
setSaved(true)
|
||||
setTimeout(() => setSaved(false), 2000)
|
||||
const data = await res.json()
|
||||
if (data.code === 0) {
|
||||
setMessage({ type: 'success', text: '配置已保存' })
|
||||
refreshConfig()
|
||||
} else {
|
||||
setMessage({ type: 'error', text: data.message || '保存失败' })
|
||||
}
|
||||
} catch {
|
||||
setMessage({ type: 'error', text: '网络错误' })
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddGroupId = () => {
|
||||
@@ -64,6 +111,14 @@ export default function S2AConfig() {
|
||||
setGroupIds(groupIds.filter(g => g !== id))
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
@@ -73,20 +128,34 @@ export default function S2AConfig() {
|
||||
<Server className="h-7 w-7 text-blue-500" />
|
||||
S2A 配置
|
||||
</h1>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">配置 S2A 号池连接和入库参数</p>
|
||||
<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" />}
|
||||
disabled={saving}
|
||||
icon={saving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
|
||||
>
|
||||
{saved ? '已保存' : '保存配置'}
|
||||
{saving ? '保存中...' : '保存配置'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
{message && (
|
||||
<div className={`p-3 rounded-lg text-sm ${message.type === 'success'
|
||||
? 'bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
||||
: 'bg-red-50 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
||||
}`}>
|
||||
{message.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* S2A Connection */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>S2A 连接配置</CardTitle>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Server className="h-5 w-5 text-blue-500" />
|
||||
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">
|
||||
@@ -102,12 +171,13 @@ export default function S2AConfig() {
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Input
|
||||
label="S2A API 地址"
|
||||
placeholder="http://localhost:8080"
|
||||
placeholder="https://your-s2a-server.com"
|
||||
value={s2aApiBase}
|
||||
onChange={(e) => setS2aApiBase(e.target.value)}
|
||||
hint="S2A 服务的 API 地址,例如 http://localhost:8080"
|
||||
hint="S2A 服务的 API 地址"
|
||||
/>
|
||||
<Input
|
||||
label="Admin API Key"
|
||||
@@ -117,6 +187,7 @@ export default function S2AConfig() {
|
||||
onChange={(e) => setS2aAdminKey(e.target.value)}
|
||||
hint="S2A 管理密钥,可在 S2A 后台 Settings 页面获取"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -157,9 +228,9 @@ export default function S2AConfig() {
|
||||
label="默认并发数"
|
||||
type="number"
|
||||
min={1}
|
||||
max={10}
|
||||
value={poolingConcurrency}
|
||||
onChange={(e) => setPoolingConcurrency(Number(e.target.value))}
|
||||
max={100}
|
||||
value={concurrency}
|
||||
onChange={(e) => setConcurrency(Number(e.target.value))}
|
||||
hint="账号的默认并发请求数"
|
||||
/>
|
||||
<Input
|
||||
@@ -167,8 +238,8 @@ export default function S2AConfig() {
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
value={poolingPriority}
|
||||
onChange={(e) => setPoolingPriority(Number(e.target.value))}
|
||||
value={priority}
|
||||
onChange={(e) => setPriority(Number(e.target.value))}
|
||||
hint="账号的默认优先级,数值越大优先级越高"
|
||||
/>
|
||||
</div>
|
||||
@@ -222,6 +293,44 @@ export default function S2AConfig() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Proxy Settings */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Globe className="h-5 w-5 text-orange-500" />
|
||||
代理设置
|
||||
</CardTitle>
|
||||
<button
|
||||
onClick={() => setProxyEnabled(!proxyEnabled)}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
>
|
||||
{proxyEnabled ? (
|
||||
<>
|
||||
<ToggleRight className="h-6 w-6 text-green-500" />
|
||||
<span className="text-green-600 dark:text-green-400">已启用</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ToggleLeft className="h-6 w-6 text-slate-400" />
|
||||
<span className="text-slate-500">已禁用</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Input
|
||||
value={proxyAddress}
|
||||
onChange={(e) => setProxyAddress(e.target.value)}
|
||||
placeholder="http://127.0.0.1:7890"
|
||||
disabled={!proxyEnabled}
|
||||
className={!proxyEnabled ? 'opacity-50' : ''}
|
||||
/>
|
||||
<p className="text-xs text-slate-500 mt-2">
|
||||
服务器部署时通常不需要代理,在本地开发或特殊网络环境下可启用
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Info */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
@@ -232,6 +341,7 @@ export default function S2AConfig() {
|
||||
<li>Admin API Key 用于管理账号池,具有完全权限</li>
|
||||
<li>入库默认设置会应用到新入库的账号</li>
|
||||
<li>分组 ID 用于将账号归类到指定分组</li>
|
||||
<li>配置会自动保存到服务器数据库</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
Reference in New Issue
Block a user