diff --git a/frontend/src/pages/S2AConfig.tsx b/frontend/src/pages/S2AConfig.tsx index c53150b..3176387 100644 --- a/frontend/src/pages/S2AConfig.tsx +++ b/frontend/src/pages/S2AConfig.tsx @@ -1,112 +1,146 @@ import { useState, useEffect, useCallback } from 'react' -import { TestTube, CheckCircle, XCircle, Loader2, Save, Server, Plus, X, Globe, ToggleLeft, ToggleRight, Bookmark, Trash2, Download } from 'lucide-react' +import { TestTube, CheckCircle, XCircle, Loader2, Save, Server, Plus, X, Globe, ToggleLeft, ToggleRight, Trash2, ChevronLeft, Zap, Edit2 } from 'lucide-react' import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common' import { useConfig } from '../hooks/useConfig' import type { S2AProfile } from '../types' -export default function S2AConfig() { - const { - testConnection, - isConnected, - refreshConfig, - } = useConfig() +type ViewMode = 'list' | 'edit' - const [testing, setTesting] = useState(false) - const [testResult, setTestResult] = useState(null) - const [saving, setSaving] = useState(false) +export default function S2AConfig() { + const { testConnection, isConnected, refreshConfig } = useConfig() + + const [viewMode, setViewMode] = useState('list') + const [profiles, setProfiles] = useState([]) const [loading, setLoading] = useState(true) const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null) + const [editingId, setEditingId] = useState(null) // null = 新建 - // S2A 连接配置 - const [s2aApiBase, setS2aApiBase] = useState('') - const [s2aAdminKey, setS2aAdminKey] = useState('') - - // 入库设置 - const [concurrency, setConcurrency] = useState(2) - const [priority, setPriority] = useState(0) - const [groupIds, setGroupIds] = useState([]) + // 表单状态 + const [formName, setFormName] = useState('') + const [formApiBase, setFormApiBase] = useState('') + const [formAdminKey, setFormAdminKey] = useState('') + const [formConcurrency, setFormConcurrency] = useState(2) + const [formPriority, setFormPriority] = useState(0) + const [formGroupIds, setFormGroupIds] = useState([]) const [newGroupId, setNewGroupId] = useState('') + const [formProxyEnabled, setFormProxyEnabled] = useState(false) + const [formProxyAddress, setFormProxyAddress] = useState('') - // 代理设置 - const [proxyEnabled, setProxyEnabled] = useState(false) - const [proxyAddress, setProxyAddress] = useState('') + const [saving, setSaving] = useState(false) + const [testing, setTesting] = useState(false) + const [testResult, setTestResult] = useState(null) + const [activating, setActivating] = useState(null) - // 预设管理 - const [profiles, setProfiles] = useState([]) - const [profileName, setProfileName] = useState('') - const [showSavePreset, setShowSavePreset] = useState(false) - const [savingPreset, setSavingPreset] = useState(false) + // 当前活动配置(从 /api/config 读取) + const [activeApiBase, setActiveApiBase] = useState('') - // 加载预设列表 const fetchProfiles = useCallback(async () => { try { const res = await fetch('/api/s2a/profiles') const data = await res.json() - if (data.code === 0 && data.data) { - setProfiles(data.data) + if (data.code === 0) { + setProfiles(data.data || []) } - } catch (error) { - console.error('Failed to fetch profiles:', error) - } + } catch { /* ignore */ } }, []) - // 从服务器加载配置 - const fetchConfig = async () => { - setLoading(true) + const fetchActiveConfig = useCallback(async () => { 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 || '') + setActiveApiBase(data.data.s2a_api_base || '') } - } catch (error) { - console.error('Failed to fetch config:', error) - } finally { - setLoading(false) - } - } + } catch { /* ignore */ } + }, []) useEffect(() => { - fetchConfig() - fetchProfiles() - }, [fetchProfiles]) + Promise.all([fetchProfiles(), fetchActiveConfig()]).finally(() => setLoading(false)) + }, [fetchProfiles, fetchActiveConfig]) - const handleTestConnection = async () => { - setTesting(true) + // 清除消息定时器 + useEffect(() => { + if (!message) return + const t = setTimeout(() => setMessage(null), 4000) + return () => clearTimeout(t) + }, [message]) + + // 重置表单 + const resetForm = () => { + setFormName('') + setFormApiBase('') + setFormAdminKey('') + setFormConcurrency(2) + setFormPriority(0) + setFormGroupIds([]) + setNewGroupId('') + setFormProxyEnabled(false) + setFormProxyAddress('') setTestResult(null) - await handleSave() - const result = await testConnection() - setTestResult(result) - setTesting(false) } - const handleSave = async () => { - setSaving(true) - setMessage(null) + // 进入新建模式 + const handleAddNew = () => { + resetForm() + setEditingId(null) + setViewMode('edit') + } + + // 进入编辑模式 + const handleEdit = (profile: S2AProfile) => { + setEditingId(profile.id) + setFormName(profile.name) + setFormApiBase(profile.api_base) + setFormAdminKey(profile.admin_key) + setFormConcurrency(profile.concurrency) + setFormPriority(profile.priority) try { - const res = await fetch('/api/config', { - method: 'PUT', + const ids = JSON.parse(profile.group_ids || '[]') + setFormGroupIds(Array.isArray(ids) ? ids : []) + } catch { setFormGroupIds([]) } + setFormProxyEnabled(profile.proxy_enabled) + setFormProxyAddress(profile.proxy_address || '') + setTestResult(null) + setViewMode('edit') + } + + // 返回列表 + const handleBack = () => { + setViewMode('list') + setMessage(null) + } + + // 保存配置(新建或更新) + const handleSaveProfile = async () => { + if (!formName.trim()) { + setMessage({ type: 'error', text: '请输入配置名称' }) + return + } + setSaving(true) + try { + // 如果是编辑,先删除旧的再新建(简单实现) + if (editingId !== null) { + await fetch(`/api/s2a/profiles?id=${editingId}`, { method: 'DELETE' }) + } + const res = await fetch('/api/s2a/profiles', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - s2a_api_base: s2aApiBase, - s2a_admin_key: s2aAdminKey, - concurrency, - priority, - group_ids: groupIds, - proxy_enabled: proxyEnabled, - default_proxy: proxyAddress, + name: formName.trim(), + api_base: formApiBase, + admin_key: formAdminKey, + concurrency: formConcurrency, + priority: formPriority, + group_ids: JSON.stringify(formGroupIds), + proxy_enabled: formProxyEnabled, + proxy_address: formProxyAddress, }), }) const data = await res.json() if (data.code === 0) { - setMessage({ type: 'success', text: '配置已保存' }) - refreshConfig() + setMessage({ type: 'success', text: `配置「${formName}」已保存` }) + await fetchProfiles() + setViewMode('list') } else { setMessage({ type: 'error', text: data.message || '保存失败' }) } @@ -117,77 +151,47 @@ export default function S2AConfig() { } } - 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)) - } - - // 保存为预设 - const handleSavePreset = async () => { - if (!profileName.trim()) return - setSavingPreset(true) + // 启用某个配置(写入活动配置) + const handleActivate = async (profile: S2AProfile) => { + setActivating(profile.id) try { - const res = await fetch('/api/s2a/profiles', { - method: 'POST', + let groupIds: number[] = [] + try { groupIds = JSON.parse(profile.group_ids || '[]') } catch { /* ignore */ } + const res = await fetch('/api/config', { + method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - name: profileName.trim(), - api_base: s2aApiBase, - admin_key: s2aAdminKey, - concurrency, - priority, - group_ids: JSON.stringify(groupIds), - proxy_enabled: proxyEnabled, - proxy_address: proxyAddress, + s2a_api_base: profile.api_base, + s2a_admin_key: profile.admin_key, + concurrency: profile.concurrency, + priority: profile.priority, + group_ids: groupIds, + proxy_enabled: profile.proxy_enabled, + default_proxy: profile.proxy_address, }), }) const data = await res.json() if (data.code === 0) { - setMessage({ type: 'success', text: `预设「${profileName}」已保存` }) - setProfileName('') - setShowSavePreset(false) - fetchProfiles() + setActiveApiBase(profile.api_base) + setMessage({ type: 'success', text: `已启用配置「${profile.name}」` }) + refreshConfig() } else { - setMessage({ type: 'error', text: data.message || '保存预设失败' }) + setMessage({ type: 'error', text: data.message || '启用失败' }) } } catch { setMessage({ type: 'error', text: '网络错误' }) } finally { - setSavingPreset(false) + setActivating(null) } } - // 加载预设到表单 - const handleLoadProfile = (profile: S2AProfile) => { - setS2aApiBase(profile.api_base) - setS2aAdminKey(profile.admin_key) - setConcurrency(profile.concurrency) - setPriority(profile.priority) - try { - const ids = JSON.parse(profile.group_ids || '[]') - setGroupIds(Array.isArray(ids) ? ids : []) - } catch { - setGroupIds([]) - } - setProxyEnabled(profile.proxy_enabled) - setProxyAddress(profile.proxy_address || '') - setMessage({ type: 'success', text: `已加载预设「${profile.name}」,请点击保存配置以应用` }) - } - - // 删除预设 - const handleDeleteProfile = async (id: number, name: string) => { + // 删除配置 + const handleDelete = async (id: number, name: string) => { try { const res = await fetch(`/api/s2a/profiles?id=${id}`, { method: 'DELETE' }) const data = await res.json() if (data.code === 0) { - setMessage({ type: 'success', text: `预设「${name}」已删除` }) + setMessage({ type: 'success', text: `配置「${name}」已删除` }) fetchProfiles() } } catch { @@ -195,6 +199,41 @@ export default function S2AConfig() { } } + // 测试连接(编辑表单中) + const handleTestInForm = async () => { + setTesting(true) + setTestResult(null) + // 临时保存到活动配置来测试 + try { + await fetch('/api/config', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + s2a_api_base: formApiBase, + s2a_admin_key: formAdminKey, + }), + }) + const result = await testConnection() + setTestResult(result) + } catch { + setTestResult(false) + } finally { + setTesting(false) + } + } + + const handleAddGroupId = () => { + const id = parseInt(newGroupId, 10) + if (!isNaN(id) && !formGroupIds.includes(id)) { + setFormGroupIds([...formGroupIds, id]) + setNewGroupId('') + } + } + + const handleRemoveGroupId = (id: number) => { + setFormGroupIds(formGroupIds.filter(g => g !== id)) + } + if (loading) { return (
@@ -203,6 +242,149 @@ export default function S2AConfig() { ) } + // ==================== 编辑/新建视图 ==================== + if (viewMode === 'edit') { + return ( +
+ {/* Header */} +
+ +
+

