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 { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react'
|
||||||
import type { AppConfig } from '../types'
|
import type { AppConfig } from '../types'
|
||||||
import { defaultConfig } from '../types'
|
import { defaultConfig } from '../types'
|
||||||
import { loadConfig, saveConfig } from '../utils/storage'
|
import { saveConfig } from '../utils/storage'
|
||||||
import { S2AClient } from '../api/s2a'
|
import { S2AClient } from '../api/s2a'
|
||||||
|
|
||||||
interface ConfigContextValue {
|
interface ConfigContextValue {
|
||||||
@@ -14,6 +14,7 @@ interface ConfigContextValue {
|
|||||||
isConnected: boolean
|
isConnected: boolean
|
||||||
testConnection: () => Promise<boolean>
|
testConnection: () => Promise<boolean>
|
||||||
s2aClient: S2AClient | null
|
s2aClient: S2AClient | null
|
||||||
|
refreshConfig: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConfigContext = createContext<ConfigContextValue | null>(null)
|
const ConfigContext = createContext<ConfigContextValue | null>(null)
|
||||||
@@ -23,24 +24,37 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
|||||||
const [isConnected, setIsConnected] = useState(false)
|
const [isConnected, setIsConnected] = useState(false)
|
||||||
const [s2aClient, setS2aClient] = useState<S2AClient | null>(null)
|
const [s2aClient, setS2aClient] = useState<S2AClient | null>(null)
|
||||||
|
|
||||||
// Load config from localStorage on mount
|
// Load config from server on mount
|
||||||
useEffect(() => {
|
const refreshConfig = useCallback(async () => {
|
||||||
const savedConfig = loadConfig()
|
try {
|
||||||
setConfig(savedConfig)
|
const res = await fetch('/api/config')
|
||||||
|
const data = await res.json()
|
||||||
// Create S2A client if config is available
|
if (data.code === 0 && data.data) {
|
||||||
if (savedConfig.s2a.apiBase && savedConfig.s2a.adminKey) {
|
const serverConfig = data.data
|
||||||
const client = new S2AClient({
|
setConfig(prev => ({
|
||||||
baseUrl: savedConfig.s2a.apiBase,
|
...prev,
|
||||||
apiKey: savedConfig.s2a.adminKey,
|
s2a: {
|
||||||
})
|
...prev.s2a,
|
||||||
setS2aClient(client)
|
apiBase: serverConfig.s2a_api_base || '',
|
||||||
|
adminKey: serverConfig.s2a_admin_key || '',
|
||||||
// Test connection on load
|
},
|
||||||
client.testConnection().then(setIsConnected)
|
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
|
// Update S2A client when config changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (config.s2a.apiBase && config.s2a.adminKey) {
|
if (config.s2a.apiBase && config.s2a.adminKey) {
|
||||||
@@ -110,8 +124,9 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
|||||||
const testConnection = useCallback(async (): Promise<boolean> => {
|
const testConnection = useCallback(async (): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
// 使用后端代理 API 来测试 S2A 连接(避免 CORS 问题)
|
// 使用后端代理 API 来测试 S2A 连接(避免 CORS 问题)
|
||||||
const res = await fetch('http://localhost:8088/api/s2a/test')
|
const res = await fetch('/api/s2a/test')
|
||||||
const connected = res.ok
|
const data = await res.json()
|
||||||
|
const connected = data.code === 0
|
||||||
setIsConnected(connected)
|
setIsConnected(connected)
|
||||||
return connected
|
return connected
|
||||||
} catch {
|
} catch {
|
||||||
@@ -132,6 +147,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
|||||||
isConnected,
|
isConnected,
|
||||||
testConnection,
|
testConnection,
|
||||||
s2aClient,
|
s2aClient,
|
||||||
|
refreshConfig,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,105 +1,27 @@
|
|||||||
import { useState, useEffect } from 'react'
|
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
Server,
|
Server,
|
||||||
Mail,
|
Mail,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Settings,
|
Settings,
|
||||||
Save,
|
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Globe,
|
CheckCircle,
|
||||||
ToggleLeft,
|
XCircle
|
||||||
ToggleRight
|
|
||||||
} from 'lucide-react'
|
} 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'
|
import { useConfig } from '../hooks/useConfig'
|
||||||
|
|
||||||
export default function Config() {
|
export default function Config() {
|
||||||
const { config, isConnected } = useConfig()
|
const { config, isConnected, refreshConfig } = 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 configItems = [
|
const configItems = [
|
||||||
{
|
{
|
||||||
to: '/config/s2a',
|
to: '/config/s2a',
|
||||||
icon: Server,
|
icon: Server,
|
||||||
title: 'S2A 高级配置',
|
title: 'S2A 号池配置',
|
||||||
description: 'S2A 号池详细设置和测试',
|
description: 'S2A 连接、入库参数和代理设置',
|
||||||
status: isConnected ? '已连接' : '未连接',
|
status: isConnected ? '已连接' : '未连接',
|
||||||
|
statusIcon: isConnected ? CheckCircle : XCircle,
|
||||||
statusColor: isConnected ? 'text-green-600 dark:text-green-400' : 'text-red-500',
|
statusColor: isConnected ? 'text-green-600 dark:text-green-400' : 'text-red-500',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -108,6 +30,7 @@ export default function Config() {
|
|||||||
title: '邮箱服务配置',
|
title: '邮箱服务配置',
|
||||||
description: '配置邮箱服务用于自动注册',
|
description: '配置邮箱服务用于自动注册',
|
||||||
status: (config.email?.services?.length ?? 0) > 0 ? '已配置' : '未配置',
|
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',
|
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
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={fetchServerConfig}
|
onClick={() => refreshConfig()}
|
||||||
disabled={loading}
|
icon={<RefreshCw className="h-4 w-4" />}
|
||||||
icon={<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />}
|
|
||||||
>
|
>
|
||||||
刷新
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Message */}
|
{/* Config Cards */}
|
||||||
{message && (
|
<div className="grid gap-4">
|
||||||
<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">
|
|
||||||
{configItems.map((item) => (
|
{configItems.map((item) => (
|
||||||
<Link key={item.to} to={item.to} className="block group">
|
<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">
|
<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-4">
|
<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">
|
<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" />
|
<item.icon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
|
||||||
</div>
|
</div>
|
||||||
@@ -278,9 +69,12 @@ export default function Config() {
|
|||||||
<h3 className="font-semibold text-slate-900 dark:text-slate-100">{item.title}</h3>
|
<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>
|
<p className="text-sm text-slate-500 dark:text-slate-400">{item.description}</p>
|
||||||
</div>
|
</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}`}>
|
<span className={`text-sm font-medium ${item.statusColor}`}>
|
||||||
{item.status}
|
{item.status}
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
<ChevronRight className="h-5 w-5 text-slate-400 group-hover:text-blue-500 transition-colors" />
|
<ChevronRight className="h-5 w-5 text-slate-400 group-hover:text-blue-500 transition-colors" />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,55 +1,102 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { TestTube, CheckCircle, XCircle, Loader2, Save, Server, Plus, X } from 'lucide-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 { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common'
|
||||||
import { useConfig } from '../hooks/useConfig'
|
import { useConfig } from '../hooks/useConfig'
|
||||||
|
|
||||||
export default function S2AConfig() {
|
export default function S2AConfig() {
|
||||||
const {
|
const {
|
||||||
config,
|
|
||||||
updateS2AConfig,
|
|
||||||
updatePoolingConfig,
|
|
||||||
testConnection,
|
testConnection,
|
||||||
isConnected,
|
isConnected,
|
||||||
|
refreshConfig,
|
||||||
} = useConfig()
|
} = useConfig()
|
||||||
|
|
||||||
const [testing, setTesting] = useState(false)
|
const [testing, setTesting] = useState(false)
|
||||||
const [testResult, setTestResult] = useState<boolean | null>(null)
|
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 连接
|
// S2A 连接配置
|
||||||
const [s2aApiBase, setS2aApiBase] = useState(config.s2a.apiBase)
|
const [s2aApiBase, setS2aApiBase] = useState('')
|
||||||
const [s2aAdminKey, setS2aAdminKey] = useState(config.s2a.adminKey)
|
const [s2aAdminKey, setS2aAdminKey] = useState('')
|
||||||
|
|
||||||
// Local form state - 入库设置
|
// 入库设置
|
||||||
const [poolingConcurrency, setPoolingConcurrency] = useState(config.pooling.concurrency)
|
const [concurrency, setConcurrency] = useState(2)
|
||||||
const [poolingPriority, setPoolingPriority] = useState(config.pooling.priority)
|
const [priority, setPriority] = useState(0)
|
||||||
const [groupIds, setGroupIds] = useState<number[]>(config.pooling.groupIds || [])
|
const [groupIds, setGroupIds] = useState<number[]>([])
|
||||||
const [newGroupId, setNewGroupId] = useState('')
|
const [newGroupId, setNewGroupId] = useState('')
|
||||||
|
|
||||||
const handleTestConnection = async () => {
|
// 代理设置
|
||||||
// Save first
|
const [proxyEnabled, setProxyEnabled] = useState(false)
|
||||||
updateS2AConfig({ apiBase: s2aApiBase, adminKey: s2aAdminKey })
|
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)
|
setTesting(true)
|
||||||
setTestResult(null)
|
setTestResult(null)
|
||||||
|
|
||||||
// Wait a bit for the client to be recreated
|
// 先保存配置
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
await handleSave()
|
||||||
|
|
||||||
const result = await testConnection()
|
const result = await testConnection()
|
||||||
setTestResult(result)
|
setTestResult(result)
|
||||||
setTesting(false)
|
setTesting(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = async () => {
|
||||||
updateS2AConfig({ apiBase: s2aApiBase, adminKey: s2aAdminKey })
|
setSaving(true)
|
||||||
updatePoolingConfig({
|
setMessage(null)
|
||||||
concurrency: poolingConcurrency,
|
try {
|
||||||
priority: poolingPriority,
|
const res = await fetch('/api/config', {
|
||||||
groupIds: groupIds,
|
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)
|
const data = await res.json()
|
||||||
setTimeout(() => setSaved(false), 2000)
|
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 = () => {
|
const handleAddGroupId = () => {
|
||||||
@@ -64,6 +111,14 @@ export default function S2AConfig() {
|
|||||||
setGroupIds(groupIds.filter(g => g !== id))
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -73,20 +128,34 @@ export default function S2AConfig() {
|
|||||||
<Server className="h-7 w-7 text-blue-500" />
|
<Server className="h-7 w-7 text-blue-500" />
|
||||||
S2A 配置
|
S2A 配置
|
||||||
</h1>
|
</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>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSave}
|
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>
|
</Button>
|
||||||
</div>
|
</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 */}
|
{/* S2A Connection */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<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">
|
<div className="flex items-center gap-2">
|
||||||
{isConnected ? (
|
{isConnected ? (
|
||||||
<span className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400">
|
<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>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<Input
|
<Input
|
||||||
label="S2A API 地址"
|
label="S2A API 地址"
|
||||||
placeholder="http://localhost:8080"
|
placeholder="https://your-s2a-server.com"
|
||||||
value={s2aApiBase}
|
value={s2aApiBase}
|
||||||
onChange={(e) => setS2aApiBase(e.target.value)}
|
onChange={(e) => setS2aApiBase(e.target.value)}
|
||||||
hint="S2A 服务的 API 地址,例如 http://localhost:8080"
|
hint="S2A 服务的 API 地址"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="Admin API Key"
|
label="Admin API Key"
|
||||||
@@ -117,6 +187,7 @@ export default function S2AConfig() {
|
|||||||
onChange={(e) => setS2aAdminKey(e.target.value)}
|
onChange={(e) => setS2aAdminKey(e.target.value)}
|
||||||
hint="S2A 管理密钥,可在 S2A 后台 Settings 页面获取"
|
hint="S2A 管理密钥,可在 S2A 后台 Settings 页面获取"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -157,9 +228,9 @@ export default function S2AConfig() {
|
|||||||
label="默认并发数"
|
label="默认并发数"
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
max={10}
|
max={100}
|
||||||
value={poolingConcurrency}
|
value={concurrency}
|
||||||
onChange={(e) => setPoolingConcurrency(Number(e.target.value))}
|
onChange={(e) => setConcurrency(Number(e.target.value))}
|
||||||
hint="账号的默认并发请求数"
|
hint="账号的默认并发请求数"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
@@ -167,8 +238,8 @@ export default function S2AConfig() {
|
|||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
max={100}
|
max={100}
|
||||||
value={poolingPriority}
|
value={priority}
|
||||||
onChange={(e) => setPoolingPriority(Number(e.target.value))}
|
onChange={(e) => setPriority(Number(e.target.value))}
|
||||||
hint="账号的默认优先级,数值越大优先级越高"
|
hint="账号的默认优先级,数值越大优先级越高"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -222,6 +293,44 @@ export default function S2AConfig() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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 */}
|
{/* Info */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -232,6 +341,7 @@ export default function S2AConfig() {
|
|||||||
<li>Admin API Key 用于管理账号池,具有完全权限</li>
|
<li>Admin API Key 用于管理账号池,具有完全权限</li>
|
||||||
<li>入库默认设置会应用到新入库的账号</li>
|
<li>入库默认设置会应用到新入库的账号</li>
|
||||||
<li>分组 ID 用于将账号归类到指定分组</li>
|
<li>分组 ID 用于将账号归类到指定分组</li>
|
||||||
|
<li>配置会自动保存到服务器数据库</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user