import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import { Card, Table, Button, Space, Tag, Popconfirm, message, List, Empty, Spin, Divider, Typography, Modal, Descriptions, Statistic, Row, Col, Tooltip, Badge } from 'antd' import { PlusOutlined, EditOutlined, DeleteOutlined, GlobalOutlined, EyeOutlined, ReloadOutlined, WalletOutlined, CopyOutlined, LineChartOutlined } from '@ant-design/icons' import { useTranslation } from 'react-i18next' import { apiService } from '../services/api' import type { Leader, LeaderBalanceResponse } from '../types' import { useMediaQuery } from 'react-responsive' import { formatUSDC } from '../utils' const { Text } = Typography const LeaderList: React.FC = () => { const { t, i18n } = useTranslation() const navigate = useNavigate() const isMobile = useMediaQuery({ maxWidth: 768 }) const [leaders, setLeaders] = useState([]) const [loading, setLoading] = useState(false) const [balanceMap, setBalanceMap] = useState>({}) const [balanceLoading, setBalanceLoading] = useState>({}) // 详情 Modal const [detailModalVisible, setDetailModalVisible] = useState(false) const [detailLeader, setDetailLeader] = useState(null) const [detailBalance, setDetailBalance] = useState(null) const [detailBalanceLoading, setDetailBalanceLoading] = useState(false) useEffect(() => { fetchLeaders() }, []) const fetchLeaders = async () => { setLoading(true) try { const response = await apiService.leaders.list() if (response.data.code === 0 && response.data.data) { setLeaders(response.data.data.list || []) } else { message.error(response.data.msg || t('leaderList.fetchFailed')) } } catch (error: any) { message.error(error.message || t('leaderList.fetchFailed')) } finally { setLoading(false) } } // 加载所有 Leader 的余额 useEffect(() => { const loadBalances = async () => { for (const leader of leaders) { if (!balanceMap[leader.id] && !balanceLoading[leader.id]) { setBalanceLoading(prev => ({ ...prev, [leader.id]: true })) try { const balanceData = await apiService.leaders.balance({ leaderId: leader.id }) if (balanceData.data.code === 0 && balanceData.data.data) { setBalanceMap(prev => ({ ...prev, [leader.id]: { total: balanceData.data.data.totalBalance || '0', available: balanceData.data.data.availableBalance || '0', position: balanceData.data.data.positionBalance || '0' } })) } } catch (error) { console.error(`获取 Leader ${leader.id} 余额失败:`, error) setBalanceMap(prev => ({ ...prev, [leader.id]: { total: '-', available: '-', position: '-' } })) } finally { setBalanceLoading(prev => ({ ...prev, [leader.id]: false })) } } } } if (leaders.length > 0) { loadBalances() } }, [leaders]) const handleDelete = async (leaderId: number) => { try { const response = await apiService.leaders.delete({ leaderId }) if (response.data.code === 0) { message.success(t('leaderList.deleteSuccess')) fetchLeaders() } else { message.error(response.data.msg || t('leaderList.deleteFailed')) } } catch (error: any) { message.error(error.message || t('leaderList.deleteFailed')) } } const handleShowDetail = async (leader: Leader) => { try { setDetailModalVisible(true) setDetailLeader(leader) setDetailBalance(null) setDetailBalanceLoading(false) // 加载详情和余额 try { const leaderDetail = await apiService.leaders.detail({ leaderId: leader.id }) if (leaderDetail.data.code === 0 && leaderDetail.data.data) { setDetailLeader(leaderDetail.data.data) } // 加载余额 setDetailBalanceLoading(true) try { const balanceData = await apiService.leaders.balance({ leaderId: leader.id }) if (balanceData.data.code === 0 && balanceData.data.data) { setDetailBalance(balanceData.data.data) } } catch (error) { console.error('获取余额失败:', error) setDetailBalance(null) } finally { setDetailBalanceLoading(false) } } catch (error: any) { console.error('获取 Leader 详情失败:', error) message.error(error.message || t('leaderList.fetchFailed')) setDetailModalVisible(false) setDetailLeader(null) } } catch (error: any) { console.error('打开详情失败:', error) message.error(error.message || t('leaderList.openDetailFailed')) setDetailModalVisible(false) setDetailLeader(null) } } const handleRefreshDetailBalance = async () => { if (!detailLeader) return setDetailBalanceLoading(true) try { const balanceData = await apiService.leaders.balance({ leaderId: detailLeader.id }) if (balanceData.data.code === 0 && balanceData.data.data) { setDetailBalance(balanceData.data.data) message.success(t('leaderDetail.refresh')) } } catch (error: any) { message.error(error.message || t('leaderDetail.fetchBalanceFailed')) } finally { setDetailBalanceLoading(false) } } const formatTimestamp = (timestamp: number) => { const date = new Date(timestamp) return date.toLocaleString(i18n.language || 'zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) } const getPositionColumns = () => { return [ { title: t('leaderDetail.market'), dataIndex: 'title', key: 'title', render: (title: string) => { if (!title) return - const displayText = isMobile && title.length > 20 ? `${title.slice(0, 20)}...` : title return {displayText} } }, { title: t('leaderDetail.side'), dataIndex: 'side', key: 'side', render: (side: string) => { const color = side === 'YES' ? 'green' : 'red' return {side} } }, { title: t('leaderDetail.quantity'), dataIndex: 'quantity', key: 'quantity', render: (quantity: string) => formatUSDC(quantity) }, { title: t('leaderDetail.avgPrice'), dataIndex: 'avgPrice', key: 'avgPrice', render: (price: string) => formatUSDC(price) }, { title: t('leaderDetail.currentValue'), dataIndex: 'currentValue', key: 'currentValue', render: (value: string) => formatUSDC(value) }, { title: t('leaderDetail.pnl'), dataIndex: 'pnl', key: 'pnl', render: (pnl: string | undefined) => { if (!pnl || pnl === '0') { return - } else { const numPnl = parseFloat(pnl) const color = numPnl > 0 ? '#52c41a' : '#ff4d4f' return {formatUSDC(pnl)} } } } ] } const columns = [ { title: t('leaderList.leaderName'), dataIndex: 'leaderName', key: 'leaderName', width: 200, render: (text: string, record: Leader) => ( {text || `Leader ${record.id}`} {record.leaderAddress} ) }, { title: t('leaderList.remark'), dataIndex: 'remark', key: 'remark', width: 180, ellipsis: true, render: (remark: string | undefined) => { if (!remark) return - return {remark} } }, { title: t('leaderDetail.availableBalance'), key: 'balance', width: 180, render: (_: any, record: Leader) => { const balance = balanceMap[record.id] if (!balance) return return ( {balance.available === '-' ? '-' : `${formatUSDC(balance.available)} USDC`} {t('leaderDetail.positionBalance')}: {formatUSDC(balance.position)} ) } }, { title: t('common.actions'), key: 'action', width: 200, fixed: 'right' as const, render: (_: any, record: Leader) => (
handleShowDetail(record)} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '32px', height: '32px', cursor: 'pointer', borderRadius: '6px', transition: 'background-color 0.2s' }} onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f0f0f0'} onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'} >
{record.website && (
window.open(record.website, '_blank', 'noopener,noreferrer')} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '32px', height: '32px', cursor: 'pointer', borderRadius: '6px', transition: 'background-color 0.2s' }} onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f0f0f0'} onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'} >
)}
navigate(`/leaders/edit?id=${record.id}`)} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '32px', height: '32px', cursor: 'pointer', borderRadius: '6px', transition: 'background-color 0.2s' }} onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f0f0f0'} onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'} >
{ if (record.copyTradingCount > 0) { navigate(`/copy-trading?leaderId=${record.id}`) } }} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '32px', height: '32px', cursor: record.copyTradingCount === 0 ? 'not-allowed' : 'pointer', borderRadius: '6px', opacity: record.copyTradingCount === 0 ? 0.4 : 1, transition: 'background-color 0.2s' }} onMouseEnter={(e) => record.copyTradingCount > 0 && (e.currentTarget.style.backgroundColor = '#f0f0f0')} onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'} >
{ if (record.backtestCount > 0) { navigate(`/backtest?leaderId=${record.id}`) } }} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '32px', height: '32px', cursor: record.backtestCount === 0 ? 'not-allowed' : 'pointer', borderRadius: '6px', opacity: record.backtestCount === 0 ? 0.4 : 1, transition: 'background-color 0.2s' }} onMouseEnter={(e) => record.backtestCount > 0 && (e.currentTarget.style.backgroundColor = '#f0f0f0')} onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'} >
0 ? t('leaderList.deleteConfirmDesc', { count: record.copyTradingCount }) : undefined} onConfirm={() => handleDelete(record.id)} okText={t('common.confirm')} cancelText={t('common.cancel')} >
e.currentTarget.style.backgroundColor = '#fff1f0'} onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'} >
) } ] return (

{t('leaderList.title')}

{isMobile ? (
{loading ? (
) : leaders.length === 0 ? ( ) : ( { const balance = balanceMap[leader.id] return ( {/* 头部区域 - 名称和地址 */}
{leader.leaderName || `Leader ${leader.id}`} {leader.website && ( window.open(leader.website, '_blank', 'noopener,noreferrer')} /> )}
{leader.leaderAddress}
{/* 资产区域 - 常驻显示 */}
{t('leaderDetail.availableBalance')}
{balance?.available && balance.available !== '-' ? `${formatUSDC(balance.available)} USDC` : '- USDC'}
{t('leaderDetail.positionBalance')}
{balance?.position && balance.position !== '-' ? formatUSDC(balance.position) : '-'}
{/* 备注区域 */} {leader.remark && (
{t('leaderList.remark')}: {leader.remark}
)} {/* 图标操作栏 */}
handleShowDetail(leader)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }} > {t('common.viewDetail')}
navigate(`/leaders/edit?id=${leader.id}`)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }} > {t('common.edit')}
navigate(`/copy-trading?leaderId=${leader.id}`)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: leader.copyTradingCount === 0 ? 'not-allowed' : 'pointer', padding: '4px 8px', opacity: leader.copyTradingCount === 0 ? 0.4 : 1 }} > {t('leaderList.viewCopyTradings')}
navigate(`/backtest?leaderId=${leader.id}`)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: leader.backtestCount === 0 ? 'not-allowed' : 'pointer', padding: '4px 8px', opacity: leader.backtestCount === 0 ? 0.4 : 1 }} > {t('leaderList.viewBacktests')}
0 ? t('leaderList.deleteConfirmDesc', { count: leader.copyTradingCount }) : undefined} onConfirm={() => handleDelete(leader.id)} okText={t('common.confirm')} cancelText={t('common.cancel')} >
{t('common.delete')}
) }} /> )}
) : ( `共 ${total} 条` }} size="large" style={{ fontSize: '14px' }} /> )} {/* 详情 Modal */} {t('leaderDetail.title')} } open={detailModalVisible} onCancel={() => setDetailModalVisible(false)} footer={[ ]} width={isMobile ? '95%' : 1000} style={{ top: 20 }} > {!detailLeader ? (
) : ( <> {/* 基本信息 */} {t('leaderDetail.basicInfo')} } bordered column={isMobile ? 1 : 2} size={isMobile ? 'small' : 'default'} > {detailLeader.leaderName || `Leader ${detailLeader.id}`} {detailLeader.leaderAddress} {detailLeader.copyTradingCount || 0} {detailLeader.remark || -} {formatTimestamp(detailLeader.updatedAt)} {detailLeader.website ? ( ) : -} {/* 余额信息 */}
{t('leaderDetail.balanceInfo')}
{detailBalanceLoading && !detailBalance ? (
) : detailBalance ? ( <>
formatUSDC(value?.toString() || '0')} /> formatUSDC(value?.toString() || '0')} /> formatUSDC(value?.toString() || '0')} /> {/* 持仓列表 */}
{t('leaderDetail.positions')} {detailBalance.positions?.length || 0}
{detailBalance.positions && detailBalance.positions.length > 0 ? (
`${record.title}-${record.side}-${index}`} pagination={{ pageSize: 10, showSizeChanger: !isMobile }} scroll={{ x: isMobile ? 800 : 'auto' }} size={isMobile ? 'small' : 'middle'} /> ) : ( )} ) : ( )} )} ) } export default LeaderList