feat: 为所有数值显示添加千分位分隔符
- 重构 formatNumber 和 formatUSDC 函数,默认添加千分位分隔符 - 更新 Statistics.tsx、CopyTradingStatistics.tsx 等统计页面 - 更新 PositionList.tsx 持仓列表的数值显示 - 更新 AccountList.tsx 账户列表的余额显示 - 所有数值(金额、数量、价格等)现在默认显示千分位 - 示例:1234567.89 显示为 1,234,567.89 影响范围: - 工具函数:utils/index.ts - 统计页面:Statistics.tsx, CopyTradingStatistics.tsx - 业务页面:PositionList.tsx, AccountList.tsx Changes: - Refactored formatNumber and formatUSDC to include thousand separators by default - Updated statistics pages to display numbers with commas - Updated position list and account list for better readability - Example: 1234567.89 now displays as 1,234,567.89
This commit is contained in:
@@ -724,47 +724,47 @@ const AccountList: React.FC = () => {
|
|||||||
{(detailAccount.totalOrders !== undefined || detailAccount.totalPnl !== undefined ||
|
{(detailAccount.totalOrders !== undefined || detailAccount.totalPnl !== undefined ||
|
||||||
detailAccount.activeOrders !== undefined ||
|
detailAccount.activeOrders !== undefined ||
|
||||||
detailAccount.completedOrders !== undefined || detailAccount.positionCount !== undefined) && (
|
detailAccount.completedOrders !== undefined || detailAccount.positionCount !== undefined) && (
|
||||||
<>
|
<>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Descriptions
|
<Descriptions
|
||||||
column={isMobile ? 1 : 2}
|
column={isMobile ? 1 : 2}
|
||||||
bordered
|
bordered
|
||||||
size={isMobile ? 'small' : 'middle'}
|
size={isMobile ? 'small' : 'middle'}
|
||||||
title={t('accountList.statistics')}
|
title={t('accountList.statistics')}
|
||||||
>
|
>
|
||||||
{detailAccount.totalOrders !== undefined && (
|
{detailAccount.totalOrders !== undefined && (
|
||||||
<Descriptions.Item label={t('accountList.totalOrders')}>
|
<Descriptions.Item label={t('accountList.totalOrders')}>
|
||||||
{detailAccount.totalOrders}
|
{detailAccount.totalOrders}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
)}
|
)}
|
||||||
{detailAccount.activeOrders !== undefined && (
|
{detailAccount.activeOrders !== undefined && (
|
||||||
<Descriptions.Item label={t('accountList.activeOrdersCount')}>
|
<Descriptions.Item label={t('accountList.activeOrdersCount')}>
|
||||||
<Tag color={detailAccount.activeOrders > 0 ? 'orange' : 'default'}>{detailAccount.activeOrders}</Tag>
|
<Tag color={detailAccount.activeOrders > 0 ? 'orange' : 'default'}>{detailAccount.activeOrders}</Tag>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
)}
|
)}
|
||||||
{detailAccount.completedOrders !== undefined && (
|
{detailAccount.completedOrders !== undefined && (
|
||||||
<Descriptions.Item label={t('accountList.completedOrders')}>
|
<Descriptions.Item label={t('accountList.completedOrders')}>
|
||||||
<Tag color="success">{detailAccount.completedOrders}</Tag>
|
<Tag color="success">{detailAccount.completedOrders}</Tag>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
)}
|
)}
|
||||||
{detailAccount.positionCount !== undefined && (
|
{detailAccount.positionCount !== undefined && (
|
||||||
<Descriptions.Item label={t('accountList.positionCount')}>
|
<Descriptions.Item label={t('accountList.positionCount')}>
|
||||||
<Tag color={detailAccount.positionCount > 0 ? 'blue' : 'default'}>{detailAccount.positionCount}</Tag>
|
<Tag color={detailAccount.positionCount > 0 ? 'blue' : 'default'}>{detailAccount.positionCount}</Tag>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
)}
|
)}
|
||||||
{detailAccount.totalPnl !== undefined && (
|
{detailAccount.totalPnl !== undefined && (
|
||||||
<Descriptions.Item label={t('accountList.totalPnl')}>
|
<Descriptions.Item label={t('accountList.totalPnl')}>
|
||||||
<span style={{
|
<span style={{
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: detailAccount.totalPnl && detailAccount.totalPnl.startsWith('-') ? '#ff4d4f' : '#52c41a'
|
color: detailAccount.totalPnl && detailAccount.totalPnl.startsWith('-') ? '#ff4d4f' : '#52c41a'
|
||||||
}}>
|
}}>
|
||||||
{formatUSDC(detailAccount.totalPnl)} USDC
|
{formatUSDC(detailAccount.totalPnl)} USDC
|
||||||
</span>
|
</span>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
)}
|
)}
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom'
|
|||||||
import { Card, Row, Col, Statistic, Tag, Button, message, Spin } from 'antd'
|
import { Card, Row, Col, Statistic, Tag, Button, message, Spin } from 'antd'
|
||||||
import { ArrowUpOutlined, ArrowDownOutlined, LeftOutlined } from '@ant-design/icons'
|
import { ArrowUpOutlined, ArrowDownOutlined, LeftOutlined } from '@ant-design/icons'
|
||||||
import { apiService } from '../services/api'
|
import { apiService } from '../services/api'
|
||||||
import { formatUSDC } from '../utils'
|
import { formatUSDC, formatNumber } from '../utils'
|
||||||
import { useMediaQuery } from 'react-responsive'
|
import { useMediaQuery } from 'react-responsive'
|
||||||
import type { CopyTradingStatistics } from '../types'
|
import type { CopyTradingStatistics } from '../types'
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ const CopyTradingStatisticsPage: React.FC = () => {
|
|||||||
<Col xs={24} sm={12} md={6}>
|
<Col xs={24} sm={12} md={6}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="总买入数量"
|
title="总买入数量"
|
||||||
value={formatUSDC(statistics.totalBuyQuantity)}
|
value={formatNumber(statistics.totalBuyQuantity, 4)}
|
||||||
suffix=""
|
suffix=""
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -151,14 +151,14 @@ const CopyTradingStatisticsPage: React.FC = () => {
|
|||||||
<Col xs={24} sm={12} md={6}>
|
<Col xs={24} sm={12} md={6}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="总买入订单数"
|
title="总买入订单数"
|
||||||
value={statistics.totalBuyOrders}
|
value={formatNumber(statistics.totalBuyOrders)}
|
||||||
suffix="笔"
|
suffix="笔"
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} md={6}>
|
<Col xs={24} sm={12} md={6}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="平均买入价格"
|
title="平均买入价格"
|
||||||
value={formatUSDC(statistics.avgBuyPrice)}
|
value={formatNumber(statistics.avgBuyPrice, 4)}
|
||||||
suffix=""
|
suffix=""
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -171,7 +171,7 @@ const CopyTradingStatisticsPage: React.FC = () => {
|
|||||||
<Col xs={24} sm={12} md={8}>
|
<Col xs={24} sm={12} md={8}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="总卖出数量"
|
title="总卖出数量"
|
||||||
value={formatUSDC(statistics.totalSellQuantity)}
|
value={formatNumber(statistics.totalSellQuantity, 4)}
|
||||||
suffix=""
|
suffix=""
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -185,7 +185,7 @@ const CopyTradingStatisticsPage: React.FC = () => {
|
|||||||
<Col xs={24} sm={12} md={8}>
|
<Col xs={24} sm={12} md={8}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="总卖出订单数"
|
title="总卖出订单数"
|
||||||
value={statistics.totalSellOrders}
|
value={formatNumber(statistics.totalSellOrders)}
|
||||||
suffix="笔"
|
suffix="笔"
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -198,14 +198,14 @@ const CopyTradingStatisticsPage: React.FC = () => {
|
|||||||
<Col xs={24} sm={12} md={12}>
|
<Col xs={24} sm={12} md={12}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="当前持仓数量"
|
title="当前持仓数量"
|
||||||
value={formatUSDC(statistics.currentPositionQuantity)}
|
value={formatNumber(statistics.currentPositionQuantity, 4)}
|
||||||
suffix=""
|
suffix=""
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} md={12}>
|
<Col xs={24} sm={12} md={12}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="平均买入价格"
|
title="平均买入价格"
|
||||||
value={formatUSDC(statistics.avgBuyPrice)}
|
value={formatNumber(statistics.avgBuyPrice, 4)}
|
||||||
suffix=""
|
suffix=""
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
+193
-193
@@ -8,7 +8,7 @@ import { getPositionKey } from '../types'
|
|||||||
import { useMediaQuery } from 'react-responsive'
|
import { useMediaQuery } from 'react-responsive'
|
||||||
import { useWebSocketSubscription } from '../hooks/useWebSocket'
|
import { useWebSocketSubscription } from '../hooks/useWebSocket'
|
||||||
import { wsManager } from '../services/websocket'
|
import { wsManager } from '../services/websocket'
|
||||||
import { formatUSDC } from '../utils'
|
import { formatUSDC, formatNumber as formatNumberUtil } from '../utils'
|
||||||
|
|
||||||
type PositionFilter = 'current' | 'historical'
|
type PositionFilter = 'current' | 'historical'
|
||||||
type ViewMode = 'card' | 'list'
|
type ViewMode = 'card' | 'list'
|
||||||
@@ -328,7 +328,7 @@ const PositionList: React.FC = () => {
|
|||||||
if (!value) return '-'
|
if (!value) return '-'
|
||||||
const num = parseFloat(value)
|
const num = parseFloat(value)
|
||||||
if (isNaN(num)) return value
|
if (isNaN(num)) return value
|
||||||
return num.toFixed(decimals)
|
return formatNumberUtil(value, decimals)
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatPercent = (value: string | undefined) => {
|
const formatPercent = (value: string | undefined) => {
|
||||||
@@ -823,132 +823,132 @@ const PositionList: React.FC = () => {
|
|||||||
// 根据仓位类型动态生成列(历史仓位不显示当前价格、当前价值、状态列)
|
// 根据仓位类型动态生成列(历史仓位不显示当前价格、当前价值、状态列)
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
const baseColumns: any[] = [
|
const baseColumns: any[] = [
|
||||||
{
|
{
|
||||||
title: '',
|
title: '',
|
||||||
key: 'icon',
|
key: 'icon',
|
||||||
width: 50,
|
width: 50,
|
||||||
render: (_: any, record: AccountPosition) => {
|
render: (_: any, record: AccountPosition) => {
|
||||||
if (!record.marketIcon) return null
|
if (!record.marketIcon) return null
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
src={record.marketIcon}
|
src={record.marketIcon}
|
||||||
alt={record.marketTitle || 'Market'}
|
alt={record.marketTitle || 'Market'}
|
||||||
style={{
|
style={{
|
||||||
width: '32px',
|
width: '32px',
|
||||||
height: '32px',
|
height: '32px',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
objectFit: 'cover'
|
objectFit: 'cover'
|
||||||
}}
|
}}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
// 图片加载失败时隐藏
|
// 图片加载失败时隐藏
|
||||||
e.currentTarget.style.display = 'none'
|
e.currentTarget.style.display = 'none'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
fixed: isMobile ? ('left' as const) : undefined
|
||||||
},
|
},
|
||||||
fixed: isMobile ? ('left' as const) : undefined
|
{
|
||||||
},
|
title: '账户',
|
||||||
{
|
dataIndex: 'accountName',
|
||||||
title: '账户',
|
key: 'accountName',
|
||||||
dataIndex: 'accountName',
|
render: (text: string | undefined, record: AccountPosition) => (
|
||||||
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.eventSlug || record.marketSlug
|
|
||||||
? `https://polymarket.com/event/${record.eventSlug || record.marketSlug}`
|
|
||||||
: null
|
|
||||||
|
|
||||||
const handleTitleClick = (e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
if (url) {
|
|
||||||
window.open(url, '_blank', 'noopener,noreferrer')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
<div>
|
||||||
{text ? (
|
<div style={{ fontWeight: 'bold' }}>
|
||||||
<div>
|
{text || `账户 ${record.accountId}`}
|
||||||
{url ? (
|
</div>
|
||||||
<a
|
<div style={{ fontSize: '12px', color: '#999', fontFamily: 'monospace' }}>
|
||||||
href={url}
|
{record.walletAddress.slice(0, 6)}...{record.walletAddress.slice(-6)}
|
||||||
target="_blank"
|
</div>
|
||||||
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>
|
</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.eventSlug || record.marketSlug
|
||||||
|
? `https://polymarket.com/event/${record.eventSlug || 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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '开仓价值',
|
||||||
|
dataIndex: 'initialValue',
|
||||||
|
key: 'initialValue',
|
||||||
|
render: (value: string) => (
|
||||||
|
<span>
|
||||||
|
{formatUSDC(value)} USDC
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
align: 'right' as const,
|
||||||
|
width: 120
|
||||||
},
|
},
|
||||||
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
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '开仓价值',
|
|
||||||
dataIndex: 'initialValue',
|
|
||||||
key: 'initialValue',
|
|
||||||
render: (value: string) => (
|
|
||||||
<span>
|
|
||||||
{formatUSDC(value)} USDC
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
align: 'right' as const,
|
|
||||||
width: 120
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
// 只有当前仓位才显示当前价格和当前价值列
|
// 只有当前仓位才显示当前价格和当前价值列
|
||||||
@@ -985,70 +985,70 @@ const PositionList: React.FC = () => {
|
|||||||
|
|
||||||
// 只有当前仓位才显示盈亏和已实现盈亏列
|
// 只有当前仓位才显示盈亏和已实现盈亏列
|
||||||
if (positionFilter === 'current') {
|
if (positionFilter === 'current') {
|
||||||
baseColumns.push(
|
baseColumns.push(
|
||||||
{
|
{
|
||||||
title: '盈亏',
|
title: '盈亏',
|
||||||
dataIndex: 'pnl',
|
dataIndex: 'pnl',
|
||||||
key: 'pnl',
|
key: 'pnl',
|
||||||
render: (pnl: string, record: AccountPosition) => {
|
render: (pnl: string, record: AccountPosition) => {
|
||||||
const pnlNum = parseFloat(pnl || '0')
|
const pnlNum = parseFloat(pnl || '0')
|
||||||
const percentPnl = parseFloat(record.percentPnl || '0')
|
const percentPnl = parseFloat(record.percentPnl || '0')
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{
|
<div style={{
|
||||||
color: pnlNum >= 0 ? '#3f8600' : '#cf1322',
|
color: pnlNum >= 0 ? '#3f8600' : '#cf1322',
|
||||||
fontWeight: 'bold'
|
fontWeight: 'bold'
|
||||||
}}>
|
}}>
|
||||||
{pnlNum >= 0 ? '+' : ''}{formatUSDC(pnl)} USDC
|
{pnlNum >= 0 ? '+' : ''}{formatUSDC(pnl)} USDC
|
||||||
</div>
|
</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 ? '+' : ''}{formatUSDC(realizedPnl)} USDC
|
|
||||||
</div>
|
|
||||||
{record.percentRealizedPnl && (
|
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
color: percentPnl >= 0 ? '#3f8600' : '#cf1322'
|
color: percentPnl >= 0 ? '#3f8600' : '#cf1322'
|
||||||
}}>
|
}}>
|
||||||
{formatPercent(record.percentRealizedPnl)}
|
{formatPercent(record.percentPnl)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</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
|
||||||
|
}
|
||||||
},
|
},
|
||||||
align: 'right' as const,
|
{
|
||||||
width: 150
|
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 ? '+' : ''}{formatUSDC(realizedPnl)} USDC
|
||||||
|
</div>
|
||||||
|
{record.percentRealizedPnl && (
|
||||||
|
<div style={{
|
||||||
|
fontSize: '12px',
|
||||||
|
color: percentPnl >= 0 ? '#3f8600' : '#cf1322'
|
||||||
|
}}>
|
||||||
|
{formatPercent(record.percentRealizedPnl)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
align: 'right' as const,
|
||||||
|
width: 150
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只有当前仓位才显示操作列
|
// 只有当前仓位才显示操作列
|
||||||
@@ -1097,7 +1097,7 @@ const PositionList: React.FC = () => {
|
|||||||
<div style={{ marginBottom: '16px' }}>
|
<div style={{ marginBottom: '16px' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '12px', marginBottom: '12px' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '12px', marginBottom: '12px' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
<h2 style={{ margin: 0 }}>仓位管理</h2>
|
<h2 style={{ margin: 0 }}>仓位管理</h2>
|
||||||
{/* WebSocket 连接状态指示器 */}
|
{/* WebSocket 连接状态指示器 */}
|
||||||
<Tag
|
<Tag
|
||||||
color={wsConnected ? 'green' : 'orange'}
|
color={wsConnected ? 'green' : 'orange'}
|
||||||
@@ -1163,9 +1163,9 @@ const PositionList: React.FC = () => {
|
|||||||
return nameA.localeCompare(nameB, 'zh-CN')
|
return nameA.localeCompare(nameB, 'zh-CN')
|
||||||
})
|
})
|
||||||
.map(account => ({
|
.map(account => ({
|
||||||
value: account.id,
|
value: account.id,
|
||||||
label: account.accountName || `账户 ${account.id}`
|
label: account.accountName || `账户 ${account.id}`
|
||||||
}))
|
}))
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap' }}>
|
||||||
@@ -1176,10 +1176,10 @@ const PositionList: React.FC = () => {
|
|||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
gap: '4px'
|
gap: '4px'
|
||||||
}}>
|
}}>
|
||||||
<Radio.Group
|
<Radio.Group
|
||||||
value={positionFilter}
|
value={positionFilter}
|
||||||
onChange={(e) => setPositionFilter(e.target.value)}
|
onChange={(e) => setPositionFilter(e.target.value)}
|
||||||
size={isMobile ? 'small' : 'middle'}
|
size={isMobile ? 'small' : 'middle'}
|
||||||
style={{ display: 'flex', gap: '4px' }}
|
style={{ display: 'flex', gap: '4px' }}
|
||||||
>
|
>
|
||||||
<Radio.Button
|
<Radio.Button
|
||||||
@@ -1215,7 +1215,7 @@ const PositionList: React.FC = () => {
|
|||||||
{currentCount}
|
{currentCount}
|
||||||
</Tag>
|
</Tag>
|
||||||
</span>
|
</span>
|
||||||
</Radio.Button>
|
</Radio.Button>
|
||||||
<Radio.Button
|
<Radio.Button
|
||||||
value="historical"
|
value="historical"
|
||||||
style={{
|
style={{
|
||||||
@@ -1249,8 +1249,8 @@ const PositionList: React.FC = () => {
|
|||||||
{historicalCount}
|
{historicalCount}
|
||||||
</Tag>
|
</Tag>
|
||||||
</span>
|
</span>
|
||||||
</Radio.Button>
|
</Radio.Button>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</div>
|
</div>
|
||||||
{redeemableSummary && redeemableSummary.totalCount > 0 && (
|
{redeemableSummary && redeemableSummary.totalCount > 0 && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import type { Dayjs } from 'dayjs'
|
import type { Dayjs } from 'dayjs'
|
||||||
import { apiService } from '../services/api'
|
import { apiService } from '../services/api'
|
||||||
import type { Statistics as StatisticsType } from '../types'
|
import type { Statistics as StatisticsType } from '../types'
|
||||||
import { formatUSDC } from '../utils'
|
import { formatUSDC, formatNumber } from '../utils'
|
||||||
import { useMediaQuery } from 'react-responsive'
|
import { useMediaQuery } from 'react-responsive'
|
||||||
|
|
||||||
const { RangePicker } = DatePicker
|
const { RangePicker } = DatePicker
|
||||||
@@ -91,7 +91,7 @@ const Statistics: React.FC = () => {
|
|||||||
<Card>
|
<Card>
|
||||||
<Statistic
|
<Statistic
|
||||||
title={t('statistics.totalOrders') || '总订单数'}
|
title={t('statistics.totalOrders') || '总订单数'}
|
||||||
value={stats?.totalOrders || 0}
|
value={formatNumber(stats?.totalOrders || 0)}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
+27
-18
@@ -1,12 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* 格式化数字,自动去除尾随零
|
* 格式化数字,添加千分位分隔符,自动去除尾随零
|
||||||
* @param value - 数字值(字符串或数字)
|
* @param value - 数字值(字符串或数字)
|
||||||
* @param maxDecimals - 最大小数位数(默认不限制)
|
* @param maxDecimals - 最大小数位数(默认不限制)
|
||||||
* @returns 格式化后的字符串,如果值为空或无效则返回 ''
|
* @returns 格式化后的字符串,如 "123,456.78",如果值为空或无效则返回 ''
|
||||||
* @example
|
* @example
|
||||||
|
* formatNumber(1234567.89) => "1,234,567.89"
|
||||||
|
* formatNumber(1234567.00) => "1,234,567"
|
||||||
|
* formatNumber(1234.5678, 2) => "1,234.56"
|
||||||
* formatNumber(100.00) => "100"
|
* formatNumber(100.00) => "100"
|
||||||
* formatNumber(100.50) => "100.5"
|
* formatNumber(100.50) => "100.5"
|
||||||
* formatNumber(100.55) => "100.55"
|
|
||||||
*/
|
*/
|
||||||
export const formatNumber = (value: string | number | undefined | null, maxDecimals?: number): string => {
|
export const formatNumber = (value: string | number | undefined | null, maxDecimals?: number): string => {
|
||||||
if (value === undefined || value === null || value === '') {
|
if (value === undefined || value === null || value === '') {
|
||||||
@@ -18,26 +20,38 @@ export const formatNumber = (value: string | number | undefined | null, maxDecim
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果有最大小数位数限制,先截断
|
// 处理小数位数
|
||||||
|
let numStr: string
|
||||||
if (maxDecimals !== undefined) {
|
if (maxDecimals !== undefined) {
|
||||||
const multiplier = Math.pow(10, maxDecimals)
|
const multiplier = Math.pow(10, maxDecimals)
|
||||||
const truncated = Math.floor(num * multiplier) / multiplier
|
const truncated = Math.floor(num * multiplier) / multiplier
|
||||||
return truncated.toFixed(maxDecimals).replace(/\.?0+$/, '')
|
numStr = truncated.toFixed(maxDecimals).replace(/\.?0+$/, '')
|
||||||
|
} else {
|
||||||
|
numStr = num.toString().replace(/\.?0+$/, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 直接转换为字符串,然后去除尾随零
|
// 分离整数和小数部分
|
||||||
return num.toString().replace(/\.?0+$/, '')
|
const parts = numStr.split('.')
|
||||||
|
const integerPart = parts[0]
|
||||||
|
const decimalPart = parts[1]
|
||||||
|
|
||||||
|
// 为整数部分添加千分位分隔符
|
||||||
|
const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||||
|
|
||||||
|
// 组合结果
|
||||||
|
return decimalPart ? `${formattedInteger}.${decimalPart}` : formattedInteger
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化 USDC 金额
|
* 格式化 USDC 金额,带千分位分隔符
|
||||||
* 最多显示 4 位小数,自动去除尾随零(截断,不四舍五入)
|
* 最多显示 4 位小数,自动去除尾随零(截断,不四舍五入)
|
||||||
* @param value - 金额值(字符串或数字)
|
* @param value - 金额值(字符串或数字)
|
||||||
* @returns 格式化后的字符串,如果值为空或无效则返回 '-'
|
* @returns 格式化后的字符串,如 "1,234.56",如果值为空或无效则返回 '-'
|
||||||
* @example
|
* @example
|
||||||
|
* formatUSDC(1234.56) => "1,234.56"
|
||||||
|
* formatUSDC(1234567.8901) => "1,234,567.8901"
|
||||||
|
* formatUSDC(1234.00) => "1,234"
|
||||||
* formatUSDC(1.23) => "1.23"
|
* formatUSDC(1.23) => "1.23"
|
||||||
* formatUSDC(1.23456) => "1.2345"
|
|
||||||
* formatUSDC(1.2) => "1.2"
|
|
||||||
* formatUSDC(1) => "1"
|
* formatUSDC(1) => "1"
|
||||||
*/
|
*/
|
||||||
export const formatUSDC = (value: string | number | undefined | null): string => {
|
export const formatUSDC = (value: string | number | undefined | null): string => {
|
||||||
@@ -50,12 +64,7 @@ export const formatUSDC = (value: string | number | undefined | null): string =>
|
|||||||
return '-'
|
return '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 Math.floor 截断到4位小数(不四舍五入)
|
return formatNumber(num, 4)
|
||||||
const multiplier = Math.pow(10, 4)
|
|
||||||
const truncated = Math.floor(num * multiplier) / multiplier
|
|
||||||
|
|
||||||
// 使用 toFixed(4) 确保格式一致,然后去除尾随零和小数点
|
|
||||||
return truncated.toFixed(4).replace(/\.?0+$/, '')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统一导出 ethers 相关工具函数
|
// 统一导出 ethers 相关工具函数
|
||||||
@@ -97,7 +106,7 @@ export const isAutoGeneratedOrderId = (orderId: string | undefined | null): bool
|
|||||||
/**
|
/**
|
||||||
* 构建 Polymarket 市场 URL
|
* 构建 Polymarket 市场 URL
|
||||||
* 对于 moneyline 市场,跳转到 moneyline 页面
|
* 对于 moneyline 市场,跳转到 moneyline 页面
|
||||||
* 注意:目前无法自动判断市场是否为 moneyline,需要后端提供标识
|
* 注意:目前无法自动判断市场是否为 moneyline,需要后端提供标识
|
||||||
* @param marketSlug - 市场 slug(用于显示)
|
* @param marketSlug - 市场 slug(用于显示)
|
||||||
* @param eventSlug - 跳转用的 slug(从 events[0].slug 获取,优先使用)
|
* @param eventSlug - 跳转用的 slug(从 events[0].slug 获取,优先使用)
|
||||||
* @param marketCategory - 市场分类(sports, crypto 等)
|
* @param marketCategory - 市场分类(sports, crypto 等)
|
||||||
|
|||||||
Reference in New Issue
Block a user