a
This commit is contained in:
parent
1fcd692368
commit
2ee6e7a009
|
|
@ -135,7 +135,7 @@ async def get_dashboard_data(account_id: int = Depends(get_account_id)):
|
||||||
logger.warning(f"获取实时持仓失败: {positions_error}", exc_info=True)
|
logger.warning(f"获取实时持仓失败: {positions_error}", exc_info=True)
|
||||||
# 回退到数据库记录
|
# 回退到数据库记录
|
||||||
try:
|
try:
|
||||||
db_trades = Trade.get_all(status='open')[:10]
|
db_trades = Trade.get_all(status='open', account_id=account_id)[:10]
|
||||||
# 格式化数据库记录,添加 entry_value_usdt 字段
|
# 格式化数据库记录,添加 entry_value_usdt 字段
|
||||||
open_trades = []
|
open_trades = []
|
||||||
for trade in db_trades:
|
for trade in db_trades:
|
||||||
|
|
|
||||||
|
|
@ -13,51 +13,26 @@ import Login from './components/Login'
|
||||||
import { api, clearAuthToken } from './services/api'
|
import { api, clearAuthToken } from './services/api'
|
||||||
import {
|
import {
|
||||||
setCurrentUser,
|
setCurrentUser,
|
||||||
setViewingUserId,
|
|
||||||
setUsers,
|
setUsers,
|
||||||
switchUser,
|
|
||||||
selectCurrentUser,
|
selectCurrentUser,
|
||||||
selectViewingUserId,
|
|
||||||
selectUsers,
|
selectUsers,
|
||||||
selectIsAdmin,
|
selectIsAdmin,
|
||||||
selectEffectiveUserId,
|
|
||||||
} from './store/appSlice'
|
} from './store/appSlice'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const currentUser = useSelector(selectCurrentUser)
|
const currentUser = useSelector(selectCurrentUser)
|
||||||
const viewingUserId = useSelector(selectViewingUserId)
|
|
||||||
const users = useSelector(selectUsers)
|
|
||||||
const isAdmin = useSelector(selectIsAdmin)
|
const isAdmin = useSelector(selectIsAdmin)
|
||||||
const effectiveUserId = useSelector(selectEffectiveUserId)
|
|
||||||
|
|
||||||
const [checking, setChecking] = useState(true)
|
const [checking, setChecking] = useState(true)
|
||||||
const [showUserPopover, setShowUserPopover] = useState(false)
|
|
||||||
const userPopoverRef = React.useRef(null)
|
|
||||||
|
|
||||||
const refreshMe = async () => {
|
const refreshMe = async () => {
|
||||||
try {
|
try {
|
||||||
const u = await api.me()
|
const u = await api.me()
|
||||||
dispatch(setCurrentUser(u))
|
dispatch(setCurrentUser(u))
|
||||||
|
|
||||||
const isAdminValue = (u?.role || '') === 'admin'
|
// 不再加载用户列表,去掉用户切换功能
|
||||||
|
|
||||||
// 管理员:加载用户列表
|
|
||||||
if (isAdminValue) {
|
|
||||||
try {
|
|
||||||
const userList = await api.getUsers()
|
|
||||||
const usersArray = Array.isArray(userList) ? userList : []
|
|
||||||
dispatch(setUsers(usersArray))
|
|
||||||
// 如果viewingUserId未设置或不在列表中,设置为当前登录用户
|
|
||||||
const currentUserId = parseInt(String(u?.id || ''), 10)
|
|
||||||
if (!viewingUserId || !usersArray.some((user) => parseInt(String(user?.id || ''), 10) === viewingUserId)) {
|
|
||||||
dispatch(setViewingUserId(currentUserId))
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('加载用户列表失败:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch(setCurrentUser(null))
|
dispatch(setCurrentUser(null))
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -65,31 +40,6 @@ function App() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 点击外部关闭popover
|
|
||||||
React.useEffect(() => {
|
|
||||||
const handleClickOutside = (event) => {
|
|
||||||
if (userPopoverRef.current && !userPopoverRef.current.contains(event.target)) {
|
|
||||||
setShowUserPopover(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (showUserPopover) {
|
|
||||||
document.addEventListener('mousedown', handleClickOutside)
|
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
||||||
}
|
|
||||||
}, [showUserPopover])
|
|
||||||
|
|
||||||
// 当前查看的用户信息
|
|
||||||
const currentViewingUser = users.find((u) => parseInt(String(u?.id || ''), 10) === viewingUserId)
|
|
||||||
|
|
||||||
const handleSwitchUser = (userId) => {
|
|
||||||
const nextUserId = parseInt(String(userId || ''), 10)
|
|
||||||
if (Number.isFinite(nextUserId) && nextUserId > 0) {
|
|
||||||
dispatch(switchUser(nextUserId))
|
|
||||||
setShowUserPopover(false)
|
|
||||||
// 触发自定义事件,保持向后兼容
|
|
||||||
window.dispatchEvent(new CustomEvent('ats:user:switched', { detail: { userId: nextUserId } }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshMe()
|
refreshMe()
|
||||||
|
|
@ -132,88 +82,16 @@ function App() {
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="nav-user">
|
<div className="nav-user">
|
||||||
{isAdmin ? (
|
|
||||||
<div className="nav-user-switcher" ref={userPopoverRef} style={{ position: 'relative', display: 'inline-block' }}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="nav-user-switch-btn"
|
|
||||||
onClick={() => setShowUserPopover(!showUserPopover)}
|
|
||||||
title="切换查看的用户视角"
|
|
||||||
style={{
|
|
||||||
background: 'transparent',
|
|
||||||
border: 'none',
|
|
||||||
color: 'white',
|
|
||||||
cursor: 'pointer',
|
|
||||||
padding: '4px 8px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '4px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>👤 {currentViewingUser?.username || currentUser?.username || '选择用户'}</span>
|
|
||||||
{currentViewingUser?.role === 'admin' ? '(管理员)' : ''}
|
|
||||||
<span>▼</span>
|
|
||||||
</button>
|
|
||||||
{showUserPopover && (
|
|
||||||
<div className="nav-user-popover" style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 'calc(100% + 8px)',
|
|
||||||
right: 0,
|
|
||||||
background: 'white',
|
|
||||||
borderRadius: '8px',
|
|
||||||
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
||||||
minWidth: '200px',
|
|
||||||
maxWidth: '300px',
|
|
||||||
zIndex: 1000,
|
|
||||||
overflow: 'hidden'
|
|
||||||
}}>
|
|
||||||
<div style={{ padding: '12px 16px', background: '#f5f5f5', borderBottom: '1px solid #e0e0e0', fontWeight: 600, fontSize: '14px', color: '#333' }}>
|
|
||||||
切换用户视角
|
|
||||||
</div>
|
|
||||||
<div style={{ maxHeight: '300px', overflowY: 'auto' }}>
|
|
||||||
{users.map((u) => (
|
|
||||||
<div
|
|
||||||
key={u.id}
|
|
||||||
onClick={() => handleSwitchUser(u.id)}
|
|
||||||
style={{
|
|
||||||
padding: '12px 16px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'background-color 0.2s',
|
|
||||||
borderBottom: '1px solid #f0f0f0',
|
|
||||||
background: viewingUserId === u.id ? '#e3f2fd' : 'white'
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
if (viewingUserId !== u.id) e.currentTarget.style.background = '#f5f5f5'
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
if (viewingUserId !== u.id) e.currentTarget.style.background = 'white'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ fontWeight: 500, fontSize: '14px', color: '#333', marginBottom: '4px' }}>
|
|
||||||
{u.username || 'user'} {u.role === 'admin' ? '(管理员)' : ''}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
|
||||||
{u.status === 'active' ? '启用' : '禁用'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span className="nav-user-name">
|
<span className="nav-user-name">
|
||||||
{currentUser?.username ? currentUser.username : 'user'}
|
{currentUser?.username ? currentUser.username : 'user'}
|
||||||
|
{isAdmin ? '(管理员)' : ''}
|
||||||
</span>
|
</span>
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="nav-logout"
|
className="nav-logout"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clearAuthToken()
|
clearAuthToken()
|
||||||
dispatch(setCurrentUser(null))
|
dispatch(setCurrentUser(null))
|
||||||
dispatch(setViewingUserId(null))
|
|
||||||
dispatch(setUsers([]))
|
dispatch(setUsers([]))
|
||||||
setChecking(false)
|
setChecking(false)
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,7 @@ import {
|
||||||
selectAccountId,
|
selectAccountId,
|
||||||
selectAccounts,
|
selectAccounts,
|
||||||
selectCurrentUser,
|
selectCurrentUser,
|
||||||
selectViewingUserId,
|
|
||||||
selectIsAdmin,
|
selectIsAdmin,
|
||||||
selectEffectiveUserId,
|
|
||||||
} from '../store/appSlice'
|
} from '../store/appSlice'
|
||||||
|
|
||||||
const AccountSelector = ({ onChanged }) => {
|
const AccountSelector = ({ onChanged }) => {
|
||||||
|
|
@ -18,65 +16,23 @@ const AccountSelector = ({ onChanged }) => {
|
||||||
const accountId = useSelector(selectAccountId)
|
const accountId = useSelector(selectAccountId)
|
||||||
const accounts = useSelector(selectAccounts)
|
const accounts = useSelector(selectAccounts)
|
||||||
const currentUser = useSelector(selectCurrentUser)
|
const currentUser = useSelector(selectCurrentUser)
|
||||||
const viewingUserId = useSelector(selectViewingUserId)
|
|
||||||
const isAdmin = useSelector(selectIsAdmin)
|
const isAdmin = useSelector(selectIsAdmin)
|
||||||
const effectiveUserId = useSelector(selectEffectiveUserId)
|
|
||||||
|
|
||||||
// 监听用户切换事件(管理员切换用户时触发)
|
// 加载当前用户的账号列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleUserSwitched = (e) => {
|
|
||||||
const userId = e?.detail?.userId
|
|
||||||
if (userId && isAdmin) {
|
|
||||||
// 重新加载该用户的账号列表
|
|
||||||
api.getUserAccounts(userId)
|
|
||||||
.then((list) => {
|
|
||||||
const accountsList = (Array.isArray(list) ? list : []).map((item) => ({
|
|
||||||
id: item.account_id || item.id,
|
|
||||||
name: item.account_name || item.name || '',
|
|
||||||
status: item.account_status || item.status || 'active',
|
|
||||||
role: item.role || 'viewer',
|
|
||||||
user_id: item.user_id
|
|
||||||
}))
|
|
||||||
dispatch(setAccounts(accountsList))
|
|
||||||
// 自动选择第一个账号(不管是否disabled)
|
|
||||||
if (accountsList.length > 0) {
|
|
||||||
dispatch(selectFirstAccount())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => dispatch(setAccounts([])))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.addEventListener('ats:user:switched', handleUserSwitched)
|
|
||||||
return () => window.removeEventListener('ats:user:switched', handleUserSwitched)
|
|
||||||
}, [isAdmin, dispatch])
|
|
||||||
|
|
||||||
// 根据effectiveUserId加载账号列表(管理员查看指定用户,普通用户查看自己)
|
|
||||||
useEffect(() => {
|
|
||||||
if (effectiveUserId) {
|
|
||||||
if (isAdmin) {
|
|
||||||
// 管理员:加载指定用户的账号列表
|
|
||||||
api.getUserAccounts(effectiveUserId)
|
|
||||||
.then((list) => {
|
|
||||||
// 转换数据格式:后端返回 {account_id, account_name, account_status},前端期望 {id, name, status}
|
|
||||||
const accountsList = (Array.isArray(list) ? list : []).map((item) => ({
|
|
||||||
id: item.account_id || item.id,
|
|
||||||
name: item.account_name || item.name || '',
|
|
||||||
status: item.account_status || item.status || 'active',
|
|
||||||
role: item.role || 'viewer',
|
|
||||||
user_id: item.user_id
|
|
||||||
}))
|
|
||||||
dispatch(setAccounts(accountsList))
|
|
||||||
// 自动选择第一个账号(不管是否disabled)
|
|
||||||
if (accountsList.length > 0) {
|
|
||||||
dispatch(selectFirstAccount())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => dispatch(setAccounts([])))
|
|
||||||
} else {
|
|
||||||
// 普通用户:直接加载自己的账号列表
|
|
||||||
const load = () => {
|
const load = () => {
|
||||||
api.getAccounts()
|
api.getAccounts()
|
||||||
.then((list) => dispatch(setAccounts(Array.isArray(list) ? list : [])))
|
.then((list) => {
|
||||||
|
const accountsList = Array.isArray(list) ? list : []
|
||||||
|
dispatch(setAccounts(accountsList))
|
||||||
|
// 如果当前没有选中的账号,或者选中的账号不在列表中,自动选择第一个账号
|
||||||
|
if (accountsList.length > 0) {
|
||||||
|
const currentAccount = accountsList.find((a) => a.id === accountId)
|
||||||
|
if (!currentAccount) {
|
||||||
|
dispatch(selectFirstAccount())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch(() => dispatch(setAccounts([])))
|
.catch(() => dispatch(setAccounts([])))
|
||||||
}
|
}
|
||||||
load()
|
load()
|
||||||
|
|
@ -85,9 +41,7 @@ const AccountSelector = ({ onChanged }) => {
|
||||||
const onUpdated = () => load()
|
const onUpdated = () => load()
|
||||||
window.addEventListener('ats:accounts:updated', onUpdated)
|
window.addEventListener('ats:accounts:updated', onUpdated)
|
||||||
return () => window.removeEventListener('ats:accounts:updated', onUpdated)
|
return () => window.removeEventListener('ats:accounts:updated', onUpdated)
|
||||||
}
|
}, [dispatch, accountId])
|
||||||
}
|
|
||||||
}, [isAdmin, effectiveUserId, dispatch])
|
|
||||||
|
|
||||||
// 当 accountId 变化时,调用 onChanged 回调
|
// 当 accountId 变化时,调用 onChanged 回调
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -148,10 +102,9 @@ const AccountSelector = ({ onChanged }) => {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
title="切换账号后:配置/持仓/交易记录/统计会按账号隔离;推荐仍是全局"
|
title="切换账号后:配置/持仓/交易记录/统计会按账号隔离;推荐仍是全局"
|
||||||
disabled={isAdmin && !effectiveUserId}
|
|
||||||
>
|
>
|
||||||
{options.length === 0 ? (
|
{options.length === 0 ? (
|
||||||
<option value="">{isAdmin && !effectiveUserId ? '请先选择用户' : '暂无账号'}</option>
|
<option value="">暂无账号</option>
|
||||||
) : (
|
) : (
|
||||||
options.map((a) => {
|
options.map((a) => {
|
||||||
const isDisabled = String(a?.status || 'active') === 'disabled'
|
const isDisabled = String(a?.status || 'active') === 'disabled'
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,7 @@ const ConfigPanel = () => {
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const accountId = useSelector(selectAccountId) // 从 Redux 获取当前账号ID
|
const accountId = useSelector(selectAccountId) // 从 Redux 获取当前账号ID
|
||||||
const currentUser = useSelector(selectCurrentUser)
|
const currentUser = useSelector(selectCurrentUser)
|
||||||
const viewingUserId = useSelector(selectViewingUserId)
|
|
||||||
const isAdmin = useSelector(selectIsAdmin)
|
const isAdmin = useSelector(selectIsAdmin)
|
||||||
const effectiveUserId = useSelector(selectEffectiveUserId)
|
|
||||||
|
|
||||||
const [configs, setConfigs] = useState({})
|
const [configs, setConfigs] = useState({})
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
|
||||||
|
|
@ -43,11 +43,23 @@ const StatsDashboard = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// 只在 accountId 存在时才加载数据,避免加载上一个账号的数据
|
||||||
|
if (!accountId) {
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置状态,避免显示上一个账号的数据
|
||||||
|
setDashboardData(null)
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
loadDashboard()
|
loadDashboard()
|
||||||
loadTradingConfig()
|
loadTradingConfig()
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
|
if (accountId) {
|
||||||
loadDashboard()
|
loadDashboard()
|
||||||
loadTradingConfig() // 同时刷新配置
|
loadTradingConfig() // 同时刷新配置
|
||||||
|
}
|
||||||
}, 30000) // 每30秒刷新
|
}, 30000) // 每30秒刷新
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user