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)
|
||||
# 回退到数据库记录
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user