删除后端无用日志
- 删除所有 logger.debug 调试日志 - 删除过于详细的 logger.info 常规操作日志 - 保留 logger.error 和 logger.warn 重要错误和警告日志 - 优化日志输出,减少生产环境日志噪音
This commit is contained in:
@@ -18,6 +18,10 @@ import TemplateAdd from './pages/TemplateAdd'
|
||||
import TemplateEdit from './pages/TemplateEdit'
|
||||
import CopyTradingList from './pages/CopyTradingList'
|
||||
import CopyTradingAdd from './pages/CopyTradingAdd'
|
||||
import CopyTradingStatistics from './pages/CopyTradingStatistics'
|
||||
import CopyTradingBuyOrders from './pages/CopyTradingBuyOrders'
|
||||
import CopyTradingSellOrders from './pages/CopyTradingSellOrders'
|
||||
import CopyTradingMatchedOrders from './pages/CopyTradingMatchedOrders'
|
||||
import { wsManager } from './services/websocket'
|
||||
import type { OrderPushMessage } from './types'
|
||||
|
||||
@@ -136,6 +140,10 @@ function App() {
|
||||
<Route path="/templates/edit/:id" element={<TemplateEdit />} />
|
||||
<Route path="/copy-trading" element={<CopyTradingList />} />
|
||||
<Route path="/copy-trading/add" element={<CopyTradingAdd />} />
|
||||
<Route path="/copy-trading/statistics/:copyTradingId" element={<CopyTradingStatistics />} />
|
||||
<Route path="/copy-trading/orders/buy/:copyTradingId" element={<CopyTradingBuyOrders />} />
|
||||
<Route path="/copy-trading/orders/sell/:copyTradingId" element={<CopyTradingSellOrders />} />
|
||||
<Route path="/copy-trading/orders/matched/:copyTradingId" element={<CopyTradingMatchedOrders />} />
|
||||
<Route path="/config" element={<ConfigPage />} />
|
||||
<Route path="/positions" element={<PositionList />} />
|
||||
<Route path="/statistics" element={<Statistics />} />
|
||||
|
||||
@@ -35,7 +35,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
// 获取当前应该打开的父菜单
|
||||
const getInitialOpenKeys = (): string[] => {
|
||||
const path = location.pathname
|
||||
if (path.startsWith('/templates') || path.startsWith('/copy-trading')) {
|
||||
if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading')) {
|
||||
return ['/copy-trading-management']
|
||||
}
|
||||
return []
|
||||
@@ -46,7 +46,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
// 当路径变化时,自动打开对应的父菜单
|
||||
useEffect(() => {
|
||||
const path = location.pathname
|
||||
if (path.startsWith('/templates') || path.startsWith('/copy-trading')) {
|
||||
if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading')) {
|
||||
setOpenKeys(['/copy-trading-management'])
|
||||
}
|
||||
}, [location.pathname])
|
||||
@@ -57,16 +57,16 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
icon: <WalletOutlined />,
|
||||
label: '账户管理'
|
||||
},
|
||||
{
|
||||
key: '/leaders',
|
||||
icon: <UserOutlined />,
|
||||
label: 'Leader 管理'
|
||||
},
|
||||
{
|
||||
key: '/copy-trading-management',
|
||||
icon: <AppstoreOutlined />,
|
||||
label: '跟单管理',
|
||||
label: '跟单交易',
|
||||
children: [
|
||||
{
|
||||
key: '/leaders',
|
||||
icon: <UserOutlined />,
|
||||
label: 'Leader 管理'
|
||||
},
|
||||
{
|
||||
key: '/templates',
|
||||
icon: <FileTextOutlined />,
|
||||
@@ -118,7 +118,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<div style={{ color: '#fff', fontSize: '18px', fontWeight: 'bold' }}>
|
||||
Polymarket 跟单
|
||||
PolyHermes
|
||||
</div>
|
||||
<Button
|
||||
type="text"
|
||||
@@ -179,7 +179,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
fontWeight: 'bold',
|
||||
flexShrink: 0
|
||||
}}>
|
||||
Polymarket 跟单
|
||||
PolyHermes
|
||||
</div>
|
||||
<Menu
|
||||
mode="inline"
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { Card, Table, Button, Tag, Select, Input, message, Space, Divider, Spin } from 'antd'
|
||||
import { LeftOutlined } from '@ant-design/icons'
|
||||
import { apiService } from '../services/api'
|
||||
import { formatUSDC } from '../utils'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import type { BuyOrderInfo, OrderTrackingRequest, OrderTrackingListResponse } from '../types'
|
||||
|
||||
const { Option } = Select
|
||||
|
||||
const CopyTradingBuyOrdersPage: React.FC = () => {
|
||||
const { copyTradingId } = useParams<{ copyTradingId: string }>()
|
||||
const navigate = useNavigate()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [orders, setOrders] = useState<BuyOrderInfo[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [limit, setLimit] = useState(20)
|
||||
const [filters, setFilters] = useState<{
|
||||
marketId?: string
|
||||
side?: string
|
||||
status?: string
|
||||
}>({})
|
||||
|
||||
useEffect(() => {
|
||||
if (copyTradingId) {
|
||||
fetchOrders()
|
||||
}
|
||||
}, [copyTradingId, page, limit, filters])
|
||||
|
||||
const fetchOrders = async () => {
|
||||
if (!copyTradingId) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const request: OrderTrackingRequest = {
|
||||
copyTradingId: parseInt(copyTradingId),
|
||||
type: 'buy',
|
||||
page,
|
||||
limit,
|
||||
...filters
|
||||
}
|
||||
|
||||
const response = await apiService.orderTracking.list(request)
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
const data = response.data.data as OrderTrackingListResponse
|
||||
setOrders((data.list || []) as BuyOrderInfo[])
|
||||
setTotal(data.total || 0)
|
||||
} else {
|
||||
message.error(response.data.msg || '获取买入订单列表失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取买入订单列表失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusTag = (status: string) => {
|
||||
const statusMap: Record<string, { color: string; text: string }> = {
|
||||
filled: { color: 'processing', text: '已完成' },
|
||||
partially_matched: { color: 'warning', text: '部分匹配' },
|
||||
fully_matched: { color: 'success', text: '完全匹配' }
|
||||
}
|
||||
const config = statusMap[status] || { color: 'default', text: status }
|
||||
return <Tag color={config.color}>{config.text}</Tag>
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '订单ID',
|
||||
dataIndex: 'orderId',
|
||||
key: 'orderId',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (text: string) => (
|
||||
<span style={{ fontFamily: 'monospace', fontSize: isMobile ? 11 : 12 }}>
|
||||
{isMobile
|
||||
? `${text.slice(0, 6)}...${text.slice(-4)}`
|
||||
: `${text.slice(0, 8)}...${text.slice(-6)}`
|
||||
}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Leader 交易ID',
|
||||
dataIndex: 'leaderTradeId',
|
||||
key: 'leaderTradeId',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (text: string) => (
|
||||
<span style={{ fontFamily: 'monospace', fontSize: isMobile ? 11 : 12 }}>
|
||||
{isMobile
|
||||
? `${text.slice(0, 6)}...${text.slice(-4)}`
|
||||
: `${text.slice(0, 8)}...${text.slice(-6)}`
|
||||
}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '市场',
|
||||
dataIndex: 'marketId',
|
||||
key: 'marketId',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (text: string) => (
|
||||
<span style={{ fontFamily: 'monospace', fontSize: isMobile ? 11 : 12 }}>
|
||||
{isMobile
|
||||
? `${text.slice(0, 6)}...${text.slice(-4)}`
|
||||
: `${text.slice(0, 8)}...${text.slice(-6)}`
|
||||
}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '方向',
|
||||
dataIndex: 'side',
|
||||
key: 'side',
|
||||
width: isMobile ? 60 : 80,
|
||||
render: (side: string) => {
|
||||
// 将0/1转换为YES/NO
|
||||
const displaySide = side === '0' ? 'YES' : side === '1' ? 'NO' : side
|
||||
return <Tag style={{ fontSize: isMobile ? 11 : 12 }}>{displaySide}</Tag>
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '买入数量',
|
||||
dataIndex: 'quantity',
|
||||
key: 'quantity',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (value: string) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{formatUSDC(value)}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '买入价格',
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (value: string) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{formatUSDC(value)}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '买入金额',
|
||||
key: 'amount',
|
||||
width: isMobile ? 100 : 120,
|
||||
render: (_: any, record: BuyOrderInfo) => {
|
||||
const amount = (parseFloat(record.quantity) * parseFloat(record.price)).toString()
|
||||
return (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>
|
||||
{isMobile ? formatUSDC(amount) : `${formatUSDC(amount)} USDC`}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '已匹配',
|
||||
dataIndex: 'matchedQuantity',
|
||||
key: 'matchedQuantity',
|
||||
width: isMobile ? 70 : 90,
|
||||
render: (value: string) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{formatUSDC(value)}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '剩余',
|
||||
dataIndex: 'remainingQuantity',
|
||||
key: 'remainingQuantity',
|
||||
width: isMobile ? 70 : 90,
|
||||
render: (value: string) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{formatUSDC(value)}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (status: string) => getStatusTag(status)
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: isMobile ? 120 : 160,
|
||||
render: (timestamp: number) => (
|
||||
<span style={{ fontSize: isMobile ? 11 : 12 }}>
|
||||
{isMobile
|
||||
? new Date(timestamp).toLocaleDateString('zh-CN')
|
||||
: new Date(timestamp).toLocaleString('zh-CN')
|
||||
}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||
<Button icon={<LeftOutlined />} onClick={() => navigate(`/copy-trading/statistics/${copyTradingId}`)}>
|
||||
返回统计
|
||||
</Button>
|
||||
<h2 style={{ margin: 0 }}>买入订单列表</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16, display: 'flex', gap: 16, flexWrap: 'wrap' }}>
|
||||
<Input
|
||||
placeholder="筛选市场ID"
|
||||
allowClear
|
||||
style={{ width: isMobile ? '100%' : 200 }}
|
||||
value={filters.marketId}
|
||||
onChange={(e) => setFilters({ ...filters, marketId: e.target.value || undefined })}
|
||||
/>
|
||||
|
||||
<Select
|
||||
placeholder="筛选方向"
|
||||
allowClear
|
||||
style={{ width: isMobile ? '100%' : 150 }}
|
||||
value={filters.side}
|
||||
onChange={(value) => setFilters({ ...filters, side: value || undefined })}
|
||||
>
|
||||
<Option value="0">YES</Option>
|
||||
<Option value="1">NO</Option>
|
||||
<Option value="YES">YES</Option>
|
||||
<Option value="NO">NO</Option>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
placeholder="筛选状态"
|
||||
allowClear
|
||||
style={{ width: isMobile ? '100%' : 150 }}
|
||||
value={filters.status}
|
||||
onChange={(value) => setFilters({ ...filters, status: value || undefined })}
|
||||
>
|
||||
<Option value="filled">已完成</Option>
|
||||
<Option value="partially_matched">部分匹配</Option>
|
||||
<Option value="fully_matched">完全匹配</Option>
|
||||
</Select>
|
||||
|
||||
<Button onClick={fetchOrders}>查询</Button>
|
||||
</div>
|
||||
|
||||
{isMobile ? (
|
||||
// 移动端卡片布局
|
||||
<div>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : orders.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
|
||||
暂无买入订单
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{orders.map((order) => {
|
||||
const date = new Date(order.createdAt)
|
||||
const formattedDate = date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
const amount = (parseFloat(order.quantity) * parseFloat(order.price)).toString()
|
||||
const displaySide = order.side === '0' ? 'YES' : order.side === '1' ? 'NO' : order.side
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={order.orderId}
|
||||
style={{
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
|
||||
border: '1px solid #e8e8e8'
|
||||
}}
|
||||
bodyStyle={{ padding: '16px' }}
|
||||
>
|
||||
{/* 订单ID和状态 */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '8px',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
{order.orderId.slice(0, 8)}...{order.orderId.slice(-6)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
|
||||
<Tag>{displaySide}</Tag>
|
||||
{getStatusTag(order.status)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
|
||||
{/* 买入信息 */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>买入信息</div>
|
||||
<div style={{ fontSize: '14px', fontWeight: '500' }}>
|
||||
数量: {formatUSDC(order.quantity)} | 价格: {formatUSDC(order.price)}
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', fontWeight: '500', marginTop: '4px' }}>
|
||||
金额: {formatUSDC(amount)} USDC
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 匹配信息 */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>匹配信息</div>
|
||||
<div style={{ fontSize: '13px', color: '#333' }}>
|
||||
已匹配: {formatUSDC(order.matchedQuantity)} | 剩余: {formatUSDC(order.remainingQuantity)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Leader 交易ID */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>Leader 交易ID</div>
|
||||
<div style={{ fontSize: '12px', color: '#999', fontFamily: 'monospace' }}>
|
||||
{order.leaderTradeId.slice(0, 8)}...{order.leaderTradeId.slice(-6)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 市场ID */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>市场ID</div>
|
||||
<div style={{ fontSize: '12px', color: '#999', fontFamily: 'monospace' }}>
|
||||
{order.marketId.slice(0, 8)}...{order.marketId.slice(-6)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 创建时间 */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#999' }}>
|
||||
创建时间: {formattedDate}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// 桌面端表格布局
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={orders}
|
||||
rowKey="orderId"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: limit,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (newPage, newLimit) => {
|
||||
setPage(newPage)
|
||||
setLimit(newLimit)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyTradingBuyOrdersPage
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Card, Table, Button, Space, Tag, Popconfirm, Switch, message, Select, Input } from 'antd'
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'
|
||||
import { Card, Table, Button, Space, Tag, Popconfirm, Switch, message, Select, Input, Dropdown, Divider, Spin } from 'antd'
|
||||
import { PlusOutlined, DeleteOutlined, BarChartOutlined, UnorderedListOutlined, ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { apiService } from '../services/api'
|
||||
import { useAccountStore } from '../store/accountStore'
|
||||
import type { CopyTrading, Account, Leader, CopyTradingTemplate } from '../types'
|
||||
import type { CopyTrading, Account, Leader, CopyTradingTemplate, CopyTradingStatistics } from '../types'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { formatUSDC } from '../utils'
|
||||
|
||||
const { Option } = Select
|
||||
|
||||
@@ -17,6 +19,8 @@ const CopyTradingList: React.FC = () => {
|
||||
const [leaders, setLeaders] = useState<Leader[]>([])
|
||||
const [templates, setTemplates] = useState<CopyTradingTemplate[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [statisticsMap, setStatisticsMap] = useState<Record<number, CopyTradingStatistics>>({})
|
||||
const [loadingStatistics, setLoadingStatistics] = useState<Set<number>>(new Set())
|
||||
const [filters, setFilters] = useState<{
|
||||
accountId?: number
|
||||
templateId?: number
|
||||
@@ -62,7 +66,12 @@ const CopyTradingList: React.FC = () => {
|
||||
try {
|
||||
const response = await apiService.copyTrading.list(filters)
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setCopyTradings(response.data.data.list || [])
|
||||
const list = response.data.data.list || []
|
||||
setCopyTradings(list)
|
||||
// 为每个跟单关系获取统计信息
|
||||
list.forEach((ct: CopyTrading) => {
|
||||
fetchStatistics(ct.id)
|
||||
})
|
||||
} else {
|
||||
message.error(response.data.msg || '获取跟单列表失败')
|
||||
}
|
||||
@@ -73,6 +82,50 @@ const CopyTradingList: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchStatistics = async (copyTradingId: number) => {
|
||||
// 如果正在加载或已有数据,跳过
|
||||
if (loadingStatistics.has(copyTradingId) || statisticsMap[copyTradingId]) {
|
||||
return
|
||||
}
|
||||
|
||||
setLoadingStatistics(prev => new Set(prev).add(copyTradingId))
|
||||
try {
|
||||
const response = await apiService.statistics.detail({ copyTradingId })
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setStatisticsMap(prev => ({
|
||||
...prev,
|
||||
[copyTradingId]: response.data.data
|
||||
}))
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`获取跟单统计失败: copyTradingId=${copyTradingId}`, error)
|
||||
} finally {
|
||||
setLoadingStatistics(prev => {
|
||||
const next = new Set(prev)
|
||||
next.delete(copyTradingId)
|
||||
return next
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)}%`
|
||||
}
|
||||
|
||||
const handleToggleStatus = async (copyTrading: CopyTrading) => {
|
||||
try {
|
||||
const response = await apiService.copyTrading.updateStatus({
|
||||
@@ -108,11 +161,17 @@ const CopyTradingList: React.FC = () => {
|
||||
{
|
||||
title: '钱包',
|
||||
key: 'account',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (_: any, record: CopyTrading) => (
|
||||
<div>
|
||||
<div>{record.accountName || `账户 ${record.accountId}`}</div>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>
|
||||
{record.walletAddress.slice(0, 6)}...{record.walletAddress.slice(-4)}
|
||||
<div style={{ fontSize: isMobile ? 13 : 14, fontWeight: 500 }}>
|
||||
{record.accountName || `账户 ${record.accountId}`}
|
||||
</div>
|
||||
<div style={{ fontSize: isMobile ? 11 : 12, color: '#999', marginTop: 2 }}>
|
||||
{isMobile
|
||||
? `${record.walletAddress.slice(0, 4)}...${record.walletAddress.slice(-3)}`
|
||||
: `${record.walletAddress.slice(0, 6)}...${record.walletAddress.slice(-4)}`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -121,16 +180,25 @@ const CopyTradingList: React.FC = () => {
|
||||
title: '模板',
|
||||
dataIndex: 'templateName',
|
||||
key: 'templateName',
|
||||
render: (text: string) => <strong>{text}</strong>
|
||||
width: isMobile ? 100 : 120,
|
||||
render: (text: string) => (
|
||||
<strong style={{ fontSize: isMobile ? 13 : 14 }}>{text}</strong>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Leader',
|
||||
key: 'leader',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (_: any, record: CopyTrading) => (
|
||||
<div>
|
||||
<div>{record.leaderName || `Leader ${record.leaderId}`}</div>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>
|
||||
{record.leaderAddress.slice(0, 6)}...{record.leaderAddress.slice(-4)}
|
||||
<div style={{ fontSize: isMobile ? 13 : 14, fontWeight: 500 }}>
|
||||
{record.leaderName || `Leader ${record.leaderId}`}
|
||||
</div>
|
||||
<div style={{ fontSize: isMobile ? 11 : 12, color: '#999', marginTop: 2 }}>
|
||||
{isMobile
|
||||
? `${record.leaderAddress.slice(0, 4)}...${record.leaderAddress.slice(-3)}`
|
||||
: `${record.leaderAddress.slice(0, 6)}...${record.leaderAddress.slice(-4)}`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -139,6 +207,7 @@ const CopyTradingList: React.FC = () => {
|
||||
title: '状态',
|
||||
dataIndex: 'enabled',
|
||||
key: 'enabled',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (enabled: boolean, record: CopyTrading) => (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
@@ -148,27 +217,137 @@ const CopyTradingList: React.FC = () => {
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '总盈亏',
|
||||
key: 'totalPnl',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (_: any, record: CopyTrading) => {
|
||||
const stats = statisticsMap[record.id]
|
||||
if (!stats) {
|
||||
return loadingStatistics.has(record.id) ? (
|
||||
<span style={{ fontSize: isMobile ? 11 : 12 }}>加载中...</span>
|
||||
) : (
|
||||
<span style={{ fontSize: isMobile ? 11 : 12 }}>-</span>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div style={{
|
||||
color: getPnlColor(stats.totalPnl),
|
||||
fontWeight: 500,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
fontSize: isMobile ? 12 : 14
|
||||
}}>
|
||||
{getPnlIcon(stats.totalPnl)}
|
||||
{isMobile ? formatUSDC(stats.totalPnl) : `${formatUSDC(stats.totalPnl)} USDC`}
|
||||
</div>
|
||||
{!isMobile && (
|
||||
<div style={{
|
||||
fontSize: 12,
|
||||
color: getPnlColor(stats.totalPnlPercent),
|
||||
marginTop: 4
|
||||
}}>
|
||||
{formatPercent(stats.totalPnlPercent)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (_: any, record: CopyTrading) => (
|
||||
<Popconfirm
|
||||
title="确定要删除这个跟单关系吗?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)
|
||||
width: isMobile ? 100 : 200,
|
||||
fixed: 'right' as const,
|
||||
render: (_: any, record: CopyTrading) => {
|
||||
const menuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: 'statistics',
|
||||
label: '查看统计',
|
||||
icon: <BarChartOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/statistics/${record.id}`)
|
||||
},
|
||||
{
|
||||
key: 'buyOrders',
|
||||
label: '买入订单',
|
||||
icon: <UnorderedListOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/orders/buy/${record.id}`)
|
||||
},
|
||||
{
|
||||
key: 'sellOrders',
|
||||
label: '卖出订单',
|
||||
icon: <UnorderedListOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/orders/sell/${record.id}`)
|
||||
},
|
||||
{
|
||||
key: 'matchedOrders',
|
||||
label: '匹配关系',
|
||||
icon: <UnorderedListOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/orders/matched/${record.id}`)
|
||||
},
|
||||
{
|
||||
type: 'divider'
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: (
|
||||
<Popconfirm
|
||||
title="确定要删除这个跟单关系吗?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
onCancel={(e) => e?.stopPropagation()}
|
||||
>
|
||||
<span style={{ color: '#ff4d4f' }}>删除</span>
|
||||
</Popconfirm>
|
||||
),
|
||||
danger: true
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<Space size={isMobile ? 'small' : 'middle'} wrap>
|
||||
{!isMobile && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<BarChartOutlined />}
|
||||
onClick={() => navigate(`/copy-trading/statistics/${record.id}`)}
|
||||
>
|
||||
统计
|
||||
</Button>
|
||||
)}
|
||||
<Dropdown menu={{ items: menuItems }} trigger={['click']}>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<UnorderedListOutlined />}
|
||||
>
|
||||
{isMobile ? '' : '订单'}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
{!isMobile && (
|
||||
<Popconfirm
|
||||
title="确定要删除这个跟单关系吗?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -241,18 +420,205 @@ const CopyTradingList: React.FC = () => {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={copyTradings}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: isMobile ? 10 : 20,
|
||||
showSizeChanger: !isMobile,
|
||||
showTotal: (total) => `共 ${total} 条`
|
||||
}}
|
||||
scroll={{ x: isMobile ? 800 : 'auto' }}
|
||||
/>
|
||||
{isMobile ? (
|
||||
// 移动端卡片布局
|
||||
<div>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : copyTradings.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
|
||||
暂无跟单配置
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{copyTradings.map((record) => {
|
||||
const stats = statisticsMap[record.id]
|
||||
const date = new Date(record.createdAt)
|
||||
const formattedDate = date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={record.id}
|
||||
style={{
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
|
||||
border: '1px solid #e8e8e8'
|
||||
}}
|
||||
bodyStyle={{ padding: '16px' }}
|
||||
>
|
||||
{/* 基本信息 */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '8px',
|
||||
color: '#1890ff'
|
||||
}}>
|
||||
{record.templateName}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
|
||||
<Tag color={record.enabled ? 'green' : 'red'}>
|
||||
{record.enabled ? '启用' : '禁用'}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
|
||||
{/* 账户信息 */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>账户</div>
|
||||
<div style={{ fontSize: '14px', fontWeight: '500' }}>
|
||||
{record.accountName || `账户 ${record.accountId}`}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#999', marginTop: '2px' }}>
|
||||
{record.walletAddress.slice(0, 6)}...{record.walletAddress.slice(-4)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Leader 信息 */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>Leader</div>
|
||||
<div style={{ fontSize: '14px', fontWeight: '500' }}>
|
||||
{record.leaderName || `Leader ${record.leaderId}`}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#999', marginTop: '2px' }}>
|
||||
{record.leaderAddress.slice(0, 6)}...{record.leaderAddress.slice(-4)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 总盈亏 */}
|
||||
{stats && (
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>总盈亏</div>
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: getPnlColor(stats.totalPnl),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}>
|
||||
{getPnlIcon(stats.totalPnl)}
|
||||
{formatUSDC(stats.totalPnl)} USDC
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: getPnlColor(stats.totalPnlPercent),
|
||||
marginTop: '4px'
|
||||
}}>
|
||||
{formatPercent(stats.totalPnlPercent)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadingStatistics.has(record.id) && (
|
||||
<div style={{ marginBottom: '12px', fontSize: '12px', color: '#999' }}>
|
||||
加载统计中...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 创建时间 */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#999' }}>
|
||||
创建时间: {formattedDate}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<BarChartOutlined />}
|
||||
onClick={() => navigate(`/copy-trading/statistics/${record.id}`)}
|
||||
style={{ flex: 1, minWidth: '80px' }}
|
||||
>
|
||||
统计
|
||||
</Button>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'statistics',
|
||||
label: '查看统计',
|
||||
icon: <BarChartOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/statistics/${record.id}`)
|
||||
},
|
||||
{
|
||||
key: 'buyOrders',
|
||||
label: '买入订单',
|
||||
icon: <UnorderedListOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/orders/buy/${record.id}`)
|
||||
},
|
||||
{
|
||||
key: 'sellOrders',
|
||||
label: '卖出订单',
|
||||
icon: <UnorderedListOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/orders/sell/${record.id}`)
|
||||
},
|
||||
{
|
||||
key: 'matchedOrders',
|
||||
label: '匹配关系',
|
||||
icon: <UnorderedListOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/orders/matched/${record.id}`)
|
||||
}
|
||||
]
|
||||
}}
|
||||
trigger={['click']}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<UnorderedListOutlined />}
|
||||
style={{ flex: 1, minWidth: '80px' }}
|
||||
>
|
||||
订单
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<Popconfirm
|
||||
title="确定要删除这个跟单关系吗?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
danger
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
style={{ flex: 1, minWidth: '80px' }}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// 桌面端表格布局
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={copyTradings}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: 20,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { Card, Table, Button, Input, message, Divider, Spin } from 'antd'
|
||||
import { LeftOutlined } from '@ant-design/icons'
|
||||
import { apiService } from '../services/api'
|
||||
import { formatUSDC } from '../utils'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import type { MatchedOrderInfo, OrderTrackingRequest, OrderTrackingListResponse } from '../types'
|
||||
|
||||
const CopyTradingMatchedOrdersPage: React.FC = () => {
|
||||
const { copyTradingId } = useParams<{ copyTradingId: string }>()
|
||||
const navigate = useNavigate()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [orders, setOrders] = useState<MatchedOrderInfo[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [limit, setLimit] = useState(20)
|
||||
const [filters, setFilters] = useState<{
|
||||
sellOrderId?: string
|
||||
buyOrderId?: string
|
||||
}>({})
|
||||
|
||||
useEffect(() => {
|
||||
if (copyTradingId) {
|
||||
fetchOrders()
|
||||
}
|
||||
}, [copyTradingId, page, limit, filters])
|
||||
|
||||
const fetchOrders = async () => {
|
||||
if (!copyTradingId) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const request: OrderTrackingRequest = {
|
||||
copyTradingId: parseInt(copyTradingId),
|
||||
type: 'matched',
|
||||
page,
|
||||
limit,
|
||||
...filters
|
||||
}
|
||||
|
||||
const response = await apiService.orderTracking.list(request)
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
const data = response.data.data as OrderTrackingListResponse
|
||||
setOrders((data.list || []) as MatchedOrderInfo[])
|
||||
setTotal(data.total || 0)
|
||||
} else {
|
||||
message.error(response.data.msg || '获取匹配关系列表失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取匹配关系列表失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getPnlColor = (value: string): string => {
|
||||
const num = parseFloat(value)
|
||||
if (isNaN(num)) return '#666'
|
||||
return num >= 0 ? '#3f8600' : '#cf1322'
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '卖出订单ID',
|
||||
dataIndex: 'sellOrderId',
|
||||
key: 'sellOrderId',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (text: string) => (
|
||||
<span style={{ fontFamily: 'monospace', fontSize: isMobile ? 11 : 12 }}>
|
||||
{isMobile
|
||||
? `${text.slice(0, 6)}...${text.slice(-4)}`
|
||||
: `${text.slice(0, 8)}...${text.slice(-6)}`
|
||||
}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '买入订单ID',
|
||||
dataIndex: 'buyOrderId',
|
||||
key: 'buyOrderId',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (text: string) => (
|
||||
<span style={{ fontFamily: 'monospace', fontSize: isMobile ? 11 : 12 }}>
|
||||
{isMobile
|
||||
? `${text.slice(0, 6)}...${text.slice(-4)}`
|
||||
: `${text.slice(0, 8)}...${text.slice(-6)}`
|
||||
}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '匹配数量',
|
||||
dataIndex: 'matchedQuantity',
|
||||
key: 'matchedQuantity',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (value: string) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{formatUSDC(value)}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '买入价格',
|
||||
dataIndex: 'buyPrice',
|
||||
key: 'buyPrice',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (value: string) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{formatUSDC(value)}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '卖出价格',
|
||||
dataIndex: 'sellPrice',
|
||||
key: 'sellPrice',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (value: string) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{formatUSDC(value)}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '盈亏',
|
||||
dataIndex: 'realizedPnl',
|
||||
key: 'realizedPnl',
|
||||
width: isMobile ? 100 : 120,
|
||||
render: (value: string) => (
|
||||
<span style={{
|
||||
color: getPnlColor(value),
|
||||
fontWeight: 500,
|
||||
fontSize: isMobile ? 12 : 14
|
||||
}}>
|
||||
{isMobile ? formatUSDC(value) : `${formatUSDC(value)} USDC`}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '匹配时间',
|
||||
dataIndex: 'matchedAt',
|
||||
key: 'matchedAt',
|
||||
width: isMobile ? 120 : 160,
|
||||
render: (timestamp: number) => (
|
||||
<span style={{ fontSize: isMobile ? 11 : 12 }}>
|
||||
{isMobile
|
||||
? new Date(timestamp).toLocaleDateString('zh-CN')
|
||||
: new Date(timestamp).toLocaleString('zh-CN')
|
||||
}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||
<Button icon={<LeftOutlined />} onClick={() => navigate(`/copy-trading/statistics/${copyTradingId}`)}>
|
||||
返回统计
|
||||
</Button>
|
||||
<h2 style={{ margin: 0 }}>匹配关系列表</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16, display: 'flex', gap: 16, flexWrap: 'wrap' }}>
|
||||
<Input
|
||||
placeholder="筛选卖出订单ID"
|
||||
allowClear
|
||||
style={{ width: isMobile ? '100%' : 200 }}
|
||||
value={filters.sellOrderId}
|
||||
onChange={(e) => setFilters({ ...filters, sellOrderId: e.target.value || undefined })}
|
||||
/>
|
||||
|
||||
<Input
|
||||
placeholder="筛选买入订单ID"
|
||||
allowClear
|
||||
style={{ width: isMobile ? '100%' : 200 }}
|
||||
value={filters.buyOrderId}
|
||||
onChange={(e) => setFilters({ ...filters, buyOrderId: e.target.value || undefined })}
|
||||
/>
|
||||
|
||||
<Button onClick={fetchOrders}>查询</Button>
|
||||
</div>
|
||||
|
||||
{isMobile ? (
|
||||
// 移动端卡片布局
|
||||
<div>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : orders.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
|
||||
暂无匹配关系
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{orders.map((order) => {
|
||||
const date = new Date(order.matchedAt)
|
||||
const formattedDate = date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={`${order.sellOrderId}-${order.buyOrderId}-${order.matchedAt}`}
|
||||
style={{
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
|
||||
border: '1px solid #e8e8e8'
|
||||
}}
|
||||
bodyStyle={{ padding: '16px' }}
|
||||
>
|
||||
{/* 订单ID */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>卖出订单ID</div>
|
||||
<div style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: '500',
|
||||
fontFamily: 'monospace',
|
||||
marginBottom: '8px'
|
||||
}}>
|
||||
{order.sellOrderId.slice(0, 8)}...{order.sellOrderId.slice(-6)}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>买入订单ID</div>
|
||||
<div style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: '500',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
{order.buyOrderId.slice(0, 8)}...{order.buyOrderId.slice(-6)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
|
||||
{/* 匹配信息 */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>匹配数量</div>
|
||||
<div style={{ fontSize: '14px', fontWeight: '500' }}>
|
||||
{formatUSDC(order.matchedQuantity)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 价格信息 */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>价格信息</div>
|
||||
<div style={{ fontSize: '13px', color: '#333' }}>
|
||||
买入: {formatUSDC(order.buyPrice)} | 卖出: {formatUSDC(order.sellPrice)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 盈亏 */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>盈亏</div>
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: getPnlColor(order.realizedPnl)
|
||||
}}>
|
||||
{formatUSDC(order.realizedPnl)} USDC
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 匹配时间 */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#999' }}>
|
||||
匹配时间: {formattedDate}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// 桌面端表格布局
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={orders}
|
||||
rowKey={(record) => `${record.sellOrderId}-${record.buyOrderId}-${record.matchedAt}`}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: limit,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (newPage, newLimit) => {
|
||||
setPage(newPage)
|
||||
setLimit(newLimit)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyTradingMatchedOrdersPage
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { Card, Table, Button, Tag, Select, Input, message, Divider, Spin } from 'antd'
|
||||
import { LeftOutlined } from '@ant-design/icons'
|
||||
import { apiService } from '../services/api'
|
||||
import { formatUSDC } from '../utils'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import type { SellOrderInfo, OrderTrackingRequest, OrderTrackingListResponse } from '../types'
|
||||
|
||||
const { Option } = Select
|
||||
|
||||
const CopyTradingSellOrdersPage: React.FC = () => {
|
||||
const { copyTradingId } = useParams<{ copyTradingId: string }>()
|
||||
const navigate = useNavigate()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [orders, setOrders] = useState<SellOrderInfo[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [limit, setLimit] = useState(20)
|
||||
const [filters, setFilters] = useState<{
|
||||
marketId?: string
|
||||
side?: string
|
||||
}>({})
|
||||
|
||||
useEffect(() => {
|
||||
if (copyTradingId) {
|
||||
fetchOrders()
|
||||
}
|
||||
}, [copyTradingId, page, limit, filters])
|
||||
|
||||
const fetchOrders = async () => {
|
||||
if (!copyTradingId) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const request: OrderTrackingRequest = {
|
||||
copyTradingId: parseInt(copyTradingId),
|
||||
type: 'sell',
|
||||
page,
|
||||
limit,
|
||||
...filters
|
||||
}
|
||||
|
||||
const response = await apiService.orderTracking.list(request)
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
const data = response.data.data as OrderTrackingListResponse
|
||||
setOrders((data.list || []) as SellOrderInfo[])
|
||||
setTotal(data.total || 0)
|
||||
} else {
|
||||
message.error(response.data.msg || '获取卖出订单列表失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取卖出订单列表失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getPnlColor = (value: string): string => {
|
||||
const num = parseFloat(value)
|
||||
if (isNaN(num)) return '#666'
|
||||
return num >= 0 ? '#3f8600' : '#cf1322'
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '订单ID',
|
||||
dataIndex: 'orderId',
|
||||
key: 'orderId',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (text: string) => (
|
||||
<span style={{ fontFamily: 'monospace', fontSize: isMobile ? 11 : 12 }}>
|
||||
{isMobile
|
||||
? `${text.slice(0, 6)}...${text.slice(-4)}`
|
||||
: `${text.slice(0, 8)}...${text.slice(-6)}`
|
||||
}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Leader 交易ID',
|
||||
dataIndex: 'leaderTradeId',
|
||||
key: 'leaderTradeId',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (text: string) => (
|
||||
<span style={{ fontFamily: 'monospace', fontSize: isMobile ? 11 : 12 }}>
|
||||
{isMobile
|
||||
? `${text.slice(0, 6)}...${text.slice(-4)}`
|
||||
: `${text.slice(0, 8)}...${text.slice(-6)}`
|
||||
}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '市场',
|
||||
dataIndex: 'marketId',
|
||||
key: 'marketId',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (text: string) => (
|
||||
<span style={{ fontFamily: 'monospace', fontSize: isMobile ? 11 : 12 }}>
|
||||
{isMobile
|
||||
? `${text.slice(0, 6)}...${text.slice(-4)}`
|
||||
: `${text.slice(0, 8)}...${text.slice(-6)}`
|
||||
}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '方向',
|
||||
dataIndex: 'side',
|
||||
key: 'side',
|
||||
width: isMobile ? 60 : 80,
|
||||
render: (side: string) => {
|
||||
// 将0/1转换为YES/NO
|
||||
const displaySide = side === '0' ? 'YES' : side === '1' ? 'NO' : side
|
||||
return <Tag style={{ fontSize: isMobile ? 11 : 12 }}>{displaySide}</Tag>
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '卖出数量',
|
||||
dataIndex: 'quantity',
|
||||
key: 'quantity',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (value: string) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{formatUSDC(value)}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '卖出价格',
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (value: string) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{formatUSDC(value)}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '卖出金额',
|
||||
key: 'amount',
|
||||
width: isMobile ? 100 : 120,
|
||||
render: (_: any, record: SellOrderInfo) => {
|
||||
const amount = (parseFloat(record.quantity) * parseFloat(record.price)).toString()
|
||||
return (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>
|
||||
{isMobile ? formatUSDC(amount) : `${formatUSDC(amount)} USDC`}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '已实现盈亏',
|
||||
dataIndex: 'realizedPnl',
|
||||
key: 'realizedPnl',
|
||||
width: isMobile ? 100 : 120,
|
||||
render: (value: string) => (
|
||||
<span style={{
|
||||
color: getPnlColor(value),
|
||||
fontWeight: 500,
|
||||
fontSize: isMobile ? 12 : 14
|
||||
}}>
|
||||
{isMobile ? formatUSDC(value) : `${formatUSDC(value)} USDC`}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: isMobile ? 120 : 160,
|
||||
render: (timestamp: number) => (
|
||||
<span style={{ fontSize: isMobile ? 11 : 12 }}>
|
||||
{isMobile
|
||||
? new Date(timestamp).toLocaleDateString('zh-CN')
|
||||
: new Date(timestamp).toLocaleString('zh-CN')
|
||||
}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||
<Button icon={<LeftOutlined />} onClick={() => navigate(`/copy-trading/statistics/${copyTradingId}`)}>
|
||||
返回统计
|
||||
</Button>
|
||||
<h2 style={{ margin: 0 }}>卖出订单列表</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16, display: 'flex', gap: 16, flexWrap: 'wrap' }}>
|
||||
<Input
|
||||
placeholder="筛选市场ID"
|
||||
allowClear
|
||||
style={{ width: isMobile ? '100%' : 200 }}
|
||||
value={filters.marketId}
|
||||
onChange={(e) => setFilters({ ...filters, marketId: e.target.value || undefined })}
|
||||
/>
|
||||
|
||||
<Select
|
||||
placeholder="筛选方向"
|
||||
allowClear
|
||||
style={{ width: isMobile ? '100%' : 150 }}
|
||||
value={filters.side}
|
||||
onChange={(value) => setFilters({ ...filters, side: value || undefined })}
|
||||
>
|
||||
<Option value="0">YES</Option>
|
||||
<Option value="1">NO</Option>
|
||||
<Option value="YES">YES</Option>
|
||||
<Option value="NO">NO</Option>
|
||||
</Select>
|
||||
|
||||
<Button onClick={fetchOrders}>查询</Button>
|
||||
</div>
|
||||
|
||||
{isMobile ? (
|
||||
// 移动端卡片布局
|
||||
<div>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : orders.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
|
||||
暂无卖出订单
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{orders.map((order) => {
|
||||
const date = new Date(order.createdAt)
|
||||
const formattedDate = date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
const amount = (parseFloat(order.quantity) * parseFloat(order.price)).toString()
|
||||
const displaySide = order.side === '0' ? 'YES' : order.side === '1' ? 'NO' : order.side
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={order.orderId}
|
||||
style={{
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
|
||||
border: '1px solid #e8e8e8'
|
||||
}}
|
||||
bodyStyle={{ padding: '16px' }}
|
||||
>
|
||||
{/* 订单ID和方向 */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '8px',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
{order.orderId.slice(0, 8)}...{order.orderId.slice(-6)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
|
||||
<Tag>{displaySide}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
|
||||
{/* 卖出信息 */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>卖出信息</div>
|
||||
<div style={{ fontSize: '14px', fontWeight: '500' }}>
|
||||
数量: {formatUSDC(order.quantity)} | 价格: {formatUSDC(order.price)}
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', fontWeight: '500', marginTop: '4px' }}>
|
||||
金额: {formatUSDC(amount)} USDC
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 已实现盈亏 */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>已实现盈亏</div>
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: getPnlColor(order.realizedPnl)
|
||||
}}>
|
||||
{formatUSDC(order.realizedPnl)} USDC
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Leader 交易ID */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>Leader 交易ID</div>
|
||||
<div style={{ fontSize: '12px', color: '#999', fontFamily: 'monospace' }}>
|
||||
{order.leaderTradeId.slice(0, 8)}...{order.leaderTradeId.slice(-6)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 市场ID */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>市场ID</div>
|
||||
<div style={{ fontSize: '12px', color: '#999', fontFamily: 'monospace' }}>
|
||||
{order.marketId.slice(0, 8)}...{order.marketId.slice(-6)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 创建时间 */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#999' }}>
|
||||
创建时间: {formattedDate}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// 桌面端表格布局
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={orders}
|
||||
rowKey="orderId"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: limit,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (newPage, newLimit) => {
|
||||
setPage(newPage)
|
||||
setLimit(newLimit)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyTradingSellOrdersPage
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
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 { useMediaQuery } from 'react-responsive'
|
||||
import type { CopyTradingStatistics } from '../types'
|
||||
|
||||
const CopyTradingStatisticsPage: React.FC = () => {
|
||||
const { copyTradingId } = useParams<{ copyTradingId: string }>()
|
||||
const navigate = useNavigate()
|
||||
const isMobile = 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) })
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setStatistics(response.data.data)
|
||||
} else {
|
||||
message.error(response.data.msg || '获取统计信息失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取统计信息失败')
|
||||
} finally {
|
||||
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' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!statistics) {
|
||||
return (
|
||||
<Card>
|
||||
<div style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<p>暂无统计数据</p>
|
||||
<Button onClick={() => navigate('/copy-trading')}>返回列表</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||
<Button icon={<LeftOutlined />} onClick={() => navigate('/copy-trading')}>
|
||||
返回
|
||||
</Button>
|
||||
<h2 style={{ margin: 0 }}>跟单关系统计</h2>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Button onClick={() => navigate(`/copy-trading/orders/buy/${copyTradingId}`)}>
|
||||
买入订单
|
||||
</Button>
|
||||
<Button onClick={() => navigate(`/copy-trading/orders/sell/${copyTradingId}`)}>
|
||||
卖出订单
|
||||
</Button>
|
||||
<Button onClick={() => navigate(`/copy-trading/orders/matched/${copyTradingId}`)}>
|
||||
匹配关系
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 基本信息卡片 */}
|
||||
<Card title="基本信息" style={{ marginBottom: 16 }}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<div>
|
||||
<div style={{ color: '#999', fontSize: 14, marginBottom: 4 }}>账户名称</div>
|
||||
<div style={{ fontSize: 16, fontWeight: 500 }}>
|
||||
{statistics.accountName || `账户 ${statistics.accountId}`}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<div>
|
||||
<div style={{ color: '#999', fontSize: 14, marginBottom: 4 }}>Leader 名称</div>
|
||||
<div style={{ fontSize: 16, fontWeight: 500 }}>
|
||||
{statistics.leaderName || `Leader ${statistics.leaderId}`}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<div>
|
||||
<div style={{ color: '#999', fontSize: 14, marginBottom: 4 }}>模板名称</div>
|
||||
<div style={{ fontSize: 16, fontWeight: 500 }}>
|
||||
{statistics.templateName || `模板 ${statistics.templateId}`}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<div>
|
||||
<div style={{ color: '#999', fontSize: 14, marginBottom: 4 }}>跟单状态</div>
|
||||
<div>
|
||||
<Tag color={statistics.enabled ? 'green' : 'red'}>
|
||||
{statistics.enabled ? '启用' : '禁用'}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</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)}
|
||||
suffix=""
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Statistic
|
||||
title="总买入金额"
|
||||
value={formatUSDC(statistics.totalBuyAmount)}
|
||||
suffix="USDC"
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Statistic
|
||||
title="总买入订单数"
|
||||
value={statistics.totalBuyOrders}
|
||||
suffix="笔"
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Statistic
|
||||
title="平均买入价格"
|
||||
value={formatUSDC(statistics.avgBuyPrice)}
|
||||
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)}
|
||||
suffix=""
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Statistic
|
||||
title="总卖出金额"
|
||||
value={formatUSDC(statistics.totalSellAmount)}
|
||||
suffix="USDC"
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Statistic
|
||||
title="总卖出订单数"
|
||||
value={statistics.totalSellOrders}
|
||||
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.currentPositionQuantity)}
|
||||
suffix=""
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Statistic
|
||||
title="当前持仓价值"
|
||||
value={formatUSDC(statistics.currentPositionValue)}
|
||||
suffix="USDC"
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Statistic
|
||||
title="平均买入价格"
|
||||
value={formatUSDC(statistics.avgBuyPrice)}
|
||||
suffix=""
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 盈亏统计卡片 */}
|
||||
<Card title="盈亏统计">
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Statistic
|
||||
title="总已实现盈亏"
|
||||
value={formatUSDC(statistics.totalRealizedPnl)}
|
||||
valueStyle={{ color: getPnlColor(statistics.totalRealizedPnl) }}
|
||||
prefix={getPnlIcon(statistics.totalRealizedPnl)}
|
||||
suffix="USDC"
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Statistic
|
||||
title="总未实现盈亏"
|
||||
value={formatUSDC(statistics.totalUnrealizedPnl)}
|
||||
valueStyle={{ color: getPnlColor(statistics.totalUnrealizedPnl) }}
|
||||
prefix={getPnlIcon(statistics.totalUnrealizedPnl)}
|
||||
suffix="USDC"
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Statistic
|
||||
title="总盈亏"
|
||||
value={formatUSDC(statistics.totalPnl)}
|
||||
valueStyle={{ color: getPnlColor(statistics.totalPnl) }}
|
||||
prefix={getPnlIcon(statistics.totalPnl)}
|
||||
suffix="USDC"
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Statistic
|
||||
title="总盈亏百分比"
|
||||
value={formatPercent(statistics.totalPnlPercent)}
|
||||
valueStyle={{ color: getPnlColor(statistics.totalPnlPercent) }}
|
||||
prefix={getPnlIcon(statistics.totalPnlPercent)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyTradingStatisticsPage
|
||||
|
||||
@@ -284,7 +284,24 @@ export const apiService = {
|
||||
* 获取分类统计
|
||||
*/
|
||||
category: (data: { category: string; startTime?: number; endTime?: number }) =>
|
||||
apiClient.post<ApiResponse<any>>('/copy-trading/statistics/category', data)
|
||||
apiClient.post<ApiResponse<any>>('/copy-trading/statistics/category', data),
|
||||
|
||||
/**
|
||||
* 获取跟单关系统计详情
|
||||
*/
|
||||
detail: (data: { copyTradingId: number }) =>
|
||||
apiClient.post<ApiResponse<any>>('/copy-trading/statistics/detail', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 订单跟踪 API
|
||||
*/
|
||||
orderTracking: {
|
||||
/**
|
||||
* 查询订单列表(买入/卖出/匹配)
|
||||
*/
|
||||
list: (data: any) =>
|
||||
apiClient.post<ApiResponse<any>>('/copy-trading/orders/tracking', data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -496,3 +496,108 @@ export interface RedeemablePositionsSummary {
|
||||
positions: RedeemablePositionInfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 跟单关系统计信息
|
||||
*/
|
||||
export interface CopyTradingStatistics {
|
||||
copyTradingId: number
|
||||
accountId: number
|
||||
accountName: string | null
|
||||
leaderId: number
|
||||
leaderName: string | null
|
||||
templateId: number
|
||||
templateName: string | null
|
||||
enabled: boolean
|
||||
|
||||
// 买入统计
|
||||
totalBuyQuantity: string
|
||||
totalBuyOrders: number
|
||||
totalBuyAmount: string
|
||||
avgBuyPrice: string
|
||||
|
||||
// 卖出统计
|
||||
totalSellQuantity: string
|
||||
totalSellOrders: number
|
||||
totalSellAmount: string
|
||||
|
||||
// 持仓统计
|
||||
currentPositionQuantity: string
|
||||
currentPositionValue: string
|
||||
|
||||
// 盈亏统计
|
||||
totalRealizedPnl: string
|
||||
totalUnrealizedPnl: string
|
||||
totalPnl: string
|
||||
totalPnlPercent: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 买入订单信息
|
||||
*/
|
||||
export interface BuyOrderInfo {
|
||||
orderId: string
|
||||
leaderTradeId: string
|
||||
marketId: string
|
||||
side: string
|
||||
quantity: string
|
||||
price: string
|
||||
amount: string
|
||||
matchedQuantity: string
|
||||
remainingQuantity: string
|
||||
status: 'filled' | 'partially_matched' | 'fully_matched'
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 卖出订单信息
|
||||
*/
|
||||
export interface SellOrderInfo {
|
||||
orderId: string
|
||||
leaderTradeId: string
|
||||
marketId: string
|
||||
side: string
|
||||
quantity: string
|
||||
price: string
|
||||
amount: string
|
||||
realizedPnl: string
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配订单信息
|
||||
*/
|
||||
export interface MatchedOrderInfo {
|
||||
sellOrderId: string
|
||||
buyOrderId: string
|
||||
matchedQuantity: string
|
||||
buyPrice: string
|
||||
sellPrice: string
|
||||
realizedPnl: string
|
||||
matchedAt: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单跟踪列表响应
|
||||
*/
|
||||
export interface OrderTrackingListResponse {
|
||||
list: BuyOrderInfo[] | SellOrderInfo[] | MatchedOrderInfo[]
|
||||
total: number
|
||||
page: number
|
||||
limit: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单跟踪查询请求
|
||||
*/
|
||||
export interface OrderTrackingRequest {
|
||||
copyTradingId: number
|
||||
type: 'buy' | 'sell' | 'matched'
|
||||
page?: number
|
||||
limit?: number
|
||||
marketId?: string
|
||||
side?: string
|
||||
status?: string
|
||||
sellOrderId?: string
|
||||
buyOrderId?: string
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user