feat: 实现跟单订单跟踪和统计功能
- 添加订单跟踪实体和Repository(CopyOrderTracking, SellMatchRecord, SellMatchDetail) - 实现订单跟踪服务(CopyOrderTrackingService),支持买入/卖出订单的创建和匹配 - 实现跟单统计服务(CopyTradingStatisticsService),支持盈亏统计和订单列表查询 - 添加失败交易记录(FailedTrade)和已处理交易去重(ProcessedTrade) - 实现轮询服务(CopyTradingPollingService),使用Data API /activity接口轮询Leader交易 - 支持多元市场(使用outcomeIndex而不是YES/NO) - 重试时重新生成salt并重新签名 - 添加风险控制检查(每日订单数、每日亏损、单笔订单大小限制) - 支持价格容忍度和延迟跟单 - 添加数据库迁移文件(V8, V9, V10) - 前端:账户管理显示代理钱包地址,添加复制功能,移除默认账户逻辑 - 修复API响应字段映射问题(orderID, transactionsHashes) - 缩短轮询间隔到2秒
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Card, Table, Button, Space, Tag, Popconfirm, message, Typography, Spin, Modal, Descriptions, Divider, Form, Input, Checkbox, Alert } from 'antd'
|
||||
import { PlusOutlined, StarOutlined, StarFilled, ReloadOutlined, EditOutlined } from '@ant-design/icons'
|
||||
import { PlusOutlined, ReloadOutlined, EditOutlined, CopyOutlined } from '@ant-design/icons'
|
||||
import { useAccountStore } from '../store/accountStore'
|
||||
import type { Account } from '../types'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
@@ -12,7 +12,7 @@ const { Title } = Typography
|
||||
const AccountList: React.FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const { accounts, loading, fetchAccounts, deleteAccount, setDefaultAccount, fetchAccountBalance, fetchAccountDetail, updateAccount } = useAccountStore()
|
||||
const { accounts, loading, fetchAccounts, deleteAccount, fetchAccountBalance, fetchAccountDetail, updateAccount } = useAccountStore()
|
||||
const [balanceMap, setBalanceMap] = useState<Record<number, { total: string; available: string; position: string }>>({})
|
||||
const [balanceLoading, setBalanceLoading] = useState<Record<number, boolean>>({})
|
||||
const [detailModalVisible, setDetailModalVisible] = useState(false)
|
||||
@@ -71,13 +71,12 @@ const AccountList: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleSetDefault = async (account: Account) => {
|
||||
try {
|
||||
await setDefaultAccount(account.id)
|
||||
message.success('设置默认账户成功')
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '设置默认账户失败')
|
||||
}
|
||||
const handleCopy = (text: string, label: string) => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
message.success(`${label}已复制到剪贴板`)
|
||||
}).catch(() => {
|
||||
message.error('复制失败')
|
||||
})
|
||||
}
|
||||
|
||||
const handleShowDetail = async (account: Account) => {
|
||||
@@ -222,21 +221,34 @@ const AccountList: React.FC = () => {
|
||||
title: '钱包地址',
|
||||
dataIndex: 'walletAddress',
|
||||
key: 'walletAddress',
|
||||
render: (address: string) => (
|
||||
<span style={{ fontFamily: 'monospace' }}>{address}</span>
|
||||
render: (text: string) => (
|
||||
<Space>
|
||||
<span style={{ fontFamily: 'monospace', fontSize: '12px' }}>{text}</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCopy(text, '钱包地址')}
|
||||
title="复制钱包地址"
|
||||
/>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '默认账户',
|
||||
dataIndex: 'isDefault',
|
||||
key: 'isDefault',
|
||||
render: (isDefault: boolean, record: Account) => (
|
||||
<Button
|
||||
type="text"
|
||||
icon={isDefault ? <StarFilled style={{ color: '#faad14' }} /> : <StarOutlined />}
|
||||
onClick={() => !isDefault && handleSetDefault(record)}
|
||||
disabled={isDefault}
|
||||
/>
|
||||
title: '代理钱包地址',
|
||||
dataIndex: 'proxyAddress',
|
||||
key: 'proxyAddress',
|
||||
render: (address: string) => (
|
||||
<Space>
|
||||
<span style={{ fontFamily: 'monospace', fontSize: '12px' }}>{address}</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCopy(address, '代理钱包地址')}
|
||||
title="复制代理钱包地址"
|
||||
/>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
@@ -342,12 +354,28 @@ const AccountList: React.FC = () => {
|
||||
fontFamily: 'monospace',
|
||||
lineHeight: '1.4'
|
||||
}}>
|
||||
{record.walletAddress}
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
<strong>钱包地址:</strong> {record.walletAddress}
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCopy(record.walletAddress, '钱包地址')}
|
||||
style={{ marginLeft: '4px', padding: '0 4px' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<strong>代理钱包:</strong> {record.proxyAddress}
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCopy(record.proxyAddress, '代理钱包地址')}
|
||||
style={{ marginLeft: '4px', padding: '0 4px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: '8px', display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
|
||||
<Tag color={record.isDefault ? 'gold' : 'default'} style={{ margin: 0 }}>
|
||||
{record.isDefault ? '默认' : '普通'}
|
||||
</Tag>
|
||||
<Tag color={allConfigured ? 'success' : partialConfigured ? 'warning' : 'default'} style={{ margin: 0 }}>
|
||||
{allConfigured ? '完整配置' : partialConfigured ? '部分配置' : '未配置'}
|
||||
</Tag>
|
||||
@@ -414,17 +442,6 @@ const AccountList: React.FC = () => {
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
{!record.isDefault && (
|
||||
<Button
|
||||
size="small"
|
||||
block
|
||||
icon={<StarOutlined />}
|
||||
onClick={() => handleSetDefault(record)}
|
||||
style={{ minHeight: '32px' }}
|
||||
>
|
||||
设为默认
|
||||
</Button>
|
||||
)}
|
||||
<Popconfirm
|
||||
title="确定要删除这个账户吗?"
|
||||
description={
|
||||
@@ -578,20 +595,44 @@ const AccountList: React.FC = () => {
|
||||
{detailAccount.accountName || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="钱包地址" span={isMobile ? 1 : 2}>
|
||||
<span style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: isMobile ? '11px' : '13px',
|
||||
wordBreak: 'break-all',
|
||||
lineHeight: '1.4',
|
||||
display: 'block'
|
||||
}}>
|
||||
{detailAccount.walletAddress || '-'}
|
||||
</span>
|
||||
<Space>
|
||||
<span style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: isMobile ? '11px' : '13px',
|
||||
wordBreak: 'break-all',
|
||||
lineHeight: '1.4',
|
||||
display: 'block'
|
||||
}}>
|
||||
{detailAccount.walletAddress || '-'}
|
||||
</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCopy(detailAccount.walletAddress || '', '钱包地址')}
|
||||
title="复制钱包地址"
|
||||
/>
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="默认账户">
|
||||
<Tag color={detailAccount.isDefault ? 'gold' : 'default'}>
|
||||
{detailAccount.isDefault ? '是' : '否'}
|
||||
</Tag>
|
||||
<Descriptions.Item label="代理钱包地址" span={isMobile ? 1 : 2}>
|
||||
<Space>
|
||||
<span style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: isMobile ? '11px' : '13px',
|
||||
wordBreak: 'break-all',
|
||||
lineHeight: '1.4',
|
||||
display: 'block'
|
||||
}}>
|
||||
{detailAccount.proxyAddress || '-'}
|
||||
</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCopy(detailAccount.proxyAddress || '', '代理钱包地址')}
|
||||
title="复制代理钱包地址"
|
||||
/>
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="总余额" span={isMobile ? 1 : 2}>
|
||||
{detailBalanceLoading ? (
|
||||
|
||||
@@ -14,7 +14,6 @@ interface AccountStore {
|
||||
importAccount: (data: any) => Promise<void>
|
||||
updateAccount: (data: any) => Promise<void>
|
||||
deleteAccount: (accountId: number) => Promise<void>
|
||||
setDefaultAccount: (accountId: number) => Promise<void>
|
||||
fetchAccountDetail: (accountId: number) => Promise<Account>
|
||||
fetchAccountBalance: (accountId: number) => Promise<{
|
||||
availableBalance: string
|
||||
@@ -38,10 +37,9 @@ export const useAccountStore = create<AccountStore>((set, get) => ({
|
||||
const accounts = response.data.data.list || []
|
||||
set({ accounts, loading: false })
|
||||
|
||||
// 设置默认账户为当前账户
|
||||
const defaultAccount = accounts.find((a: Account) => a.isDefault)
|
||||
if (defaultAccount) {
|
||||
set({ currentAccount: defaultAccount })
|
||||
// 如果没有当前账户,设置第一个账户为当前账户
|
||||
if (accounts.length > 0 && !get().currentAccount) {
|
||||
set({ currentAccount: accounts[0] })
|
||||
}
|
||||
} else {
|
||||
set({ error: response.data.msg || '获取账户列表失败', loading: false })
|
||||
@@ -103,22 +101,6 @@ export const useAccountStore = create<AccountStore>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
setDefaultAccount: async (accountId) => {
|
||||
set({ loading: true, error: null })
|
||||
try {
|
||||
const response = await apiService.accounts.setDefault({ accountId })
|
||||
if (response.data.code === 0) {
|
||||
await get().fetchAccounts()
|
||||
} else {
|
||||
set({ error: response.data.msg || '设置默认账户失败', loading: false })
|
||||
throw new Error(response.data.msg || '设置默认账户失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || '设置默认账户失败', loading: false })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
fetchAccountDetail: async (accountId) => {
|
||||
set({ loading: true, error: null })
|
||||
try {
|
||||
|
||||
@@ -13,8 +13,10 @@ export interface ApiResponse<T> {
|
||||
export interface Account {
|
||||
id: number
|
||||
walletAddress: string
|
||||
proxyAddress: string // Polymarket 代理钱包地址
|
||||
accountName?: string
|
||||
isDefault: boolean
|
||||
isEnabled?: boolean // 是否启用
|
||||
apiKeyConfigured: boolean
|
||||
apiSecretConfigured: boolean
|
||||
apiPassphraseConfigured: boolean
|
||||
|
||||
Reference in New Issue
Block a user