import { useEffect, useState } from 'react' import { useSearchParams } 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, EditOutlined } from '@ant-design/icons' import { useTranslation } from 'react-i18next' import type { MenuProps } from 'antd' import { apiService } from '../services/api' import { useAccountStore } from '../store/accountStore' import type { CopyTrading, Leader, CopyTradingStatistics } from '../types' import { useMediaQuery } from 'react-responsive' import { formatUSDC } from '../utils' import CopyTradingOrdersModal from './CopyTradingOrders/index' import StatisticsModal from './CopyTradingOrders/StatisticsModal' import FilteredOrdersModal from './CopyTradingOrders/FilteredOrdersModal' import EditModal from './CopyTradingOrders/EditModal' import AddModal from './CopyTradingOrders/AddModal' import LeaderSelect from '../components/LeaderSelect' const { Option } = Select const CopyTradingList: React.FC = () => { const { t } = useTranslation() const [searchParams] = useSearchParams() const isMobile = useMediaQuery({ maxWidth: 768 }) const { accounts, fetchAccounts } = useAccountStore() const [copyTradings, setCopyTradings] = useState([]) const [leaders, setLeaders] = useState([]) const [loading, setLoading] = useState(false) const [statisticsMap, setStatisticsMap] = useState>({}) const [loadingStatistics, setLoadingStatistics] = useState>(new Set()) const [filters, setFilters] = useState<{ accountId?: number leaderId?: number enabled?: boolean }>(() => { const leaderIdParam = searchParams.get('leaderId') if (leaderIdParam) { const leaderId = parseInt(leaderIdParam, 10) if (!isNaN(leaderId)) return { leaderId } } return {} }) // Modal 状态 const [ordersModalOpen, setOrdersModalOpen] = useState(false) const [ordersModalCopyTradingId, setOrdersModalCopyTradingId] = useState('') const [ordersModalTab, setOrdersModalTab] = useState<'buy' | 'sell' | 'matched'>('buy') const [statisticsModalOpen, setStatisticsModalOpen] = useState(false) const [statisticsModalCopyTradingId, setStatisticsModalCopyTradingId] = useState('') const [filteredOrdersModalOpen, setFilteredOrdersModalOpen] = useState(false) const [filteredOrdersModalCopyTradingId, setFilteredOrdersModalCopyTradingId] = useState('') const [editModalOpen, setEditModalOpen] = useState(false) const [editModalCopyTradingId, setEditModalCopyTradingId] = useState('') const [addModalOpen, setAddModalOpen] = useState(false) // 从 URL 读取 leaderId 并应用筛选(如从 Leader 管理页跳转过来) useEffect(() => { const leaderIdParam = searchParams.get('leaderId') if (leaderIdParam) { const leaderId = parseInt(leaderIdParam, 10) if (!isNaN(leaderId)) { setFilters(prev => ({ ...prev, leaderId })) } } }, [searchParams]) useEffect(() => { fetchAccounts() fetchLeaders() 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 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 || t('copyTradingList.fetchFailed') || '获取跟单列表失败') } } catch (error: any) { message.error(error.message || t('copyTradingList.fetchFailed') || '获取跟单列表失败') } 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 ? (t('copyTradingList.stopSuccess') || '停止跟单成功') : (t('copyTradingList.startSuccess') || '开启跟单成功')) fetchCopyTradings() } else { message.error(response.data.msg || t('copyTradingList.updateStatusFailed') || '更新跟单状态失败') } } catch (error: any) { message.error(error.message || t('copyTradingList.updateStatusFailed') || '更新跟单状态失败') } } const handleDelete = async (copyTradingId: number) => { try { const response = await apiService.copyTrading.delete({ copyTradingId }) if (response.data.code === 0) { message.success(t('copyTradingList.deleteSuccess') || '删除跟单成功') fetchCopyTradings() } else { message.error(response.data.msg || t('copyTradingList.deleteFailed') || '删除跟单失败') } } catch (error: any) { message.error(error.message || t('copyTradingList.deleteFailed') || '删除跟单失败') } } const columns = [ { title: t('copyTradingList.configName') || '配置名', key: 'configName', width: isMobile ? 100 : 150, render: (_: any, record: CopyTrading) => (
{record.configName || t('copyTradingList.configNameNotProvided') || '未提供'}
) }, { title: t('copyTradingList.wallet') || '钱包', key: 'account', width: isMobile ? 100 : 150, render: (_: any, record: CopyTrading) => (
{record.accountName || `${t('copyTradingList.account') || '账户'} ${record.accountId}`}
{isMobile ? `${record.walletAddress.slice(0, 4)}...${record.walletAddress.slice(-3)}` : `${record.walletAddress.slice(0, 6)}...${record.walletAddress.slice(-4)}` }
) }, { title: t('copyTradingList.copyMode') || '跟单模式', key: 'copyMode', width: isMobile ? 100 : 120, render: (_: any, record: CopyTrading) => ( {record.copyMode === 'RATIO' ? `${t('copyTradingList.ratioMode') || '比例'} ${(parseFloat(record.copyRatio || '0') * 100).toFixed(2).replace(/\.0+$/, '')}%` : `${t('copyTradingList.fixedAmountMode') || '固定'} ${formatUSDC(record.fixedAmount || '0')}` } ) }, { title: t('copyTradingList.leader') || '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: t('common.status') || '状态', dataIndex: 'enabled', key: 'enabled', width: isMobile ? 80 : 100, render: (enabled: boolean, record: CopyTrading) => ( handleToggleStatus(record)} checkedChildren={t('copyTradingList.enabled') || '开启'} unCheckedChildren={t('copyTradingList.disabled') || '停止'} /> ) }, { title: t('copyTradingList.totalPnl') || '总盈亏', key: 'totalPnl', width: isMobile ? 100 : 150, render: (_: any, record: CopyTrading) => { const stats = statisticsMap[record.id] if (!stats) { return loadingStatistics.has(record.id) ? ( {t('common.loading') || '加载中...'} ) : ( - ) } return (
{getPnlIcon(stats.totalPnl)} {isMobile ? formatUSDC(stats.totalPnl) : `${formatUSDC(stats.totalPnl)} USDC`}
{!isMobile && (
{formatPercent(stats.totalPnlPercent)}
)}
) } }, { title: t('common.actions') || '操作', key: 'action', width: isMobile ? 100 : 200, fixed: 'right' as const, render: (_: any, record: CopyTrading) => { const menuItems: MenuProps['items'] = [ { key: 'matchedOrders', label: t('copyTradingList.matchedOrders') || '已成交订单', icon: , onClick: () => { setOrdersModalCopyTradingId(record.id.toString()) setOrdersModalTab('buy') setOrdersModalOpen(true) } }, { key: 'filteredOrders', label: t('copyTradingList.filteredOrders') || '已过滤订单', icon: , onClick: () => { setFilteredOrdersModalCopyTradingId(record.id.toString()) setFilteredOrdersModalOpen(true) } } ] return ( {!isMobile && ( <> )} {!isMobile && ( handleDelete(record.id)} okText={t('common.confirm') || '确定'} cancelText={t('common.cancel') || '取消'} > )} ) } } ] return (

{t('copyTradingList.title') || '跟单配置管理'}

setFilters({ ...filters, leaderId: value || undefined })} leaders={leaders} />
{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.configName || t('copyTradingList.configNameNotProvided') || '未提供'}
{record.copyMode === 'RATIO' ? `${t('copyTradingList.ratioMode') || '比例'} ${(parseFloat(record.copyRatio || '0') * 100).toFixed(2).replace(/\.0+$/, '')}%` : `${t('copyTradingList.fixedAmountMode') || '固定'} ${formatUSDC(record.fixedAmount || '0')}` }
{record.enabled ? '启用' : '禁用'} handleToggleStatus(record)} checkedChildren="开启" unCheckedChildren="停止" size="small" />
{/* 账户信息 */}
账户
{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: () => { setOrdersModalCopyTradingId(record.id.toString()) setOrdersModalTab('buy') setOrdersModalOpen(true) } }, { key: 'filteredOrders', label: t('copyTradingList.filteredOrders') || '已过滤订单', icon: , onClick: () => { setFilteredOrdersModalCopyTradingId(record.id.toString()) setFilteredOrdersModalOpen(true) } } ] }} trigger={['click']} > handleDelete(record.id)} okText={t('common.confirm') || '确定'} cancelText={t('common.cancel') || '取消'} >
) })}
)}
) : ( // 桌面端表格布局 `共 ${total} 条` }} /> )} {/* Modal 组件 */} setOrdersModalOpen(false)} copyTradingId={ordersModalCopyTradingId} defaultTab={ordersModalTab} /> setStatisticsModalOpen(false)} copyTradingId={statisticsModalCopyTradingId} /> setFilteredOrdersModalOpen(false)} copyTradingId={filteredOrdersModalCopyTradingId} /> setEditModalOpen(false)} copyTradingId={editModalCopyTradingId} onSuccess={() => { fetchCopyTradings() }} /> setAddModalOpen(false)} onSuccess={() => { fetchCopyTradings() }} /> ) } export default CopyTradingList