+ {editingId ? '编辑配置' : '添加配置'} +

+
+ +
+ + {message && ( +
+ {message.text} +
+ )} + + {/* 配置名称 */} + + + setFormName(e.target.value)} + /> + + + + {/* S2A 连接 */} + + + + + S2A 连接 + + + +
+ setFormApiBase(e.target.value)} + hint="S2A 服务的 API 地址" + /> + setFormAdminKey(e.target.value)} + hint="S2A 管理密钥" + /> +
+
+ + {testResult !== null && ( + + {testResult ? <> 连接成功 : <> 连接失败} + + )} +
+
+
+ + {/* 入库设置 */} + + + 入库默认设置 + + +
+ setFormConcurrency(Number(e.target.value))} hint="账号的默认并发请求数" /> + setFormPriority(Number(e.target.value))} hint="数值越大优先级越高" /> +
+
+ +
+ {formGroupIds.map(id => ( + + {id} + + + ))} + {formGroupIds.length === 0 && 未设置分组} +
+
+
+ setNewGroupId(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleAddGroupId()} className="h-10" /> +
+ +
+
+
+
+ + {/* 代理设置 */} + + + + + 代理设置 + + + + + setFormProxyAddress(e.target.value)} placeholder="http://127.0.0.1:7890" disabled={!formProxyEnabled} className={!formProxyEnabled ? 'opacity-50' : ''} /> +

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

