import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import { Card, Table, Button, Space, Tag, Popconfirm, Switch, message, Select, 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, Leader, CopyTradingTemplate, CopyTradingStatistics } from '../types' import { useMediaQuery } from 'react-responsive' import { formatUSDC } from '../utils' const { Option } = Select const CopyTradingList: React.FC = () => { const navigate = useNavigate() const isMobile = useMediaQuery({ maxWidth: 768 }) const { accounts, fetchAccounts } = useAccountStore() const [copyTradings, setCopyTradings] = useState([]) const [leaders, setLeaders] = useState([]) const [templates, setTemplates] = useState([]) const [loading, setLoading] = useState(false) const [statisticsMap, setStatisticsMap] = useState>({}) const [loadingStatistics, setLoadingStatistics] = useState>(new Set()) const [filters, setFilters] = useState<{ accountId?: number templateId?: number leaderId?: number enabled?: boolean }>({}) useEffect(() => { fetchAccounts() fetchLeaders() fetchTemplates() fetchCopyTradings() }, []) useEffect(() => { fetchCopyTradings() }, [filters]) const fetchLeaders = async () => { try { const response = await apiService.leaders.list({}) if (response.data.code === 0 && response.data.data) { setLeaders(response.data.data.list || []) } } catch (error: any) { console.error('获取 Leader 列表失败:', error) } } const fetchTemplates = async () => { try { const response = await apiService.templates.list() if (response.data.code === 0 && response.data.data) { setTemplates(response.data.data.list || []) } } catch (error: any) { console.error('获取模板列表失败:', error) } } const fetchCopyTradings = async () => { setLoading(true) try { const response = await apiService.copyTrading.list(filters) if (response.data.code === 0 && response.data.data) { const list = response.data.data.list || [] setCopyTradings(list) // 为每个跟单关系获取统计信息 list.forEach((ct: CopyTrading) => { fetchStatistics(ct.id) }) } else { message.error(response.data.msg || '获取跟单列表失败') } } catch (error: any) { message.error(error.message || '获取跟单列表失败') } finally { setLoading(false) } } 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 ? : } 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({ copyTradingId: copyTrading.id, enabled: !copyTrading.enabled }) if (response.data.code === 0) { message.success(`${copyTrading.enabled ? '停止' : '开启'}跟单成功`) fetchCopyTradings() } else { message.error(response.data.msg || '更新跟单状态失败') } } catch (error: any) { message.error(error.message || '更新跟单状态失败') } } const handleDelete = async (copyTradingId: number) => { try { const response = await apiService.copyTrading.delete({ copyTradingId }) if (response.data.code === 0) { message.success('删除跟单成功') fetchCopyTradings() } else { message.error(response.data.msg || '删除跟单失败') } } catch (error: any) { message.error(error.message || '删除跟单失败') } } const columns = [ { title: '钱包', key: 'account', width: isMobile ? 100 : 150, render: (_: any, record: CopyTrading) => (
{record.accountName || `账户 ${record.accountId}`}
{isMobile ? `${record.walletAddress.slice(0, 4)}...${record.walletAddress.slice(-3)}` : `${record.walletAddress.slice(0, 6)}...${record.walletAddress.slice(-4)}` }
) }, { title: '模板', dataIndex: 'templateName', key: 'templateName', width: isMobile ? 100 : 120, render: (text: string) => ( {text} ) }, { title: 'Leader', key: 'leader', width: isMobile ? 100 : 150, render: (_: any, record: CopyTrading) => (
{record.leaderName || `Leader ${record.leaderId}`}
{isMobile ? `${record.leaderAddress.slice(0, 4)}...${record.leaderAddress.slice(-3)}` : `${record.leaderAddress.slice(0, 6)}...${record.leaderAddress.slice(-4)}` }
) }, { title: '状态', dataIndex: 'enabled', key: 'enabled', width: isMobile ? 80 : 100, render: (enabled: boolean, record: CopyTrading) => ( handleToggleStatus(record)} checkedChildren="开启" unCheckedChildren="停止" /> ) }, { title: '总盈亏', key: 'totalPnl', width: isMobile ? 100 : 150, render: (_: any, record: CopyTrading) => { const stats = statisticsMap[record.id] if (!stats) { return loadingStatistics.has(record.id) ? ( 加载中... ) : ( - ) } return (
{getPnlIcon(stats.totalPnl)} {isMobile ? formatUSDC(stats.totalPnl) : `${formatUSDC(stats.totalPnl)} USDC`}
{!isMobile && (
{formatPercent(stats.totalPnlPercent)}
)}
) } }, { title: '操作', key: 'action', width: isMobile ? 100 : 200, fixed: 'right' as const, render: (_: any, record: CopyTrading) => { const menuItems: MenuProps['items'] = [ { key: 'statistics', label: '查看统计', icon: , onClick: () => navigate(`/copy-trading/statistics/${record.id}`) }, { key: 'buyOrders', label: '买入订单', icon: , onClick: () => navigate(`/copy-trading/orders/buy/${record.id}`) }, { key: 'sellOrders', label: '卖出订单', icon: , onClick: () => navigate(`/copy-trading/orders/sell/${record.id}`) }, { key: 'matchedOrders', label: '匹配关系', icon: , onClick: () => navigate(`/copy-trading/orders/matched/${record.id}`) }, { type: 'divider' }, { key: 'delete', label: ( handleDelete(record.id)} okText="确定" cancelText="取消" onCancel={(e) => e?.stopPropagation()} > 删除 ), danger: true } ] return ( {!isMobile && ( )} {!isMobile && ( handleDelete(record.id)} okText="确定" cancelText="取消" > )} ) } } ] return (

跟单配置管理

{isMobile ? ( // 移动端卡片布局
{loading ? (
) : copyTradings.length === 0 ? (
暂无跟单配置
) : (
{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 ( {/* 基本信息 */}
{record.templateName}
{record.enabled ? '启用' : '禁用'}
{/* 账户信息 */}
账户
{record.accountName || `账户 ${record.accountId}`}
{record.walletAddress.slice(0, 6)}...{record.walletAddress.slice(-4)}
{/* Leader 信息 */}
Leader
{record.leaderName || `Leader ${record.leaderId}`}
{record.leaderAddress.slice(0, 6)}...{record.leaderAddress.slice(-4)}
{/* 总盈亏 */} {stats && (
总盈亏
{getPnlIcon(stats.totalPnl)} {formatUSDC(stats.totalPnl)} USDC
{formatPercent(stats.totalPnlPercent)}
)} {loadingStatistics.has(record.id) && (
加载统计中...
)} {/* 创建时间 */}
创建时间: {formattedDate}
{/* 操作按钮 */}
, onClick: () => navigate(`/copy-trading/statistics/${record.id}`) }, { key: 'buyOrders', label: '买入订单', icon: , onClick: () => navigate(`/copy-trading/orders/buy/${record.id}`) }, { key: 'sellOrders', label: '卖出订单', icon: , onClick: () => navigate(`/copy-trading/orders/sell/${record.id}`) }, { key: 'matchedOrders', label: '匹配关系', icon: , onClick: () => navigate(`/copy-trading/orders/matched/${record.id}`) } ] }} trigger={['click']} > handleDelete(record.id)} okText="确定" cancelText="取消" >
) })}
)}
) : ( // 桌面端表格布局 `共 ${total} 条` }} /> )} ) } export default CopyTradingList