1
This commit is contained in:
parent
654103177d
commit
9958af7c3f
|
|
@ -2,9 +2,14 @@ import React, { useEffect, useState } from 'react'
|
|||
import { api } from '../services/api'
|
||||
import './AdminDashboard.css'
|
||||
|
||||
const UserAccountGroup = ({ user, onServiceAction }) => {
|
||||
const UserAccountGroup = ({ user, allAccounts, onServiceAction }) => {
|
||||
const [expanded, setExpanded] = useState(true)
|
||||
const [processing, setProcessing] = useState(false)
|
||||
|
||||
// 关联管理状态
|
||||
const [linkAccountId, setLinkAccountId] = useState('')
|
||||
const [linkRole, setLinkRole] = useState('viewer')
|
||||
const [associating, setAssociating] = useState(false)
|
||||
|
||||
const handleUserAction = async (action) => {
|
||||
if (!window.confirm(`确定要${action === 'start' ? '启动' : '停止'}用户 ${user.username} 下所有账号的交易服务吗?`)) return
|
||||
|
|
@ -17,11 +22,7 @@ const UserAccountGroup = ({ user, onServiceAction }) => {
|
|||
.catch(e => console.error(`Failed to ${action} account ${acc.id}:`, e))
|
||||
)
|
||||
await Promise.all(promises)
|
||||
// 触发刷新(通过父组件回调或简单的延迟刷新,这里父组件会定时刷新,或者我们可以在父组件传递 refresh)
|
||||
// 这里简单点,父组件会定时刷新,或者我们可以手动触发父组件刷新
|
||||
// 由于 onServiceAction 只是单个操作,我们这里最好能通知父组件刷新。
|
||||
// 暂时依赖父组件的定时刷新或手动刷新。
|
||||
if (onServiceAction) onServiceAction(null, 'refresh') // Hacky way to trigger refresh if supported
|
||||
if (onServiceAction) onServiceAction(null, 'refresh')
|
||||
} catch (e) {
|
||||
alert(`操作失败: ${e.message}`)
|
||||
} finally {
|
||||
|
|
@ -29,10 +30,39 @@ const UserAccountGroup = ({ user, onServiceAction }) => {
|
|||
}
|
||||
}
|
||||
|
||||
const handleGrant = async () => {
|
||||
if (!linkAccountId) return
|
||||
setAssociating(true)
|
||||
try {
|
||||
await api.grantUserAccount(user.id, linkAccountId, linkRole)
|
||||
setLinkAccountId('')
|
||||
if (onServiceAction) onServiceAction(null, 'refresh')
|
||||
} catch (e) {
|
||||
alert(`关联失败: ${e.message}`)
|
||||
} finally {
|
||||
setAssociating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRevoke = async (accountId) => {
|
||||
if (!window.confirm('确定要取消该账号的关联吗?')) return
|
||||
try {
|
||||
await api.revokeUserAccount(user.id, accountId)
|
||||
if (onServiceAction) onServiceAction(null, 'refresh')
|
||||
} catch (e) {
|
||||
alert(`取消关联失败: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算该用户下所有账号的汇总状态
|
||||
const allRunning = user.accounts.every(a => a.serviceStatus?.running)
|
||||
const allStopped = user.accounts.every(a => !a.serviceStatus?.running)
|
||||
|
||||
// 可供关联的账号(排除已关联的)
|
||||
const availableAccounts = (allAccounts || []).filter(a =>
|
||||
!user.accounts.some(ua => ua.id === a.id)
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="user-group-card">
|
||||
<div className="user-group-header" onClick={() => setExpanded(!expanded)}>
|
||||
|
|
@ -70,6 +100,7 @@ const UserAccountGroup = ({ user, onServiceAction }) => {
|
|||
<tr>
|
||||
<th>ID</th>
|
||||
<th>名称</th>
|
||||
<th>权限</th>
|
||||
<th>账户状态</th>
|
||||
<th>服务状态</th>
|
||||
<th>总资产</th>
|
||||
|
|
@ -83,6 +114,9 @@ const UserAccountGroup = ({ user, onServiceAction }) => {
|
|||
<tr key={acc.id}>
|
||||
<td>{acc.id}</td>
|
||||
<td>{acc.name}</td>
|
||||
<td>
|
||||
<span className="role-tag">{acc.role || 'viewer'}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span className={`status-badge ${acc.status}`}>
|
||||
{acc.status === 'active' ? '启用' : '禁用'}
|
||||
|
|
@ -126,6 +160,14 @@ const UserAccountGroup = ({ user, onServiceAction }) => {
|
|||
>
|
||||
停止
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleRevoke(acc.id)}
|
||||
className="btn-mini danger"
|
||||
style={{ marginLeft: '4px' }}
|
||||
title="取消关联"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -133,6 +175,37 @@ const UserAccountGroup = ({ user, onServiceAction }) => {
|
|||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
|
||||
{/* 关联管理区域 */}
|
||||
<div className="association-controls" style={{ marginTop: '15px', padding: '12px', backgroundColor: '#f8f9fa', borderRadius: '6px', border: '1px solid #e9ecef', display: 'flex', gap: '10px', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: '13px', fontWeight: 'bold', color: '#495057' }}>新增关联:</span>
|
||||
<select
|
||||
value={linkAccountId}
|
||||
onChange={e => setLinkAccountId(e.target.value)}
|
||||
style={{ padding: '4px 8px', borderRadius: '4px', border: '1px solid #ced4da' }}
|
||||
>
|
||||
<option value="">选择账号...</option>
|
||||
{availableAccounts.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)}
|
||||
style={{ padding: '4px 8px', borderRadius: '4px', border: '1px solid #ced4da' }}
|
||||
>
|
||||
<option value="viewer">观察者 (Viewer)</option>
|
||||
<option value="trader">交易员 (Trader)</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={handleGrant}
|
||||
disabled={!linkAccountId || associating}
|
||||
className="btn-mini primary"
|
||||
style={{ padding: '4px 12px' }}
|
||||
>
|
||||
关联
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -149,15 +222,17 @@ const AdminDashboard = () => {
|
|||
// Don't set loading on background refresh (if data exists)
|
||||
if (!data) setLoading(true)
|
||||
|
||||
const [usersRes, dashboardRes, servicesRes] = await Promise.all([
|
||||
const [usersRes, dashboardRes, servicesRes, accountsRes] = await Promise.all([
|
||||
api.get('/admin/users/detailed').catch(() => ({ data: [] })),
|
||||
api.getAdminDashboard(),
|
||||
api.get('/system/trading/services').catch(() => ({ data: { services: [] } }))
|
||||
api.get('/system/trading/services').catch(() => ({ data: { services: [] } })),
|
||||
api.get('/accounts').catch(() => ({ data: [] }))
|
||||
])
|
||||
|
||||
const users = usersRes.data || []
|
||||
const globalStats = dashboardRes // summary, accounts (list of stats)
|
||||
const services = servicesRes.data.services || []
|
||||
const allAccounts = accountsRes.data || []
|
||||
|
||||
// Index stats and services by account ID
|
||||
const statsMap = {}
|
||||
|
|
@ -194,7 +269,8 @@ const AdminDashboard = () => {
|
|||
|
||||
setData({
|
||||
summary: globalStats.summary,
|
||||
users: enrichedUsers
|
||||
users: enrichedUsers,
|
||||
allAccounts
|
||||
})
|
||||
setError(null)
|
||||
} catch (err) {
|
||||
|
|
@ -265,6 +341,7 @@ const AdminDashboard = () => {
|
|||
<UserAccountGroup
|
||||
key={user.id}
|
||||
user={user}
|
||||
allAccounts={data.allAccounts}
|
||||
onServiceAction={handleServiceAction}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -186,24 +186,6 @@ const GlobalConfig = () => {
|
|||
const [loading, setLoading] = useState(true)
|
||||
const [message, setMessage] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
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)
|
||||
const [backendStatus, setBackendStatus] = useState(null)
|
||||
|
|
@ -1175,131 +1157,7 @@ const GlobalConfig = () => {
|
|||
</section>
|
||||
)}
|
||||
|
||||
{/* 用户-账号授权管理(仅管理员) */}
|
||||
{isAdmin && (
|
||||
<section className="global-section user-account-section">
|
||||
<div className="section-header">
|
||||
<h3>用户-账号授权管理</h3>
|
||||
<p style={{ fontSize: '14px', color: '#666', marginTop: '8px' }}>
|
||||
管理用户与交易账号的关联权限。用户登录后只能看到被授权的账号。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="user-list">
|
||||
{usersDetailed.length === 0 ? (
|
||||
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>暂无用户或加载中...</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>
|
||||
</section>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user