This commit is contained in:
薇薇安 2026-02-03 15:42:29 +08:00
parent 654103177d
commit 9958af7c3f
2 changed files with 86 additions and 151 deletions

View File

@ -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}
/>
))}

View File

@ -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>
)}