feat: 优化仓位管理页面功能和样式

- 添加卡片和列表视图切换功能
- 移动端支持卡片折叠/展开,默认只显示关键信息
- 历史仓位不显示盈亏和平仓收益
- 优化账户筛选:移除搜索功能,按账户名称排序
- 移动端强制使用卡片视图,隐藏视图切换按钮
- 优化侧边栏布局:固定高度,内容区域可滚动
- 优化当前仓位/历史仓位切换组件样式
- 修复账户筛选全部账户选项不生效的问题
This commit is contained in:
WrBug
2025-11-27 01:13:52 +08:00
parent 389a758c89
commit f53ba1ca49
10 changed files with 1083 additions and 19 deletions
+2 -2
View File
@@ -9,7 +9,7 @@ import AccountEdit from './pages/AccountEdit'
import LeaderList from './pages/LeaderList'
import LeaderAdd from './pages/LeaderAdd'
import ConfigPage from './pages/ConfigPage'
import OrderList from './pages/OrderList'
import PositionList from './pages/PositionList'
import Statistics from './pages/Statistics'
function App() {
@@ -26,7 +26,7 @@ function App() {
<Route path="/leaders" element={<LeaderList />} />
<Route path="/leaders/add" element={<LeaderAdd />} />
<Route path="/config" element={<ConfigPage />} />
<Route path="/orders" element={<OrderList />} />
<Route path="/positions" element={<PositionList />} />
<Route path="/statistics" element={<Statistics />} />
</Routes>
</Layout>
+28 -8
View File
@@ -41,9 +41,9 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
label: '跟单配置'
},
{
key: '/orders',
key: '/positions',
icon: <UnorderedListOutlined />,
label: '订单管理'
label: '仓位管理'
},
{
key: '/statistics',
@@ -108,8 +108,18 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
// 桌面端布局
return (
<AntLayout style={{ minHeight: '100vh' }}>
<Sider width={200} style={{ background: '#001529' }}>
<AntLayout style={{ height: '100vh', overflow: 'hidden' }}>
<Sider
width={200}
style={{
background: '#001529',
height: '100vh',
position: 'fixed',
left: 0,
top: 0,
overflow: 'hidden'
}}
>
<div style={{
height: '64px',
display: 'flex',
@@ -117,7 +127,8 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
justifyContent: 'center',
color: '#fff',
fontSize: '18px',
fontWeight: 'bold'
fontWeight: 'bold',
flexShrink: 0
}}>
Polymarket
</div>
@@ -126,11 +137,20 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
selectedKeys={[location.pathname]}
items={menuItems}
onClick={({ key }) => handleMenuClick(key)}
style={{ height: 'calc(100vh - 64px)', borderRight: 0 }}
style={{
height: 'calc(100vh - 64px)',
borderRight: 0,
overflowY: 'auto'
}}
/>
</Sider>
<AntLayout>
<Content style={{ padding: '24px', background: '#f0f2f5', minHeight: '100vh' }}>
<AntLayout style={{ marginLeft: 200, height: '100vh' }}>
<Content style={{
padding: '24px',
background: '#f0f2f5',
height: '100vh',
overflowY: 'auto'
}}>
{children}
</Content>
</AntLayout>
+835
View File
@@ -0,0 +1,835 @@
import { useEffect, useState, useMemo } from 'react'
import { Card, Table, Tag, message, Space, Input, Radio, Select, Button, Row, Col, Empty } from 'antd'
import { SearchOutlined, AppstoreOutlined, UnorderedListOutlined, UpOutlined, DownOutlined } from '@ant-design/icons'
import { apiService } from '../services/api'
import type { AccountPosition, Account } from '../types'
import { useMediaQuery } from 'react-responsive'
type PositionFilter = 'current' | 'historical'
type ViewMode = 'card' | 'list'
const PositionList: React.FC = () => {
const isMobile = useMediaQuery({ maxWidth: 768 })
const [currentPositions, setCurrentPositions] = useState<AccountPosition[]>([])
const [historyPositions, setHistoryPositions] = useState<AccountPosition[]>([])
const [accounts, setAccounts] = useState<Account[]>([])
const [loading, setLoading] = useState(false)
const [accountsLoading, setAccountsLoading] = useState(false)
const [searchKeyword, setSearchKeyword] = useState('')
const [positionFilter, setPositionFilter] = useState<PositionFilter>('current')
const [selectedAccountId, setSelectedAccountId] = useState<number | undefined>(undefined)
const [viewMode, setViewMode] = useState<ViewMode>(isMobile ? 'card' : 'list')
const [expandedCards, setExpandedCards] = useState<Set<string>>(new Set())
useEffect(() => {
fetchAccounts()
fetchPositions()
}, [])
const fetchAccounts = async () => {
setAccountsLoading(true)
try {
const response = await apiService.accounts.list()
if (response.data.code === 0 && response.data.data) {
setAccounts(response.data.data.list || [])
} else {
message.error(response.data.msg || '获取账户列表失败')
}
} catch (error: any) {
message.error(error.message || '获取账户列表失败')
} finally {
setAccountsLoading(false)
}
}
const fetchPositions = async () => {
setLoading(true)
try {
const response = await apiService.accounts.positionsList()
if (response.data.code === 0 && response.data.data) {
setCurrentPositions(response.data.data.currentPositions || [])
setHistoryPositions(response.data.data.historyPositions || [])
} else {
message.error(response.data.msg || '获取仓位列表失败')
}
} catch (error: any) {
message.error(error.message || '获取仓位列表失败')
} finally {
setLoading(false)
}
}
// 根据筛选器选择对应的仓位列表
const basePositions = useMemo(() => {
return positionFilter === 'current' ? currentPositions : historyPositions
}, [positionFilter, currentPositions, historyPositions])
// 本地搜索和筛选过滤
const filteredPositions = useMemo(() => {
let filtered = basePositions
// 1. 先按账户筛选
if (selectedAccountId !== undefined) {
filtered = filtered.filter(p => p.accountId === selectedAccountId)
}
// 2. 最后按关键词搜索
if (searchKeyword.trim()) {
const keyword = searchKeyword.trim().toLowerCase()
filtered = filtered.filter(position => {
// 搜索账户名
if (position.accountName?.toLowerCase().includes(keyword)) {
return true
}
// 搜索钱包地址
if (position.walletAddress.toLowerCase().includes(keyword)) {
return true
}
// 搜索市场标题
if (position.marketTitle?.toLowerCase().includes(keyword)) {
return true
}
// 搜索市场slug
if (position.marketSlug?.toLowerCase().includes(keyword)) {
return true
}
// 搜索市场ID
if (position.marketId.toLowerCase().includes(keyword)) {
return true
}
// 搜索方向(YES/NO
if (position.side.toLowerCase().includes(keyword)) {
return true
}
return false
})
}
return filtered
}, [basePositions, searchKeyword, selectedAccountId])
const getSideColor = (side: string) => {
return side === 'YES' ? 'green' : 'red'
}
const formatNumber = (value: string | undefined, decimals: number = 2) => {
if (!value) return '-'
const num = parseFloat(value)
if (isNaN(num)) return value
return num.toFixed(decimals)
}
const formatPercent = (value: string | undefined) => {
if (!value) return '-'
const num = parseFloat(value)
if (isNaN(num)) return value
return `${num >= 0 ? '+' : ''}${num.toFixed(2)}%`
}
// 切换卡片展开/折叠状态
const toggleCard = (cardKey: string) => {
setExpandedCards(prev => {
const newSet = new Set(prev)
if (newSet.has(cardKey)) {
newSet.delete(cardKey)
} else {
newSet.add(cardKey)
}
return newSet
})
}
// 渲染卡片视图
const renderCardView = () => {
if (filteredPositions.length === 0) {
return (
<Empty
description="暂无仓位数据"
style={{ padding: '60px 0' }}
/>
)
}
return (
<Row gutter={[16, 16]}>
{filteredPositions.map((position, index) => {
const pnlNum = parseFloat(position.pnl || '0')
const isProfit = pnlNum >= 0
// 只有当前仓位才根据盈亏显示边框颜色
const borderColor = positionFilter === 'current'
? (isProfit ? 'rgba(82, 196, 26, 0.2)' : 'rgba(245, 34, 45, 0.2)')
: 'rgba(0,0,0,0.06)'
const cardKey = `${position.accountId}-${position.marketId}-${index}`
const isExpanded = expandedCards.has(cardKey)
// 移动端需要折叠功能,桌面端始终展开
const shouldCollapse = isMobile && !isExpanded
return (
<Col
key={cardKey}
xs={24}
sm={12}
lg={8}
xl={6}
>
<Card
hoverable={!isMobile}
onClick={() => isMobile && toggleCard(cardKey)}
style={{
height: '100%',
borderRadius: '12px',
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
transition: 'all 0.3s ease',
border: `1px solid ${borderColor}`,
cursor: isMobile ? 'pointer' : 'default'
}}
bodyStyle={{ padding: '16px' }}
>
{/* 头部:市场图标和标题 */}
<div style={{ marginBottom: '12px' }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '12px' }}>
{position.marketIcon && (
<img
src={position.marketIcon}
alt={position.marketTitle || 'Market'}
style={{
width: '48px',
height: '48px',
borderRadius: '8px',
objectFit: 'cover',
flexShrink: 0
}}
onError={(e) => {
e.currentTarget.style.display = 'none'
}}
/>
)}
<div style={{ flex: 1, minWidth: 0 }}>
{position.marketTitle ? (
position.marketSlug ? (
<a
href={`https://polymarket.com/event/${position.marketSlug}`}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
style={{
fontWeight: 'bold',
color: '#1890ff',
textDecoration: 'none',
fontSize: '15px',
lineHeight: '1.4',
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical'
}}
>
{position.marketTitle}
</a>
) : (
<div style={{ fontWeight: 'bold', fontSize: '15px', lineHeight: '1.4' }}>
{position.marketTitle}
</div>
)
) : (
<div style={{ fontFamily: 'monospace', fontSize: '12px', color: '#999' }}>
{position.marketId.slice(0, 16)}...
</div>
)}
{position.marketSlug && (
<div style={{ fontSize: '12px', color: '#999', marginTop: '4px' }}>
{position.marketSlug}
</div>
)}
</div>
</div>
</div>
{/* 账户信息 */}
<div style={{ marginBottom: '12px', paddingBottom: '12px', borderBottom: '1px solid #f0f0f0' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ fontWeight: '500', fontSize: '14px', color: '#333' }}>
{position.accountName || `账户 ${position.accountId}`}
</div>
<div style={{ fontSize: '12px', color: '#999', fontFamily: 'monospace', marginTop: '2px' }}>
{position.walletAddress.slice(0, 6)}...{position.walletAddress.slice(-4)}
</div>
</div>
<Tag color={getSideColor(position.side)} style={{ margin: 0 }}>
{position.side}
</Tag>
</div>
</div>
{/* 关键数据 */}
<div style={{ marginBottom: '12px' }}>
{/* 移动端折叠时,显示盈亏(使用简单样式) */}
{shouldCollapse && positionFilter === 'current' && (
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontSize: '13px', color: '#666' }}></span>
<span style={{
fontSize: '13px',
fontWeight: '500',
color: isProfit ? '#52c41a' : '#f5222d'
}}>
{pnlNum >= 0 ? '+' : ''}{formatNumber(position.pnl, 2)} USDC
</span>
</div>
)}
{/* 展开时显示所有数据 */}
{!shouldCollapse && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontSize: '13px', color: '#666' }}></span>
<span style={{ fontSize: '13px', fontWeight: '500' }}>
{formatNumber(position.quantity, 4)}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontSize: '13px', color: '#666' }}></span>
<span style={{ fontSize: '13px', fontWeight: '500' }}>
{formatNumber(position.avgPrice, 4)}
</span>
</div>
{positionFilter === 'current' && position.currentPrice && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontSize: '13px', color: '#666' }}></span>
<span style={{ fontSize: '13px', fontWeight: '500' }}>
{formatNumber(position.currentPrice, 4)}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontSize: '13px', color: '#666' }}></span>
<span style={{ fontSize: '13px', fontWeight: '600' }}>
{formatNumber(position.currentValue, 2)} USDC
</span>
</div>
</>
)}
</>
)}
{/* 移动端展开/折叠指示器 */}
{isMobile && (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginTop: '8px',
paddingTop: '8px',
borderTop: '1px solid #f0f0f0'
}}>
{isExpanded ? (
<UpOutlined style={{ color: '#999', fontSize: '14px' }} />
) : (
<DownOutlined style={{ color: '#999', fontSize: '14px' }} />
)}
</div>
)}
</div>
{/* 盈亏信息 - 突出显示(仅当前仓位显示,仅展开时显示) */}
{positionFilter === 'current' && !shouldCollapse && (
<div style={{
marginBottom: '12px',
padding: '12px',
borderRadius: '8px',
background: isProfit ? 'rgba(82, 196, 26, 0.08)' : 'rgba(245, 34, 45, 0.08)',
border: `1px solid ${isProfit ? 'rgba(82, 196, 26, 0.2)' : 'rgba(245, 34, 45, 0.2)'}`
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
<span style={{ fontSize: '13px', color: '#666' }}></span>
<span style={{
fontSize: '16px',
fontWeight: 'bold',
color: isProfit ? '#52c41a' : '#f5222d'
}}>
{pnlNum >= 0 ? '+' : ''}{formatNumber(position.pnl, 2)} USDC
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<span style={{
fontSize: '14px',
color: isProfit ? '#52c41a' : '#f5222d',
fontWeight: '500'
}}>
{formatPercent(position.percentPnl)}
</span>
</div>
{position.realizedPnl && (
<div style={{
marginTop: '8px',
paddingTop: '8px',
borderTop: '1px solid rgba(0,0,0,0.06)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<span style={{ fontSize: '12px', color: '#999' }}></span>
<span style={{
fontSize: '13px',
color: parseFloat(position.realizedPnl) >= 0 ? '#52c41a' : '#f5222d',
fontWeight: '500'
}}>
{parseFloat(position.realizedPnl) >= 0 ? '+' : ''}{formatNumber(position.realizedPnl, 2)} USDC
</span>
</div>
)}
</div>
)}
{/* 状态标签(移动端折叠时隐藏) */}
{positionFilter === 'current' && !shouldCollapse && (position.redeemable || position.mergeable) && (
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
{position.redeemable && (
<Tag color="green" style={{ margin: 0 }}></Tag>
)}
{position.mergeable && (
<Tag color="blue" style={{ margin: 0 }}></Tag>
)}
</div>
)}
</Card>
</Col>
)
})}
</Row>
)
}
// 根据仓位类型动态生成列(历史仓位不显示当前价格、当前价值、状态列)
const columns = useMemo(() => {
const baseColumns: any[] = [
{
title: '',
key: 'icon',
width: 50,
render: (_: any, record: AccountPosition) => {
if (!record.marketIcon) return null
return (
<img
src={record.marketIcon}
alt={record.marketTitle || 'Market'}
style={{
width: '32px',
height: '32px',
borderRadius: '4px',
objectFit: 'cover'
}}
onError={(e) => {
// 图片加载失败时隐藏
e.currentTarget.style.display = 'none'
}}
/>
)
},
fixed: isMobile ? ('left' as const) : undefined
},
{
title: '账户',
dataIndex: 'accountName',
key: 'accountName',
render: (text: string | undefined, record: AccountPosition) => (
<div>
<div style={{ fontWeight: 'bold' }}>
{text || `账户 ${record.accountId}`}
</div>
<div style={{ fontSize: '12px', color: '#999', fontFamily: 'monospace' }}>
{record.walletAddress.slice(0, 6)}...{record.walletAddress.slice(-6)}
</div>
</div>
),
fixed: isMobile ? ('left' as const) : undefined,
width: isMobile ? 150 : 200
},
{
title: '市场',
dataIndex: 'marketTitle',
key: 'marketTitle',
render: (text: string | undefined, record: AccountPosition) => {
const url = record.marketSlug
? `https://polymarket.com/event/${record.marketSlug}`
: null
const handleTitleClick = (e: React.MouseEvent) => {
e.stopPropagation()
if (url) {
window.open(url, '_blank', 'noopener,noreferrer')
}
}
return (
<div>
{text ? (
<div>
{url ? (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
onClick={handleTitleClick}
style={{ fontWeight: 'bold', color: '#1890ff', textDecoration: 'none', cursor: 'pointer' }}
>
{text}
</a>
) : (
<div style={{ fontWeight: 'bold' }}>{text}</div>
)}
</div>
) : (
<div style={{ fontFamily: 'monospace', fontSize: '12px' }}>
{record.marketId.slice(0, 10)}...
</div>
)}
{record.marketSlug && (
<div style={{ fontSize: '12px', color: '#999' }}>{record.marketSlug}</div>
)}
</div>
)
},
width: isMobile ? 200 : 250
},
{
title: '方向',
dataIndex: 'side',
key: 'side',
render: (side: string) => (
<Tag color={getSideColor(side)}>{side}</Tag>
),
width: 80
},
{
title: '数量',
dataIndex: 'quantity',
key: 'quantity',
render: (quantity: string) => formatNumber(quantity, 4),
align: 'right' as const,
width: 100
},
{
title: '平均价格',
dataIndex: 'avgPrice',
key: 'avgPrice',
render: (price: string) => formatNumber(price, 4),
align: 'right' as const,
width: 120
},
]
// 只有当前仓位才显示当前价格和当前价值列
if (positionFilter === 'current') {
baseColumns.push(
{
title: '当前价格',
dataIndex: 'currentPrice',
key: 'currentPrice',
render: (price: string) => formatNumber(price, 4),
align: 'right' as const,
width: 120
},
{
title: '当前价值',
dataIndex: 'currentValue',
key: 'currentValue',
render: (value: string) => (
<span style={{ fontWeight: 'bold' }}>
{formatNumber(value, 2)} USDC
</span>
),
align: 'right' as const,
width: 120,
sorter: (a: AccountPosition, b: AccountPosition) => {
const valA = parseFloat(a.currentValue || '0')
const valB = parseFloat(b.currentValue || '0')
return valA - valB
},
defaultSortOrder: 'descend' as const
}
)
}
// 只有当前仓位才显示盈亏和已实现盈亏列
if (positionFilter === 'current') {
baseColumns.push(
{
title: '盈亏',
dataIndex: 'pnl',
key: 'pnl',
render: (pnl: string, record: AccountPosition) => {
const pnlNum = parseFloat(pnl || '0')
const percentPnl = parseFloat(record.percentPnl || '0')
return (
<div>
<div style={{
color: pnlNum >= 0 ? '#3f8600' : '#cf1322',
fontWeight: 'bold'
}}>
{pnlNum >= 0 ? '+' : ''}{formatNumber(pnl, 2)} USDC
</div>
<div style={{
fontSize: '12px',
color: percentPnl >= 0 ? '#3f8600' : '#cf1322'
}}>
{formatPercent(record.percentPnl)}
</div>
</div>
)
},
align: 'right' as const,
width: 150,
sorter: (a: AccountPosition, b: AccountPosition) => {
const pnlA = parseFloat(a.pnl || '0')
const pnlB = parseFloat(b.pnl || '0')
return pnlA - pnlB
}
},
{
title: '已实现盈亏',
dataIndex: 'realizedPnl',
key: 'realizedPnl',
render: (realizedPnl: string | undefined, record: AccountPosition) => {
if (!realizedPnl) return '-'
const pnlNum = parseFloat(realizedPnl)
const percentPnl = parseFloat(record.percentRealizedPnl || '0')
return (
<div>
<div style={{
color: pnlNum >= 0 ? '#3f8600' : '#cf1322',
fontWeight: 'bold'
}}>
{pnlNum >= 0 ? '+' : ''}{formatNumber(realizedPnl, 2)} USDC
</div>
{record.percentRealizedPnl && (
<div style={{
fontSize: '12px',
color: percentPnl >= 0 ? '#3f8600' : '#cf1322'
}}>
{formatPercent(record.percentRealizedPnl)}
</div>
)}
</div>
)
},
align: 'right' as const,
width: 150
}
)
}
// 只有当前仓位才显示状态列
if (positionFilter === 'current') {
baseColumns.push({
title: '状态',
key: 'status',
render: (_: any, record: AccountPosition) => (
<Space size="small">
{record.redeemable && <Tag color="green"></Tag>}
{record.mergeable && <Tag color="blue"></Tag>}
</Space>
),
width: 120
})
}
return baseColumns
}, [positionFilter, isMobile])
// 统计当前和历史仓位数量(根据账户筛选)
const filteredCurrentPositions = useMemo(() => {
if (selectedAccountId === undefined) return currentPositions
return currentPositions.filter(p => p.accountId === selectedAccountId)
}, [currentPositions, selectedAccountId])
const filteredHistoryPositions = useMemo(() => {
if (selectedAccountId === undefined) return historyPositions
return historyPositions.filter(p => p.accountId === selectedAccountId)
}, [historyPositions, selectedAccountId])
const currentCount = filteredCurrentPositions.length
const historicalCount = filteredHistoryPositions.length
return (
<div>
<div style={{ marginBottom: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '12px', marginBottom: '12px' }}>
<h2 style={{ margin: 0 }}></h2>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flex: isMobile ? '1 1 100%' : '0 0 auto', flexWrap: 'wrap' }}>
<Input
placeholder="搜索账户、市场、方向..."
prefix={<SearchOutlined />}
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
allowClear
style={{ width: isMobile ? '100%' : 300 }}
/>
{!isMobile && (
<Button.Group>
<Button
type={viewMode === 'list' ? 'primary' : 'default'}
icon={<UnorderedListOutlined />}
onClick={() => setViewMode('list')}
title="列表视图"
/>
<Button
type={viewMode === 'card' ? 'primary' : 'default'}
icon={<AppstoreOutlined />}
onClick={() => setViewMode('card')}
title="卡片视图"
/>
</Button.Group>
)}
<span style={{ color: '#999', fontSize: '14px', whiteSpace: 'nowrap' }}>
{searchKeyword || selectedAccountId !== undefined
? `找到 ${filteredPositions.length} / ${basePositions.length} 个仓位`
: `${basePositions.length} 个仓位`}
</span>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px', flexWrap: 'wrap' }}>
<Select
placeholder="选择账户"
value={selectedAccountId ?? null}
onChange={(value) => setSelectedAccountId(value ?? undefined)}
style={{ width: isMobile ? '100%' : 200 }}
loading={accountsLoading}
options={[
{ value: null, label: '全部账户' },
...accounts
.sort((a, b) => {
const nameA = (a.accountName || `账户 ${a.id}`).toLowerCase()
const nameB = (b.accountName || `账户 ${b.id}`).toLowerCase()
return nameA.localeCompare(nameB, 'zh-CN')
})
.map(account => ({
value: account.id,
label: account.accountName || `账户 ${account.id}`
}))
]}
/>
<div style={{
background: '#f5f5f5',
padding: '4px',
borderRadius: '8px',
display: 'inline-flex',
gap: '4px'
}}>
<Radio.Group
value={positionFilter}
onChange={(e) => setPositionFilter(e.target.value)}
size={isMobile ? 'small' : 'middle'}
style={{ display: 'flex', gap: '4px' }}
>
<Radio.Button
value="current"
style={{
border: 'none',
borderRadius: '6px',
padding: '8px 16px',
height: 'auto',
lineHeight: '1.5',
transition: 'all 0.3s ease',
background: positionFilter === 'current' ? '#1890ff' : 'transparent',
color: positionFilter === 'current' ? '#fff' : '#666',
fontWeight: positionFilter === 'current' ? '500' : 'normal',
boxShadow: positionFilter === 'current' ? '0 2px 4px rgba(24, 144, 255, 0.2)' : 'none'
}}
>
<span style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span></span>
<Tag
color={positionFilter === 'current' ? 'default' : 'blue'}
style={{
margin: 0,
borderRadius: '10px',
fontSize: '12px',
lineHeight: '20px',
padding: '0 8px',
background: positionFilter === 'current' ? 'rgba(255, 255, 255, 0.3)' : undefined,
color: positionFilter === 'current' ? '#fff' : undefined,
border: positionFilter === 'current' ? 'none' : undefined
}}
>
{currentCount}
</Tag>
</span>
</Radio.Button>
<Radio.Button
value="historical"
style={{
border: 'none',
borderRadius: '6px',
padding: '8px 16px',
height: 'auto',
lineHeight: '1.5',
transition: 'all 0.3s ease',
background: positionFilter === 'historical' ? '#1890ff' : 'transparent',
color: positionFilter === 'historical' ? '#fff' : '#666',
fontWeight: positionFilter === 'historical' ? '500' : 'normal',
boxShadow: positionFilter === 'historical' ? '0 2px 4px rgba(24, 144, 255, 0.2)' : 'none'
}}
>
<span style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span></span>
<Tag
color={positionFilter === 'historical' ? 'default' : 'default'}
style={{
margin: 0,
borderRadius: '10px',
fontSize: '12px',
lineHeight: '20px',
padding: '0 8px',
background: positionFilter === 'historical' ? 'rgba(255, 255, 255, 0.3)' : undefined,
color: positionFilter === 'historical' ? '#fff' : undefined,
border: positionFilter === 'historical' ? 'none' : undefined
}}
>
{historicalCount}
</Tag>
</span>
</Radio.Button>
</Radio.Group>
</div>
</div>
</div>
{(isMobile || viewMode === 'card') ? (
<Card loading={loading}>
{renderCardView()}
{filteredPositions.length > 0 && (
<div style={{
marginTop: '24px',
textAlign: 'center',
color: '#999',
fontSize: '14px'
}}>
{filteredPositions.length} {searchKeyword ? `(已过滤)` : ''}
</div>
)}
</Card>
) : (
<Card>
<Table
dataSource={filteredPositions}
columns={columns}
rowKey={(record, index) => `${record.accountId}-${record.marketId}-${index}`}
loading={loading}
pagination={{
pageSize: 20,
showSizeChanger: !isMobile,
showTotal: (total) => `${total} 个仓位${searchKeyword ? `(已过滤)` : ''}`
}}
scroll={isMobile ? { x: 1500 } : undefined}
/>
</Card>
)}
</div>
)
}
export default PositionList
+7 -1
View File
@@ -91,7 +91,13 @@ export const apiService = {
* 设置默认账户
*/
setDefault: (data: { accountId: number }) =>
apiClient.post<ApiResponse<void>>('/copy-trading/accounts/set-default', data)
apiClient.post<ApiResponse<void>>('/copy-trading/accounts/set-default', data),
/**
* 查询所有账户的仓位列表
*/
positionsList: () =>
apiClient.post<ApiResponse<any>>('/copy-trading/accounts/positions/list', {})
},
/**
+36
View File
@@ -151,3 +151,39 @@ export interface Statistics {
maxLoss: string
}
/**
* 账户仓位信息
*/
export interface AccountPosition {
accountId: number
accountName?: string
walletAddress: string
proxyAddress: string
marketId: string
marketTitle?: string
marketSlug?: string
marketIcon?: string // 市场图标 URL
side: string // YES 或 NO
quantity: string
avgPrice: string
currentPrice: string
currentValue: string
initialValue: string
pnl: string
percentPnl: string
realizedPnl?: string
percentRealizedPnl?: string
redeemable: boolean
mergeable: boolean
endDate?: string
isCurrent: boolean // true: 当前仓位(有持仓),false: 历史仓位(已平仓)
}
/**
* 仓位列表响应
*/
export interface PositionListResponse {
currentPositions: AccountPosition[]
historyPositions: AccountPosition[]
}