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 { api } from '../services/api'
|
||||||
import './AdminDashboard.css'
|
import './AdminDashboard.css'
|
||||||
|
|
||||||
const UserAccountGroup = ({ user, onServiceAction }) => {
|
const UserAccountGroup = ({ user, allAccounts, onServiceAction }) => {
|
||||||
const [expanded, setExpanded] = useState(true)
|
const [expanded, setExpanded] = useState(true)
|
||||||
const [processing, setProcessing] = useState(false)
|
const [processing, setProcessing] = useState(false)
|
||||||
|
|
||||||
|
// 关联管理状态
|
||||||
|
const [linkAccountId, setLinkAccountId] = useState('')
|
||||||
|
const [linkRole, setLinkRole] = useState('viewer')
|
||||||
|
const [associating, setAssociating] = useState(false)
|
||||||
|
|
||||||
const handleUserAction = async (action) => {
|
const handleUserAction = async (action) => {
|
||||||
if (!window.confirm(`确定要${action === 'start' ? '启动' : '停止'}用户 ${user.username} 下所有账号的交易服务吗?`)) return
|
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))
|
.catch(e => console.error(`Failed to ${action} account ${acc.id}:`, e))
|
||||||
)
|
)
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
// 触发刷新(通过父组件回调或简单的延迟刷新,这里父组件会定时刷新,或者我们可以在父组件传递 refresh)
|
if (onServiceAction) onServiceAction(null, 'refresh')
|
||||||
// 这里简单点,父组件会定时刷新,或者我们可以手动触发父组件刷新
|
|
||||||
// 由于 onServiceAction 只是单个操作,我们这里最好能通知父组件刷新。
|
|
||||||
// 暂时依赖父组件的定时刷新或手动刷新。
|
|
||||||
if (onServiceAction) onServiceAction(null, 'refresh') // Hacky way to trigger refresh if supported
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(`操作失败: ${e.message}`)
|
alert(`操作失败: ${e.message}`)
|
||||||
} finally {
|
} 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 allRunning = user.accounts.every(a => a.serviceStatus?.running)
|
||||||
const allStopped = 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 (
|
return (
|
||||||
<div className="user-group-card">
|
<div className="user-group-card">
|
||||||
<div className="user-group-header" onClick={() => setExpanded(!expanded)}>
|
<div className="user-group-header" onClick={() => setExpanded(!expanded)}>
|
||||||
|
|
@ -70,6 +100,7 @@ const UserAccountGroup = ({ user, onServiceAction }) => {
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>名称</th>
|
<th>名称</th>
|
||||||
|
<th>权限</th>
|
||||||
<th>账户状态</th>
|
<th>账户状态</th>
|
||||||
<th>服务状态</th>
|
<th>服务状态</th>
|
||||||
<th>总资产</th>
|
<th>总资产</th>
|
||||||
|
|
@ -83,6 +114,9 @@ const UserAccountGroup = ({ user, onServiceAction }) => {
|
||||||
<tr key={acc.id}>
|
<tr key={acc.id}>
|
||||||
<td>{acc.id}</td>
|
<td>{acc.id}</td>
|
||||||
<td>{acc.name}</td>
|
<td>{acc.name}</td>
|
||||||
|
<td>
|
||||||
|
<span className="role-tag">{acc.role || 'viewer'}</span>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span className={`status-badge ${acc.status}`}>
|
<span className={`status-badge ${acc.status}`}>
|
||||||
{acc.status === 'active' ? '启用' : '禁用'}
|
{acc.status === 'active' ? '启用' : '禁用'}
|
||||||
|
|
@ -126,6 +160,14 @@ const UserAccountGroup = ({ user, onServiceAction }) => {
|
||||||
>
|
>
|
||||||
停止
|
停止
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleRevoke(acc.id)}
|
||||||
|
className="btn-mini danger"
|
||||||
|
style={{ marginLeft: '4px' }}
|
||||||
|
title="取消关联"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -133,6 +175,37 @@ const UserAccountGroup = ({ user, onServiceAction }) => {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -149,15 +222,17 @@ const AdminDashboard = () => {
|
||||||
// Don't set loading on background refresh (if data exists)
|
// Don't set loading on background refresh (if data exists)
|
||||||
if (!data) setLoading(true)
|
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.get('/admin/users/detailed').catch(() => ({ data: [] })),
|
||||||
api.getAdminDashboard(),
|
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 users = usersRes.data || []
|
||||||
const globalStats = dashboardRes // summary, accounts (list of stats)
|
const globalStats = dashboardRes // summary, accounts (list of stats)
|
||||||
const services = servicesRes.data.services || []
|
const services = servicesRes.data.services || []
|
||||||
|
const allAccounts = accountsRes.data || []
|
||||||
|
|
||||||
// Index stats and services by account ID
|
// Index stats and services by account ID
|
||||||
const statsMap = {}
|
const statsMap = {}
|
||||||
|
|
@ -194,7 +269,8 @@ const AdminDashboard = () => {
|
||||||
|
|
||||||
setData({
|
setData({
|
||||||
summary: globalStats.summary,
|
summary: globalStats.summary,
|
||||||
users: enrichedUsers
|
users: enrichedUsers,
|
||||||
|
allAccounts
|
||||||
})
|
})
|
||||||
setError(null)
|
setError(null)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -265,6 +341,7 @@ const AdminDashboard = () => {
|
||||||
<UserAccountGroup
|
<UserAccountGroup
|
||||||
key={user.id}
|
key={user.id}
|
||||||
user={user}
|
user={user}
|
||||||
|
allAccounts={data.allAccounts}
|
||||||
onServiceAction={handleServiceAction}
|
onServiceAction={handleServiceAction}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -186,24 +186,6 @@ const GlobalConfig = () => {
|
||||||
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 [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 [systemStatus, setSystemStatus] = useState(null)
|
||||||
const [backendStatus, setBackendStatus] = useState(null)
|
const [backendStatus, setBackendStatus] = useState(null)
|
||||||
|
|
@ -1175,131 +1157,7 @@ const GlobalConfig = () => {
|
||||||
</section>
|
</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