+
+
+
+ ) + } + + // ==================== 列表视图 ==================== return (
{/* Header */} @@ -210,332 +392,154 @@ export default function S2AConfig() {

- S2A 配置 + S2A 配置管理

-

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

-
-
- - +

管理多个 S2A 号池配置,点击启用切换环境

+
{/* Message */} {message && (
+ : 'bg-red-50 text-red-700 dark:bg-red-900/30 dark:text-red-400'}`}> {message.text}
)} - {/* Save Preset Inline */} - {showSavePreset && ( - - -
-
- setProfileName(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleSavePreset()} - /> -
- - -
-

- 将当前所有配置(连接、入库参数、代理)保存为预设,方便快速切换 -

-
-
+ {/* 当前活动状态 */} + {activeApiBase && ( +
+ + {isConnected ? : } + 当前活动: {activeApiBase} + +
)} - {/* Main Grid Layout */} -
- {/* Left: Main Config (col-span-2) */} -
- {/* S2A Connection */} - - - - - S2A 连接配置 - -
- {isConnected ? ( - - - 已连接 - - ) : ( - - - 未连接 - - )} -
-
- -
- setS2aApiBase(e.target.value)} - hint="S2A 服务的 API 地址" - /> - setS2aAdminKey(e.target.value)} - hint="S2A 管理密钥" - /> -
-
- - {testResult !== null && ( - - {testResult ? '连接成功' : '连接失败'} - - )} -
-
-
- - {/* Pooling Settings */} - - - 入库默认设置 - - -
- setConcurrency(Number(e.target.value))} - hint="账号的默认并发请求数" - /> - setPriority(Number(e.target.value))} - hint="数值越大优先级越高" - /> -
- {/* Group IDs */} -
- -
- {groupIds.map(id => ( - - {id} - - - ))} - {groupIds.length === 0 && ( - 未设置分组 - )} -
-
-
- setNewGroupId(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleAddGroupId()} - className="h-10" - /> -
- -
-
-
-
- - {/* Proxy Settings */} - - - - - 代理设置 - - - - - setProxyAddress(e.target.value)} - placeholder="http://127.0.0.1:7890" - disabled={!proxyEnabled} - className={!proxyEnabled ? 'opacity-50' : ''} - /> -

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

-
-
+ {/* 配置列表 */} + {profiles.length === 0 ? ( + + +
+ +

暂无配置

+

点击「添加配置」创建第一个 S2A 连接配置

+ +
+
+
+ ) : ( +
+ {profiles.map(profile => ( + handleActivate(profile)} + onEdit={() => handleEdit(profile)} + onDelete={() => handleDelete(profile.id, profile.name)} + /> + ))}
- - {/* Right: Saved Profiles Sidebar (col-span-1) */} -
- - - - - 已保存配置 - - {profiles.length} 个预设 - - - {profiles.length === 0 ? ( -
- -

暂无保存的配置

-

点击上方「保存为预设」按钮保存当前配置

-
- ) : ( -
- {profiles.map(profile => ( - - ))} -
- )} -
-
-
-
+ )}
) } -// 预设列表项组件 -function ProfileItem({ profile, onLoad, onDelete }: { +// ==================== 配置卡片组件 ==================== +function ProfileCard({ profile, isActive, isActivating, onActivate, onEdit, onDelete }: { profile: S2AProfile - onLoad: (p: S2AProfile) => void - onDelete: (id: number, name: string) => void + isActive: boolean + isActivating: boolean + onActivate: () => void + onEdit: () => void + onDelete: () => void }) { const [confirming, setConfirming] = useState(false) let parsedGroups: number[] = [] - try { - parsedGroups = JSON.parse(profile.group_ids || '[]') - } catch { /* ignore */ } + try { parsedGroups = JSON.parse(profile.group_ids || '[]') } catch { /* ignore */ } return ( -
-
- {profile.name} -
- - {confirming ? ( - - ) : ( - + + {/* 活动标记 */} + {isActive && ( +
+ 使用中 +
+ )} + + {/* 名称 + 操作 */} +
+
+

{profile.name}

+

{profile.api_base || '未设置 API 地址'}

+
+
+ + {/* 参数摘要 */} +
+ + 并发 {profile.concurrency} + + + 优先级 {profile.priority} + + {parsedGroups.length > 0 && ( + + 分组 {parsedGroups.join(', ')} + + )} + {profile.proxy_enabled && ( + + 代理 + )}
-
-
-

{profile.api_base || '未设置 API'}

-
- 并发: {profile.concurrency} - 优先级: {profile.priority} - {parsedGroups.length > 0 && 分组: {parsedGroups.join(',')}} - {profile.proxy_enabled && 代理} + + {/* 操作按钮 */} +
+ {!isActive ? ( + + ) : ( +
+ 当前使用中 +
+ )} + + {confirming ? ( + + ) : ( +
-
-
+ + ) } \ No newline at end of file