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:
+113
-113
@@ -26,18 +26,18 @@ const AccountList: React.FC = () => {
|
||||
const [editLoading, setEditLoading] = useState(false)
|
||||
const [accountImportModalVisible, setAccountImportModalVisible] = useState(false)
|
||||
const [accountImportForm] = Form.useForm()
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccounts()
|
||||
}, [fetchAccounts])
|
||||
|
||||
|
||||
const handleAccountImportSuccess = async () => {
|
||||
message.success(t('accountImport.importSuccess'))
|
||||
setAccountImportModalVisible(false)
|
||||
accountImportForm.resetFields()
|
||||
fetchAccounts()
|
||||
}
|
||||
|
||||
|
||||
// 加载所有账户的余额
|
||||
useEffect(() => {
|
||||
const loadBalances = async () => {
|
||||
@@ -46,8 +46,8 @@ const AccountList: React.FC = () => {
|
||||
setBalanceLoading(prev => ({ ...prev, [account.id]: true }))
|
||||
try {
|
||||
const balanceData = await fetchAccountBalance(account.id)
|
||||
setBalanceMap(prev => ({
|
||||
...prev,
|
||||
setBalanceMap(prev => ({
|
||||
...prev,
|
||||
[account.id]: {
|
||||
total: balanceData.totalBalance || '0',
|
||||
available: balanceData.availableBalance || '0',
|
||||
@@ -56,8 +56,8 @@ const AccountList: React.FC = () => {
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error(`获取账户 ${account.id} 余额失败:`, error)
|
||||
setBalanceMap(prev => ({
|
||||
...prev,
|
||||
setBalanceMap(prev => ({
|
||||
...prev,
|
||||
[account.id]: { total: '-', available: '-', position: '-' }
|
||||
}))
|
||||
} finally {
|
||||
@@ -66,12 +66,12 @@ const AccountList: React.FC = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (accounts.length > 0) {
|
||||
loadBalances()
|
||||
}
|
||||
}, [accounts])
|
||||
|
||||
|
||||
const handleDelete = async (account: Account) => {
|
||||
try {
|
||||
await deleteAccount(account.id)
|
||||
@@ -80,13 +80,13 @@ const AccountList: React.FC = () => {
|
||||
message.error(error.message || t('accountList.deleteFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const handleCopy = (text: string) => {
|
||||
if (!text) {
|
||||
message.warning(t('accountList.copyFailed') || '复制失败:地址为空')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
message.success({
|
||||
@@ -103,7 +103,7 @@ const AccountList: React.FC = () => {
|
||||
fallbackCopyTextToClipboard(text)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const fallbackCopyTextToClipboard = (text: string) => {
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = text
|
||||
@@ -113,7 +113,7 @@ const AccountList: React.FC = () => {
|
||||
document.body.appendChild(textArea)
|
||||
textArea.focus()
|
||||
textArea.select()
|
||||
|
||||
|
||||
try {
|
||||
const successful = document.execCommand('copy')
|
||||
if (successful) {
|
||||
@@ -131,19 +131,19 @@ const AccountList: React.FC = () => {
|
||||
document.body.removeChild(textArea)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const handleShowDetail = async (account: Account) => {
|
||||
try {
|
||||
setDetailModalVisible(true)
|
||||
setDetailAccount(account)
|
||||
setDetailBalance(null)
|
||||
setDetailBalanceLoading(false)
|
||||
|
||||
|
||||
// 加载详情和余额
|
||||
try {
|
||||
const accountDetail = await fetchAccountDetail(account.id)
|
||||
setDetailAccount(accountDetail)
|
||||
|
||||
|
||||
// 加载余额
|
||||
setDetailBalanceLoading(true)
|
||||
try {
|
||||
@@ -173,10 +173,10 @@ const AccountList: React.FC = () => {
|
||||
setDetailAccount(null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const handleRefreshDetailBalance = async () => {
|
||||
if (!detailAccount) return
|
||||
|
||||
|
||||
setDetailBalanceLoading(true)
|
||||
try {
|
||||
const balanceData = await fetchAccountBalance(detailAccount.id)
|
||||
@@ -193,16 +193,16 @@ const AccountList: React.FC = () => {
|
||||
setDetailBalanceLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const handleShowEdit = async (account: Account) => {
|
||||
try {
|
||||
setEditModalVisible(true)
|
||||
setEditAccount(account)
|
||||
|
||||
|
||||
// 加载账户详情并设置表单初始值
|
||||
const accountDetail = await fetchAccountDetail(account.id)
|
||||
setEditAccount(accountDetail)
|
||||
|
||||
|
||||
editForm.setFieldsValue({
|
||||
accountName: accountDetail.accountName || '',
|
||||
apiKey: '', // 不显示实际值,留空表示不修改
|
||||
@@ -216,10 +216,10 @@ const AccountList: React.FC = () => {
|
||||
setEditAccount(null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const handleEditSubmit = async (values: any) => {
|
||||
if (!editAccount) return
|
||||
|
||||
|
||||
setEditLoading(true)
|
||||
try {
|
||||
// 构建更新请求,只支持编辑账户名称
|
||||
@@ -227,17 +227,17 @@ const AccountList: React.FC = () => {
|
||||
accountId: editAccount.id,
|
||||
accountName: values.accountName || undefined
|
||||
}
|
||||
|
||||
|
||||
await updateAccount(updateData)
|
||||
|
||||
|
||||
message.success(t('accountList.updateSuccess'))
|
||||
setEditModalVisible(false)
|
||||
setEditAccount(null)
|
||||
editForm.resetFields()
|
||||
|
||||
|
||||
// 刷新账户列表
|
||||
await fetchAccounts()
|
||||
|
||||
|
||||
// 如果详情 Modal 打开着,也刷新详情
|
||||
if (detailModalVisible && detailAccount && detailAccount.id === editAccount.id) {
|
||||
const accountDetail = await fetchAccountDetail(editAccount.id)
|
||||
@@ -249,7 +249,7 @@ const AccountList: React.FC = () => {
|
||||
setEditLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('accountList.accountName'),
|
||||
@@ -339,7 +339,7 @@ const AccountList: React.FC = () => {
|
||||
<Popconfirm
|
||||
title={t('accountList.deleteConfirm')}
|
||||
description={
|
||||
record.apiKeyConfigured
|
||||
record.apiKeyConfigured
|
||||
? t('accountList.deleteConfirmDesc')
|
||||
: t('accountList.deleteConfirmDescSimple')
|
||||
}
|
||||
@@ -356,7 +356,7 @@ const AccountList: React.FC = () => {
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
const mobileColumns = [
|
||||
{
|
||||
title: t('accountList.accountName'),
|
||||
@@ -364,16 +364,16 @@ const AccountList: React.FC = () => {
|
||||
render: (_: any, record: Account) => {
|
||||
return (
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<div style={{
|
||||
fontWeight: 'bold',
|
||||
<div style={{
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '8px',
|
||||
fontSize: '16px'
|
||||
}}>
|
||||
{record.accountName || `${t('accountList.accountName')} ${record.id}`}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
color: '#666',
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
color: '#666',
|
||||
marginBottom: '8px',
|
||||
wordBreak: 'break-all',
|
||||
fontFamily: 'monospace',
|
||||
@@ -406,7 +406,7 @@ const AccountList: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
color: '#1890ff'
|
||||
@@ -420,7 +420,7 @@ const AccountList: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
{balanceMap[record.id] && balanceMap[record.id].available !== '-' && (
|
||||
<div style={{
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
marginTop: '4px'
|
||||
@@ -459,7 +459,7 @@ const AccountList: React.FC = () => {
|
||||
<Popconfirm
|
||||
title={t('accountList.deleteConfirm')}
|
||||
description={
|
||||
record.apiKeyConfigured
|
||||
record.apiKeyConfigured
|
||||
? t('accountList.deleteConfirmDesc')
|
||||
: t('accountList.deleteConfirmDescSimple')
|
||||
}
|
||||
@@ -468,9 +468,9 @@ const AccountList: React.FC = () => {
|
||||
cancelText={t('common.cancel')}
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
block
|
||||
<Button
|
||||
size="small"
|
||||
block
|
||||
danger
|
||||
style={{ minHeight: '32px' }}
|
||||
>
|
||||
@@ -481,15 +481,15 @@ const AccountList: React.FC = () => {
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
<div style={{
|
||||
padding: isMobile ? '0' : undefined,
|
||||
margin: isMobile ? '0 -8px' : undefined
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: isMobile ? '12px' : '16px',
|
||||
flexWrap: 'wrap',
|
||||
@@ -510,8 +510,8 @@ const AccountList: React.FC = () => {
|
||||
{t('accountList.importAccount')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card style={{
|
||||
|
||||
<Card style={{
|
||||
margin: isMobile ? '0 -8px' : '0',
|
||||
borderRadius: isMobile ? '0' : undefined
|
||||
}}>
|
||||
@@ -544,7 +544,7 @@ const AccountList: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
|
||||
{/* 账户详情 Modal */}
|
||||
<Modal
|
||||
title={detailAccount ? (detailAccount.accountName || `${t('accountList.accountName')} ${detailAccount.id}`) : t('accountList.accountDetail')}
|
||||
@@ -555,19 +555,19 @@ const AccountList: React.FC = () => {
|
||||
setDetailBalance(null)
|
||||
}}
|
||||
footer={[
|
||||
<Button
|
||||
key="refresh"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleRefreshDetailBalance}
|
||||
<Button
|
||||
key="refresh"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleRefreshDetailBalance}
|
||||
loading={detailBalanceLoading}
|
||||
disabled={!detailAccount}
|
||||
>
|
||||
{t('accountList.refreshBalance')}
|
||||
</Button>,
|
||||
<Button
|
||||
key="edit"
|
||||
<Button
|
||||
key="edit"
|
||||
type="primary"
|
||||
icon={<EditOutlined />}
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => {
|
||||
if (detailAccount) {
|
||||
setDetailModalVisible(false)
|
||||
@@ -578,8 +578,8 @@ const AccountList: React.FC = () => {
|
||||
>
|
||||
{t('accountList.edit')}
|
||||
</Button>,
|
||||
<Button
|
||||
key="close"
|
||||
<Button
|
||||
key="close"
|
||||
onClick={() => {
|
||||
setDetailModalVisible(false)
|
||||
setDetailAccount(null)
|
||||
@@ -610,8 +610,8 @@ const AccountList: React.FC = () => {
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t('accountList.walletAddress')} span={isMobile ? 1 : 2}>
|
||||
<Space>
|
||||
<span style={{
|
||||
fontFamily: 'monospace',
|
||||
<span style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: isMobile ? '11px' : '13px',
|
||||
wordBreak: 'break-all',
|
||||
lineHeight: '1.4',
|
||||
@@ -633,8 +633,8 @@ const AccountList: React.FC = () => {
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t('accountList.proxyAddress')} span={isMobile ? 1 : 2}>
|
||||
<Space>
|
||||
<span style={{
|
||||
fontFamily: 'monospace',
|
||||
<span style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: isMobile ? '11px' : '13px',
|
||||
wordBreak: 'break-all',
|
||||
lineHeight: '1.4',
|
||||
@@ -688,9 +688,9 @@ const AccountList: React.FC = () => {
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
|
||||
<Divider />
|
||||
|
||||
|
||||
<Descriptions
|
||||
column={isMobile ? 1 : 2}
|
||||
bordered
|
||||
@@ -720,51 +720,51 @@ const AccountList: React.FC = () => {
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
{(detailAccount.totalOrders !== undefined || detailAccount.totalPnl !== undefined ||
|
||||
detailAccount.activeOrders !== undefined ||
|
||||
|
||||
{(detailAccount.totalOrders !== undefined || detailAccount.totalPnl !== undefined ||
|
||||
detailAccount.activeOrders !== undefined ||
|
||||
detailAccount.completedOrders !== undefined || detailAccount.positionCount !== undefined) && (
|
||||
<>
|
||||
<Divider />
|
||||
<Descriptions
|
||||
column={isMobile ? 1 : 2}
|
||||
bordered
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
title={t('accountList.statistics')}
|
||||
>
|
||||
{detailAccount.totalOrders !== undefined && (
|
||||
<Descriptions.Item label={t('accountList.totalOrders')}>
|
||||
{detailAccount.totalOrders}
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{detailAccount.activeOrders !== undefined && (
|
||||
<Descriptions.Item label={t('accountList.activeOrdersCount')}>
|
||||
<Tag color={detailAccount.activeOrders > 0 ? 'orange' : 'default'}>{detailAccount.activeOrders}</Tag>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{detailAccount.completedOrders !== undefined && (
|
||||
<Descriptions.Item label={t('accountList.completedOrders')}>
|
||||
<Tag color="success">{detailAccount.completedOrders}</Tag>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{detailAccount.positionCount !== undefined && (
|
||||
<Descriptions.Item label={t('accountList.positionCount')}>
|
||||
<Tag color={detailAccount.positionCount > 0 ? 'blue' : 'default'}>{detailAccount.positionCount}</Tag>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{detailAccount.totalPnl !== undefined && (
|
||||
<Descriptions.Item label={t('accountList.totalPnl')}>
|
||||
<span style={{
|
||||
fontWeight: 'bold',
|
||||
color: detailAccount.totalPnl && detailAccount.totalPnl.startsWith('-') ? '#ff4d4f' : '#52c41a'
|
||||
}}>
|
||||
{formatUSDC(detailAccount.totalPnl)} USDC
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<Divider />
|
||||
<Descriptions
|
||||
column={isMobile ? 1 : 2}
|
||||
bordered
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
title={t('accountList.statistics')}
|
||||
>
|
||||
{detailAccount.totalOrders !== undefined && (
|
||||
<Descriptions.Item label={t('accountList.totalOrders')}>
|
||||
{detailAccount.totalOrders}
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{detailAccount.activeOrders !== undefined && (
|
||||
<Descriptions.Item label={t('accountList.activeOrdersCount')}>
|
||||
<Tag color={detailAccount.activeOrders > 0 ? 'orange' : 'default'}>{detailAccount.activeOrders}</Tag>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{detailAccount.completedOrders !== undefined && (
|
||||
<Descriptions.Item label={t('accountList.completedOrders')}>
|
||||
<Tag color="success">{detailAccount.completedOrders}</Tag>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{detailAccount.positionCount !== undefined && (
|
||||
<Descriptions.Item label={t('accountList.positionCount')}>
|
||||
<Tag color={detailAccount.positionCount > 0 ? 'blue' : 'default'}>{detailAccount.positionCount}</Tag>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{detailAccount.totalPnl !== undefined && (
|
||||
<Descriptions.Item label={t('accountList.totalPnl')}>
|
||||
<span style={{
|
||||
fontWeight: 'bold',
|
||||
color: detailAccount.totalPnl && detailAccount.totalPnl.startsWith('-') ? '#ff4d4f' : '#52c41a'
|
||||
}}>
|
||||
{formatUSDC(detailAccount.totalPnl)} USDC
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
@@ -773,7 +773,7 @@ const AccountList: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
|
||||
{/* 编辑账户 Modal */}
|
||||
<Modal
|
||||
title={editAccount ? `${t('accountList.editAccount')} - ${editAccount.accountName || `${t('accountList.accountName')} ${editAccount.id}`}` : t('accountList.editAccount')}
|
||||
@@ -804,17 +804,17 @@ const AccountList: React.FC = () => {
|
||||
showIcon
|
||||
style={{ marginBottom: '24px' }}
|
||||
/>
|
||||
|
||||
|
||||
<Form.Item
|
||||
label={t('accountList.accountName') || '账户名称'}
|
||||
name="accountName"
|
||||
>
|
||||
<Input placeholder={t('accountList.accountNamePlaceholder') || '请输入账户名称(可选)'} />
|
||||
</Form.Item>
|
||||
|
||||
|
||||
<Form.Item>
|
||||
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
<Button
|
||||
onClick={() => {
|
||||
setEditModalVisible(false)
|
||||
setEditAccount(null)
|
||||
@@ -844,7 +844,7 @@ const AccountList: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
|
||||
{/* 导入账户 Modal */}
|
||||
<Modal
|
||||
title={t('accountImport.title')}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { Card, Row, Col, Statistic, Tag, Button, message, Spin } from 'antd'
|
||||
import { ArrowUpOutlined, ArrowDownOutlined, LeftOutlined } from '@ant-design/icons'
|
||||
import { apiService } from '../services/api'
|
||||
import { formatUSDC } from '../utils'
|
||||
import { formatUSDC, formatNumber } from '../utils'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import type { CopyTradingStatistics } from '../types'
|
||||
|
||||
@@ -13,16 +13,16 @@ const CopyTradingStatisticsPage: React.FC = () => {
|
||||
useMediaQuery({ maxWidth: 768 }) // 用于响应式布局,但当前页面未使用
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [statistics, setStatistics] = useState<CopyTradingStatistics | null>(null)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (copyTradingId) {
|
||||
fetchStatistics()
|
||||
}
|
||||
}, [copyTradingId])
|
||||
|
||||
|
||||
const fetchStatistics = async () => {
|
||||
if (!copyTradingId) return
|
||||
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await apiService.statistics.detail({ copyTradingId: parseInt(copyTradingId) })
|
||||
@@ -37,25 +37,25 @@ const CopyTradingStatisticsPage: React.FC = () => {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const getPnlColor = (value: string): string => {
|
||||
const num = parseFloat(value)
|
||||
if (isNaN(num)) return '#666'
|
||||
return num >= 0 ? '#3f8600' : '#cf1322'
|
||||
}
|
||||
|
||||
|
||||
const getPnlIcon = (value: string) => {
|
||||
const num = parseFloat(value)
|
||||
if (isNaN(num)) return null
|
||||
return num >= 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />
|
||||
}
|
||||
|
||||
|
||||
const formatPercent = (value: string): string => {
|
||||
const num = parseFloat(value)
|
||||
if (isNaN(num)) return '-'
|
||||
return `${num >= 0 ? '+' : ''}${num.toFixed(2)}%`
|
||||
}
|
||||
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '50px' }}>
|
||||
@@ -63,7 +63,7 @@ const CopyTradingStatisticsPage: React.FC = () => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
if (!statistics) {
|
||||
return (
|
||||
<Card>
|
||||
@@ -74,7 +74,7 @@ const CopyTradingStatisticsPage: React.FC = () => {
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
@@ -98,7 +98,7 @@ const CopyTradingStatisticsPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
||||
{/* 基本信息卡片 */}
|
||||
<Card title="基本信息" style={{ marginBottom: 16 }}>
|
||||
<Row gutter={[16, 16]}>
|
||||
@@ -130,14 +130,14 @@ const CopyTradingStatisticsPage: React.FC = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
|
||||
{/* 买入统计卡片 */}
|
||||
<Card title="买入统计" style={{ marginBottom: 16 }}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Statistic
|
||||
title="总买入数量"
|
||||
value={formatUSDC(statistics.totalBuyQuantity)}
|
||||
value={formatNumber(statistics.totalBuyQuantity, 4)}
|
||||
suffix=""
|
||||
/>
|
||||
</Col>
|
||||
@@ -151,27 +151,27 @@ const CopyTradingStatisticsPage: React.FC = () => {
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Statistic
|
||||
title="总买入订单数"
|
||||
value={statistics.totalBuyOrders}
|
||||
value={formatNumber(statistics.totalBuyOrders)}
|
||||
suffix="笔"
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Statistic
|
||||
title="平均买入价格"
|
||||
value={formatUSDC(statistics.avgBuyPrice)}
|
||||
value={formatNumber(statistics.avgBuyPrice, 4)}
|
||||
suffix=""
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
|
||||
{/* 卖出统计卡片 */}
|
||||
<Card title="卖出统计" style={{ marginBottom: 16 }}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Statistic
|
||||
title="总卖出数量"
|
||||
value={formatUSDC(statistics.totalSellQuantity)}
|
||||
value={formatNumber(statistics.totalSellQuantity, 4)}
|
||||
suffix=""
|
||||
/>
|
||||
</Col>
|
||||
@@ -185,33 +185,33 @@ const CopyTradingStatisticsPage: React.FC = () => {
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Statistic
|
||||
title="总卖出订单数"
|
||||
value={statistics.totalSellOrders}
|
||||
value={formatNumber(statistics.totalSellOrders)}
|
||||
suffix="笔"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
|
||||
{/* 持仓统计卡片 */}
|
||||
<Card title="持仓统计" style={{ marginBottom: 16 }}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} md={12}>
|
||||
<Statistic
|
||||
title="当前持仓数量"
|
||||
value={formatUSDC(statistics.currentPositionQuantity)}
|
||||
value={formatNumber(statistics.currentPositionQuantity, 4)}
|
||||
suffix=""
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={12}>
|
||||
<Statistic
|
||||
title="平均买入价格"
|
||||
value={formatUSDC(statistics.avgBuyPrice)}
|
||||
value={formatNumber(statistics.avgBuyPrice, 4)}
|
||||
suffix=""
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
|
||||
{/* 盈亏统计卡片 */}
|
||||
<Card title="盈亏统计">
|
||||
<Row gutter={[16, 16]}>
|
||||
|
||||
+331
-331
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import { apiService } from '../services/api'
|
||||
import type { Statistics as StatisticsType } from '../types'
|
||||
import { formatUSDC } from '../utils'
|
||||
import { formatUSDC, formatNumber } from '../utils'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
|
||||
const { RangePicker } = DatePicker
|
||||
@@ -17,17 +17,17 @@ const Statistics: React.FC = () => {
|
||||
const [stats, setStats] = useState<StatisticsType | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [dateRange, setDateRange] = useState<[Dayjs | null, Dayjs | null]>([null, null])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetchStatistics()
|
||||
}, [])
|
||||
|
||||
|
||||
const fetchStatistics = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const startTime = dateRange[0] ? dateRange[0].valueOf() : undefined
|
||||
const endTime = dateRange[1] ? dateRange[1].valueOf() : undefined
|
||||
|
||||
|
||||
const response = await apiService.statistics.global({ startTime, endTime })
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setStats(response.data.data)
|
||||
@@ -40,11 +40,11 @@ const Statistics: React.FC = () => {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const handleDateRangeChange = (dates: [Dayjs | null, Dayjs | null] | null) => {
|
||||
setDateRange(dates || [null, null])
|
||||
}
|
||||
|
||||
|
||||
const handleReset = () => {
|
||||
setDateRange([null, null])
|
||||
// 重置后自动刷新
|
||||
@@ -52,7 +52,7 @@ const Statistics: React.FC = () => {
|
||||
fetchStatistics()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: '16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '12px' }}>
|
||||
@@ -85,13 +85,13 @@ const Statistics: React.FC = () => {
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title={t('statistics.totalOrders') || '总订单数'}
|
||||
value={stats?.totalOrders || 0}
|
||||
value={formatNumber(stats?.totalOrders || 0)}
|
||||
loading={loading}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
+35
-26
@@ -1,61 +1,70 @@
|
||||
/**
|
||||
* 格式化数字,自动去除尾随零
|
||||
* 格式化数字,添加千分位分隔符,自动去除尾随零
|
||||
* @param value - 数字值(字符串或数字)
|
||||
* @param maxDecimals - 最大小数位数(默认不限制)
|
||||
* @returns 格式化后的字符串,如果值为空或无效则返回 ''
|
||||
* @returns 格式化后的字符串,如 "123,456.78",如果值为空或无效则返回 ''
|
||||
* @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.50) => "100.5"
|
||||
* formatNumber(100.55) => "100.55"
|
||||
*/
|
||||
export const formatNumber = (value: string | number | undefined | null, maxDecimals?: number): string => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return ''
|
||||
}
|
||||
|
||||
|
||||
const num = typeof value === 'string' ? parseFloat(value) : value
|
||||
if (isNaN(num)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// 如果有最大小数位数限制,先截断
|
||||
|
||||
// 处理小数位数
|
||||
let numStr: string
|
||||
if (maxDecimals !== undefined) {
|
||||
const multiplier = Math.pow(10, maxDecimals)
|
||||
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 位小数,自动去除尾随零(截断,不四舍五入)
|
||||
* @param value - 金额值(字符串或数字)
|
||||
* @returns 格式化后的字符串,如果值为空或无效则返回 '-'
|
||||
* @returns 格式化后的字符串,如 "1,234.56",如果值为空或无效则返回 '-'
|
||||
* @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.23456) => "1.2345"
|
||||
* formatUSDC(1.2) => "1.2"
|
||||
* formatUSDC(1) => "1"
|
||||
*/
|
||||
export const formatUSDC = (value: string | number | undefined | null): string => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return '-'
|
||||
}
|
||||
|
||||
|
||||
const num = typeof value === 'string' ? parseFloat(value) : value
|
||||
if (isNaN(num)) {
|
||||
return '-'
|
||||
}
|
||||
|
||||
// 使用 Math.floor 截断到4位小数(不四舍五入)
|
||||
const multiplier = Math.pow(10, 4)
|
||||
const truncated = Math.floor(num * multiplier) / multiplier
|
||||
|
||||
// 使用 toFixed(4) 确保格式一致,然后去除尾随零和小数点
|
||||
return truncated.toFixed(4).replace(/\.?0+$/, '')
|
||||
|
||||
return formatNumber(num, 4)
|
||||
}
|
||||
|
||||
// 统一导出 ethers 相关工具函数
|
||||
@@ -97,7 +106,7 @@ export const isAutoGeneratedOrderId = (orderId: string | undefined | null): bool
|
||||
/**
|
||||
* 构建 Polymarket 市场 URL
|
||||
* 对于 moneyline 市场,跳转到 moneyline 页面
|
||||
* 注意:目前无法自动判断市场是否为 moneyline,需要后端提供标识
|
||||
* 注意:目前无法自动判断市场是否为 moneyline,需要后端提供标识
|
||||
* @param marketSlug - 市场 slug(用于显示)
|
||||
* @param eventSlug - 跳转用的 slug(从 events[0].slug 获取,优先使用)
|
||||
* @param marketCategory - 市场分类(sports, crypto 等)
|
||||
@@ -114,7 +123,7 @@ export const getPolymarketUrl = (
|
||||
): string | null => {
|
||||
// 优先使用 eventSlug(跳转用的 slug)
|
||||
const slug = eventSlug || marketSlug
|
||||
|
||||
|
||||
if (slug) {
|
||||
// 如果是 moneyline 市场,跳转到 moneyline 页面
|
||||
if (isMoneyline === true) {
|
||||
@@ -123,12 +132,12 @@ export const getPolymarketUrl = (
|
||||
// 其他市场跳转到普通市场页面
|
||||
return `https://polymarket.com/event/${slug}`
|
||||
}
|
||||
|
||||
|
||||
// 如果没有 slug,使用 marketId(作为后备)
|
||||
if (marketId && marketId.startsWith('0x')) {
|
||||
return `https://polymarket.com/condition/${marketId}`
|
||||
}
|
||||
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user