This commit is contained in:
薇薇安 2026-01-22 09:06:10 +08:00
parent dc49c2717b
commit 156acc92e0
6 changed files with 136 additions and 27 deletions

View File

@ -81,7 +81,7 @@ function App() {
<div className="nav-container"> <div className="nav-container">
<div className="nav-left"> <div className="nav-left">
<h1 className="nav-title">自动交易系统</h1> <h1 className="nav-title">自动交易系统</h1>
<AccountSelector /> <AccountSelector currentUser={me} />
</div> </div>
<div className="nav-links"> <div className="nav-links">
<Link to="/">仪表板</Link> <Link to="/">仪表板</Link>

View File

@ -1,23 +1,67 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { api, getCurrentAccountId, setCurrentAccountId } from '../services/api' import { api, getCurrentAccountId, setCurrentAccountId } from '../services/api'
const AccountSelector = ({ onChanged }) => { const AccountSelector = ({ onChanged, currentUser }) => {
const [accounts, setAccounts] = useState([]) const [accounts, setAccounts] = useState([])
const [accountId, setAccountId] = useState(getCurrentAccountId()) const [accountId, setAccountId] = useState(getCurrentAccountId())
const [users, setUsers] = useState([])
const [selectedUserId, setSelectedUserId] = useState(null)
const isAdmin = (currentUser?.role || '') === 'admin'
//
useEffect(() => { useEffect(() => {
const load = () => { if (isAdmin) {
api.getAccounts() api.getUsers()
.then((list) => setAccounts(Array.isArray(list) ? list : [])) .then((list) => {
setUsers(Array.isArray(list) ? list : [])
//
const currentUserId = parseInt(String(currentUser?.id || ''), 10)
if (currentUserId && list.some((u) => parseInt(String(u?.id || ''), 10) === currentUserId)) {
setSelectedUserId(currentUserId)
} else if (list.length > 0) {
setSelectedUserId(parseInt(String(list[0]?.id || ''), 10))
}
})
.catch(() => setUsers([]))
}
}, [isAdmin, currentUser?.id])
//
useEffect(() => {
if (isAdmin && selectedUserId) {
api.getUserAccounts(selectedUserId)
.then((list) => {
const accountsList = Array.isArray(list) ? list : []
setAccounts(accountsList)
// active
const firstActive = accountsList.find((a) => String(a?.status || 'active') === 'active') || accountsList[0]
if (firstActive) {
const nextAccountId = parseInt(String(firstActive.id || ''), 10)
if (Number.isFinite(nextAccountId) && nextAccountId > 0) {
setAccountId(nextAccountId)
}
}
})
.catch(() => setAccounts([])) .catch(() => setAccounts([]))
} }
load() }, [isAdmin, selectedUserId])
// / //
const onUpdated = () => load() useEffect(() => {
window.addEventListener('ats:accounts:updated', onUpdated) if (!isAdmin) {
return () => window.removeEventListener('ats:accounts:updated', onUpdated) const load = () => {
}, []) api.getAccounts()
.then((list) => setAccounts(Array.isArray(list) ? list : []))
.catch(() => setAccounts([]))
}
load()
// /
const onUpdated = () => load()
window.addEventListener('ats:accounts:updated', onUpdated)
return () => window.removeEventListener('ats:accounts:updated', onUpdated)
}
}, [isAdmin])
useEffect(() => { useEffect(() => {
setCurrentAccountId(accountId) setCurrentAccountId(accountId)
@ -50,22 +94,49 @@ const AccountSelector = ({ onChanged }) => {
return ( return (
<div className="nav-account"> <div className="nav-account">
{isAdmin ? (
<>
<span className="nav-account-label">用户</span>
<select
className="nav-account-select"
value={selectedUserId || ''}
onChange={(e) => {
const v = parseInt(e.target.value, 10)
setSelectedUserId(Number.isFinite(v) && v > 0 ? v : null)
//
setAccountId(null)
}}
title="管理员:先选择用户,再查看该用户下的账号"
>
{users.map((u) => (
<option key={u.id} value={u.id}>
{u.username || 'user'} {u.role === 'admin' ? '(管理员)' : ''}
</option>
))}
</select>
</>
) : null}
<span className="nav-account-label">账号</span> <span className="nav-account-label">账号</span>
<select <select
className="nav-account-select" className="nav-account-select"
value={accountId} value={accountId || ''}
onChange={(e) => { onChange={(e) => {
const v = parseInt(e.target.value, 10) const v = parseInt(e.target.value, 10)
setAccountId(Number.isFinite(v) && v > 0 ? v : 1) setAccountId(Number.isFinite(v) && v > 0 ? v : 1)
}} }}
title="切换账号后:配置/持仓/交易记录/统计会按账号隔离;推荐仍是全局" title={isAdmin ? "切换账号后:配置/持仓/交易记录/统计会按账号隔离;推荐仍是全局" : "切换账号后:配置/持仓/交易记录/统计会按账号隔离;推荐仍是全局"}
disabled={isAdmin && !selectedUserId}
> >
{options.map((a) => ( {options.length === 0 ? (
<option key={a.id} value={a.id}> <option value="">{isAdmin && !selectedUserId ? '请先选择用户' : '暂无账号'}</option>
#{a.id} {a.name || 'account'} ) : (
{String(a?.status || 'active') === 'disabled' ? '(已禁用)' : ''} options.map((a) => (
</option> <option key={a.id} value={a.id}>
))} #{a.id} {a.name || 'account'}
{String(a?.status || 'active') === 'disabled' ? '(已禁用)' : ''}
</option>
))
)}
</select> </select>
</div> </div>
) )

View File

@ -900,6 +900,7 @@ const ConfigPanel = ({ currentUser }) => {
</div> </div>
{/* 我的交易进程按账号owner/admin 可启停) */} {/* 我的交易进程按账号owner/admin 可启停) */}
{currentAccountMeta && String(currentAccountMeta?.status || 'active') === 'active' ? (
<div className="system-section"> <div className="system-section">
<div className="system-header"> <div className="system-header">
<h3>我的交易进程当前账号 #{accountId}</h3> <h3>我的交易进程当前账号 #{accountId}</h3>
@ -967,8 +968,10 @@ const ConfigPanel = ({ currentUser }) => {
</details> </details>
) : null} ) : null}
</div> </div>
) : null}
{/* 账号密钥当前账号owner/admin 可改) */} {/* 账号密钥当前账号owner/admin 可改) */}
{currentAccountMeta && String(currentAccountMeta?.status || 'active') === 'active' ? (
<div className="system-section"> <div className="system-section">
<div className="system-header"> <div className="system-header">
<h3>账号密钥当前账号</h3> <h3>账号密钥当前账号</h3>
@ -1037,6 +1040,7 @@ const ConfigPanel = ({ currentUser }) => {
</div> </div>
</div> </div>
</div> </div>
) : null}
{/* 系统控制:清缓存 / 启停 / 重启supervisor */} {/* 系统控制:清缓存 / 启停 / 重启supervisor */}
{isAdmin ? ( {isAdmin ? (
@ -1495,7 +1499,7 @@ const ConfigPanel = ({ currentUser }) => {
</div> </div>
{/* 配置可行性检查提示 */} {/* 配置可行性检查提示 */}
{feasibilityCheck && ( {currentAccountMeta && String(currentAccountMeta?.status || 'active') === 'active' && feasibilityCheck && (
<div className={`feasibility-check ${feasibilityCheck.feasible ? 'feasible' : 'infeasible'}`}> <div className={`feasibility-check ${feasibilityCheck.feasible ? 'feasible' : 'infeasible'}`}>
<div className="feasibility-header"> <div className="feasibility-header">
<h4> <h4>

View File

@ -46,7 +46,18 @@ const StatsDashboard = () => {
loadDashboard() loadDashboard()
loadTradingConfig() // loadTradingConfig() //
}, 30000) // 30 }, 30000) // 30
return () => clearInterval(interval)
//
const handleAccountChange = () => {
loadDashboard()
loadTradingConfig()
}
window.addEventListener('ats:account:changed', handleAccountChange)
return () => {
clearInterval(interval)
window.removeEventListener('ats:account:changed', handleAccountChange)
}
}, []) }, [])
const loadTradingConfig = async () => { const loadTradingConfig = async () => {

View File

@ -19,13 +19,16 @@ const TradeList = () => {
useEffect(() => { useEffect(() => {
loadData() loadData()
// 30
// const interval = setInterval(() => {
// loadData()
// }, 30000) // 30
// return () => clearInterval(interval) //
loadData() const handleAccountChange = () => {
loadData()
}
window.addEventListener('ats:account:changed', handleAccountChange)
return () => {
window.removeEventListener('ats:account:changed', handleAccountChange)
}
}, []) }, [])
const loadData = async () => { const loadData = async () => {

View File

@ -610,4 +610,24 @@ export const api = {
} }
return response.json(); return response.json();
}, },
// 管理员:获取用户列表
getUsers: async () => {
const response = await fetch(buildUrl('/api/admin/users'), { headers: withAuthHeaders() });
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: '获取用户列表失败' }));
throw new Error(error.detail || '获取用户列表失败');
}
return response.json();
},
// 管理员:获取用户关联的账号列表
getUserAccounts: async (userId) => {
const response = await fetch(buildUrl(`/api/admin/users/${userId}/accounts`), { headers: withAuthHeaders() });
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: '获取用户账号列表失败' }));
throw new Error(error.detail || '获取用户账号列表失败');
}
return response.json();
},
}; };