1
This commit is contained in:
parent
d34e3cc998
commit
46eb9f1187
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user