This commit is contained in:
薇薇安 2026-02-03 14:01:11 +08:00
parent d34e3cc998
commit 46eb9f1187

View File

@ -2,96 +2,69 @@ import React, { useEffect, useState } from 'react'
import { api } from '../services/api' import { api } from '../services/api'
import './AdminDashboard.css' import './AdminDashboard.css'
const AdminDashboard = () => { const UserAccountGroup = ({ user, onServiceAction }) => {
const [data, setData] = useState(null) const [expanded, setExpanded] = useState(true)
const [loading, setLoading] = useState(true) const [processing, setProcessing] = useState(false)
const [error, setError] = useState(null)
const loadData = async () => { const handleUserAction = async (action) => {
if (!window.confirm(`确定要${action === 'start' ? '启动' : '停止'}用户 ${user.username} 下所有账号的交易服务吗?`)) return
setProcessing(true)
try { try {
// Don't set loading on background refresh (if data exists) //
if (!data) setLoading(true) const promises = user.accounts.map(acc =>
api.post(`/accounts/${acc.id}/service/${action}`)
const [dashboardRes, servicesRes] = await Promise.all([ .catch(e => console.error(`Failed to ${action} account ${acc.id}:`, e))
api.getAdminDashboard(), )
api.get('/system/trading/services').catch(() => ({ data: { services: [] } })) await Promise.all(promises)
]) // refresh
//
const services = servicesRes.data.services || [] // onServiceAction
//
// Merge service info if (onServiceAction) onServiceAction(null, 'refresh') // Hacky way to trigger refresh if supported
const accountsWithService = dashboardRes.accounts.map(acc => {
const programName = `auto_sys_acc${acc.id}`
const service = services.find(s => s.program === programName)
return {
...acc,
serviceStatus: service
}
})
setData({
...dashboardRes,
accounts: accountsWithService
})
setError(null)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
const handleServiceAction = async (accountId, action) => {
if (!window.confirm(`确定要${action === 'start' ? '启动' : '停止'}账号 #${accountId} 的交易服务吗?`)) return
try {
await api.post(`/accounts/${accountId}/service/${action}`)
// Short delay then refresh
setTimeout(loadData, 1000)
} catch (e) { } catch (e) {
alert(`操作失败: ${e.message || '未知错误'}`) alert(`操作失败: ${e.message}`)
} finally {
setProcessing(false)
} }
} }
useEffect(() => { //
loadData() const allRunning = user.accounts.every(a => a.serviceStatus?.running)
const timer = setInterval(loadData, 30000) // 30 const allStopped = user.accounts.every(a => !a.serviceStatus?.running)
return () => clearInterval(timer)
}, [])
if (loading && !data) return <div className="loading">加载中...</div>
if (error) return <div className="error">加载失败: {error}</div>
if (!data) return null
const { summary, accounts } = data
return ( return (
<div className="admin-dashboard"> <div className="user-group-card">
<div className="dashboard-header"> <div className="user-group-header" onClick={() => setExpanded(!expanded)}>
<h2>全局交易监控看板</h2> <div className="user-info">
<button className="refresh-btn" onClick={loadData}>刷新</button> <span className={`expand-icon ${expanded ? 'expanded' : ''}`}></span>
<span className="user-name">{user.username}</span>
<span className={`user-role-badge ${user.role}`}>{user.role}</span>
<span className="account-count">({user.accounts.length} 账号)</span>
</div> </div>
<div className="user-actions" onClick={e => e.stopPropagation()}>
<div className="summary-cards"> <button
<div className="card"> className="btn-text-action start"
<div className="card-title">总资产 (USDT)</div> onClick={() => handleUserAction('start')}
<div className="card-value">{summary.total_assets_usdt.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</div> disabled={processing || allRunning || user.accounts.length === 0}
</div> >
<div className="card"> 全部启动
<div className="card-title">总盈亏 (USDT)</div> </button>
<div className={`card-value ${summary.total_pnl_usdt >= 0 ? 'profit' : 'loss'}`}> <button
{summary.total_pnl_usdt > 0 ? '+' : ''}{summary.total_pnl_usdt.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} className="btn-text-action stop"
</div> onClick={() => handleUserAction('stop')}
</div> disabled={processing || allStopped || user.accounts.length === 0}
<div className="card"> >
<div className="card-title">活跃账户数</div> 全部停止
<div className="card-value">{summary.active_accounts} / {summary.total_accounts}</div> </button>
</div> </div>
</div> </div>
<div className="accounts-section"> {expanded && (
<h3>账户列表</h3> <div className="user-accounts-table">
<div className="table-container"> {user.accounts.length === 0 ? (
<div className="no-accounts">暂无关联账号</div>
) : (
<table className="data-table"> <table className="data-table">
<thead> <thead>
<tr> <tr>
@ -106,7 +79,7 @@ const AdminDashboard = () => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{accounts.map(acc => ( {user.accounts.map(acc => (
<tr key={acc.id}> <tr key={acc.id}>
<td>{acc.id}</td> <td>{acc.id}</td>
<td>{acc.name}</td> <td>{acc.name}</td>
@ -140,34 +113,16 @@ const AdminDashboard = () => {
<td> <td>
<div style={{ display: 'flex', gap: '8px' }}> <div style={{ display: 'flex', gap: '8px' }}>
<button <button
onClick={() => handleServiceAction(acc.id, 'start')} onClick={() => onServiceAction(acc.id, 'start')}
disabled={acc.serviceStatus?.running} disabled={acc.serviceStatus?.running}
style={{ className="btn-mini start"
padding: '4px 12px',
fontSize: '12px',
cursor: acc.serviceStatus?.running ? 'not-allowed' : 'pointer',
backgroundColor: '#4caf50',
color: 'white',
border: 'none',
borderRadius: '4px',
opacity: acc.serviceStatus?.running ? 0.5 : 1
}}
> >
启动 启动
</button> </button>
<button <button
onClick={() => handleServiceAction(acc.id, 'stop')} onClick={() => onServiceAction(acc.id, 'stop')}
disabled={!acc.serviceStatus?.running} disabled={!acc.serviceStatus?.running}
style={{ className="btn-mini stop"
padding: '4px 12px',
fontSize: '12px',
cursor: !acc.serviceStatus?.running ? 'not-allowed' : 'pointer',
backgroundColor: '#f44336',
color: 'white',
border: 'none',
borderRadius: '4px',
opacity: !acc.serviceStatus?.running ? 0.5 : 1
}}
> >
停止 停止
</button> </button>
@ -177,6 +132,142 @@ const AdminDashboard = () => {
))} ))}
</tbody> </tbody>
</table> </table>
)}
</div>
)}
</div>
)
}
const AdminDashboard = () => {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const loadData = async () => {
try {
// Don't set loading on background refresh (if data exists)
if (!data) setLoading(true)
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 || []
// 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 {
...u,
accounts: enrichedAccounts
}
})
setData({
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 {
await api.post(`/accounts/${accountId}/service/${action}`)
// Short delay then refresh
setTimeout(loadData, 1000)
} catch (e) {
alert(`操作失败: ${e.message || '未知错误'}`)
}
}
useEffect(() => {
loadData()
const timer = setInterval(loadData, 30000) // 30
return () => clearInterval(timer)
}, [])
if (loading && !data) return <div className="loading">加载中...</div>
if (error) return <div className="error">加载失败: {error}</div>
if (!data) return null
const { summary, users } = data
return (
<div className="admin-dashboard">
<div className="dashboard-header">
<h2>全局交易监控看板</h2>
<button className="refresh-btn" onClick={loadData}>刷新</button>
</div>
<div className="summary-cards">
<div className="card">
<div className="card-title">总资产 (USDT)</div>
<div className="card-value">{summary.total_assets_usdt.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</div>
</div>
<div className="card">
<div className="card-title">总盈亏 (USDT)</div>
<div className={`card-value ${summary.total_pnl_usdt >= 0 ? 'profit' : 'loss'}`}>
{summary.total_pnl_usdt > 0 ? '+' : ''}{summary.total_pnl_usdt.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</div>
</div>
<div className="card">
<div className="card-title">活跃账户数</div>
<div className="card-value">{summary.active_accounts} / {summary.total_accounts}</div>
</div>
</div>
<div className="users-section">
<h3>用户管理 ({users.length})</h3>
<div className="users-list">
{users.map(user => (
<UserAccountGroup
key={user.id}
user={user}
onServiceAction={handleServiceAction}
/>
))}
</div> </div>
</div> </div>
</div> </div>