This commit is contained in:
薇薇安 2026-01-23 19:41:44 +08:00
parent 1fcd692368
commit 2ee6e7a009
5 changed files with 40 additions and 199 deletions

View File

@ -135,7 +135,7 @@ async def get_dashboard_data(account_id: int = Depends(get_account_id)):
logger.warning(f"获取实时持仓失败: {positions_error}", exc_info=True)
# 回退到数据库记录
try:
db_trades = Trade.get_all(status='open')[:10]
db_trades = Trade.get_all(status='open', account_id=account_id)[:10]
# 格式化数据库记录,添加 entry_value_usdt 字段
open_trades = []
for trade in db_trades:

View File

@ -13,51 +13,26 @@ import Login from './components/Login'
import { api, clearAuthToken } from './services/api'
import {
setCurrentUser,
setViewingUserId,
setUsers,
switchUser,
selectCurrentUser,
selectViewingUserId,
selectUsers,
selectIsAdmin,
selectEffectiveUserId,
} from './store/appSlice'
import './App.css'
function App() {
const dispatch = useDispatch()
const currentUser = useSelector(selectCurrentUser)
const viewingUserId = useSelector(selectViewingUserId)
const users = useSelector(selectUsers)
const isAdmin = useSelector(selectIsAdmin)
const effectiveUserId = useSelector(selectEffectiveUserId)
const [checking, setChecking] = useState(true)
const [showUserPopover, setShowUserPopover] = useState(false)
const userPopoverRef = React.useRef(null)
const refreshMe = async () => {
try {
const u = await api.me()
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) {
dispatch(setCurrentUser(null))
} 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(() => {
refreshMe()
@ -132,88 +82,16 @@ function App() {
) : null}
</div>
<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">
{currentUser?.username ? currentUser.username : 'user'}
{isAdmin ? '(管理员)' : ''}
</span>
)}
<button
type="button"
className="nav-logout"
onClick={() => {
clearAuthToken()
dispatch(setCurrentUser(null))
dispatch(setViewingUserId(null))
dispatch(setUsers([]))
setChecking(false)
}}

View File

@ -8,9 +8,7 @@ import {
selectAccountId,
selectAccounts,
selectCurrentUser,
selectViewingUserId,
selectIsAdmin,
selectEffectiveUserId,
} from '../store/appSlice'
const AccountSelector = ({ onChanged }) => {
@ -18,65 +16,23 @@ const AccountSelector = ({ onChanged }) => {
const accountId = useSelector(selectAccountId)
const accounts = useSelector(selectAccounts)
const currentUser = useSelector(selectCurrentUser)
const viewingUserId = useSelector(selectViewingUserId)
const isAdmin = useSelector(selectIsAdmin)
const effectiveUserId = useSelector(selectEffectiveUserId)
//
//
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 = () => {
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([])))
}
load()
@ -85,9 +41,7 @@ const AccountSelector = ({ onChanged }) => {
const onUpdated = () => load()
window.addEventListener('ats:accounts:updated', onUpdated)
return () => window.removeEventListener('ats:accounts:updated', onUpdated)
}
}
}, [isAdmin, effectiveUserId, dispatch])
}, [dispatch, accountId])
// accountId onChanged
useEffect(() => {
@ -148,10 +102,9 @@ const AccountSelector = ({ onChanged }) => {
}
}}
title="切换账号后:配置/持仓/交易记录/统计会按账号隔离;推荐仍是全局"
disabled={isAdmin && !effectiveUserId}
>
{options.length === 0 ? (
<option value="">{isAdmin && !effectiveUserId ? '请先选择用户' : '暂无账号'}</option>
<option value="">暂无账号</option>
) : (
options.map((a) => {
const isDisabled = String(a?.status || 'active') === 'disabled'

View File

@ -16,9 +16,7 @@ const ConfigPanel = () => {
const dispatch = useDispatch()
const accountId = useSelector(selectAccountId) // Redux ID
const currentUser = useSelector(selectCurrentUser)
const viewingUserId = useSelector(selectViewingUserId)
const isAdmin = useSelector(selectIsAdmin)
const effectiveUserId = useSelector(selectEffectiveUserId)
const [configs, setConfigs] = useState({})
const [loading, setLoading] = useState(true)

View File

@ -43,11 +43,23 @@ const StatsDashboard = () => {
}
useEffect(() => {
// accountId
if (!accountId) {
setLoading(false)
return
}
//
setDashboardData(null)
setLoading(true)
loadDashboard()
loadTradingConfig()
const interval = setInterval(() => {
if (accountId) {
loadDashboard()
loadTradingConfig() //
}
}, 30000) // 30
return () => {