-
用户管理 ({users.length})
-
-
-
➕ 添加用户
-
创建新用户后,可在下方为其关联交易账号。
-
-
-
- {users.map(user => (
-
- ))}
-
+
+
+
整体订单统计(最近 7 天)
+ {orderStats ? (
+ <>
+ {orderStats.summary && (
+
+
汇总
+
+ 笔数 {orderStats.summary.trade_count ?? 0}
+ ,盈 {orderStats.summary.win_count ?? 0} / 亏 {orderStats.summary.loss_count ?? 0}
+ ,净盈亏 = 0 ? 'positive' : 'negative'}>
+ {Number(orderStats.summary.net_pnl) >= 0 ? '+' : ''}{Number(orderStats.summary.net_pnl).toFixed(2)} USDT
+
+
+
+ )}
+
+
按交易对:净盈亏、胜率
+ {orderStats.by_symbol?.length > 0 ? (
+
+
+
+
+ | 交易对 |
+ 笔数 |
+ 胜率% |
+ 净盈亏(USDT) |
+
+
+
+ {orderStats.by_symbol.map((row) => (
+
+ | {row.symbol} |
+ {row.trade_count} |
+ {row.win_rate_pct} |
+ = 0 ? 'positive' : 'negative'}>
+ {Number(row.net_pnl) >= 0 ? '+' : ''}{Number(row.net_pnl).toFixed(2)}
+ |
+
+ ))}
+
+
+
+ ) : (
+
暂无按交易对统计(需先运行 scripts/aggregate_trade_stats.py 或依赖定时任务)
+ )}
+
+
+
按小时:净盈亏(0–23 时,北京时间)
+ {orderStats.hourly_agg?.length > 0 ? (
+
+
+
+
+ | 小时 |
+ 笔数 |
+ 净盈亏(USDT) |
+
+
+
+ {orderStats.hourly_agg.map((row) => (
+
+ | {row.hour}:00 |
+ {row.trade_count} |
+ = 0 ? 'positive' : 'negative'}>
+ {Number(row.net_pnl) >= 0 ? '+' : ''}{Number(row.net_pnl).toFixed(2)}
+ |
+
+ ))}
+
+
+
+ ) : (
+
暂无按小时统计
+ )}
+
+ {orderStats.suggestions && (orderStats.suggestions.blacklist?.length > 0 || orderStats.suggestions.whitelist?.length > 0) && (
+
+
策略建议(仅供参考)
+ {orderStats.suggestions.blacklist?.length > 0 && (
+
+
建议降权/观察:
+
+ {orderStats.suggestions.blacklist.map((item) => (
+ -
+ {item.symbol}
+ 笔数 {item.trade_count},净盈亏 {item.net_pnl} USDT,胜率 {item.win_rate_pct}%
+
+ ))}
+
+
+ )}
+ {orderStats.suggestions.whitelist?.length > 0 && (
+
+
可优先考虑:
+
+ {orderStats.suggestions.whitelist.map((item) => (
+ -
+ {item.symbol}
+ 笔数 {item.trade_count},净盈亏 +{item.net_pnl} USDT,胜率 {item.win_rate_pct}%
+
+ ))}
+
+
+ )}
+
+ )}
+ >
+ ) : (
+
加载统计中…(若长期无数据,请先运行 scripts/aggregate_trade_stats.py 或依赖定时任务更新)
+ )}
-
-
)
}
-
-export default AdminDashboard
diff --git a/frontend/src/components/AdminShared.jsx b/frontend/src/components/AdminShared.jsx
new file mode 100644
index 0000000..a638edf
--- /dev/null
+++ b/frontend/src/components/AdminShared.jsx
@@ -0,0 +1,335 @@
+/**
+ * 管理员共用组件:用户管理、系统账号管理
+ */
+import React, { useState } from 'react'
+import { api } from '../services/api'
+import './AdminDashboard.css'
+
+export 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
+ 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)
+ if (onServiceAction) onServiceAction(null, 'refresh')
+ } catch (e) {
+ alert(`操作失败: ${e.message}`)
+ } finally {
+ setProcessing(false)
+ }
+ }
+
+ const handleGrant = async () => {
+ if (!linkAccountId) return
+ const accountId = Number(linkAccountId)
+ if (!Number.isInteger(accountId) || accountId < 1) {
+ alert('请选择有效的账号')
+ return
+ }
+ setAssociating(true)
+ try {
+ await api.grantUserAccount(user.id, accountId, 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 (
+
+
setExpanded(!expanded)}>
+
+ ▶
+ {user.username}
+ {user.role}
+ ({user.accounts.length} 账号)
+
+ {user.role !== 'admin' && (
+
e.stopPropagation()}>
+
+
+
+ )}
+
+ {expanded && user.role !== 'admin' && (
+
+ {user.accounts.length === 0 ? (
+
暂无关联账号
+ ) : (
+
+
+
+ | ID | 名称 | 权限 | 账户状态 | 服务状态 | 总资产 | 总盈亏 | 持仓数 | 操作 |
+
+
+
+ {user.accounts.map(acc => (
+
+ | {acc.id} |
+ {acc.name} |
+ {acc.role || 'viewer'} |
+ {acc.status === 'active' ? '启用' : '禁用'} |
+
+ {acc.serviceStatus ? (
+ {acc.serviceStatus.running ? '运行中' : '停止'}
+ ) : (
+ 未启动
+ )}
+ |
+ {acc.total_balance?.toFixed(2) || '-'} |
+ = 0 ? 'profit' : 'loss'}>{acc.total_pnl?.toFixed(2) || '-'} |
+ {acc.open_positions || 0} |
+
+
+
+ |
+
+ ))}
+
+
+ )}
+
+
新增关联
+
+
+
+
+
+
+
+ )}
+
+ )
+}
+
+export const CreateUserForm = ({ onSuccess }) => {
+ const [form, setForm] = useState({ username: '', password: '', role: 'user', status: 'active' })
+ const [busy, setBusy] = useState(false)
+ const [message, setMessage] = useState('')
+ const handleCreate = async () => {
+ if (!form.username.trim()) { setMessage('请输入用户名'); return }
+ if (!form.password.trim()) { setMessage('请输入密码'); return }
+ setBusy(true)
+ setMessage('')
+ try {
+ await api.createUser({ username: form.username.trim(), password: form.password, role: form.role, status: form.status })
+ setMessage('用户已创建')
+ setForm({ username: '', password: '', role: 'user', status: 'active' })
+ if (onSuccess) onSuccess()
+ } catch (e) {
+ setMessage('创建用户失败: ' + (e?.message || '未知错误'))
+ } finally {
+ setBusy(false)
+ }
+ }
+ return (
+
+
新建用户
+ {message &&
{message}
}
+
+
+ setForm({ ...form, username: e.target.value })} placeholder="登录用,例如:trader1" />
+
+
+
+ setForm({ ...form, password: e.target.value })} placeholder="设置登录密码" />
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export const AccountManager = ({ accounts, onRefresh }) => {
+ const [newAccount, setNewAccount] = useState({ name: '', api_key: '', api_secret: '', use_testnet: false, status: 'active' })
+ const [credEditId, setCredEditId] = useState(null)
+ const [credForm, setCredForm] = useState({ api_key: '', api_secret: '', use_testnet: false })
+ const [busy, setBusy] = useState(false)
+ const [message, setMessage] = useState('')
+ const notifyAccountsUpdated = () => {
+ try { window.dispatchEvent(new Event('ats:accounts:updated')) } catch (e) { /* ignore */ }
+ }
+ const handleCreate = async () => {
+ if (!newAccount.name.trim()) return
+ setBusy(true)
+ setMessage('')
+ try {
+ await api.createAccount(newAccount)
+ setMessage('账号已创建')
+ setNewAccount({ name: '', api_key: '', api_secret: '', use_testnet: false, status: 'active' })
+ if (onRefresh) onRefresh()
+ notifyAccountsUpdated()
+ } catch (e) {
+ setMessage('创建账号失败: ' + (e?.message || '未知错误'))
+ } finally {
+ setBusy(false)
+ }
+ }
+ const handleUpdateStatus = async (account) => {
+ setBusy(true)
+ setMessage('')
+ try {
+ const next = account.status === 'active' ? 'disabled' : 'active'
+ await api.updateAccount(account.id, { status: next })
+ setMessage(`账号 #${account.id} 已${next === 'active' ? '启用' : '禁用'}`)
+ if (onRefresh) onRefresh()
+ notifyAccountsUpdated()
+ } catch (e) {
+ setMessage('更新账号失败: ' + (e?.message || '未知错误'))
+ } finally {
+ setBusy(false)
+ }
+ }
+ const handleUpdateCreds = async () => {
+ if (!credEditId) return
+ setBusy(true)
+ setMessage('')
+ try {
+ const payload = {}
+ if (credForm.api_key) payload.api_key = credForm.api_key
+ if (credForm.api_secret) payload.api_secret = credForm.api_secret
+ payload.use_testnet = !!credForm.use_testnet
+ await api.updateAccountCredentials(credEditId, payload)
+ setMessage(`账号 #${credEditId} 密钥已更新`)
+ setCredEditId(null)
+ if (onRefresh) onRefresh()
+ notifyAccountsUpdated()
+ } catch (e) {
+ setMessage('更新密钥失败: ' + (e?.message || '未知错误'))
+ } finally {
+ setBusy(false)
+ }
+ }
+ return (
+
+
系统账号池管理
+ {message &&
{message}
}
+
+
+
新增账号
+
+
+ setNewAccount({ ...newAccount, name: e.target.value })} placeholder="例如:user_a" />
+
+
+
+ setNewAccount({ ...newAccount, api_key: e.target.value })} />
+
+
+
+ setNewAccount({ ...newAccount, api_secret: e.target.value })} />
+
+
+
+
+
+
+
+
+
+
+
+
账号列表 ({accounts?.length || 0})
+
+
+
+ | ID | 名称 | 状态 | 测试网 | API配置 | 操作 |
+
+
+ {(accounts || []).map(a => (
+
+ | #{a.id} |
+ {a.name} |
+ {a.status === 'active' ? '启用' : '禁用'} |
+ {a.use_testnet ? '是' : '否'} |
+ {a.has_api_key ? '✅' : '❌'} / {a.has_api_secret ? '✅' : '❌'} |
+
+
+
+
+
+ |
+
+ ))}
+
+
+
+
+
+ {credEditId && (
+
+
+
更新密钥 (账号 #{credEditId})
+
+
+ setCredForm({ ...credForm, api_key: e.target.value })} />
+
+
+
+ setCredForm({ ...credForm, api_secret: e.target.value })} />
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ )
+}
diff --git a/frontend/src/components/AdminUserManagement.jsx b/frontend/src/components/AdminUserManagement.jsx
new file mode 100644
index 0000000..8b539c3
--- /dev/null
+++ b/frontend/src/components/AdminUserManagement.jsx
@@ -0,0 +1,105 @@
+import React, { useEffect, useState } from 'react'
+import { api } from '../services/api'
+import { UserAccountGroup, CreateUserForm } from './AdminShared'
+import './AdminDashboard.css'
+
+export default function AdminUserManagement() {
+ const [data, setData] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+
+ const loadData = async () => {
+ try {
+ if (!data) setLoading(true)
+ 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('/accounts').catch(() => ({ data: [] }))
+ ])
+ const users = usersRes.data || []
+ const globalStats = dashboardRes
+ const services = servicesRes.data?.services || []
+ const allAccounts = accountsRes.data || []
+ const statsMap = {}
+ globalStats.accounts?.forEach(a => { statsMap[a.id] = a })
+ const serviceMap = {}
+ services.forEach(s => {
+ const match = s.program?.match(/auto_sys_acc(\d+)/)
+ if (match) serviceMap[match[1]] = s
+ })
+ const enrichedUsers = users.map(u => ({
+ ...u,
+ accounts: (u.accounts || []).map(acc => ({
+ ...acc,
+ total_balance: statsMap[acc.id]?.total_balance ?? 0,
+ total_pnl: statsMap[acc.id]?.total_pnl ?? 0,
+ open_positions: statsMap[acc.id]?.open_positions ?? 0,
+ serviceStatus: serviceMap[acc.id]
+ }))
+ }))
+ setData({ users: enrichedUsers, allAccounts })
+ setError(null)
+ } catch (err) {
+ setError(err?.message)
+ } 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}`)
+ setTimeout(loadData, 1000)
+ } catch (e) {
+ alert(`操作失败: ${e?.message || '未知错误'}`)
+ }
+ }
+
+ useEffect(() => {
+ loadData()
+ const t = setInterval(loadData, 30000)
+ return () => clearInterval(t)
+ }, [])
+ useEffect(() => {
+ const onUpdated = () => loadData()
+ window.addEventListener('ats:accounts:updated', onUpdated)
+ return () => window.removeEventListener('ats:accounts:updated', onUpdated)
+ }, [])
+
+ if (loading && !data) return
加载中...
+ if (error) return
加载失败: {error}
+ if (!data) return null
+
+ const { users, allAccounts } = data
+ return (
+
+
+
用户管理
+
+
+
+
用户管理 ({users.length})
+
+
+
➕ 添加用户
+
创建新用户后,可在下方为其关联交易账号。
+
+
+
+ {users.map(user => (
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js
index a925569..8f8ad2c 100644
--- a/frontend/src/services/api.js
+++ b/frontend/src/services/api.js
@@ -475,6 +475,16 @@ export const api = {
}
return response.json();
},
+
+ getAdminOverallTradeStats: async (days = 7) => {
+ const url = buildUrl(`/api/stats/admin/overall-trade-stats?days=${days}`);
+ const response = await fetch(url, { headers: withAuthHeaders() });
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({ detail: '获取整体订单统计失败' }));
+ throw new Error(error.detail || '获取整体订单统计失败');
+ }
+ return response.json();
+ },
// 平仓操作
closePosition: async (symbol) => {