This commit is contained in:
薇薇安 2026-02-03 15:22:25 +08:00
parent 50c933a8b0
commit 1e9b27f8b4

View File

@ -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,7 +162,7 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
handleChange(newValue)
}}
onBlur={handleBlur}
onKeyPress={(e) => {
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleBlur()
}
@ -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) {
//
loadConfigs().catch(() => {})
loadConfigMeta().catch(() => {}) //
loadSystemStatus().catch(() => {}) //
loadBackendStatus().catch(() => {}) //
await Promise.allSettled([
loadConfigs(),
loadSystemStatus(),
loadBackendStatus()
])
}
setLoading(false)
}
init()
//
if (isAdmin) {
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 <div className="global-config">加载中...</div>
@ -1306,91 +1172,127 @@ const GlobalConfig = () => {
</section>
)}
{/* 用户-账号授权管理(仅管理员) */}
{isAdmin && (
<section className="global-section">
<section className="global-section user-account-section">
<div className="section-header">
<h3>用户管理与账号授权</h3>
<h3>用户-账号授权管理</h3>
<p style={{ fontSize: '14px', color: '#666', marginTop: '8px' }}>
管理用户与其关联的交易账号用户登录后仅能看到已授权的账号并可在顶部下拉切换
管理用户与交易账号的关联权限用户登录后只能看到被授权的账号
</p>
</div>
<div className="accounts-table">
{usersDetailed.length ? (
<table>
<thead>
<tr>
<th>用户</th>
<th>角色</th>
<th>状态</th>
<th>已关联账号</th>
<th>关联新账号</th>
</tr>
</thead>
<tbody>
{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 (
<tr key={u.id}>
<td>{u.username}</td>
<td><span className={`acct-badge ${u.role === 'admin' ? 'ok' : 'off'}`}>{u.role}</span></td>
<td><span className={`acct-badge ${u.status === 'active' ? 'ok' : 'off'}`}>{u.status === 'active' ? '启用' : '禁用'}</span></td>
<td>
{userAccs.length === 0 ? (
<span style={{ color: '#999' }}>暂无</span>
<div className="user-list">
{usersDetailed.length === 0 ? (
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>暂无用户或加载中...</div>
) : (
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
{userAccs.map(a => (
<div key={a.id} style={{ display: 'flex', alignItems: 'center', gap: '6px', border: '1px solid #eee', padding: '4px 8px', borderRadius: '6px' }}>
<span>#{a.id} {a.name}</span>
<span className={`acct-badge ${a.status === 'active' ? 'ok' : 'off'}`}>{a.status === 'active' ? '启用' : '禁用'}</span>
<button
type="button"
className="system-btn danger"
onClick={() => handleRevoke(u.id, a.id)}
disabled={busy}
usersDetailed.map(user => (
<div key={user.id} className="user-item" style={{ border: '1px solid #eee', borderRadius: '8px', marginBottom: '16px', backgroundColor: '#fff', overflow: 'hidden' }}>
<div
className="user-header"
onClick={() => toggleUserExpand(user.id)}
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '16px',
cursor: 'pointer',
backgroundColor: expandedUserIds.has(user.id) ? '#f8f9fa' : '#fff'
}}
>
取消关联
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<span style={{ fontSize: '18px', color: '#666' }}>
{expandedUserIds.has(user.id) ? '▼' : '▶'}
</span>
<div style={{ fontWeight: 'bold', fontSize: '16px' }}>
{user.username}
<span style={{ marginLeft: '8px', fontSize: '12px', padding: '2px 6px', borderRadius: '4px', backgroundColor: user.role === 'admin' ? '#e3f2fd' : '#f5f5f5', color: user.role === 'admin' ? '#1976d2' : '#666' }}>
{user.role}
</span>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
<div style={{ fontSize: '13px', color: '#666' }}>
关联账号: {user.accounts ? user.accounts.length : 0}
</div>
<div style={{ fontSize: '12px', color: '#999' }}>ID: {user.id}</div>
</div>
</div>
{expandedUserIds.has(user.id) && (
<div className="user-body" style={{ padding: '16px', borderTop: '1px solid #eee' }}>
{/* 已关联账号列表 */}
<div className="user-accounts" style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '14px', fontWeight: '500', marginBottom: '8px', color: '#333' }}>已关联账号:</div>
{!user.accounts || user.accounts.length === 0 ? (
<div style={{ fontSize: '13px', color: '#999', fontStyle: 'italic' }}>未关联任何账号</div>
) : (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{user.accounts.map(acc => (
<div key={acc.id} className="account-tag" style={{ display: 'flex', alignItems: 'center', padding: '4px 10px', backgroundColor: '#f0f4c3', borderRadius: '20px', border: '1px solid #dce775', fontSize: '13px' }}>
<span>{acc.name || acc.id}</span>
<span style={{ marginLeft: '6px', fontSize: '11px', color: '#827717' }}>({acc.role})</span>
<button
onClick={(e) => {
e.stopPropagation()
handleRevoke(user.id, acc.id)
}}
style={{ marginLeft: '8px', border: 'none', background: 'none', cursor: 'pointer', color: '#d32f2f', padding: '0 2px', fontWeight: 'bold' }}
title="取消关联"
>
</button>
</div>
))}
</div>
)}
</td>
<td>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
</div>
{/* 添加关联操作区 */}
<div className="add-account-control" style={{ display: 'flex', gap: '10px', alignItems: 'center', backgroundColor: '#fafafa', padding: '10px', borderRadius: '6px' }}>
<select
value={linkAccountMap[u.id] || ''}
onChange={(e) => setLinkAccountMap({ ...linkAccountMap, [u.id]: e.target.value })}
style={{ minWidth: '200px' }}
value={linkAccountMap[user.id] || ''}
onChange={(e) => setLinkAccountMap(prev => ({ ...prev, [user.id]: e.target.value }))}
onClick={(e) => e.stopPropagation()}
style={{ padding: '6px', borderRadius: '4px', border: '1px solid #ddd', minWidth: '150px' }}
>
<option value="">选择账号</option>
{available.map(a => (
<option key={a.id} value={a.id}>#{a.id} {a.name}</option>
))}
<option value="">选择账号...</option>
{accountsAdmin
.filter(a => !(user.accounts || []).find(ua => ua.id === a.id)) //
.map(a => (
<option key={a.id} value={a.id}>
{a.name || `Account ${a.id}`}
</option>
))
}
</select>
<select value={linkRole} onChange={(e) => setLinkRole(e.target.value)}>
<option value="viewer">viewer</option>
<option value="owner">owner</option>
<select
value={linkRole}
onChange={(e) => setLinkRole(e.target.value)}
onClick={(e) => e.stopPropagation()}
style={{ padding: '6px', borderRadius: '4px', border: '1px solid #ddd' }}
>
<option value="viewer">观察者 (Viewer)</option>
<option value="trader">交易员 (Trader)</option>
</select>
<button
type="button"
className="system-btn primary"
onClick={() => handleGrant(u.id)}
disabled={busy || !linkAccountMap[u.id]}
onClick={(e) => {
e.stopPropagation()
handleGrant(user.id)
}}
disabled={!linkAccountMap[user.id] || busy}
className="system-btn primary small"
style={{ padding: '6px 12px', fontSize: '13px' }}
>
关联账号
关联
</button>
</div>
</td>
</tr>
)
})}
</tbody>
</table>
) : (
<div className="accounts-empty">暂无用户数据</div>
</div>
)}
</div>
))
)}
</div>
</section>
@ -1398,6 +1300,8 @@ const GlobalConfig = () => {
{/* 预设方案快速切换(仅管理员 + 全局策略账号) */}
{isAdmin && isGlobalStrategyAccount && (
<section className="global-section preset-section">
@ -1628,202 +1532,7 @@ const GlobalConfig = () => {
{/* 用户管理 */}
<section className="global-section">
<div className="section-header">
<h3>用户管理</h3>
<button
type="button"
className="btn-primary"
onClick={() => setShowUserForm(!showUserForm)}
disabled={busy}
>
{showUserForm ? '取消' : '+ 创建用户'}
</button>
</div>
{showUserForm && (
<div className="form-card">
<h4>创建新用户</h4>
<div className="form-group">
<label>用户名</label>
<input
type="text"
value={newUser.username}
onChange={(e) => setNewUser({ ...newUser, username: e.target.value })}
placeholder="输入用户名"
/>
</div>
<div className="form-group">
<label>密码</label>
<input
type="password"
value={newUser.password}
onChange={(e) => setNewUser({ ...newUser, password: e.target.value })}
placeholder="输入密码"
/>
</div>
<div className="form-group">
<label>角色</label>
<select
value={newUser.role}
onChange={(e) => setNewUser({ ...newUser, role: e.target.value })}
>
<option value="user">普通用户</option>
<option value="admin">管理员</option>
</select>
</div>
<div className="form-group">
<label>状态</label>
<select
value={newUser.status}
onChange={(e) => setNewUser({ ...newUser, status: e.target.value })}
>
<option value="active">启用</option>
<option value="disabled">禁用</option>
</select>
</div>
<div className="form-actions">
<button type="button" className="btn-primary" onClick={handleCreateUser} disabled={busy}>
创建
</button>
<button type="button" onClick={() => setShowUserForm(false)} disabled={busy}>
取消
</button>
</div>
</div>
)}
<div className="table-container">
<table className="data-table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>角色</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id}>
<td>{user.id}</td>
<td>{user.username}</td>
<td>
<select
value={user.role || 'user'}
onChange={(e) => handleUpdateUserRole(user.id, e.target.value)}
disabled={busy}
>
<option value="user">普通用户</option>
<option value="admin">管理员</option>
</select>
</td>
<td>
<select
value={user.status || 'active'}
onChange={(e) => handleUpdateUserStatus(user.id, e.target.value)}
disabled={busy}
>
<option value="active">启用</option>
<option value="disabled">禁用</option>
</select>
</td>
<td>
{editingUserId === user.id ? (
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<input
type="password"
data-user-id={user.id}
placeholder="新密码"
style={{ padding: '4px 8px', border: '1px solid #ddd', borderRadius: '4px', fontSize: '14px' }}
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleUpdateUserPassword(user.id)
}
}}
/>
<button
type="button"
className="btn-primary"
onClick={() => handleUpdateUserPassword(user.id)}
disabled={busy}
>
保存
</button>
<button
type="button"
onClick={() => {
setEditingUserId(null)
const input = document.querySelector(`input[data-user-id="${user.id}"]`)
if (input) input.value = ''
}}
disabled={busy}
>
取消
</button>
</div>
) : (
<button
type="button"
onClick={() => setEditingUserId(user.id)}
disabled={busy}
>
修改密码
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
{/* 账号管理 */}
<section className="global-section">
<div className="section-header">
<h3>账号管理</h3>
<button
type="button"
className="btn-primary"
onClick={loadAccounts}
disabled={busy}
>
刷新
</button>
</div>
<div className="table-container">
<table className="data-table">
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>状态</th>
<th>测试网</th>
<th>API Key</th>
</tr>
</thead>
<tbody>
{accounts.map((account) => (
<tr key={account.id}>
<td>{account.id}</td>
<td>{account.name || '未命名'}</td>
<td>
<span className={`status-badge ${account.status === 'active' ? 'active' : 'disabled'}`}>
{account.status === 'active' ? '启用' : '禁用'}
</span>
</td>
<td>{account.use_testnet ? '是' : '否'}</td>
<td>{account.has_api_key ? '已配置' : '未配置'}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
{/* 配置快照 Modal */}
{showSnapshot && (