From 9dfa61ac053c8131cf759c7ed2c59c1c4fe2c8c3 Mon Sep 17 00:00:00 2001 From: kyx236 Date: Fri, 30 Jan 2026 08:21:12 +0800 Subject: [PATCH] feat: introduce a new configuration system with dedicated pages for S2A and email settings. --- frontend/src/context/ConfigContext.tsx | 52 ++++-- frontend/src/pages/Config.tsx | 246 ++----------------------- frontend/src/pages/S2AConfig.tsx | 216 ++++++++++++++++------ 3 files changed, 217 insertions(+), 297 deletions(-) diff --git a/frontend/src/context/ConfigContext.tsx b/frontend/src/context/ConfigContext.tsx index 6b9f0c2..17611ec 100644 --- a/frontend/src/context/ConfigContext.tsx +++ b/frontend/src/context/ConfigContext.tsx @@ -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 s2aClient: S2AClient | null + refreshConfig: () => Promise } const ConfigContext = createContext(null) @@ -23,24 +24,37 @@ export function ConfigProvider({ children }: { children: ReactNode }) { const [isConnected, setIsConnected] = useState(false) const [s2aClient, setS2aClient] = useState(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 => { 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} diff --git a/frontend/src/pages/Config.tsx b/frontend/src/pages/Config.tsx index e652b4d..0da38fc 100644 --- a/frontend/src/pages/Config.tsx +++ b/frontend/src/pages/Config.tsx @@ -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() { - {/* Message */} - {message && ( -
- {message.text} -
- )} - - {/* Quick Config Card */} - - - - - 核心配置 - - - - {/* S2A Config */} -
-
- - setEditS2ABase(e.target.value)} - placeholder="https://your-s2a-server.com" - /> -
-
- - setEditS2AKey(e.target.value)} - placeholder="admin-xxxxxx" - /> -
-
- - {/* Pooling Config */} -
-
- - setEditConcurrency(parseInt(e.target.value) || 1)} - min={1} - max={100} - /> -
-
- - setEditPriority(parseInt(e.target.value) || 0)} - min={0} - max={100} - /> -
-
- - setEditGroupIds(e.target.value)} - placeholder="1, 2, 3" - /> -
-
- - {/* Proxy Config */} -
-
-
- - 代理设置 -
- -
- setProxyAddress(e.target.value)} - placeholder="http://127.0.0.1:7890" - disabled={!proxyEnabled} - className={!proxyEnabled ? 'opacity-50' : ''} - /> -

- 服务器部署时通常不需要代理,在本地开发或特殊网络环境下可启用 -

-
- - {/* Save Button */} -
- -
-
-
- - {/* Sub Config Cards */} -
+ {/* Config Cards */} +
{configItems.map((item) => ( - - + +
@@ -278,9 +69,12 @@ export default function Config() {

{item.title}

{item.description}

- - {item.status} - +
+ + + {item.status} + +
diff --git a/frontend/src/pages/S2AConfig.tsx b/frontend/src/pages/S2AConfig.tsx index db0415e..2be7265 100644 --- a/frontend/src/pages/S2AConfig.tsx +++ b/frontend/src/pages/S2AConfig.tsx @@ -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(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(config.pooling.groupIds || []) + // 入库设置 + const [concurrency, setConcurrency] = useState(2) + const [priority, setPriority] = useState(0) + const [groupIds, setGroupIds] = useState([]) 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, - }) - setSaved(true) - setTimeout(() => setSaved(false), 2000) + 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, + }), + }) + 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 ( +
+ +
+ ) + } + return (
{/* Header */} @@ -73,20 +128,34 @@ export default function S2AConfig() { S2A 配置 -

配置 S2A 号池连接和入库参数

+

配置 S2A 号池连接、入库参数和代理设置

+ {/* Message */} + {message && ( +
+ {message.text} +
+ )} + {/* S2A Connection */} - S2A 连接配置 + + + S2A 连接配置 +
{isConnected ? ( @@ -102,21 +171,23 @@ export default function S2AConfig() {
- setS2aApiBase(e.target.value)} - hint="S2A 服务的 API 地址,例如 http://localhost:8080" - /> - setS2aAdminKey(e.target.value)} - hint="S2A 管理密钥,可在 S2A 后台 Settings 页面获取" - /> +
+ setS2aApiBase(e.target.value)} + hint="S2A 服务的 API 地址" + /> + setS2aAdminKey(e.target.value)} + hint="S2A 管理密钥,可在 S2A 后台 Settings 页面获取" + /> +
@@ -222,6 +293,44 @@ export default function S2AConfig() {
+ {/* Proxy Settings */} + + + + + 代理设置 + + + + + setProxyAddress(e.target.value)} + placeholder="http://127.0.0.1:7890" + disabled={!proxyEnabled} + className={!proxyEnabled ? 'opacity-50' : ''} + /> +

+ 服务器部署时通常不需要代理,在本地开发或特殊网络环境下可启用 +

+
+
+ {/* Info */} @@ -232,6 +341,7 @@ export default function S2AConfig() {
  • Admin API Key 用于管理账号池,具有完全权限
  • 入库默认设置会应用到新入库的账号
  • 分组 ID 用于将账号归类到指定分组
  • +
  • 配置会自动保存到服务器数据库