1
This commit is contained in:
parent
50c933a8b0
commit
1e9b27f8b4
|
|
@ -1,8 +1,8 @@
|
||||||
import React, { useState, useEffect, useRef } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { api } from '../services/api'
|
import { api } from '../services/api'
|
||||||
import { selectCurrentUser, selectIsAdmin } from '../store/appSlice'
|
import { selectIsAdmin } from '../store/appSlice'
|
||||||
import './GlobalConfig.css'
|
import './GlobalConfig.css'
|
||||||
import './ConfigPanel.css' // 复用 ConfigPanel 的样式
|
import './ConfigPanel.css' // 复用 ConfigPanel 的样式
|
||||||
|
|
||||||
|
|
@ -68,12 +68,10 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
return val === null || val === undefined ? '' : val
|
return val === null || val === undefined ? '' : val
|
||||||
}
|
}
|
||||||
|
|
||||||
const [value, setValue] = useState(config.value)
|
|
||||||
const [localValue, setLocalValue] = useState(getInitialDisplayValue(config.value))
|
const [localValue, setLocalValue] = useState(getInitialDisplayValue(config.value))
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue(config.value)
|
|
||||||
setIsEditing(false)
|
setIsEditing(false)
|
||||||
setLocalValue(getInitialDisplayValue(config.value))
|
setLocalValue(getInitialDisplayValue(config.value))
|
||||||
}, [config.value])
|
}, [config.value])
|
||||||
|
|
@ -164,11 +162,11 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
handleChange(newValue)
|
handleChange(newValue)
|
||||||
}}
|
}}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onKeyPress={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
handleBlur()
|
handleBlur()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
|
style={{ width: '100%', padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}
|
||||||
/>
|
/>
|
||||||
|
|
@ -183,22 +181,28 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const GlobalConfig = () => {
|
const GlobalConfig = () => {
|
||||||
const currentUser = useSelector(selectCurrentUser)
|
|
||||||
const isAdmin = useSelector(selectIsAdmin)
|
const isAdmin = useSelector(selectIsAdmin)
|
||||||
|
|
||||||
const [users, setUsers] = useState([])
|
|
||||||
const [accounts, setAccounts] = useState([])
|
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [message, setMessage] = useState('')
|
const [message, setMessage] = useState('')
|
||||||
const [busy, setBusy] = useState(false)
|
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 [usersDetailed, setUsersDetailed] = useState([])
|
||||||
const [accountsAdmin, setAccountsAdmin] = useState([])
|
const [accountsAdmin, setAccountsAdmin] = useState([])
|
||||||
const [linkRole, setLinkRole] = useState('viewer')
|
const [linkRole, setLinkRole] = useState('viewer')
|
||||||
const [linkAccountMap, setLinkAccountMap] = useState({})
|
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)
|
const [systemStatus, setSystemStatus] = useState(null)
|
||||||
|
|
@ -209,7 +213,6 @@ const GlobalConfig = () => {
|
||||||
// 预设方案相关
|
// 预设方案相关
|
||||||
const [configs, setConfigs] = useState({})
|
const [configs, setConfigs] = useState({})
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [configMeta, setConfigMeta] = useState(null)
|
|
||||||
|
|
||||||
// 配置快照相关
|
// 配置快照相关
|
||||||
const [showSnapshot, setShowSnapshot] = useState(false)
|
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 = {
|
const KNOWN_GLOBAL_CONFIG_DEFAULTS = {
|
||||||
MAX_RSI_FOR_LONG: { value: 70, type: 'number', category: 'strategy', description: '做多时 RSI 超过此值则不开多(避免超买区追多)。2026-01-31新增。' },
|
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(() => {
|
useEffect(() => {
|
||||||
loadUsers()
|
const init = async () => {
|
||||||
loadAccounts()
|
if (isAdmin) {
|
||||||
// 只有管理员才加载配置和系统状态
|
// 加载全局配置(独立于账户)
|
||||||
if (isAdmin) {
|
await Promise.allSettled([
|
||||||
// 加载全局配置(独立于账户)
|
loadConfigs(),
|
||||||
loadConfigs().catch(() => {})
|
loadSystemStatus(),
|
||||||
loadConfigMeta().catch(() => {}) // 静默失败
|
loadBackendStatus()
|
||||||
loadSystemStatus().catch(() => {}) // 静默失败
|
])
|
||||||
loadBackendStatus().catch(() => {}) // 静默失败
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
init()
|
||||||
|
|
||||||
|
// 只有管理员才轮询系统状态
|
||||||
|
if (isAdmin) {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
loadSystemStatus().catch(() => {})
|
loadSystemStatus().catch(() => {})
|
||||||
loadBackendStatus().catch(() => {})
|
loadBackendStatus().catch(() => {})
|
||||||
|
|
@ -784,7 +719,7 @@ const GlobalConfig = () => {
|
||||||
displayNew = `${value} (${(value * 100).toFixed(2)}%)`
|
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)) {
|
if (!window.confirm(confirmMsg)) {
|
||||||
// 如果用户取消,理论上 UI 应该回滚。
|
// 如果用户取消,理论上 UI 应该回滚。
|
||||||
// 但 ConfigItem 内部状态已经变了(onBlur 触发)。
|
// 但 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) {
|
if (loading) {
|
||||||
return <div className="global-config">加载中...</div>
|
return <div className="global-config">加载中...</div>
|
||||||
|
|
@ -1306,91 +1172,127 @@ const GlobalConfig = () => {
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 用户-账号授权管理(仅管理员) */}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<section className="global-section">
|
<section className="global-section user-account-section">
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
<h3>用户管理与账号授权</h3>
|
<h3>用户-账号授权管理</h3>
|
||||||
<p style={{ fontSize: '14px', color: '#666', marginTop: '8px' }}>
|
<p style={{ fontSize: '14px', color: '#666', marginTop: '8px' }}>
|
||||||
管理用户与其关联的交易账号;用户登录后仅能看到已授权的账号,并可在顶部下拉切换
|
管理用户与交易账号的关联权限。用户登录后只能看到被授权的账号。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="accounts-table">
|
|
||||||
{usersDetailed.length ? (
|
<div className="user-list">
|
||||||
<table>
|
{usersDetailed.length === 0 ? (
|
||||||
<thead>
|
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>暂无用户或加载中...</div>
|
||||||
<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 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}
|
|
||||||
>
|
|
||||||
取消关联
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
|
||||||
<select
|
|
||||||
value={linkAccountMap[u.id] || ''}
|
|
||||||
onChange={(e) => setLinkAccountMap({ ...linkAccountMap, [u.id]: e.target.value })}
|
|
||||||
style={{ minWidth: '200px' }}
|
|
||||||
>
|
|
||||||
<option value="">选择账号</option>
|
|
||||||
{available.map(a => (
|
|
||||||
<option key={a.id} value={a.id}>#{a.id} {a.name}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<select value={linkRole} onChange={(e) => setLinkRole(e.target.value)}>
|
|
||||||
<option value="viewer">viewer</option>
|
|
||||||
<option value="owner">owner</option>
|
|
||||||
</select>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="system-btn primary"
|
|
||||||
onClick={() => handleGrant(u.id)}
|
|
||||||
disabled={busy || !linkAccountMap[u.id]}
|
|
||||||
>
|
|
||||||
关联账号
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="accounts-empty">暂无用户数据</div>
|
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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 添加关联操作区 */}
|
||||||
|
<div className="add-account-control" style={{ display: 'flex', gap: '10px', alignItems: 'center', backgroundColor: '#fafafa', padding: '10px', borderRadius: '6px' }}>
|
||||||
|
<select
|
||||||
|
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>
|
||||||
|
{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)}
|
||||||
|
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
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -1398,6 +1300,8 @@ const GlobalConfig = () => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* 预设方案快速切换(仅管理员 + 全局策略账号) */}
|
{/* 预设方案快速切换(仅管理员 + 全局策略账号) */}
|
||||||
{isAdmin && isGlobalStrategyAccount && (
|
{isAdmin && isGlobalStrategyAccount && (
|
||||||
<section className="global-section preset-section">
|
<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 */}
|
{/* 配置快照 Modal */}
|
||||||
{showSnapshot && (
|
{showSnapshot && (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user