feat(frontend): 优化多个列表页面 UI,添加空状态提示
- 优化 AccountList、BacktestList、CopyTradingList、CryptoTailStrategyList、LeaderList、TemplateList、UserList 页面操作栏 - 添加空数据状态提示 - 添加相关多语言翻译 key Made-with: Cursor
This commit is contained in:
@@ -42,7 +42,8 @@
|
||||
"pageOf": "Page",
|
||||
"ascending": "Ascending",
|
||||
"descending": "Descending",
|
||||
"day": "day"
|
||||
"day": "day",
|
||||
"orders": "orders"
|
||||
},
|
||||
"account": {
|
||||
"title": "Account Management",
|
||||
@@ -176,7 +177,8 @@
|
||||
"updateFailed": "Failed to update account",
|
||||
"getDetailFailedForEdit": "Failed to get account detail",
|
||||
"loading": "Loading...",
|
||||
"fetchFailed": "Failed to get account list"
|
||||
"fetchFailed": "Failed to get account list",
|
||||
"noData": "No account data"
|
||||
},
|
||||
"accountImport": {
|
||||
"title": "Import Account",
|
||||
@@ -660,7 +662,8 @@
|
||||
"deleteSuccess": "User deleted successfully",
|
||||
"deleteFailed": "Failed to delete user",
|
||||
"deleteConfirm": "Are you sure you want to delete this user?",
|
||||
"total": "Total {{total}} items"
|
||||
"total": "Total {{total}} items",
|
||||
"noData": "No user data"
|
||||
},
|
||||
"statistics": {
|
||||
"title": "Statistics",
|
||||
@@ -1121,7 +1124,9 @@
|
||||
"updateStatusFailed": "Failed to update copy trading status",
|
||||
"deleteSuccess": "Copy trading deleted successfully",
|
||||
"deleteFailed": "Failed to delete copy trading",
|
||||
"deleteConfirm": "Are you sure you want to delete this copy trading relationship?"
|
||||
"deleteConfirm": "Are you sure you want to delete this copy trading relationship?",
|
||||
"profitRate": "Profit Rate",
|
||||
"noData": "No copy trading configuration"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"title": "Notification Settings",
|
||||
@@ -1351,6 +1356,8 @@
|
||||
"title": "Backtest",
|
||||
"taskName": "Task Name",
|
||||
"leader": "Leader",
|
||||
"balance": "Balance (Init→Final)",
|
||||
"profit": "Profit (Amount/Rate)",
|
||||
"initialBalance": "Initial Balance",
|
||||
"backtestDays": "Backtest Days",
|
||||
"status": "Status",
|
||||
@@ -1399,6 +1406,7 @@
|
||||
"createCopyTradingSuccess": "Copy trading config created successfully",
|
||||
"noTasks": "No backtest tasks",
|
||||
"noTrades": "No trade records",
|
||||
"noData": "No backtest data",
|
||||
"fetchTasksFailed": "Failed to fetch task list",
|
||||
"fetchTaskDetailFailed": "Failed to fetch task detail",
|
||||
"fetchTradesFailed": "Failed to fetch trade records",
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
"copyFailed": "复制失败",
|
||||
"ascending": "升序",
|
||||
"descending": "降序",
|
||||
"day": "天"
|
||||
"day": "天",
|
||||
"orders": "单"
|
||||
},
|
||||
"login": {
|
||||
"title": "登录",
|
||||
@@ -176,7 +177,8 @@
|
||||
"updateFailed": "更新账户失败",
|
||||
"getDetailFailedForEdit": "获取账户详情失败",
|
||||
"loading": "加载中...",
|
||||
"fetchFailed": "获取账户列表失败"
|
||||
"fetchFailed": "获取账户列表失败",
|
||||
"noData": "暂无账户数据"
|
||||
},
|
||||
"accountImport": {
|
||||
"title": "导入账户",
|
||||
@@ -660,7 +662,8 @@
|
||||
"deleteSuccess": "删除用户成功",
|
||||
"deleteFailed": "删除用户失败",
|
||||
"deleteConfirm": "确定要删除这个用户吗?",
|
||||
"total": "共 {{total}} 条"
|
||||
"total": "共 {{total}} 条",
|
||||
"noData": "暂无用户数据"
|
||||
},
|
||||
"statistics": {
|
||||
"title": "统计信息",
|
||||
@@ -1121,7 +1124,9 @@
|
||||
"updateStatusFailed": "更新跟单状态失败",
|
||||
"deleteSuccess": "删除跟单成功",
|
||||
"deleteFailed": "删除跟单失败",
|
||||
"deleteConfirm": "确定要删除这个跟单关系吗?"
|
||||
"deleteConfirm": "确定要删除这个跟单关系吗?",
|
||||
"profitRate": "收益率",
|
||||
"noData": "暂无跟单配置"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"title": "消息推送设置",
|
||||
@@ -1351,6 +1356,8 @@
|
||||
"title": "回测",
|
||||
"taskName": "任务名称",
|
||||
"leader": "Leader",
|
||||
"balance": "资金 (初始→最终)",
|
||||
"profit": "收益 (金额/比例)",
|
||||
"initialBalance": "初始资金",
|
||||
"backtestDays": "回测天数",
|
||||
"status": "状态",
|
||||
@@ -1399,6 +1406,7 @@
|
||||
"createCopyTradingSuccess": "跟单配置创建成功",
|
||||
"noTasks": "暂无回测任务",
|
||||
"noTrades": "暂无交易记录",
|
||||
"noData": "暂无回测数据",
|
||||
"fetchTasksFailed": "获取任务列表失败",
|
||||
"fetchTaskDetailFailed": "获取任务详情失败",
|
||||
"fetchTradesFailed": "获取交易记录失败",
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
"pageOf": "第",
|
||||
"ascending": "升序",
|
||||
"descending": "降序",
|
||||
"day": "天"
|
||||
"day": "天",
|
||||
"orders": "單"
|
||||
},
|
||||
"account": {
|
||||
"title": "賬戶管理",
|
||||
@@ -176,7 +177,8 @@
|
||||
"updateFailed": "更新賬戶失敗",
|
||||
"getDetailFailedForEdit": "獲取賬戶詳情失敗",
|
||||
"loading": "加載中...",
|
||||
"fetchFailed": "獲取賬戶列表失敗"
|
||||
"fetchFailed": "獲取賬戶列表失敗",
|
||||
"noData": "暫無賬戶數據"
|
||||
},
|
||||
"accountImport": {
|
||||
"title": "導入賬戶",
|
||||
@@ -660,7 +662,8 @@
|
||||
"deleteSuccess": "刪除用戶成功",
|
||||
"deleteFailed": "刪除用戶失敗",
|
||||
"deleteConfirm": "確定要刪除這個用戶嗎?",
|
||||
"total": "共 {{total}} 條"
|
||||
"total": "共 {{total}} 條",
|
||||
"noData": "暫無用戶數據"
|
||||
},
|
||||
"statistics": {
|
||||
"title": "統計信息",
|
||||
@@ -1121,7 +1124,9 @@
|
||||
"updateStatusFailed": "更新跟單狀態失敗",
|
||||
"deleteSuccess": "刪除跟單成功",
|
||||
"deleteFailed": "刪除跟單失敗",
|
||||
"deleteConfirm": "確定要刪除這個跟單關係嗎?"
|
||||
"deleteConfirm": "確定要刪除這個跟單關係嗎?",
|
||||
"profitRate": "收益率",
|
||||
"noData": "暫無跟單配置"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"title": "消息推送設置",
|
||||
@@ -1351,6 +1356,8 @@
|
||||
"title": "回測",
|
||||
"taskName": "任務名稱",
|
||||
"leader": "Leader",
|
||||
"balance": "資金 (初始→最終)",
|
||||
"profit": "收益 (金額/比例)",
|
||||
"initialBalance": "初始資金",
|
||||
"backtestDays": "回測天數",
|
||||
"status": "狀態",
|
||||
@@ -1399,6 +1406,7 @@
|
||||
"createCopyTradingSuccess": "跟單配置創建成功",
|
||||
"noTasks": "暫無回測任務",
|
||||
"noTrades": "暫無交易記錄",
|
||||
"noData": "暫無回測數據",
|
||||
"fetchTasksFailed": "獲取任務列表失敗",
|
||||
"fetchTaskDetailFailed": "獲取任務詳情失敗",
|
||||
"fetchTradesFailed": "獲取交易記錄失敗",
|
||||
|
||||
+232
-190
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Table, Button, Space, Tag, Popconfirm, message, Typography, Spin, Modal, Descriptions, Divider, Form, Input, Alert } from 'antd'
|
||||
import { PlusOutlined, ReloadOutlined, EditOutlined, CopyOutlined } from '@ant-design/icons'
|
||||
import { Card, Table, Button, Space, Tag, Popconfirm, message, Typography, Spin, Modal, Descriptions, Divider, Form, Input, Alert, Tooltip, List, Empty } from 'antd'
|
||||
import { PlusOutlined, ReloadOutlined, EditOutlined, CopyOutlined, EyeOutlined, DeleteOutlined, WalletOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAccountStore } from '../store/accountStore'
|
||||
import type { Account } from '../types'
|
||||
@@ -331,171 +331,79 @@ const AccountList: React.FC = () => {
|
||||
{
|
||||
title: t('accountList.action'),
|
||||
key: 'action',
|
||||
width: 140,
|
||||
render: (_: any, record: Account) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => handleShowDetail(record)}
|
||||
>
|
||||
{t('accountList.detail')}
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleShowEdit(record)}
|
||||
>
|
||||
{t('accountList.edit')}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={t('accountList.deleteConfirm')}
|
||||
description={
|
||||
record.apiKeyConfigured
|
||||
? t('accountList.deleteConfirmDesc')
|
||||
: t('accountList.deleteConfirmDescSimple')
|
||||
}
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText={t('accountList.deleteConfirmOk')}
|
||||
cancelText={t('common.cancel')}
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Button type="link" size="small" danger>
|
||||
{t('accountList.delete')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
const mobileColumns = [
|
||||
{
|
||||
title: t('accountList.accountName'),
|
||||
key: 'info',
|
||||
render: (_: any, record: Account) => {
|
||||
return (
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<div style={{
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '8px',
|
||||
fontSize: '16px'
|
||||
}}>
|
||||
{record.accountName || `${t('accountList.accountName')} ${record.id}`}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
color: '#666',
|
||||
marginBottom: '8px',
|
||||
wordBreak: 'break-all',
|
||||
fontFamily: 'monospace',
|
||||
lineHeight: '1.4'
|
||||
}}>
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
<strong>{t('accountList.walletAddress')}:</strong> {record.walletAddress ? `${record.walletAddress.slice(0, 6)}...${record.walletAddress.slice(-4)}` : '-'}
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleCopy(record.walletAddress)
|
||||
}}
|
||||
style={{ marginLeft: '4px', padding: '0 4px' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
<strong>{t('accountList.proxyAddress')}:</strong> {record.proxyAddress ? `${record.proxyAddress.slice(0, 6)}...${record.proxyAddress.slice(-4)}` : '-'}
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleCopy(record.proxyAddress)
|
||||
}}
|
||||
style={{ marginLeft: '4px', padding: '0 4px' }}
|
||||
/>
|
||||
</div>
|
||||
{record.walletType && (
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
<strong>{t('accountList.walletType')}:</strong>{' '}
|
||||
<Tag color={record.walletType.toLowerCase() === 'magic' ? 'purple' : 'blue'} style={{ marginLeft: '4px' }}>
|
||||
{record.walletType.toLowerCase() === 'magic' ? 'Magic' : 'Safe'}
|
||||
</Tag>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
color: '#1890ff'
|
||||
}}>
|
||||
{t('accountList.totalBalance')}: {balanceLoading[record.id] ? (
|
||||
<Spin size="small" style={{ marginLeft: '4px' }} />
|
||||
) : balanceMap[record.id]?.total && balanceMap[record.id].total !== '-' ? (
|
||||
`${formatUSDC(balanceMap[record.id].total)} USDC`
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</div>
|
||||
{balanceMap[record.id] && balanceMap[record.id].available !== '-' && (
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
marginTop: '4px'
|
||||
}}>
|
||||
{t('accountList.available')}: {formatUSDC(balanceMap[record.id].available)} USDC | {t('accountList.position')}: {formatUSDC(balanceMap[record.id].position)} USDC
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('accountList.action'),
|
||||
key: 'action',
|
||||
width: 100,
|
||||
render: (_: any, record: Account) => (
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
block
|
||||
onClick={() => handleShowDetail(record)}
|
||||
style={{ minHeight: '32px' }}
|
||||
>
|
||||
{t('accountList.viewDetail')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
block
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleShowEdit(record)}
|
||||
style={{ minHeight: '32px' }}
|
||||
>
|
||||
{t('accountList.edit')}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={t('accountList.deleteConfirm')}
|
||||
description={
|
||||
record.apiKeyConfigured
|
||||
? t('accountList.deleteConfirmDesc')
|
||||
: t('accountList.deleteConfirmDescSimple')
|
||||
}
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText={t('accountList.deleteConfirmOk')}
|
||||
cancelText={t('common.cancel')}
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
block
|
||||
danger
|
||||
style={{ minHeight: '32px' }}
|
||||
<Space size={4}>
|
||||
<Tooltip title={t('accountList.detail')}>
|
||||
<div
|
||||
onClick={() => 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'}
|
||||
>
|
||||
{t('accountList.delete')}
|
||||
</Button>
|
||||
<EyeOutlined style={{ fontSize: '16px', color: '#1890ff' }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={t('accountList.edit')}>
|
||||
<div
|
||||
onClick={() => handleShowEdit(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'}
|
||||
>
|
||||
<EditOutlined style={{ fontSize: '16px', color: '#1890ff' }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Popconfirm
|
||||
title={t('accountList.deleteConfirm')}
|
||||
description={
|
||||
record.apiKeyConfigured
|
||||
? t('accountList.deleteConfirmDesc')
|
||||
: t('accountList.deleteConfirmDescSimple')
|
||||
}
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText={t('accountList.deleteConfirmOk')}
|
||||
cancelText={t('common.cancel')}
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Tooltip title={t('accountList.delete')}>
|
||||
<div
|
||||
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 = '#fff1f0'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
|
||||
>
|
||||
<DeleteOutlined style={{ fontSize: '16px', color: '#ff4d4f' }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
)
|
||||
@@ -519,16 +427,15 @@ const AccountList: React.FC = () => {
|
||||
<Title level={isMobile ? 3 : 2} style={{ margin: 0, fontSize: isMobile ? '18px' : undefined }}>
|
||||
{t('accountList.title')}
|
||||
</Title>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setAccountImportModalVisible(true)}
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
block={isMobile}
|
||||
style={isMobile ? { minHeight: '44px' } : undefined}
|
||||
>
|
||||
{t('accountList.importAccount')}
|
||||
</Button>
|
||||
<Tooltip title={t('accountList.importAccount')}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setAccountImportModalVisible(true)}
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px', fontSize: isMobile ? '14px' : '16px' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Card style={{
|
||||
@@ -536,21 +443,156 @@ const AccountList: React.FC = () => {
|
||||
borderRadius: isMobile ? '0' : undefined
|
||||
}}>
|
||||
{isMobile ? (
|
||||
<Table
|
||||
dataSource={accounts}
|
||||
columns={mobileColumns}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: false,
|
||||
simple: true,
|
||||
size: 'small'
|
||||
}}
|
||||
scroll={{ x: 'max-content' }}
|
||||
size="small"
|
||||
style={{ fontSize: '14px' }}
|
||||
/>
|
||||
loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : accounts.length === 0 ? (
|
||||
<Empty description={t('accountList.noData')} />
|
||||
) : (
|
||||
<List
|
||||
dataSource={accounts}
|
||||
renderItem={(account) => {
|
||||
const balance = balanceMap[account.id]
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={account.id}
|
||||
style={{
|
||||
marginBottom: '10px',
|
||||
borderRadius: '10px',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.08)',
|
||||
border: '1px solid #e8e8e8',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
bodyStyle={{ padding: '0' }}
|
||||
>
|
||||
{/* 头部区域 */}
|
||||
<div style={{
|
||||
padding: '10px 12px',
|
||||
background: 'var(--ant-color-primary, #1677ff)',
|
||||
color: '#fff'
|
||||
}}>
|
||||
<div style={{ fontSize: '15px', fontWeight: '600', marginBottom: '2px', display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<WalletOutlined style={{ fontSize: '14px' }} />
|
||||
<span>{account.accountName || `${t('accountList.accountName')} ${account.id}`}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '10px', opacity: '0.85', fontFamily: 'monospace', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<span>{account.walletAddress ? `${account.walletAddress.slice(0, 6)}...${account.walletAddress.slice(-4)}` : '-'}</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined style={{ fontSize: '12px', color: 'rgba(255,255,255,0.85)' }} />}
|
||||
onClick={() => handleCopy(account.walletAddress)}
|
||||
style={{ padding: '0 4px', height: 'auto' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 资产区域 */}
|
||||
<div style={{
|
||||
padding: '8px 12px',
|
||||
backgroundColor: '#fafafa',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
minHeight: '42px',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '10px', color: '#8c8c8c' }}>
|
||||
{t('accountList.totalBalance')}
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', fontWeight: '600', color: '#52c41a' }}>
|
||||
{balance?.total && balance.total !== '-' ? `${formatUSDC(balance.total)} USDC` : '- USDC'}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontSize: '10px', color: '#8c8c8c' }}>
|
||||
{t('accountList.walletType')}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px' }}>
|
||||
{account.walletType ? (
|
||||
<Tag color={account.walletType.toLowerCase() === 'magic' ? 'purple' : 'blue'} style={{ margin: 0 }}>
|
||||
{account.walletType.toLowerCase() === 'magic' ? 'Magic' : 'Safe'}
|
||||
</Tag>
|
||||
) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 地址信息区域 */}
|
||||
<div style={{
|
||||
padding: '8px 12px',
|
||||
fontSize: '11px',
|
||||
color: '#8c8c8c',
|
||||
borderBottom: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<span>{t('accountList.proxyAddress')}: {account.proxyAddress ? `${account.proxyAddress.slice(0, 6)}...${account.proxyAddress.slice(-4)}` : '-'}</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined style={{ fontSize: '12px' }} />}
|
||||
onClick={() => handleCopy(account.proxyAddress)}
|
||||
style={{ padding: '0 4px', height: 'auto' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 图标操作栏 */}
|
||||
<div style={{
|
||||
padding: '8px 12px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<Tooltip title={t('accountList.detail')}>
|
||||
<div
|
||||
onClick={() => handleShowDetail(account)}
|
||||
style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}
|
||||
>
|
||||
<EyeOutlined style={{ fontSize: '18px', color: '#1890ff' }} />
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('accountList.detail')}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={t('accountList.edit')}>
|
||||
<div
|
||||
onClick={() => handleShowEdit(account)}
|
||||
style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}
|
||||
>
|
||||
<EditOutlined style={{ fontSize: '18px', color: '#52c41a' }} />
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('accountList.edit')}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Popconfirm
|
||||
title={t('accountList.deleteConfirm')}
|
||||
description={
|
||||
account.apiKeyConfigured
|
||||
? t('accountList.deleteConfirmDesc')
|
||||
: t('accountList.deleteConfirmDescSimple')
|
||||
}
|
||||
onConfirm={() => handleDelete(account)}
|
||||
okText={t('accountList.deleteConfirmOk')}
|
||||
cancelText={t('common.cancel')}
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Tooltip title={t('accountList.delete')}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
|
||||
<DeleteOutlined style={{ fontSize: '18px', color: '#ff4d4f' }} />
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('accountList.delete')}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Table
|
||||
dataSource={accounts}
|
||||
|
||||
+396
-240
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { Table, Card, Button, Select, Tag, Space, Modal, message, Row, Col, Form, Input, InputNumber, Switch, Statistic, Descriptions } from 'antd'
|
||||
import { Table, Card, Button, Select, Tag, Space, Modal, message, Row, Col, Form, Input, InputNumber, Switch, Statistic, Descriptions, List, Empty, Spin, Tooltip, Popconfirm } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PlusOutlined, ReloadOutlined, DeleteOutlined, StopOutlined, EyeOutlined, RedoOutlined, CopyOutlined, SyncOutlined } from '@ant-design/icons'
|
||||
import { formatUSDC } from '../utils'
|
||||
@@ -118,49 +118,35 @@ const BacktestList: React.FC = () => {
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
const handleDelete = (id: number) => {
|
||||
Modal.confirm({
|
||||
title: t('backtest.deleteConfirm'),
|
||||
okText: t('common.confirm'),
|
||||
cancelText: t('common.cancel'),
|
||||
onOk: async () => {
|
||||
try {
|
||||
const response = await backtestService.delete({ id })
|
||||
if (response.data.code === 0) {
|
||||
message.success(t('backtest.deleteSuccess'))
|
||||
fetchTasks()
|
||||
} else {
|
||||
message.error(response.data.msg || t('backtest.deleteFailed'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete backtest task:', error)
|
||||
message.error(t('backtest.deleteFailed'))
|
||||
}
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
const response = await backtestService.delete({ id })
|
||||
if (response.data.code === 0) {
|
||||
message.success(t('backtest.deleteSuccess'))
|
||||
fetchTasks()
|
||||
} else {
|
||||
message.error(response.data.msg || t('backtest.deleteFailed'))
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to delete backtest task:', error)
|
||||
message.error(t('backtest.deleteFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
// 停止任务
|
||||
const handleStop = (id: number) => {
|
||||
Modal.confirm({
|
||||
title: t('backtest.stopConfirm'),
|
||||
okText: t('common.confirm'),
|
||||
cancelText: t('common.cancel'),
|
||||
onOk: async () => {
|
||||
try {
|
||||
const response = await backtestService.stop({ id })
|
||||
if (response.data.code === 0) {
|
||||
message.success(t('backtest.stopSuccess'))
|
||||
fetchTasks()
|
||||
} else {
|
||||
message.error(response.data.msg || t('backtest.stopFailed'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to stop backtest task:', error)
|
||||
message.error(t('backtest.stopFailed'))
|
||||
}
|
||||
const handleStop = async (id: number) => {
|
||||
try {
|
||||
const response = await backtestService.stop({ id })
|
||||
if (response.data.code === 0) {
|
||||
message.success(t('backtest.stopSuccess'))
|
||||
fetchTasks()
|
||||
} else {
|
||||
message.error(response.data.msg || t('backtest.stopFailed'))
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to stop backtest task:', error)
|
||||
message.error(t('backtest.stopFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
// 按配置重新测试(仅已完成任务)
|
||||
@@ -502,168 +488,227 @@ const BacktestList: React.FC = () => {
|
||||
title: t('backtest.taskName'),
|
||||
dataIndex: 'taskName',
|
||||
key: 'taskName',
|
||||
width: isMobile ? 120 : 150
|
||||
width: isMobile ? 120 : 140,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: t('backtest.leader'),
|
||||
dataIndex: 'leaderName',
|
||||
key: 'leaderName',
|
||||
width: isMobile ? 100 : 150,
|
||||
width: isMobile ? 100 : 120,
|
||||
ellipsis: true,
|
||||
render: (_: any, record: BacktestTaskDto) => record.leaderName || `Leader ${record.leaderId}`
|
||||
},
|
||||
{
|
||||
title: t('backtest.initialBalance'),
|
||||
dataIndex: 'initialBalance',
|
||||
key: 'initialBalance',
|
||||
width: 120,
|
||||
render: (value: string) => formatUSDC(value)
|
||||
title: t('backtest.balance'),
|
||||
key: 'balance',
|
||||
width: 160,
|
||||
render: (_: any, record: BacktestTaskDto) => (
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#8c8c8c' }}>{formatUSDC(record.initialBalance)}</div>
|
||||
<div style={{ fontWeight: 500 }}>{record.finalBalance ? formatUSDC(record.finalBalance) : '-'}</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('backtest.finalBalance'),
|
||||
dataIndex: 'finalBalance',
|
||||
key: 'finalBalance',
|
||||
width: 120,
|
||||
render: (value: string | null) => value ? formatUSDC(value) : '-'
|
||||
},
|
||||
{
|
||||
title: t('backtest.profitAmount'),
|
||||
dataIndex: 'profitAmount',
|
||||
key: 'profitAmount',
|
||||
width: 120,
|
||||
render: (value: string | null) => value ? (
|
||||
<span style={{ color: parseFloat(value) >= 0 ? '#52c41a' : '#ff4d4f' }}>
|
||||
{formatUSDC(value)}
|
||||
</span>
|
||||
) : '-'
|
||||
},
|
||||
{
|
||||
title: t('backtest.profitRate'),
|
||||
dataIndex: 'profitRate',
|
||||
key: 'profitRate',
|
||||
width: 100,
|
||||
render: (value: string | null) => value ? (
|
||||
<span style={{ color: parseFloat(value) >= 0 ? '#52c41a' : '#ff4d4f' }}>
|
||||
{value}%
|
||||
</span>
|
||||
) : '-'
|
||||
},
|
||||
{
|
||||
title: t('backtest.backtestDays'),
|
||||
dataIndex: 'backtestDays',
|
||||
key: 'backtestDays',
|
||||
width: 100,
|
||||
render: (value: number) => `${value} ${t('common.day')}`
|
||||
title: t('backtest.profit'),
|
||||
key: 'profit',
|
||||
width: 140,
|
||||
render: (_: any, record: BacktestTaskDto) => {
|
||||
const profitAmount = record.profitAmount ? parseFloat(record.profitAmount) : null
|
||||
const profitRate = record.profitRate ? parseFloat(record.profitRate) : null
|
||||
const color = profitAmount !== null ? (profitAmount >= 0 ? '#52c41a' : '#ff4d4f') : undefined
|
||||
return (
|
||||
<div>
|
||||
<div style={{ fontWeight: 500, color }}>
|
||||
{profitAmount !== null ? formatUSDC(record.profitAmount) : '-'}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color }}>
|
||||
{profitRate !== null ? `${record.profitRate}%` : '-'}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('backtest.status'),
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (status: string) => (
|
||||
<Tag color={getStatusColor(status)}>{getStatusText(status)}</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('backtest.progress'),
|
||||
dataIndex: 'progress',
|
||||
key: 'progress',
|
||||
width: 120,
|
||||
render: (progress: number) => (
|
||||
<div style={{ width: '100%' }}>
|
||||
<div style={{ marginBottom: 4 }}>{progress}%</div>
|
||||
<div style={{ width: '100%', height: 6, backgroundColor: '#f0f0f0', borderRadius: 3 }}>
|
||||
<div
|
||||
style={{
|
||||
width: `${progress}%`,
|
||||
height: '100%',
|
||||
backgroundColor: progress === 100 ? '#52c41a' : '#1890ff',
|
||||
borderRadius: 3,
|
||||
transition: 'width 0.3s ease'
|
||||
}}
|
||||
/>
|
||||
width: 130,
|
||||
render: (status: string, record: BacktestTaskDto) => {
|
||||
const isRunning = status === 'RUNNING' || status === 'PENDING'
|
||||
return (
|
||||
<div>
|
||||
<Tag color={getStatusColor(status)}>{getStatusText(status)}</Tag>
|
||||
{isRunning && record.progress !== undefined && (
|
||||
<div style={{ marginTop: 4, fontSize: 11, color: '#8c8c8c' }}>
|
||||
{record.progress}%
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('backtest.totalTrades'),
|
||||
dataIndex: 'totalTrades',
|
||||
key: 'totalTrades',
|
||||
width: 100
|
||||
width: 80,
|
||||
align: 'center' as const
|
||||
},
|
||||
{
|
||||
title: t('backtest.createdAt'),
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: isMobile ? 150 : 180,
|
||||
width: isMobile ? 150 : 150,
|
||||
render: (timestamp: number) => new Date(timestamp).toLocaleString()
|
||||
},
|
||||
{
|
||||
title: t('common.actions'),
|
||||
key: 'actions',
|
||||
fixed: isMobile ? false : ('right' as const),
|
||||
width: isMobile ? 100 : 150,
|
||||
width: isMobile ? 100 : 180,
|
||||
render: (_: any, record: BacktestTaskDto) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleViewDetail(record.id)}
|
||||
>
|
||||
{t('common.viewDetail')}
|
||||
</Button>
|
||||
<Space size={4}>
|
||||
<Tooltip title={t('common.viewDetail')}>
|
||||
<div
|
||||
onClick={() => handleViewDetail(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'}
|
||||
>
|
||||
<EyeOutlined style={{ fontSize: '16px', color: '#1890ff' }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{record.status === 'COMPLETED' && (
|
||||
<>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<SyncOutlined />}
|
||||
onClick={() => handleRerun(record)}
|
||||
>
|
||||
{t('backtest.rerun')}
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCreateCopyTrading(record)}
|
||||
>
|
||||
{t('backtest.createCopyTrading')}
|
||||
</Button>
|
||||
<Tooltip title={t('backtest.rerun')}>
|
||||
<div
|
||||
onClick={() => handleRerun(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'}
|
||||
>
|
||||
<SyncOutlined style={{ fontSize: '16px', color: '#1890ff' }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('backtest.createCopyTrading')}>
|
||||
<div
|
||||
onClick={() => handleCreateCopyTrading(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'}
|
||||
>
|
||||
<CopyOutlined style={{ fontSize: '16px', color: '#1890ff' }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
{record.status === 'RUNNING' && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
icon={<StopOutlined />}
|
||||
onClick={() => handleStop(record.id)}
|
||||
<Popconfirm
|
||||
title={t('backtest.stopConfirm')}
|
||||
onConfirm={() => handleStop(record.id)}
|
||||
okText={t('common.confirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
>
|
||||
{t('backtest.stop')}
|
||||
</Button>
|
||||
<Tooltip title={t('backtest.stop')}>
|
||||
<div
|
||||
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'}
|
||||
>
|
||||
<StopOutlined style={{ fontSize: '16px', color: '#1890ff' }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
)}
|
||||
|
||||
{(record.status === 'STOPPED' || record.status === 'FAILED') && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<RedoOutlined />}
|
||||
onClick={() => handleRetry(record.id)}
|
||||
>
|
||||
{t('backtest.retry')}
|
||||
</Button>
|
||||
<Tooltip title={t('backtest.retry')}>
|
||||
<div
|
||||
onClick={() => handleRetry(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'}
|
||||
>
|
||||
<RedoOutlined style={{ fontSize: '16px', color: '#1890ff' }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{(record.status === 'PENDING' || record.status === 'COMPLETED' || record.status === 'STOPPED' || record.status === 'FAILED') && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDelete(record.id)}
|
||||
<Popconfirm
|
||||
title={t('backtest.deleteConfirm')}
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText={t('common.confirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
<Tooltip title={t('common.delete')}>
|
||||
<div
|
||||
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 = '#fff1f0'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
|
||||
>
|
||||
<DeleteOutlined style={{ fontSize: '16px', color: '#ff4d4f' }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
@@ -671,94 +716,205 @@ const BacktestList: React.FC = () => {
|
||||
]
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<Card>
|
||||
<div style={{ padding: isMobile ? 0 : 24 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px', flexWrap: 'wrap', gap: '12px', padding: isMobile ? '0 8px' : 0 }}>
|
||||
<h2 style={{ margin: 0, fontSize: isMobile ? '20px' : '24px' }}>{t('backtest.title')}</h2>
|
||||
<Space size={8}>
|
||||
<Tooltip title={t('backtest.createTask')}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleCreate}
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px', fontSize: isMobile ? '14px' : '16px' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('common.refresh')}>
|
||||
<Button icon={<ReloadOutlined />} onClick={handleRefresh} loading={loading} size={isMobile ? 'middle' : 'large'} style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px' }} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Card style={{ borderRadius: '12px', boxShadow: '0 2px 8px rgba(0,0,0,0.08)', border: '1px solid #e8e8e8' }} bodyStyle={{ padding: isMobile ? '12px' : '24px' }}>
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
{/* 头部操作栏 */}
|
||||
<Row justify="space-between" align="middle" gutter={[16, 16]}>
|
||||
<Col xs={24} sm={24} md={12} lg={16}>
|
||||
<Space size="middle" direction={isMobile ? 'vertical' : 'horizontal'} style={{ width: isMobile ? '100%' : 'auto' }}>
|
||||
<LeaderSelect
|
||||
style={{ width: isMobile ? '100%' : 180 }}
|
||||
placeholder={t('backtest.leader')}
|
||||
allowClear
|
||||
value={leaderIdFilter}
|
||||
onChange={(value) => setLeaderIdFilter(value)}
|
||||
leaders={leaders}
|
||||
/>
|
||||
<Select
|
||||
style={{ width: isMobile ? '100%' : 150 }}
|
||||
placeholder={t('backtest.status')}
|
||||
allowClear
|
||||
onChange={(value) => setStatusFilter(value)}
|
||||
value={statusFilter}
|
||||
>
|
||||
<Select.Option value="PENDING">{t('backtest.statusPending')}</Select.Option>
|
||||
<Select.Option value="RUNNING">{t('backtest.statusRunning')}</Select.Option>
|
||||
<Select.Option value="COMPLETED">{t('backtest.statusCompleted')}</Select.Option>
|
||||
<Select.Option value="STOPPED">{t('backtest.statusStopped')}</Select.Option>
|
||||
<Select.Option value="FAILED">{t('backtest.statusFailed')}</Select.Option>
|
||||
</Select>
|
||||
<Select
|
||||
style={{ width: isMobile ? '100%' : 150 }}
|
||||
placeholder={t('backtest.sortBy')}
|
||||
onChange={(value) => setSortBy(value)}
|
||||
value={sortBy}
|
||||
>
|
||||
<Select.Option value="profitAmount">{t('backtest.profitAmount')}</Select.Option>
|
||||
<Select.Option value="profitRate">{t('backtest.profitRate')}</Select.Option>
|
||||
<Select.Option value="createdAt">{t('backtest.createdAt')}</Select.Option>
|
||||
</Select>
|
||||
<Select
|
||||
style={{ width: isMobile ? '100%' : 120 }}
|
||||
placeholder={t('backtest.sortOrder')}
|
||||
onChange={(value) => setSortOrder(value)}
|
||||
value={sortOrder}
|
||||
>
|
||||
<Select.Option value="asc">{t('common.ascending')}</Select.Option>
|
||||
<Select.Option value="desc">{t('common.descending')}</Select.Option>
|
||||
</Select>
|
||||
</Space>
|
||||
{/* 筛选栏 */}
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: isMobile ? 12 : 0 }}>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<LeaderSelect
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('backtest.leader')}
|
||||
allowClear
|
||||
value={leaderIdFilter}
|
||||
onChange={(value) => setLeaderIdFilter(value)}
|
||||
leaders={leaders}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={12} lg={8} style={{ textAlign: isMobile ? 'left' : 'right' }}>
|
||||
<Space style={{ width: isMobile ? '100%' : 'auto' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleCreate}
|
||||
style={{ flex: isMobile ? 1 : undefined }}
|
||||
>
|
||||
{isMobile ? t('common.create') : t('backtest.createTask')}
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleRefresh}
|
||||
loading={loading}
|
||||
style={{ flex: isMobile ? 1 : undefined }}
|
||||
>
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
</Space>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('backtest.status')}
|
||||
allowClear
|
||||
onChange={(value) => setStatusFilter(value)}
|
||||
value={statusFilter}
|
||||
>
|
||||
<Select.Option value="PENDING">{t('backtest.statusPending')}</Select.Option>
|
||||
<Select.Option value="RUNNING">{t('backtest.statusRunning')}</Select.Option>
|
||||
<Select.Option value="COMPLETED">{t('backtest.statusCompleted')}</Select.Option>
|
||||
<Select.Option value="STOPPED">{t('backtest.statusStopped')}</Select.Option>
|
||||
<Select.Option value="FAILED">{t('backtest.statusFailed')}</Select.Option>
|
||||
</Select>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Select style={{ width: '100%' }} placeholder={t('backtest.sortBy')} onChange={(value) => setSortBy(value)} value={sortBy}>
|
||||
<Select.Option value="profitAmount">{t('backtest.profitAmount')}</Select.Option>
|
||||
<Select.Option value="profitRate">{t('backtest.profitRate')}</Select.Option>
|
||||
<Select.Option value="createdAt">{t('backtest.createdAt')}</Select.Option>
|
||||
</Select>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Select style={{ width: '100%' }} placeholder={t('backtest.sortOrder')} onChange={(value) => setSortOrder(value)} value={sortOrder}>
|
||||
<Select.Option value="asc">{t('common.ascending')}</Select.Option>
|
||||
<Select.Option value="desc">{t('common.descending')}</Select.Option>
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 数据表格 */}
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tasks}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: size,
|
||||
total,
|
||||
showSizeChanger: false,
|
||||
showTotal: (total) => `${t('common.total')} ${total} ${t('common.items')}`,
|
||||
onChange: (newPage) => setPage(newPage),
|
||||
simple: isMobile
|
||||
}}
|
||||
scroll={isMobile ? { x: 1200 } : { x: 1400 }}
|
||||
/>
|
||||
{/* 数据:移动端卡片 / 桌面端表格 */}
|
||||
{isMobile ? (
|
||||
<>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : tasks.length === 0 ? (
|
||||
<Empty description={t('backtest.noData') || t('common.noData')} />
|
||||
) : (
|
||||
<List
|
||||
dataSource={tasks}
|
||||
renderItem={(task) => (
|
||||
<Card
|
||||
key={task.id}
|
||||
style={{
|
||||
marginBottom: '10px',
|
||||
borderRadius: '10px',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.08)',
|
||||
border: '1px solid #e8e8e8',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<div style={{
|
||||
padding: '10px 12px',
|
||||
background: 'var(--ant-color-primary, #1677ff)',
|
||||
color: '#fff'
|
||||
}}>
|
||||
<div style={{ fontSize: '15px', fontWeight: '600', marginBottom: '2px' }}>{task.taskName}</div>
|
||||
<div style={{ fontSize: '12px', opacity: '0.9' }}>{task.leaderName || `Leader ${task.leaderId}`}</div>
|
||||
</div>
|
||||
<div style={{ padding: '8px 12px', backgroundColor: '#fafafa', borderBottom: '1px solid #f0f0f0', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '10px', color: '#8c8c8c' }}>{t('backtest.profitAmount')}</div>
|
||||
{task.profitAmount != null ? (
|
||||
<div style={{ fontSize: '14px', fontWeight: '600', color: parseFloat(task.profitAmount) >= 0 ? '#52c41a' : '#ff4d4f' }}>{formatUSDC(task.profitAmount)} USDC</div>
|
||||
) : (
|
||||
<div style={{ fontSize: '14px', color: '#8c8c8c' }}>-</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontSize: '10px', color: '#8c8c8c' }}>{t('backtest.profitRate')}</div>
|
||||
{task.profitRate != null ? (
|
||||
<div style={{ fontSize: '14px', fontWeight: '600', color: parseFloat(task.profitRate) >= 0 ? '#52c41a' : '#ff4d4f' }}>{task.profitRate}%</div>
|
||||
) : (
|
||||
<div style={{ fontSize: '14px', color: '#8c8c8c' }}>-</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontSize: '10px', color: '#8c8c8c' }}>{t('backtest.status')}</div>
|
||||
<Tag color={getStatusColor(task.status)}>{getStatusText(task.status)}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '6px 12px', fontSize: '11px', color: '#8c8c8c', borderBottom: '1px solid #f0f0f0' }}>
|
||||
{t('backtest.initialBalance')}: {formatUSDC(task.initialBalance)} · {t('backtest.backtestDays')}: {task.backtestDays} {t('common.day')} · {t('backtest.progress')}: {task.progress}%
|
||||
</div>
|
||||
<div style={{ padding: '8px 12px', display: 'flex', justifyContent: 'space-around', alignItems: 'center', flexWrap: 'wrap', gap: 4 }}>
|
||||
<Tooltip title={t('common.viewDetail')}>
|
||||
<div onClick={() => handleViewDetail(task.id)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
|
||||
<EyeOutlined style={{ fontSize: '18px', color: '#1890ff' }} />
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('common.viewDetail')}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{task.status === 'COMPLETED' && (
|
||||
<>
|
||||
<Tooltip title={t('backtest.rerun')}>
|
||||
<div onClick={() => handleRerun(task)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
|
||||
<SyncOutlined style={{ fontSize: '18px', color: '#1890ff' }} />
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('backtest.rerun')}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('backtest.createCopyTrading')}>
|
||||
<div onClick={() => handleCreateCopyTrading(task)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
|
||||
<CopyOutlined style={{ fontSize: '18px', color: '#1890ff' }} />
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('backtest.createCopyTrading')}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
{task.status === 'RUNNING' && (
|
||||
<Tooltip title={t('backtest.stop')}>
|
||||
<div onClick={() => handleStop(task.id)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
|
||||
<StopOutlined style={{ fontSize: '18px', color: '#1890ff' }} />
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('backtest.stop')}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{(task.status === 'STOPPED' || task.status === 'FAILED') && (
|
||||
<Tooltip title={t('backtest.retry')}>
|
||||
<div onClick={() => handleRetry(task.id)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
|
||||
<RedoOutlined style={{ fontSize: '18px', color: '#1890ff' }} />
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('backtest.retry')}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{(task.status === 'PENDING' || task.status === 'COMPLETED' || task.status === 'STOPPED' || task.status === 'FAILED') && (
|
||||
<Tooltip title={t('common.delete')}>
|
||||
<div onClick={() => handleDelete(task.id)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
|
||||
<DeleteOutlined style={{ fontSize: '18px', color: '#ff4d4f' }} />
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('common.delete')}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{/* 移动端分页 */}
|
||||
{!loading && tasks.length > 0 && total > size && (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', marginTop: 16 }}>
|
||||
<Button size="small" disabled={page <= 1} onClick={() => setPage(p => Math.max(1, p - 1))}>{t('common.prev')}</Button>
|
||||
<span style={{ margin: '0 12px', lineHeight: '24px' }}>{page} / {Math.ceil(total / size)}</span>
|
||||
<Button size="small" disabled={page >= Math.ceil(total / size)} onClick={() => setPage(p => p + 1)}>{t('common.next')}</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tasks}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: size,
|
||||
total,
|
||||
showSizeChanger: false,
|
||||
showTotal: (totalCount) => `${t('common.total')} ${totalCount} ${t('common.items')}`,
|
||||
onChange: (newPage) => setPage(newPage),
|
||||
simple: isMobile
|
||||
}}
|
||||
scroll={{ x: 1000 }}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { Card, Table, Button, Space, Tag, Popconfirm, Switch, message, Select, Dropdown, Spin, List, Empty, Tooltip } from 'antd'
|
||||
import { PlusOutlined, DeleteOutlined, BarChartOutlined, UnorderedListOutlined, ArrowUpOutlined, ArrowDownOutlined, EditOutlined, WalletOutlined, UserOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { apiService } from '../services/api'
|
||||
@@ -384,19 +384,21 @@ const CopyTradingList: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
|
||||
<h2 style={{ margin: 0 }}>{t('copyTradingList.title') || '跟单配置管理'}</h2>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px', flexWrap: 'wrap', gap: '12px' }}>
|
||||
<h2 style={{ margin: 0, fontSize: isMobile ? '20px' : '24px' }}>{t('copyTradingList.title') || '跟单配置管理'}</h2>
|
||||
<Tooltip title={t('copyTradingList.addCopyTrading') || '新增跟单'}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setAddModalOpen(true)}
|
||||
>
|
||||
{t('copyTradingList.addCopyTrading') || '新增跟单'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16, display: 'flex', gap: 16, flexWrap: 'wrap' }}>
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px', fontSize: isMobile ? '14px' : '16px' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Card style={{ borderRadius: '12px', boxShadow: '0 2px 8px rgba(0,0,0,0.08)', border: '1px solid #e8e8e8' }} bodyStyle={{ padding: isMobile ? '12px' : '24px' }}>
|
||||
<div style={{ marginBottom: 16, display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
||||
<Select
|
||||
placeholder={t('copyTradingList.filterWallet') || '筛选钱包'}
|
||||
allowClear
|
||||
@@ -421,14 +423,14 @@ const CopyTradingList: React.FC = () => {
|
||||
/>
|
||||
|
||||
<Select
|
||||
placeholder="筛选状态"
|
||||
placeholder={t('common.status') || '状态'}
|
||||
allowClear
|
||||
style={{ width: isMobile ? '100%' : 150 }}
|
||||
value={filters.enabled}
|
||||
onChange={(value) => setFilters({ ...filters, enabled: value !== undefined ? value : undefined })}
|
||||
>
|
||||
<Option value={true}>开启</Option>
|
||||
<Option value={false}>停止</Option>
|
||||
<Option value={true}>{t('common.enabled') || '开启'}</Option>
|
||||
<Option value={false}>{t('common.disabled') || '停止'}</Option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -440,207 +442,180 @@ const CopyTradingList: React.FC = () => {
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : copyTradings.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
|
||||
暂无跟单配置
|
||||
</div>
|
||||
<Empty description={t('copyTradingList.noData') || '暂无跟单配置'} />
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{copyTradings.map((record) => {
|
||||
<List
|
||||
dataSource={copyTradings}
|
||||
renderItem={(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'
|
||||
marginBottom: '10px',
|
||||
borderRadius: '10px',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.08)',
|
||||
border: '1px solid #e8e8e8',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
bodyStyle={{ padding: '16px' }}
|
||||
bodyStyle={{ padding: '0' }}
|
||||
>
|
||||
{/* 基本信息 */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '8px',
|
||||
color: '#1890ff'
|
||||
}}>
|
||||
{record.configName || t('copyTradingList.configNameNotProvided') || '未提供'}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
marginBottom: '8px',
|
||||
color: '#666'
|
||||
}}>
|
||||
{record.copyMode === 'RATIO'
|
||||
? `${t('copyTradingList.ratioMode') || '比例'} ${(parseFloat(record.copyRatio || '0') * 100).toFixed(2).replace(/\.0+$/, '')}%`
|
||||
: `${t('copyTradingList.fixedAmountMode') || '固定'} ${formatUSDC(record.fixedAmount || '0')}`
|
||||
}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Tag color={record.enabled ? 'green' : 'red'}>
|
||||
{record.enabled ? '启用' : '禁用'}
|
||||
</Tag>
|
||||
{/* 头部区域 - 配置名称 */}
|
||||
<div style={{
|
||||
padding: '10px 12px',
|
||||
background: 'var(--ant-color-primary, #1677ff)',
|
||||
color: '#fff'
|
||||
}}>
|
||||
<div style={{ fontSize: '15px', fontWeight: '600', marginBottom: '2px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<span>{record.configName || t('copyTradingList.configNameNotProvided') || '未提供'}</span>
|
||||
<Switch
|
||||
checked={record.enabled}
|
||||
onChange={() => handleToggleStatus(record)}
|
||||
checkedChildren="开启"
|
||||
unCheckedChildren="停止"
|
||||
checkedChildren={t('copyTradingList.enabled') || '开启'}
|
||||
unCheckedChildren={t('copyTradingList.disabled') || '停止'}
|
||||
size="small"
|
||||
/>
|
||||
</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 style={{ fontSize: '12px', opacity: '0.9' }}>
|
||||
{record.copyMode === 'RATIO'
|
||||
? `${t('copyTradingList.ratioMode') || '比例'} ${(parseFloat(record.copyRatio || '0') * 100).toFixed(0).replace(/\.0+$/, '')}%`
|
||||
: `${t('copyTradingList.fixedAmountMode') || '固定'} ${formatUSDC(record.fixedAmount || '0')} USDC`
|
||||
}
|
||||
</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 style={{
|
||||
padding: '8px 12px',
|
||||
backgroundColor: '#fafafa',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
minHeight: '42px',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '10px', color: '#8c8c8c' }}>
|
||||
{t('copyTradingList.totalPnl') || '总盈亏'}
|
||||
</div>
|
||||
{stats ? (
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
color: getPnlColor(stats.totalPnl),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}>
|
||||
{getPnlIcon(stats.totalPnl)}
|
||||
{formatUSDC(stats.totalPnl)} USDC
|
||||
</div>
|
||||
) : loadingStatistics.has(record.id) ? (
|
||||
<Spin size="small" />
|
||||
) : (
|
||||
<div style={{ fontSize: '14px', color: '#8c8c8c' }}>-</div>
|
||||
)}
|
||||
</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}
|
||||
{stats && (
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontSize: '10px', color: '#8c8c8c' }}>
|
||||
{t('copyTradingList.profitRate') || '收益率'}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
fontWeight: '500',
|
||||
color: getPnlColor(stats.totalPnlPercent)
|
||||
}}>
|
||||
{formatPercent(stats.totalPnlPercent)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => {
|
||||
setEditModalCopyTradingId(record.id.toString())
|
||||
setEditModalOpen(true)
|
||||
}}
|
||||
style={{ flex: 1, minWidth: '80px' }}
|
||||
>
|
||||
{t('common.edit') || '编辑'}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<BarChartOutlined />}
|
||||
onClick={() => {
|
||||
setStatisticsModalCopyTradingId(record.id.toString())
|
||||
setStatisticsModalOpen(true)
|
||||
}}
|
||||
style={{ flex: 1, minWidth: '80px' }}
|
||||
>
|
||||
{t('copyTradingList.statistics') || '统计'}
|
||||
</Button>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'matchedOrders',
|
||||
label: t('copyTradingList.matchedOrders') || '已成交订单',
|
||||
icon: <UnorderedListOutlined />,
|
||||
onClick: () => {
|
||||
setOrdersModalCopyTradingId(record.id.toString())
|
||||
setOrdersModalTab('buy')
|
||||
setOrdersModalOpen(true)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'filteredOrders',
|
||||
label: t('copyTradingList.filteredOrders') || '已过滤订单',
|
||||
icon: <UnorderedListOutlined />,
|
||||
onClick: () => {
|
||||
setFilteredOrdersModalCopyTradingId(record.id.toString())
|
||||
setFilteredOrdersModalOpen(true)
|
||||
}
|
||||
}
|
||||
]
|
||||
}}
|
||||
trigger={['click']}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<UnorderedListOutlined />}
|
||||
style={{ flex: 1, minWidth: '80px' }}
|
||||
|
||||
{/* 账户和Leader信息区域 */}
|
||||
<div style={{
|
||||
padding: '8px 12px',
|
||||
fontSize: '11px',
|
||||
color: '#8c8c8c',
|
||||
borderBottom: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '4px' }}>
|
||||
<WalletOutlined style={{ fontSize: '12px', marginRight: '4px', color: '#1890ff' }} />
|
||||
<span>{t('copyTradingList.wallet') || '账户'}: {record.accountName || `#${record.accountId}`}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<UserOutlined style={{ fontSize: '12px', marginRight: '4px', color: '#722ed1' }} />
|
||||
<span>Leader: {record.leaderName || `#${record.leaderId}`}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 图标操作栏 */}
|
||||
<div style={{
|
||||
padding: '8px 12px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<Tooltip title={t('common.edit') || '编辑'}>
|
||||
<div
|
||||
onClick={() => {
|
||||
setEditModalCopyTradingId(record.id.toString())
|
||||
setEditModalOpen(true)
|
||||
}}
|
||||
style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}
|
||||
>
|
||||
{t('copyTradingList.orders') || '订单'}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<EditOutlined style={{ fontSize: '18px', color: '#1890ff' }} />
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('common.edit') || '编辑'}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={t('copyTradingList.statistics') || '统计'}>
|
||||
<div
|
||||
onClick={() => {
|
||||
setStatisticsModalCopyTradingId(record.id.toString())
|
||||
setStatisticsModalOpen(true)
|
||||
}}
|
||||
style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}
|
||||
>
|
||||
<BarChartOutlined style={{ fontSize: '18px', color: '#1890ff' }} />
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('copyTradingList.statistics') || '统计'}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={t('copyTradingList.orders') || '订单'}>
|
||||
<div
|
||||
onClick={() => {
|
||||
setOrdersModalCopyTradingId(record.id.toString())
|
||||
setOrdersModalTab('buy')
|
||||
setOrdersModalOpen(true)
|
||||
}}
|
||||
style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}
|
||||
>
|
||||
<UnorderedListOutlined style={{ fontSize: '18px', color: '#1890ff' }} />
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('copyTradingList.orders') || '订单'}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Popconfirm
|
||||
title={t('copyTradingList.deleteConfirm') || '确定要删除这个跟单关系吗?'}
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText={t('common.confirm') || '确定'}
|
||||
cancelText={t('common.cancel') || '取消'}
|
||||
>
|
||||
<Button
|
||||
danger
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
style={{ flex: 1, minWidth: '80px' }}
|
||||
>
|
||||
{t('common.delete') || '删除'}
|
||||
</Button>
|
||||
<Tooltip title={t('common.delete') || '删除'}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
|
||||
<DeleteOutlined style={{ fontSize: '18px', color: '#ff4d4f' }} />
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('common.delete') || '删除'}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -21,12 +21,11 @@ import {
|
||||
Tabs,
|
||||
DatePicker,
|
||||
Empty,
|
||||
Typography,
|
||||
Divider
|
||||
Typography
|
||||
} from 'antd'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import dayjs from 'dayjs'
|
||||
import { PlusOutlined, EditOutlined, UnorderedListOutlined, InfoCircleOutlined, WarningOutlined, CalendarOutlined, FileTextOutlined } from '@ant-design/icons'
|
||||
import { PlusOutlined, EditOutlined, UnorderedListOutlined, InfoCircleOutlined, WarningOutlined, CalendarOutlined, FileTextOutlined, DeleteOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { apiService } from '../services/api'
|
||||
@@ -543,17 +542,28 @@ const CryptoTailStrategyList: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: isMobile ? 12 : 24 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16, flexWrap: 'wrap' }}>
|
||||
<h1 style={{ margin: 0, fontSize: isMobile ? 20 : 24 }}>{t('cryptoTailStrategy.list.title')}</h1>
|
||||
<Button
|
||||
type="link"
|
||||
icon={<FileTextOutlined />}
|
||||
onClick={() => window.open(getGuideUrl(), '_blank')}
|
||||
style={{ padding: 0, height: 'auto', fontSize: isMobile ? 14 : 16 }}
|
||||
>
|
||||
{t('cryptoTailStrategy.list.configGuide')}
|
||||
</Button>
|
||||
<div style={{ padding: isMobile ? 0 : 24 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px', flexWrap: 'wrap', gap: '12px', padding: isMobile ? '0 8px' : 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
|
||||
<h2 style={{ margin: 0, fontSize: isMobile ? '20px' : '24px' }}>{t('cryptoTailStrategy.list.title')}</h2>
|
||||
<Button
|
||||
type="link"
|
||||
icon={<FileTextOutlined />}
|
||||
onClick={() => window.open(getGuideUrl(), '_blank')}
|
||||
style={{ padding: 0, height: 'auto', fontSize: isMobile ? 14 : 16 }}
|
||||
>
|
||||
{t('cryptoTailStrategy.list.configGuide')}
|
||||
</Button>
|
||||
</div>
|
||||
<Tooltip title={t('cryptoTailStrategy.list.addStrategy')}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={openAddModal}
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px', fontSize: isMobile ? '14px' : '16px' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{binanceUnhealthy.length > 0 && list.some((s) => s.enabled) && (
|
||||
<Alert
|
||||
@@ -583,20 +593,17 @@ const CryptoTailStrategyList: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
style={{ marginBottom: 16 }}
|
||||
style={{ marginBottom: 16, margin: isMobile ? '0 8px 16px' : undefined }}
|
||||
/>
|
||||
)}
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message={t('cryptoTailStrategy.list.walletTip')}
|
||||
style={{ marginBottom: 16 }}
|
||||
style={{ marginBottom: 16, margin: isMobile ? '0 8px 16px' : undefined }}
|
||||
/>
|
||||
<Card>
|
||||
<Card style={{ borderRadius: isMobile ? 0 : '12px', boxShadow: '0 2px 8px rgba(0,0,0,0.08)', border: isMobile ? 'none' : '1px solid #e8e8e8' }} styles={{ body: { padding: isMobile ? 12 : 24 } }}>
|
||||
<div style={{ marginBottom: 16, display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' }}>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openAddModal}>
|
||||
{t('cryptoTailStrategy.list.addStrategy')}
|
||||
</Button>
|
||||
<Select
|
||||
placeholder={t('cryptoTailStrategy.form.selectAccount')}
|
||||
allowClear
|
||||
@@ -619,86 +626,80 @@ const CryptoTailStrategyList: React.FC = () => {
|
||||
</div>
|
||||
<Spin spinning={loading}>
|
||||
{isMobile ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{list.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
size="small"
|
||||
styles={{ body: { padding: 16 } }}
|
||||
style={{ borderLeft: `3px solid ${item.enabled ? 'var(--ant-colorSuccess)' : 'var(--ant-colorBorder)'}` }}
|
||||
>
|
||||
<Typography.Text strong style={{ fontSize: 15, wordBreak: 'break-word', whiteSpace: 'normal', display: 'block', marginBottom: 8 }}>
|
||||
{item.name || (item.marketTitle ?? item.marketSlugPrefix) || '-'}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 4, fontSize: 13, wordBreak: 'break-word', whiteSpace: 'normal' }}>
|
||||
{t('cryptoTailStrategy.list.account')}: {getAccountLabel(item.accountId)}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 8, fontSize: 13, wordBreak: 'break-word', whiteSpace: 'normal' }}>
|
||||
{marketOptions.find((m) => m.slug === item.marketSlugPrefix)?.title ?? item.marketSlugPrefix ?? '-'}
|
||||
</Typography.Text>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px 16px', marginBottom: 12 }}>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12, wordBreak: 'break-word', whiteSpace: 'normal' }}>
|
||||
{t('cryptoTailStrategy.list.timeWindow')}
|
||||
</Typography.Text>
|
||||
<Typography.Text style={{ fontSize: 12 }}>{formatTimeWindow(item.windowStartSeconds, item.windowEndSeconds, false)}</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12, wordBreak: 'break-word', whiteSpace: 'normal' }}>
|
||||
{t('cryptoTailStrategy.list.priceRange')}
|
||||
</Typography.Text>
|
||||
<Typography.Text style={{ fontSize: 12, wordBreak: 'break-word', whiteSpace: 'normal' }}>{formatPriceRange(item.minPrice, item.maxPrice)}</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12, wordBreak: 'break-word', whiteSpace: 'normal' }}>
|
||||
{t('cryptoTailStrategy.list.amountMode')}
|
||||
</Typography.Text>
|
||||
<Typography.Text style={{ fontSize: 12, wordBreak: 'break-word', whiteSpace: 'normal' }}>
|
||||
{(item.amountMode?.toUpperCase() ?? '') === 'RATIO'
|
||||
? `${t('cryptoTailStrategy.list.ratio')} ${formatNumber(item.amountValue, 2) || '0'}%`
|
||||
: `${t('cryptoTailStrategy.list.fixed')} ${formatUSDC(item.amountValue)} USDC`}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<Space wrap style={{ marginBottom: 12 }}>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{t('cryptoTailStrategy.list.totalRealizedPnl')}:{' '}
|
||||
</Typography.Text>
|
||||
{item.totalRealizedPnl != null ? (
|
||||
<Typography.Text style={{ color: pnlColor(item.totalRealizedPnl) ?? undefined, fontWeight: 500, fontSize: 12 }}>
|
||||
{formatUSDC(item.totalRealizedPnl)} USDC
|
||||
</Typography.Text>
|
||||
) : (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>-</Typography.Text>
|
||||
)}
|
||||
{item.winRate != null && (
|
||||
<>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>·</Typography.Text>
|
||||
<Tag color="blue" style={{ margin: 0 }}>{t('cryptoTailStrategy.list.winRate')} {(Number(item.winRate) * 100).toFixed(1)}%</Tag>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<Space wrap size="small">
|
||||
<Switch
|
||||
checked={item.enabled}
|
||||
onChange={() => handleToggle(item)}
|
||||
size="small"
|
||||
/>
|
||||
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => openEditModal(item)}>
|
||||
{t('cryptoTailStrategy.list.edit')}
|
||||
</Button>
|
||||
<Button type="link" size="small" icon={<UnorderedListOutlined />} onClick={() => openTriggers(item.id)}>
|
||||
{t('cryptoTailStrategy.list.viewTriggers')}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={t('cryptoTailStrategy.list.deleteConfirm')}
|
||||
onConfirm={() => handleDelete(item.id)}
|
||||
okText={t('common.confirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
>
|
||||
<Button type="link" size="small" danger>
|
||||
{t('cryptoTailStrategy.list.delete')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{list.map((item) => {
|
||||
const accountLabel = getAccountLabel(item.accountId)
|
||||
const marketTitle = marketOptions.find((m) => m.slug === item.marketSlugPrefix)?.title ?? item.marketTitle ?? item.marketSlugPrefix ?? '-'
|
||||
return (
|
||||
<Card
|
||||
key={item.id}
|
||||
style={{
|
||||
marginBottom: 0,
|
||||
borderRadius: '10px',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.08)',
|
||||
border: '1px solid #e8e8e8',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<div style={{
|
||||
padding: '10px 12px',
|
||||
background: item.enabled ? 'var(--ant-color-primary, #1677ff)' : 'var(--ant-color-fill-secondary, #f0f0f0)',
|
||||
color: item.enabled ? '#fff' : 'var(--ant-color-text-secondary, #666)'
|
||||
}}>
|
||||
<div style={{ fontSize: '15px', fontWeight: '600', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<span style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}>{item.name || marketTitle || '-'}</span>
|
||||
<Switch checked={item.enabled} onChange={() => handleToggle(item)} size="small" />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '8px 12px', backgroundColor: '#fafafa', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '10px', color: '#8c8c8c' }}>{t('cryptoTailStrategy.list.totalRealizedPnl')}</div>
|
||||
{item.totalRealizedPnl != null ? (
|
||||
<div style={{ fontSize: '14px', fontWeight: '600', color: pnlColor(item.totalRealizedPnl) }}>{formatUSDC(item.totalRealizedPnl)} USDC</div>
|
||||
) : (
|
||||
<div style={{ fontSize: '14px', color: '#8c8c8c' }}>-</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontSize: '10px', color: '#8c8c8c' }}>{t('cryptoTailStrategy.list.winRate')}</div>
|
||||
{item.winRate != null ? <Tag color="blue" style={{ margin: 0 }}>{(Number(item.winRate) * 100).toFixed(1)}%</Tag> : <span style={{ fontSize: '12px', color: '#8c8c8c' }}>-</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '6px 12px', fontSize: '11px', color: '#8c8c8c', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<div>{t('cryptoTailStrategy.list.account')}: {accountLabel}</div>
|
||||
<div>{t('cryptoTailStrategy.list.market')}: {marketTitle}</div>
|
||||
</div>
|
||||
<div style={{ padding: '6px 12px', fontSize: '11px', color: '#8c8c8c', borderBottom: '1px solid #f0f0f0' }}>
|
||||
{t('cryptoTailStrategy.list.timeWindow')}: {formatTimeWindow(item.windowStartSeconds, item.windowEndSeconds, false)} · {t('cryptoTailStrategy.list.priceRange')}: {formatPriceRange(item.minPrice, item.maxPrice)}
|
||||
</div>
|
||||
<div style={{ padding: '8px 12px', display: 'flex', justifyContent: 'space-around', alignItems: 'center' }}>
|
||||
<Tooltip title={t('cryptoTailStrategy.list.edit')}>
|
||||
<div onClick={() => openEditModal(item)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
|
||||
<EditOutlined style={{ fontSize: '18px', color: '#52c41a' }} />
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('cryptoTailStrategy.list.edit')}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('cryptoTailStrategy.list.viewTriggers')}>
|
||||
<div onClick={() => openTriggers(item.id)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
|
||||
<UnorderedListOutlined style={{ fontSize: '18px', color: '#1890ff' }} />
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('cryptoTailStrategy.list.viewTriggers')}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Popconfirm title={t('cryptoTailStrategy.list.deleteConfirm')} onConfirm={() => handleDelete(item.id)} okText={t('common.confirm')} cancelText={t('common.cancel')}>
|
||||
<Tooltip title={t('cryptoTailStrategy.list.delete')}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
|
||||
<DeleteOutlined style={{ fontSize: '18px', color: '#ff4d4f' }} />
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('cryptoTailStrategy.list.delete')}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<Table
|
||||
|
||||
@@ -307,7 +307,7 @@ const LeaderList: React.FC = () => {
|
||||
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f0f0f0'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
|
||||
>
|
||||
<GlobalOutlined style={{ fontSize: '16px', color: '#fa8c16' }} />
|
||||
<GlobalOutlined style={{ fontSize: '16px', color: '#1890ff' }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -328,7 +328,7 @@ const LeaderList: React.FC = () => {
|
||||
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f0f0f0'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
|
||||
>
|
||||
<EditOutlined style={{ fontSize: '16px', color: '#52c41a' }} />
|
||||
<EditOutlined style={{ fontSize: '16px', color: '#1890ff' }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
@@ -354,7 +354,7 @@ const LeaderList: React.FC = () => {
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
|
||||
>
|
||||
<Badge count={record.copyTradingCount} size="small" offset={[-4, -4]}>
|
||||
<CopyOutlined style={{ fontSize: '16px', color: '#13c2c2' }} />
|
||||
<CopyOutlined style={{ fontSize: '16px', color: '#1890ff' }} />
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
@@ -381,7 +381,7 @@ const LeaderList: React.FC = () => {
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
|
||||
>
|
||||
<Badge count={record.backtestCount} size="small" offset={[-4, -4]}>
|
||||
<LineChartOutlined style={{ fontSize: '16px', color: '#722ed1' }} />
|
||||
<LineChartOutlined style={{ fontSize: '16px', color: '#1890ff' }} />
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
@@ -421,9 +421,9 @@ const LeaderList: React.FC = () => {
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px', flexWrap: 'wrap', gap: '12px' }}>
|
||||
<h2 style={{ margin: 0, fontSize: isMobile ? '20px' : '24px' }}>{t('leaderList.title')}</h2>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => navigate('/leaders/add')} size={isMobile ? 'middle' : 'large'} style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px', fontSize: isMobile ? '14px' : '16px' }}>
|
||||
{t('leaderList.addLeader')}
|
||||
</Button>
|
||||
<Tooltip title={t('leaderList.addLeader')}>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => navigate('/leaders/add')} size={isMobile ? 'middle' : 'large'} style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px', fontSize: isMobile ? '14px' : '16px' }} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Card style={{ borderRadius: '12px', boxShadow: '0 2px 8px rgba(0,0,0,0.08)', border: '1px solid #e8e8e8' }} bodyStyle={{ padding: isMobile ? '12px' : '24px' }}>
|
||||
@@ -440,7 +440,6 @@ const LeaderList: React.FC = () => {
|
||||
dataSource={leaders}
|
||||
renderItem={(leader) => {
|
||||
const balance = balanceMap[leader.id]
|
||||
const isLoading = balanceLoading[leader.id]
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -457,7 +456,7 @@ const LeaderList: React.FC = () => {
|
||||
{/* 头部区域 - 名称和地址 */}
|
||||
<div style={{
|
||||
padding: '10px 12px',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
background: 'var(--ant-color-primary, #1677ff)',
|
||||
color: '#fff'
|
||||
}}>
|
||||
<div style={{ fontSize: '15px', fontWeight: '600', marginBottom: '2px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
@@ -539,7 +538,7 @@ const LeaderList: React.FC = () => {
|
||||
onClick={() => navigate(`/leaders/edit?id=${leader.id}`)}
|
||||
style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}
|
||||
>
|
||||
<EditOutlined style={{ fontSize: '18px', color: '#52c41a' }} />
|
||||
<EditOutlined style={{ fontSize: '18px', color: '#1890ff' }} />
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('common.edit')}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
@@ -557,7 +556,7 @@ const LeaderList: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
<Badge count={leader.copyTradingCount} size="small" offset={[-2, -2]}>
|
||||
<CopyOutlined style={{ fontSize: '18px', color: '#13c2c2' }} />
|
||||
<CopyOutlined style={{ fontSize: '18px', color: '#1890ff' }} />
|
||||
</Badge>
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('leaderList.viewCopyTradings')}</span>
|
||||
</div>
|
||||
@@ -576,7 +575,7 @@ const LeaderList: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
<Badge count={leader.backtestCount} size="small" offset={[-2, -2]}>
|
||||
<LineChartOutlined style={{ fontSize: '18px', color: '#722ed1' }} />
|
||||
<LineChartOutlined style={{ fontSize: '18px', color: '#1890ff' }} />
|
||||
</Badge>
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('leaderList.viewBacktests')}</span>
|
||||
</div>
|
||||
|
||||
+198
-145
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Card, Table, Button, Space, Tag, Popconfirm, message, Input, Modal, Form, Radio, InputNumber, Switch, Divider, Spin } from 'antd'
|
||||
import { Card, Table, Button, Space, Tag, Popconfirm, message, Input, Modal, Form, Radio, InputNumber, Switch, Divider, Spin, Empty, List, Tooltip } from 'antd'
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, CopyOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { apiService } from '../services/api'
|
||||
@@ -212,25 +212,50 @@ const TemplateList: React.FC = () => {
|
||||
{
|
||||
title: t('common.actions') || '操作',
|
||||
key: 'action',
|
||||
width: isMobile ? 120 : 200,
|
||||
width: isMobile ? 120 : 120,
|
||||
fixed: 'right' as const,
|
||||
render: (_: any, record: CopyTradingTemplate) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => navigate(`/templates/edit/${record.id}`)}
|
||||
>
|
||||
{t('common.edit') || '编辑'}
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCopy(record)}
|
||||
>
|
||||
{t('templateList.copy') || '复制'}
|
||||
</Button>
|
||||
<Space size={4}>
|
||||
<Tooltip title={t('common.edit') || '编辑'}>
|
||||
<div
|
||||
onClick={() => navigate(`/templates/edit/${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'}
|
||||
>
|
||||
<EditOutlined style={{ fontSize: '16px', color: '#1890ff' }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={t('templateList.copy') || '复制'}>
|
||||
<div
|
||||
onClick={() => handleCopy(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'}
|
||||
>
|
||||
<CopyOutlined style={{ fontSize: '16px', color: '#1890ff' }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Popconfirm
|
||||
title={t('templateList.deleteConfirm') || '确定要删除这个模板吗?'}
|
||||
description={t('templateList.deleteConfirmDesc') || '删除后无法恢复,请确保没有跟单关系在使用该模板'}
|
||||
@@ -238,14 +263,24 @@ const TemplateList: React.FC = () => {
|
||||
okText={t('common.confirm') || '确定'}
|
||||
cancelText={t('common.cancel') || '取消'}
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
{t('common.delete') || '删除'}
|
||||
</Button>
|
||||
<Tooltip title={t('common.delete') || '删除'}>
|
||||
<div
|
||||
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 = '#fff1f0'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
|
||||
>
|
||||
<DeleteOutlined style={{ fontSize: '16px', color: '#ff4d4f' }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
)
|
||||
@@ -254,26 +289,29 @@ const TemplateList: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
|
||||
<h2 style={{ margin: 0 }}>{t('templateList.title') || '跟单模板管理'}</h2>
|
||||
<Space>
|
||||
<Search
|
||||
placeholder={t('templateList.searchPlaceholder') || '搜索模板名称'}
|
||||
allowClear
|
||||
style={{ width: isMobile ? 150 : 250 }}
|
||||
onSearch={setSearchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
/>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px', flexWrap: 'wrap', gap: '12px' }}>
|
||||
<h2 style={{ margin: 0, fontSize: isMobile ? '20px' : '24px' }}>{t('templateList.title') || '跟单模板管理'}</h2>
|
||||
<Space size={8}>
|
||||
<Search
|
||||
placeholder={t('templateList.searchPlaceholder') || '搜索模板名称'}
|
||||
allowClear
|
||||
style={{ width: isMobile ? 120 : 200 }}
|
||||
onSearch={setSearchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
/>
|
||||
<Tooltip title={t('templateList.addTemplate') || '新增模板'}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => navigate('/templates/add')}
|
||||
>
|
||||
{t('templateList.addTemplate') || '新增模板'}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px', fontSize: isMobile ? '14px' : '16px' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Card style={{ borderRadius: '12px', boxShadow: '0 2px 8px rgba(0,0,0,0.08)', border: '1px solid #e8e8e8' }} bodyStyle={{ padding: isMobile ? '12px' : '24px' }}>
|
||||
|
||||
{isMobile ? (
|
||||
// 移动端卡片布局
|
||||
@@ -283,116 +321,133 @@ const TemplateList: React.FC = () => {
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : filteredTemplates.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
|
||||
{t('templateList.noData') || '暂无模板数据'}
|
||||
</div>
|
||||
<Empty description={t('templateList.noData') || '暂无模板数据'} />
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{filteredTemplates.map((template) => {
|
||||
const date = new Date(template.createdAt)
|
||||
const formattedDate = date.toLocaleString(i18n.language || 'zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
|
||||
<List
|
||||
dataSource={filteredTemplates}
|
||||
renderItem={(template) => {
|
||||
return (
|
||||
<Card
|
||||
key={template.id}
|
||||
style={{
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
|
||||
border: '1px solid #e8e8e8'
|
||||
marginBottom: '10px',
|
||||
borderRadius: '10px',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.08)',
|
||||
border: '1px solid #e8e8e8',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
bodyStyle={{ padding: '16px' }}
|
||||
bodyStyle={{ padding: '0' }}
|
||||
>
|
||||
{/* 模板名称和模式 */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '8px',
|
||||
color: '#1890ff'
|
||||
}}>
|
||||
{/* 头部区域 - 模板名称 */}
|
||||
<div style={{
|
||||
padding: '10px 12px',
|
||||
background: 'var(--ant-color-primary, #1677ff)',
|
||||
color: '#fff'
|
||||
}}>
|
||||
<div style={{ fontSize: '15px', fontWeight: '600', marginBottom: '2px' }}>
|
||||
{template.templateName}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
|
||||
<Tag color={template.copyMode === 'RATIO' ? 'blue' : 'green'}>
|
||||
{template.copyMode === 'RATIO' ? (t('templateList.ratioMode') || '比例模式') : (t('templateList.fixedAmountMode') || '固定金额模式')}
|
||||
</Tag>
|
||||
<Tag color={template.supportSell ? 'green' : 'red'}>
|
||||
{template.supportSell ? (t('templateList.supportSell') || '跟单卖出') : (t('templateList.notSupportSell') || '不跟单卖出')}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
|
||||
{/* 跟单配置 */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('templateList.copyConfig') || '跟单配置'}</div>
|
||||
<div style={{ fontSize: '14px', fontWeight: '500' }}>
|
||||
<div style={{ fontSize: '12px', opacity: '0.9' }}>
|
||||
{template.copyMode === 'RATIO'
|
||||
? `${t('templateList.ratio') || '比例'} ${template.copyRatio}x`
|
||||
: template.fixedAmount
|
||||
? `${t('templateList.fixedAmount') || '固定'} ${formatUSDC(template.fixedAmount)} USDC`
|
||||
: '-'
|
||||
? `${t('templateList.ratioMode') || '比例模式'} ${(parseFloat(template.copyRatio || '0') * 100).toFixed(0).replace(/\.0+$/, '')}%`
|
||||
: `${t('templateList.fixedAmountMode') || '固定金额'} ${formatUSDC(template.fixedAmount || '0')} USDC`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 其他配置信息 */}
|
||||
{template.copyMode === 'RATIO' && (
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('templateList.amountLimit') || '金额限制'}</div>
|
||||
<div style={{ fontSize: '13px', color: '#333' }}>
|
||||
{template.maxOrderSize && (
|
||||
<span>{t('templateList.max') || '最大'}: {formatUSDC(template.maxOrderSize)} USDC</span>
|
||||
)}
|
||||
{template.maxOrderSize && template.minOrderSize && <span> | </span>}
|
||||
{template.minOrderSize && (
|
||||
<span>{t('templateList.min') || '最小'}: {formatUSDC(template.minOrderSize)} USDC</span>
|
||||
)}
|
||||
{!template.maxOrderSize && !template.minOrderSize && <span style={{ color: '#999' }}>{t('templateList.notSet') || '未设置'}</span>}
|
||||
|
||||
{/* 配置信息区域 */}
|
||||
<div style={{
|
||||
padding: '8px 12px',
|
||||
backgroundColor: '#fafafa',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
minHeight: '42px',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '10px', color: '#8c8c8c' }}>
|
||||
{t('templateList.supportSell') || '跟单卖出'}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', fontWeight: '500' }}>
|
||||
<Tag color={template.supportSell ? 'green' : 'red'} style={{ margin: 0, fontSize: '10px' }}>
|
||||
{template.supportSell ? (t('common.yes') || '是') : (t('common.no') || '否')}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontSize: '10px', color: '#8c8c8c' }}>
|
||||
{t('templateList.maxDailyOrders') || '每日最大'}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', fontWeight: '500', color: '#1890ff' }}>
|
||||
{template.maxDailyOrders} {t('common.orders') || '单'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 金额限制区域(仅比例模式显示) */}
|
||||
{template.copyMode === 'RATIO' && (
|
||||
<div style={{
|
||||
padding: '6px 12px',
|
||||
fontSize: '11px',
|
||||
color: '#8c8c8c',
|
||||
borderBottom: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<span style={{ color: '#d48806' }}>{t('templateList.amountLimit') || '金额限制'}: </span>
|
||||
{template.maxOrderSize && (
|
||||
<span>{t('templateList.max') || '最大'} {formatUSDC(template.maxOrderSize)} USDC</span>
|
||||
)}
|
||||
{template.maxOrderSize && template.minOrderSize && <span> | </span>}
|
||||
{template.minOrderSize && (
|
||||
<span>{t('templateList.min') || '最小'} {formatUSDC(template.minOrderSize)} USDC</span>
|
||||
)}
|
||||
{!template.maxOrderSize && !template.minOrderSize && <span style={{ color: '#bfbfbf' }}>{t('templateList.notSet') || '未设置'}</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('templateList.otherConfig') || '其他配置'}</div>
|
||||
<div style={{ fontSize: '13px', color: '#333' }}>
|
||||
{t('templateList.maxDailyOrders') || '每日最大订单'}: {template.maxDailyOrders} | {t('templateList.priceTolerance') || '价格容忍度'}: {template.priceTolerance}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 创建时间 */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#999' }}>
|
||||
{t('common.createdAt') || '创建时间'}: {formattedDate}
|
||||
</div>
|
||||
<div style={{
|
||||
padding: '6px 12px',
|
||||
fontSize: '11px',
|
||||
color: '#8c8c8c'
|
||||
}}>
|
||||
{t('common.createdAt') || '创建时间'}: {new Date(template.createdAt).toLocaleString(i18n.language || 'zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => navigate(`/templates/edit/${template.id}`)}
|
||||
style={{ flex: 1, minWidth: '80px' }}
|
||||
>
|
||||
{t('common.edit') || '编辑'}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCopy(template)}
|
||||
style={{ flex: 1, minWidth: '80px' }}
|
||||
>
|
||||
{t('templateList.copy') || '复制'}
|
||||
</Button>
|
||||
|
||||
{/* 图标操作栏 */}
|
||||
<div style={{
|
||||
padding: '8px 12px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<Tooltip title={t('common.edit') || '编辑'}>
|
||||
<div
|
||||
onClick={() => navigate(`/templates/edit/${template.id}`)}
|
||||
style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}
|
||||
>
|
||||
<EditOutlined style={{ fontSize: '18px', color: '#1890ff' }} />
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('common.edit') || '编辑'}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={t('templateList.copy') || '复制'}>
|
||||
<div
|
||||
onClick={() => handleCopy(template)}
|
||||
style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}
|
||||
>
|
||||
<CopyOutlined style={{ fontSize: '18px', color: '#1890ff' }} />
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('templateList.copy') || '复制'}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Popconfirm
|
||||
title={t('templateList.deleteConfirm') || '确定要删除这个模板吗?'}
|
||||
description={t('templateList.deleteConfirmDesc') || '删除后无法恢复,请确保没有跟单关系在使用该模板'}
|
||||
@@ -400,20 +455,18 @@ const TemplateList: React.FC = () => {
|
||||
okText={t('common.confirm') || '确定'}
|
||||
cancelText={t('common.cancel') || '取消'}
|
||||
>
|
||||
<Button
|
||||
danger
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
style={{ flex: 1, minWidth: '80px' }}
|
||||
>
|
||||
{t('common.delete') || '删除'}
|
||||
</Button>
|
||||
<Tooltip title={t('common.delete') || '删除'}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
|
||||
<DeleteOutlined style={{ fontSize: '18px', color: '#ff4d4f' }} />
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('common.delete') || '删除'}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
+155
-37
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Table, Button, Space, Tag, Popconfirm, message, Typography, Modal, Form, Input } from 'antd'
|
||||
import { PlusOutlined, ReloadOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
|
||||
import { Card, Table, Button, Space, Tag, Popconfirm, message, Typography, Modal, Form, Input, List, Empty, Tooltip, Spin } from 'antd'
|
||||
import { PlusOutlined, ReloadOutlined, DeleteOutlined, EditOutlined, UserOutlined, KeyOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { apiService } from '../services/api'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
@@ -219,46 +219,164 @@ const UserList: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<Title level={4} style={{ margin: 0 }}>{t('userList.title') || '用户管理'}</Title>
|
||||
<Space>
|
||||
<Button
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => setUpdateOwnPasswordModalVisible(true)}
|
||||
>
|
||||
{t('userList.updateMyPassword') || '修改我的密码'}
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={fetchUsers}
|
||||
loading={loading}
|
||||
>
|
||||
{t('common.refresh') || '刷新'}
|
||||
</Button>
|
||||
{isDefaultUser && (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px', flexWrap: 'wrap', gap: '12px' }}>
|
||||
<Title level={isMobile ? 4 : 3} style={{ margin: 0, fontSize: isMobile ? '18px' : undefined }}>{t('userList.title') || '用户管理'}</Title>
|
||||
<Space size={8} wrap>
|
||||
<Button
|
||||
icon={<KeyOutlined />}
|
||||
onClick={() => setUpdateOwnPasswordModalVisible(true)}
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px' }}
|
||||
>
|
||||
{isMobile ? (t('userList.updateMyPassword') || '改密') : (t('userList.updateMyPassword') || '修改我的密码')}
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={fetchUsers}
|
||||
loading={loading}
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px' }}
|
||||
>
|
||||
{t('common.refresh') || '刷新'}
|
||||
</Button>
|
||||
{isDefaultUser && (
|
||||
<Tooltip title={t('userList.addUser') || '新增用户'}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setCreateModalVisible(true)}
|
||||
>
|
||||
{t('userList.addUser') || '新增用户'}
|
||||
</Button>
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px', fontSize: isMobile ? '14px' : '16px' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Card style={{ borderRadius: '12px', boxShadow: '0 2px 8px rgba(0,0,0,0.08)', border: '1px solid #e8e8e8' }} bodyStyle={{ padding: isMobile ? '12px' : '24px' }}>
|
||||
{isMobile ? (
|
||||
// 移动端卡片布局
|
||||
<div>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : users.length === 0 ? (
|
||||
<Empty description={t('userList.noData') || '暂无用户数据'} />
|
||||
) : (
|
||||
<List
|
||||
dataSource={users}
|
||||
renderItem={(user) => (
|
||||
<Card
|
||||
key={user.id}
|
||||
style={{
|
||||
marginBottom: '10px',
|
||||
borderRadius: '10px',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.08)',
|
||||
border: '1px solid #e8e8e8',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
bodyStyle={{ padding: '0' }}
|
||||
>
|
||||
{/* 头部区域 - 用户名 */}
|
||||
<div style={{
|
||||
padding: '10px 12px',
|
||||
background: 'var(--ant-color-primary, #1677ff)',
|
||||
color: '#fff'
|
||||
}}>
|
||||
<div style={{ fontSize: '15px', fontWeight: '600', marginBottom: '2px', display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<UserOutlined style={{ fontSize: '14px' }} />
|
||||
<span>{user.username}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', opacity: '0.9' }}>
|
||||
ID: {user.id}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 角色信息区域 */}
|
||||
<div style={{
|
||||
padding: '8px 12px',
|
||||
backgroundColor: '#fafafa',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
minHeight: '42px',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '10px', color: '#8c8c8c' }}>
|
||||
{t('userList.role') || '角色'}
|
||||
</div>
|
||||
<Tag color={user.isDefault ? 'red' : 'blue'} style={{ margin: 0 }}>
|
||||
{user.isDefault ? (t('userList.defaultAccount') || '默认账户') : (t('userList.normalUser') || '普通用户')}
|
||||
</Tag>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontSize: '10px', color: '#8c8c8c' }}>
|
||||
{t('common.createdAt') || '创建时间'}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', fontWeight: '500', color: '#666' }}>
|
||||
{new Date(user.createdAt).toLocaleDateString(i18n.language || 'zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作区域(仅管理员可见) */}
|
||||
{isDefaultUser && !user.isDefault && (
|
||||
<div style={{
|
||||
padding: '8px 12px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<Tooltip title={t('userList.updatePassword') || '修改密码'}>
|
||||
<div
|
||||
onClick={() => {
|
||||
setSelectedUser(user)
|
||||
setUpdatePasswordModalVisible(true)
|
||||
}}
|
||||
style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}
|
||||
>
|
||||
<KeyOutlined style={{ fontSize: '18px', color: '#1890ff' }} />
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('userList.updatePassword') || '改密'}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Popconfirm
|
||||
title={t('userList.deleteConfirm') || '确定要删除这个用户吗?'}
|
||||
onConfirm={() => handleDelete(user)}
|
||||
okText={t('common.confirm') || '确定'}
|
||||
cancelText={t('common.cancel') || '取消'}
|
||||
>
|
||||
<Tooltip title={t('common.delete') || '删除'}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
|
||||
<DeleteOutlined style={{ fontSize: '18px', color: '#ff4d4f' }} />
|
||||
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('common.delete') || '删除'}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={users}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: isMobile ? 10 : 20,
|
||||
showSizeChanger: !isMobile,
|
||||
showTotal: (total) => t('userList.total', { total }) || `共 ${total} 条`
|
||||
}}
|
||||
scroll={isMobile ? { x: 600 } : undefined}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={users}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: isMobile ? 10 : 20,
|
||||
showSizeChanger: !isMobile,
|
||||
showTotal: (total) => t('userList.total', { total }) || `共 ${total} 条`
|
||||
}}
|
||||
scroll={isMobile ? { x: 600 } : undefined}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 创建用户弹窗 */}
|
||||
|
||||
Reference in New Issue
Block a user