From 1e9b27f8b4f9e895a66f5d2d6b17f94660eda02d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Tue, 3 Feb 2026 15:22:25 +0800 Subject: [PATCH] 1 --- frontend/src/components/GlobalConfig.jsx | 595 ++++++----------------- 1 file changed, 152 insertions(+), 443 deletions(-) diff --git a/frontend/src/components/GlobalConfig.jsx b/frontend/src/components/GlobalConfig.jsx index d825362..2dff426 100644 --- a/frontend/src/components/GlobalConfig.jsx +++ b/frontend/src/components/GlobalConfig.jsx @@ -1,8 +1,8 @@ -import React, { useState, useEffect, useRef } from 'react' +import React, { useState, useEffect } from 'react' import { Link } from 'react-router-dom' import { useSelector } from 'react-redux' import { api } from '../services/api' -import { selectCurrentUser, selectIsAdmin } from '../store/appSlice' +import { selectIsAdmin } from '../store/appSlice' import './GlobalConfig.css' import './ConfigPanel.css' // 复用 ConfigPanel 的样式 @@ -68,12 +68,10 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => { return val === null || val === undefined ? '' : val } - const [value, setValue] = useState(config.value) const [localValue, setLocalValue] = useState(getInitialDisplayValue(config.value)) const [isEditing, setIsEditing] = useState(false) useEffect(() => { - setValue(config.value) setIsEditing(false) setLocalValue(getInitialDisplayValue(config.value)) }, [config.value]) @@ -164,11 +162,11 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => { handleChange(newValue) }} onBlur={handleBlur} - onKeyPress={(e) => { - if (e.key === 'Enter') { - handleBlur() - } - }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleBlur() + } + }} disabled={disabled} style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }} /> @@ -183,22 +181,28 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => { } const GlobalConfig = () => { - const currentUser = useSelector(selectCurrentUser) const isAdmin = useSelector(selectIsAdmin) - const [users, setUsers] = useState([]) - const [accounts, setAccounts] = useState([]) const [loading, setLoading] = useState(true) const [message, setMessage] = useState('') const [busy, setBusy] = useState(false) - const [selectedUserId, setSelectedUserId] = useState(null) - const [showUserForm, setShowUserForm] = useState(false) - const [newUser, setNewUser] = useState({ username: '', password: '', role: 'user', status: 'active' }) - const [editingUserId, setEditingUserId] = useState(null) const [usersDetailed, setUsersDetailed] = useState([]) const [accountsAdmin, setAccountsAdmin] = useState([]) const [linkRole, setLinkRole] = useState('viewer') const [linkAccountMap, setLinkAccountMap] = useState({}) + const [expandedUserIds, setExpandedUserIds] = useState(new Set()) + + const toggleUserExpand = (userId) => { + setExpandedUserIds(prev => { + const next = new Set(prev) + if (next.has(userId)) { + next.delete(userId) + } else { + next.add(userId) + } + return next + }) + } // 系统控制相关 const [systemStatus, setSystemStatus] = useState(null) @@ -209,7 +213,6 @@ const GlobalConfig = () => { // 预设方案相关 const [configs, setConfigs] = useState({}) const [saving, setSaving] = useState(false) - const [configMeta, setConfigMeta] = useState(null) // 配置快照相关 const [showSnapshot, setShowSnapshot] = useState(false) @@ -447,38 +450,6 @@ const GlobalConfig = () => { } } - // 所有函数定义(必须在 useEffect 之前) - const loadUsers = async () => { - try { - const list = await api.getUsers() - setUsers(Array.isArray(list) ? list : []) - } catch (error) { - setMessage('加载用户列表失败: ' + (error.message || '未知错误')) - } finally { - setLoading(false) - } - } - - const loadAccounts = async () => { - try { - const list = await api.getAccounts() - setAccounts(Array.isArray(list) ? list : []) - } catch (error) { - console.error('加载账号列表失败:', error) - } - } - - const loadConfigMeta = async () => { - try { - const m = await api.getConfigMeta() - setConfigMeta(m || null) - } catch (e) { - // 静默失败,可能是权限问题 - console.error('loadConfigMeta failed:', e) - setConfigMeta(null) - } - } - // 已知全局配置项默认值(兜底:后端未返回时前端仍能显示,避免看不到新配置项) const KNOWN_GLOBAL_CONFIG_DEFAULTS = { MAX_RSI_FOR_LONG: { value: 70, type: 'number', category: 'strategy', description: '做多时 RSI 超过此值则不开多(避免超买区追多)。2026-01-31新增。' }, @@ -545,58 +516,22 @@ const GlobalConfig = () => { } } - // 检测当前配置匹配哪个预设方案 - const detectCurrentPreset = () => { - if (!configs || Object.keys(configs).length === 0) return null - - for (const [presetKey, preset] of Object.entries(presets)) { - let match = true - for (const [key, expectedValue] of Object.entries(preset.configs)) { - const currentConfig = configs[key] - if (!currentConfig) { - match = false - break - } - - let currentValue = currentConfig.value - if (key.includes('PERCENT') || key.includes('PCT')) { - if (PCT_LIKE_KEYS.has(key)) { - currentValue = currentValue <= 0.05 ? currentValue * 100 : currentValue - } else { - currentValue = currentValue * 100 - } - } - - if (typeof expectedValue === 'number' && typeof currentValue === 'number') { - if (Math.abs(currentValue - expectedValue) > 0.01) { - match = false - break - } - } else if (currentValue !== expectedValue) { - match = false - break - } - } - - if (match) { - return presetKey - } - } - - return null - } - useEffect(() => { - loadUsers() - loadAccounts() - // 只有管理员才加载配置和系统状态 + const init = async () => { + if (isAdmin) { + // 加载全局配置(独立于账户) + await Promise.allSettled([ + loadConfigs(), + loadSystemStatus(), + loadBackendStatus() + ]) + } + setLoading(false) + } + init() + + // 只有管理员才轮询系统状态 if (isAdmin) { - // 加载全局配置(独立于账户) - loadConfigs().catch(() => {}) - loadConfigMeta().catch(() => {}) // 静默失败 - loadSystemStatus().catch(() => {}) // 静默失败 - loadBackendStatus().catch(() => {}) // 静默失败 - const timer = setInterval(() => { loadSystemStatus().catch(() => {}) loadBackendStatus().catch(() => {}) @@ -784,7 +719,7 @@ const GlobalConfig = () => { displayNew = `${value} (${(value * 100).toFixed(2)}%)` } - const confirmMsg = `确定修改配置项【${key}】吗?\n\n原值: ${config.value}\n新值: ${value}\n\n修改将立即生效。` + const confirmMsg = `确定修改配置项【${key}】吗?\n\n原值: ${displayOld}\n新值: ${displayNew}\n\n修改将立即生效。` if (!window.confirm(confirmMsg)) { // 如果用户取消,理论上 UI 应该回滚。 // 但 ConfigItem 内部状态已经变了(onBlur 触发)。 @@ -973,75 +908,6 @@ const GlobalConfig = () => { } } - const handleCreateUser = async () => { - if (!newUser.username || !newUser.password) { - setMessage('用户名和密码不能为空') - return - } - setBusy(true) - setMessage('') - try { - await api.createUser(newUser) - setMessage('用户创建成功') - setShowUserForm(false) - setNewUser({ username: '', password: '', role: 'user', status: 'active' }) - await loadUsers() - } catch (error) { - setMessage('创建用户失败: ' + (error.message || '未知错误')) - } finally { - setBusy(false) - } - } - - const handleUpdateUserPassword = async (userId) => { - const passwordInput = document.querySelector(`input[data-user-id="${userId}"]`) - const password = passwordInput?.value - if (!password) { - setMessage('密码不能为空') - return - } - setBusy(true) - setMessage('') - try { - await api.updateUserPassword(userId, password) - setMessage('密码更新成功') - setEditingUserId(null) - if (passwordInput) passwordInput.value = '' - await loadUsers() - } catch (error) { - setMessage('更新密码失败: ' + (error.message || '未知错误')) - } finally { - setBusy(false) - } - } - - const handleUpdateUserRole = async (userId, role) => { - setBusy(true) - setMessage('') - try { - await api.updateUserRole(userId, role) - setMessage('角色更新成功') - await loadUsers() - } catch (error) { - setMessage('更新角色失败: ' + (error.message || '未知错误')) - } finally { - setBusy(false) - } - } - - const handleUpdateUserStatus = async (userId, status) => { - setBusy(true) - setMessage('') - try { - await api.updateUserStatus(userId, status) - setMessage('状态更新成功') - await loadUsers() - } catch (error) { - setMessage('更新状态失败: ' + (error.message || '未知错误')) - } finally { - setBusy(false) - } - } if (loading) { return
加载中...
@@ -1306,91 +1172,127 @@ const GlobalConfig = () => { )} + {/* 用户-账号授权管理(仅管理员) */} {isAdmin && ( -
+
-

用户管理与账号授权

+

用户-账号授权管理

- 管理用户与其关联的交易账号;用户登录后仅能看到已授权的账号,并可在顶部下拉切换 + 管理用户与交易账号的关联权限。用户登录后只能看到被授权的账号。

-
- {usersDetailed.length ? ( - - - - - - - - - - - - {usersDetailed.map(u => { - const userAccs = Array.isArray(u.accounts) ? u.accounts : [] - const linkedIds = new Set(userAccs.map(a => a.id)) - const available = accountsAdmin.filter(a => !linkedIds.has(a.id)) - return ( - - - - - - - - ) - })} - -
用户角色状态已关联账号关联新账号
{u.username}{u.role}{u.status === 'active' ? '启用' : '禁用'} - {userAccs.length === 0 ? ( - 暂无 - ) : ( -
- {userAccs.map(a => ( -
- #{a.id} {a.name} - {a.status === 'active' ? '启用' : '禁用'} - -
- ))} -
- )} -
-
- - - -
-
+ +
+ {usersDetailed.length === 0 ? ( +
暂无用户或加载中...
) : ( -
暂无用户数据
+ usersDetailed.map(user => ( +
+
toggleUserExpand(user.id)} + style={{ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '16px', + cursor: 'pointer', + backgroundColor: expandedUserIds.has(user.id) ? '#f8f9fa' : '#fff' + }} + > +
+ + {expandedUserIds.has(user.id) ? '▼' : '▶'} + +
+ {user.username} + + {user.role} + +
+
+
+
+ 关联账号: {user.accounts ? user.accounts.length : 0} +
+
ID: {user.id}
+
+
+ + {expandedUserIds.has(user.id) && ( +
+ {/* 已关联账号列表 */} +
+
已关联账号:
+ {!user.accounts || user.accounts.length === 0 ? ( +
未关联任何账号
+ ) : ( +
+ {user.accounts.map(acc => ( +
+ {acc.name || acc.id} + ({acc.role}) + +
+ ))} +
+ )} +
+ + {/* 添加关联操作区 */} +
+ + + + + +
+
+ )} +
+ )) )}
@@ -1398,6 +1300,8 @@ const GlobalConfig = () => { + + {/* 预设方案快速切换(仅管理员 + 全局策略账号) */} {isAdmin && isGlobalStrategyAccount && (
@@ -1628,202 +1532,7 @@ const GlobalConfig = () => { - {/* 用户管理 */} -
-
-

用户管理

- -
- {showUserForm && ( -
-

创建新用户

-
- - setNewUser({ ...newUser, username: e.target.value })} - placeholder="输入用户名" - /> -
-
- - setNewUser({ ...newUser, password: e.target.value })} - placeholder="输入密码" - /> -
-
- - -
-
- - -
-
- - -
-
- )} - -
- - - - - - - - - - - - {users.map((user) => ( - - - - - - - - ))} - -
ID用户名角色状态操作
{user.id}{user.username} - - - - - {editingUserId === user.id ? ( -
- { - if (e.key === 'Enter') { - handleUpdateUserPassword(user.id) - } - }} - /> - - -
- ) : ( - - )} -
-
-
- - {/* 账号管理 */} -
-
-

账号管理

- -
- -
- - - - - - - - - - - - {accounts.map((account) => ( - - - - - - - - ))} - -
ID名称状态测试网API Key
{account.id}{account.name || '未命名'} - - {account.status === 'active' ? '启用' : '禁用'} - - {account.use_testnet ? '是' : '否'}{account.has_api_key ? '已配置' : '未配置'}
-
-
{/* 配置快照 Modal */} {showSnapshot && (