diff --git a/frontend/src/components/AdminDashboard.jsx b/frontend/src/components/AdminDashboard.jsx index 9cebfd7..9c370e1 100644 --- a/frontend/src/components/AdminDashboard.jsx +++ b/frontend/src/components/AdminDashboard.jsx @@ -2,6 +2,143 @@ import React, { useEffect, useState } from 'react' import { api } from '../services/api' import './AdminDashboard.css' +const UserAccountGroup = ({ user, onServiceAction }) => { + const [expanded, setExpanded] = useState(true) + const [processing, setProcessing] = useState(false) + + const handleUserAction = async (action) => { + if (!window.confirm(`确定要${action === 'start' ? '启动' : '停止'}用户 ${user.username} 下所有账号的交易服务吗?`)) return + + setProcessing(true) + try { + // 并行执行 + const promises = user.accounts.map(acc => + api.post(`/accounts/${acc.id}/service/${action}`) + .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 + } catch (e) { + alert(`操作失败: ${e.message}`) + } finally { + setProcessing(false) + } + } + + // 计算该用户下所有账号的汇总状态 + const allRunning = user.accounts.every(a => a.serviceStatus?.running) + const allStopped = user.accounts.every(a => !a.serviceStatus?.running) + + return ( +
+
setExpanded(!expanded)}> +
+ + {user.username} + {user.role} + ({user.accounts.length} 账号) +
+
e.stopPropagation()}> + + +
+
+ + {expanded && ( +
+ {user.accounts.length === 0 ? ( +
暂无关联账号
+ ) : ( + + + + + + + + + + + + + + + {user.accounts.map(acc => ( + + + + + + + + + + + ))} + +
ID名称账户状态服务状态总资产总盈亏持仓数操作
{acc.id}{acc.name} + + {acc.status === 'active' ? '启用' : '禁用'} + + + {acc.serviceStatus ? ( + + {acc.serviceStatus.state} + + ) : ( + UNKNOWN + )} + {Number(acc.total_balance).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}= 0 ? 'profit' : 'loss'}> + {Number(acc.total_pnl) > 0 ? '+' : ''}{Number(acc.total_pnl).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + {acc.open_positions} +
+ + +
+
+ )} +
+ )} +
+ ) +} + const AdminDashboard = () => { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) @@ -12,36 +149,68 @@ const AdminDashboard = () => { // Don't set loading on background refresh (if data exists) if (!data) setLoading(true) - const [dashboardRes, servicesRes] = await Promise.all([ + const [usersRes, dashboardRes, servicesRes] = await Promise.all([ + api.get('/admin/users/detailed').catch(() => ({ data: [] })), api.getAdminDashboard(), api.get('/system/trading/services').catch(() => ({ data: { services: [] } })) ]) + const users = usersRes.data || [] + const globalStats = dashboardRes // summary, accounts (list of stats) const services = servicesRes.data.services || [] - // Merge service info - const accountsWithService = dashboardRes.accounts.map(acc => { - const programName = `auto_sys_acc${acc.id}` - const service = services.find(s => s.program === programName) + // Index stats and services by account ID + const statsMap = {} + globalStats.accounts.forEach(a => { statsMap[a.id] = a }) + + const serviceMap = {} + services.forEach(s => { + // program name format: auto_sys_acc{id} + const match = s.program.match(/auto_sys_acc(\d+)/) + if (match) { + serviceMap[match[1]] = s + } + }) + + // Merge data into users structure + const enrichedUsers = users.map(u => { + const enrichedAccounts = (u.accounts || []).map(acc => { + const st = statsMap[acc.id] || {} + const sv = serviceMap[acc.id] + + return { + ...acc, + total_balance: st.total_balance || 0, + total_pnl: st.total_pnl || 0, + open_positions: st.open_positions || 0, + serviceStatus: sv + } + }) return { - ...acc, - serviceStatus: service + ...u, + accounts: enrichedAccounts } }) setData({ - ...dashboardRes, - accounts: accountsWithService + summary: globalStats.summary, + users: enrichedUsers }) setError(null) } catch (err) { setError(err.message) + console.error(err) } finally { setLoading(false) } } const handleServiceAction = async (accountId, action) => { + if (action === 'refresh') { + loadData() + return + } + if (!window.confirm(`确定要${action === 'start' ? '启动' : '停止'}账号 #${accountId} 的交易服务吗?`)) return try { @@ -63,7 +232,7 @@ const AdminDashboard = () => { if (error) return
加载失败: {error}
if (!data) return null - const { summary, accounts } = data + const { summary, users } = data return (
@@ -89,94 +258,16 @@ const AdminDashboard = () => {
-
-

账户列表

-
- - - - - - - - - - - - - - - {accounts.map(acc => ( - - - - - - - - - - - ))} - -
ID名称账户状态服务状态总资产总盈亏持仓数操作
{acc.id}{acc.name} - - {acc.status === 'active' ? '启用' : '禁用'} - - - {acc.serviceStatus ? ( - - {acc.serviceStatus.state} - - ) : ( - UNKNOWN - )} - {Number(acc.total_balance).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}= 0 ? 'profit' : 'loss'}> - {Number(acc.total_pnl) > 0 ? '+' : ''}{Number(acc.total_pnl).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} - {acc.open_positions} -
- - -
-
+
+

用户管理 ({users.length})

+
+ {users.map(user => ( + + ))}