diff --git a/frontend/src/locales/en/common.json b/frontend/src/locales/en/common.json
index 2ecb7c7..fbe3f3a 100644
--- a/frontend/src/locales/en/common.json
+++ b/frontend/src/locales/en/common.json
@@ -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",
diff --git a/frontend/src/locales/zh-CN/common.json b/frontend/src/locales/zh-CN/common.json
index 853c74c..c7f34bd 100644
--- a/frontend/src/locales/zh-CN/common.json
+++ b/frontend/src/locales/zh-CN/common.json
@@ -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": "获取交易记录失败",
diff --git a/frontend/src/locales/zh-TW/common.json b/frontend/src/locales/zh-TW/common.json
index e4ea674..7b149d0 100644
--- a/frontend/src/locales/zh-TW/common.json
+++ b/frontend/src/locales/zh-TW/common.json
@@ -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": "獲取交易記錄失敗",
diff --git a/frontend/src/pages/AccountList.tsx b/frontend/src/pages/AccountList.tsx
index 0ed3693..e9b5f7f 100644
--- a/frontend/src/pages/AccountList.tsx
+++ b/frontend/src/pages/AccountList.tsx
@@ -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) => (
-
-
- }
- onClick={() => handleShowEdit(record)}
- >
- {t('accountList.edit')}
-
- handleDelete(record)}
- okText={t('accountList.deleteConfirmOk')}
- cancelText={t('common.cancel')}
- okButtonProps={{ danger: true }}
- >
-
-
-
- )
- }
- ]
-
- const mobileColumns = [
- {
- title: t('accountList.accountName'),
- key: 'info',
- render: (_: any, record: Account) => {
- return (
-
-
- {record.accountName || `${t('accountList.accountName')} ${record.id}`}
-
-
-
- {t('accountList.walletAddress')}: {record.walletAddress ? `${record.walletAddress.slice(0, 6)}...${record.walletAddress.slice(-4)}` : '-'}
- }
- onClick={(e) => {
- e.stopPropagation()
- handleCopy(record.walletAddress)
- }}
- style={{ marginLeft: '4px', padding: '0 4px' }}
- />
-
-
- {t('accountList.proxyAddress')}: {record.proxyAddress ? `${record.proxyAddress.slice(0, 6)}...${record.proxyAddress.slice(-4)}` : '-'}
- }
- onClick={(e) => {
- e.stopPropagation()
- handleCopy(record.proxyAddress)
- }}
- style={{ marginLeft: '4px', padding: '0 4px' }}
- />
-
- {record.walletType && (
-
- {t('accountList.walletType')}:{' '}
-
- {record.walletType.toLowerCase() === 'magic' ? 'Magic' : 'Safe'}
-
-
- )}
-
-
- {t('accountList.totalBalance')}: {balanceLoading[record.id] ? (
-
- ) : balanceMap[record.id]?.total && balanceMap[record.id].total !== '-' ? (
- `${formatUSDC(balanceMap[record.id].total)} USDC`
- ) : (
- '-'
- )}
-
- {balanceMap[record.id] && balanceMap[record.id].available !== '-' && (
-
- {t('accountList.available')}: {formatUSDC(balanceMap[record.id].available)} USDC | {t('accountList.position')}: {formatUSDC(balanceMap[record.id].position)} USDC
-
- )}
-
- )
- }
- },
- {
- title: t('accountList.action'),
- key: 'action',
- width: 100,
- render: (_: any, record: Account) => (
-
-
- }
- onClick={() => handleShowEdit(record)}
- style={{ minHeight: '32px' }}
- >
- {t('accountList.edit')}
-
- handleDelete(record)}
- okText={t('accountList.deleteConfirmOk')}
- cancelText={t('common.cancel')}
- okButtonProps={{ danger: true }}
- >
-
)
@@ -519,16 +427,15 @@ const AccountList: React.FC = () => {
{t('accountList.title')}
- }
- onClick={() => setAccountImportModalVisible(true)}
- size={isMobile ? 'middle' : 'large'}
- block={isMobile}
- style={isMobile ? { minHeight: '44px' } : undefined}
- >
- {t('accountList.importAccount')}
-
+
+ }
+ onClick={() => setAccountImportModalVisible(true)}
+ size={isMobile ? 'middle' : 'large'}
+ style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px', fontSize: isMobile ? '14px' : '16px' }}
+ />
+
{
borderRadius: isMobile ? '0' : undefined
}}>
{isMobile ? (
-
+ loading ? (
+
+
+
+ ) : accounts.length === 0 ? (
+
+ ) : (
+ {
+ const balance = balanceMap[account.id]
+
+ return (
+
+ {/* 头部区域 */}
+
+
+
+ {account.accountName || `${t('accountList.accountName')} ${account.id}`}
+
+
+ {account.walletAddress ? `${account.walletAddress.slice(0, 6)}...${account.walletAddress.slice(-4)}` : '-'}
+ }
+ onClick={() => handleCopy(account.walletAddress)}
+ style={{ padding: '0 4px', height: 'auto' }}
+ />
+
+
+
+ {/* 资产区域 */}
+
+
+
+
+ {t('accountList.totalBalance')}
+
+
+ {balance?.total && balance.total !== '-' ? `${formatUSDC(balance.total)} USDC` : '- USDC'}
+
+
+
+
+ {t('accountList.walletType')}
+
+
+ {account.walletType ? (
+
+ {account.walletType.toLowerCase() === 'magic' ? 'Magic' : 'Safe'}
+
+ ) : '-'}
+
+
+
+
+
+ {/* 地址信息区域 */}
+
+
+ {t('accountList.proxyAddress')}: {account.proxyAddress ? `${account.proxyAddress.slice(0, 6)}...${account.proxyAddress.slice(-4)}` : '-'}
+ }
+ onClick={() => handleCopy(account.proxyAddress)}
+ style={{ padding: '0 4px', height: 'auto' }}
+ />
+
+
+
+ {/* 图标操作栏 */}
+
+
+ handleShowDetail(account)}
+ style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}
+ >
+
+ {t('accountList.detail')}
+
+
+
+
+ handleShowEdit(account)}
+ style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}
+ >
+
+ {t('accountList.edit')}
+
+
+
+
handleDelete(account)}
+ okText={t('accountList.deleteConfirmOk')}
+ cancelText={t('common.cancel')}
+ okButtonProps={{ danger: true }}
+ >
+
+
+
+ {t('accountList.delete')}
+
+
+
+
+
+ )
+ }}
+ />
+ )
) : (
{
}
// 删除任务
- 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) => (
+
+
{formatUSDC(record.initialBalance)}
+
{record.finalBalance ? formatUSDC(record.finalBalance) : '-'}
+
+ )
},
{
- 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 ? (
- = 0 ? '#52c41a' : '#ff4d4f' }}>
- {formatUSDC(value)}
-
- ) : '-'
- },
- {
- title: t('backtest.profitRate'),
- dataIndex: 'profitRate',
- key: 'profitRate',
- width: 100,
- render: (value: string | null) => value ? (
- = 0 ? '#52c41a' : '#ff4d4f' }}>
- {value}%
-
- ) : '-'
- },
- {
- 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 (
+
+
+ {profitAmount !== null ? formatUSDC(record.profitAmount) : '-'}
+
+
+ {profitRate !== null ? `${record.profitRate}%` : '-'}
+
+
+ )
+ }
},
{
title: t('backtest.status'),
dataIndex: 'status',
key: 'status',
- width: 100,
- render: (status: string) => (
- {getStatusText(status)}
- )
- },
- {
- title: t('backtest.progress'),
- dataIndex: 'progress',
- key: 'progress',
- width: 120,
- render: (progress: number) => (
-
-
{progress}%
-
-
+ width: 130,
+ render: (status: string, record: BacktestTaskDto) => {
+ const isRunning = status === 'RUNNING' || status === 'PENDING'
+ return (
+
+
{getStatusText(status)}
+ {isRunning && record.progress !== undefined && (
+
+ {record.progress}%
+
+ )}
-
- )
+ )
+ }
},
{
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) => (
-
- }
- onClick={() => handleViewDetail(record.id)}
- >
- {t('common.viewDetail')}
-
+
+
+ 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'}
+ >
+
+
+
+
{record.status === 'COMPLETED' && (
<>
- }
- onClick={() => handleRerun(record)}
- >
- {t('backtest.rerun')}
-
- }
- onClick={() => handleCreateCopyTrading(record)}
- >
- {t('backtest.createCopyTrading')}
-
+
+ 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'}
+ >
+
+
+
+
+ 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'}
+ >
+
+
+
>
)}
+
{record.status === 'RUNNING' && (
- }
- onClick={() => handleStop(record.id)}
+ handleStop(record.id)}
+ okText={t('common.confirm')}
+ cancelText={t('common.cancel')}
>
- {t('backtest.stop')}
-
+
+ e.currentTarget.style.backgroundColor = '#f0f0f0'}
+ onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
+ >
+
+
+
+
)}
+
{(record.status === 'STOPPED' || record.status === 'FAILED') && (
- }
- onClick={() => handleRetry(record.id)}
- >
- {t('backtest.retry')}
-
+
+ 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'}
+ >
+
+
+
)}
+
{(record.status === 'PENDING' || record.status === 'COMPLETED' || record.status === 'STOPPED' || record.status === 'FAILED') && (
- }
- onClick={() => handleDelete(record.id)}
+ handleDelete(record.id)}
+ okText={t('common.confirm')}
+ cancelText={t('common.cancel')}
>
- {t('common.delete')}
-
+
+ e.currentTarget.style.backgroundColor = '#fff1f0'}
+ onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
+ >
+
+
+
+
)}
)
@@ -671,94 +716,205 @@ const BacktestList: React.FC = () => {
]
return (
-
-
+
+
+
{t('backtest.title')}
+
+
+ }
+ onClick={handleCreate}
+ size={isMobile ? 'middle' : 'large'}
+ style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px', fontSize: isMobile ? '14px' : '16px' }}
+ />
+
+
+ } onClick={handleRefresh} loading={loading} size={isMobile ? 'middle' : 'large'} style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px' }} />
+
+
+
+
+
- {/* 头部操作栏 */}
-
-
-
- setLeaderIdFilter(value)}
- leaders={leaders}
- />
-
-
-
-
+ {/* 筛选栏 */}
+
+
+ setLeaderIdFilter(value)}
+ leaders={leaders}
+ />
-
-
- }
- onClick={handleCreate}
- style={{ flex: isMobile ? 1 : undefined }}
- >
- {isMobile ? t('common.create') : t('backtest.createTask')}
-
- }
- onClick={handleRefresh}
- loading={loading}
- style={{ flex: isMobile ? 1 : undefined }}
- >
- {t('common.refresh')}
-
-
+
+
+
+
+
+
+
+
- {/* 数据表格 */}
- `${t('common.total')} ${total} ${t('common.items')}`,
- onChange: (newPage) => setPage(newPage),
- simple: isMobile
- }}
- scroll={isMobile ? { x: 1200 } : { x: 1400 }}
- />
+ {/* 数据:移动端卡片 / 桌面端表格 */}
+ {isMobile ? (
+ <>
+ {loading ? (
+
+
+
+ ) : tasks.length === 0 ? (
+
+ ) : (
+ (
+
+
+
{task.taskName}
+
{task.leaderName || `Leader ${task.leaderId}`}
+
+
+
+
{t('backtest.profitAmount')}
+ {task.profitAmount != null ? (
+
= 0 ? '#52c41a' : '#ff4d4f' }}>{formatUSDC(task.profitAmount)} USDC
+ ) : (
+
-
+ )}
+
+
+
{t('backtest.profitRate')}
+ {task.profitRate != null ? (
+
= 0 ? '#52c41a' : '#ff4d4f' }}>{task.profitRate}%
+ ) : (
+
-
+ )}
+
+
+
{t('backtest.status')}
+
{getStatusText(task.status)}
+
+
+
+ {t('backtest.initialBalance')}: {formatUSDC(task.initialBalance)} · {t('backtest.backtestDays')}: {task.backtestDays} {t('common.day')} · {t('backtest.progress')}: {task.progress}%
+
+
+
+ handleViewDetail(task.id)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
+
+ {t('common.viewDetail')}
+
+
+ {task.status === 'COMPLETED' && (
+ <>
+
+ handleRerun(task)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
+
+ {t('backtest.rerun')}
+
+
+
+ handleCreateCopyTrading(task)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
+
+ {t('backtest.createCopyTrading')}
+
+
+ >
+ )}
+ {task.status === 'RUNNING' && (
+
+ handleStop(task.id)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
+
+ {t('backtest.stop')}
+
+
+ )}
+ {(task.status === 'STOPPED' || task.status === 'FAILED') && (
+
+ handleRetry(task.id)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
+
+ {t('backtest.retry')}
+
+
+ )}
+ {(task.status === 'PENDING' || task.status === 'COMPLETED' || task.status === 'STOPPED' || task.status === 'FAILED') && (
+
+ handleDelete(task.id)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
+
+ {t('common.delete')}
+
+
+ )}
+
+
+ )}
+ />
+ )}
+ {/* 移动端分页 */}
+ {!loading && tasks.length > 0 && total > size && (
+
+
+ {page} / {Math.ceil(total / size)}
+
+
+ )}
+ >
+ ) : (
+ `${t('common.total')} ${totalCount} ${t('common.items')}`,
+ onChange: (newPage) => setPage(newPage),
+ simple: isMobile
+ }}
+ scroll={{ x: 1000 }}
+ />
+ )}
diff --git a/frontend/src/pages/CopyTradingList.tsx b/frontend/src/pages/CopyTradingList.tsx
index 540ce5e..58c8c4d 100644
--- a/frontend/src/pages/CopyTradingList.tsx
+++ b/frontend/src/pages/CopyTradingList.tsx
@@ -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 (
-
-
-
{t('copyTradingList.title') || '跟单配置管理'}
+
+
{t('copyTradingList.title') || '跟单配置管理'}
+
}
onClick={() => setAddModalOpen(true)}
- >
- {t('copyTradingList.addCopyTrading') || '新增跟单'}
-
-
-
-
+ size={isMobile ? 'middle' : 'large'}
+ style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px', fontSize: isMobile ? '14px' : '16px' }}
+ />
+
+
+
+
+
@@ -440,207 +442,180 @@ const CopyTradingList: React.FC = () => {
) : copyTradings.length === 0 ? (
-
- 暂无跟单配置
-
+
) : (
-
- {copyTradings.map((record) => {
+
{
const stats = statisticsMap[record.id]
- const date = new Date(record.createdAt)
- const formattedDate = date.toLocaleString('zh-CN', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit'
- })
return (
- {/* 基本信息 */}
-
-
- {record.configName || t('copyTradingList.configNameNotProvided') || '未提供'}
-
-
- {record.copyMode === 'RATIO'
- ? `${t('copyTradingList.ratioMode') || '比例'} ${(parseFloat(record.copyRatio || '0') * 100).toFixed(2).replace(/\.0+$/, '')}%`
- : `${t('copyTradingList.fixedAmountMode') || '固定'} ${formatUSDC(record.fixedAmount || '0')}`
- }
-
-
-
- {record.enabled ? '启用' : '禁用'}
-
+ {/* 头部区域 - 配置名称 */}
+
+
+ {record.configName || t('copyTradingList.configNameNotProvided') || '未提供'}
handleToggleStatus(record)}
- checkedChildren="开启"
- unCheckedChildren="停止"
+ checkedChildren={t('copyTradingList.enabled') || '开启'}
+ unCheckedChildren={t('copyTradingList.disabled') || '停止'}
size="small"
/>
-
-
-
-
- {/* 账户信息 */}
-
-
账户
-
- {record.accountName || `账户 ${record.accountId}`}
-
-
- {record.walletAddress.slice(0, 6)}...{record.walletAddress.slice(-4)}
+
+ {record.copyMode === 'RATIO'
+ ? `${t('copyTradingList.ratioMode') || '比例'} ${(parseFloat(record.copyRatio || '0') * 100).toFixed(0).replace(/\.0+$/, '')}%`
+ : `${t('copyTradingList.fixedAmountMode') || '固定'} ${formatUSDC(record.fixedAmount || '0')} USDC`
+ }
-
- {/* Leader 信息 */}
-
-
Leader
-
- {record.leaderName || `Leader ${record.leaderId}`}
-
-
- {record.leaderAddress.slice(0, 6)}...{record.leaderAddress.slice(-4)}
-
-
-
- {/* 总盈亏 */}
- {stats && (
-
-
总盈亏
-
- {getPnlIcon(stats.totalPnl)}
- {formatUSDC(stats.totalPnl)} USDC
+
+ {/* 盈亏区域 - 常驻显示 */}
+
+
+
+
+ {t('copyTradingList.totalPnl') || '总盈亏'}
+
+ {stats ? (
+
+ {getPnlIcon(stats.totalPnl)}
+ {formatUSDC(stats.totalPnl)} USDC
+
+ ) : loadingStatistics.has(record.id) ? (
+
+ ) : (
+
-
+ )}
-
- {formatPercent(stats.totalPnlPercent)}
-
-
- )}
-
- {loadingStatistics.has(record.id) && (
-
- 加载统计中...
-
- )}
-
- {/* 创建时间 */}
-
-
- 创建时间: {formattedDate}
+ {stats && (
+
+
+ {t('copyTradingList.profitRate') || '收益率'}
+
+
+ {formatPercent(stats.totalPnlPercent)}
+
+
+ )}
-
- {/* 操作按钮 */}
-
-
}
- onClick={() => {
- setEditModalCopyTradingId(record.id.toString())
- setEditModalOpen(true)
- }}
- style={{ flex: 1, minWidth: '80px' }}
- >
- {t('common.edit') || '编辑'}
-
-
}
- onClick={() => {
- setStatisticsModalCopyTradingId(record.id.toString())
- setStatisticsModalOpen(true)
- }}
- style={{ flex: 1, minWidth: '80px' }}
- >
- {t('copyTradingList.statistics') || '统计'}
-
-
,
- onClick: () => {
- setOrdersModalCopyTradingId(record.id.toString())
- setOrdersModalTab('buy')
- setOrdersModalOpen(true)
- }
- },
- {
- key: 'filteredOrders',
- label: t('copyTradingList.filteredOrders') || '已过滤订单',
- icon:
,
- onClick: () => {
- setFilteredOrdersModalCopyTradingId(record.id.toString())
- setFilteredOrdersModalOpen(true)
- }
- }
- ]
- }}
- trigger={['click']}
- >
-
}
- style={{ flex: 1, minWidth: '80px' }}
+
+ {/* 账户和Leader信息区域 */}
+
+
+
+ {t('copyTradingList.wallet') || '账户'}: {record.accountName || `#${record.accountId}`}
+
+
+
+ Leader: {record.leaderName || `#${record.leaderId}`}
+
+
+
+ {/* 图标操作栏 */}
+
+
+ {
+ setEditModalCopyTradingId(record.id.toString())
+ setEditModalOpen(true)
+ }}
+ style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}
>
- {t('copyTradingList.orders') || '订单'}
-
-
+
+ {t('common.edit') || '编辑'}
+
+
+
+
+ {
+ setStatisticsModalCopyTradingId(record.id.toString())
+ setStatisticsModalOpen(true)
+ }}
+ style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}
+ >
+
+ {t('copyTradingList.statistics') || '统计'}
+
+
+
+
+ {
+ setOrdersModalCopyTradingId(record.id.toString())
+ setOrdersModalTab('buy')
+ setOrdersModalOpen(true)
+ }}
+ style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}
+ >
+
+ {t('copyTradingList.orders') || '订单'}
+
+
+
handleDelete(record.id)}
okText={t('common.confirm') || '确定'}
cancelText={t('common.cancel') || '取消'}
>
- }
- style={{ flex: 1, minWidth: '80px' }}
- >
- {t('common.delete') || '删除'}
-
+
+
+
+ {t('common.delete') || '删除'}
+
+
)
- })}
-
+ }}
+ />
)}
) : (
diff --git a/frontend/src/pages/CryptoTailStrategyList.tsx b/frontend/src/pages/CryptoTailStrategyList.tsx
index bbd8f90..f12f5e0 100644
--- a/frontend/src/pages/CryptoTailStrategyList.tsx
+++ b/frontend/src/pages/CryptoTailStrategyList.tsx
@@ -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 (
-
-
-
{t('cryptoTailStrategy.list.title')}
-
}
- onClick={() => window.open(getGuideUrl(), '_blank')}
- style={{ padding: 0, height: 'auto', fontSize: isMobile ? 14 : 16 }}
- >
- {t('cryptoTailStrategy.list.configGuide')}
-
+
+
+
+
{t('cryptoTailStrategy.list.title')}
+ }
+ onClick={() => window.open(getGuideUrl(), '_blank')}
+ style={{ padding: 0, height: 'auto', fontSize: isMobile ? 14 : 16 }}
+ >
+ {t('cryptoTailStrategy.list.configGuide')}
+
+
+
+ }
+ onClick={openAddModal}
+ size={isMobile ? 'middle' : 'large'}
+ style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px', fontSize: isMobile ? '14px' : '16px' }}
+ />
+
{binanceUnhealthy.length > 0 && list.some((s) => s.enabled) && (
{
}
- style={{ marginBottom: 16 }}
+ style={{ marginBottom: 16, margin: isMobile ? '0 8px 16px' : undefined }}
/>
)}
-
+
- } onClick={openAddModal}>
- {t('cryptoTailStrategy.list.addStrategy')}
-
{
{isMobile ? (
-
- {list.map((item) => (
-
-
- {item.name || (item.marketTitle ?? item.marketSlugPrefix) || '-'}
-
-
- {t('cryptoTailStrategy.list.account')}: {getAccountLabel(item.accountId)}
-
-
- {marketOptions.find((m) => m.slug === item.marketSlugPrefix)?.title ?? item.marketSlugPrefix ?? '-'}
-
-
-
- {t('cryptoTailStrategy.list.timeWindow')}
-
- {formatTimeWindow(item.windowStartSeconds, item.windowEndSeconds, false)}
-
- {t('cryptoTailStrategy.list.priceRange')}
-
- {formatPriceRange(item.minPrice, item.maxPrice)}
-
- {t('cryptoTailStrategy.list.amountMode')}
-
-
- {(item.amountMode?.toUpperCase() ?? '') === 'RATIO'
- ? `${t('cryptoTailStrategy.list.ratio')} ${formatNumber(item.amountValue, 2) || '0'}%`
- : `${t('cryptoTailStrategy.list.fixed')} ${formatUSDC(item.amountValue)} USDC`}
-
-
-
-
-
- {t('cryptoTailStrategy.list.totalRealizedPnl')}:{' '}
-
- {item.totalRealizedPnl != null ? (
-
- {formatUSDC(item.totalRealizedPnl)} USDC
-
- ) : (
- -
- )}
- {item.winRate != null && (
- <>
- ·
- {t('cryptoTailStrategy.list.winRate')} {(Number(item.winRate) * 100).toFixed(1)}%
- >
- )}
-
-
-
- handleToggle(item)}
- size="small"
- />
- } onClick={() => openEditModal(item)}>
- {t('cryptoTailStrategy.list.edit')}
-
- } onClick={() => openTriggers(item.id)}>
- {t('cryptoTailStrategy.list.viewTriggers')}
-
- handleDelete(item.id)}
- okText={t('common.confirm')}
- cancelText={t('common.cancel')}
- >
-
-
-
-
- ))}
+
+ {list.map((item) => {
+ const accountLabel = getAccountLabel(item.accountId)
+ const marketTitle = marketOptions.find((m) => m.slug === item.marketSlugPrefix)?.title ?? item.marketTitle ?? item.marketSlugPrefix ?? '-'
+ return (
+
+
+
+ {item.name || marketTitle || '-'}
+ handleToggle(item)} size="small" />
+
+
+
+
+
+
{t('cryptoTailStrategy.list.totalRealizedPnl')}
+ {item.totalRealizedPnl != null ? (
+
{formatUSDC(item.totalRealizedPnl)} USDC
+ ) : (
+
-
+ )}
+
+
+
{t('cryptoTailStrategy.list.winRate')}
+ {item.winRate != null ?
{(Number(item.winRate) * 100).toFixed(1)}% :
-}
+
+
+
+
+
{t('cryptoTailStrategy.list.account')}: {accountLabel}
+
{t('cryptoTailStrategy.list.market')}: {marketTitle}
+
+
+ {t('cryptoTailStrategy.list.timeWindow')}: {formatTimeWindow(item.windowStartSeconds, item.windowEndSeconds, false)} · {t('cryptoTailStrategy.list.priceRange')}: {formatPriceRange(item.minPrice, item.maxPrice)}
+
+
+
+ openEditModal(item)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
+
+ {t('cryptoTailStrategy.list.edit')}
+
+
+
+ openTriggers(item.id)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}>
+
+ {t('cryptoTailStrategy.list.viewTriggers')}
+
+
+
handleDelete(item.id)} okText={t('common.confirm')} cancelText={t('common.cancel')}>
+
+
+
+ {t('cryptoTailStrategy.list.delete')}
+
+
+
+
+
+ )
+ })}
) : (
{
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f0f0f0'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
>
-
+
)}
@@ -328,7 +328,7 @@ const LeaderList: React.FC = () => {
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f0f0f0'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
>
-
+
@@ -354,7 +354,7 @@ const LeaderList: React.FC = () => {
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
>
-
+
@@ -381,7 +381,7 @@ const LeaderList: React.FC = () => {
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
>
-
+
@@ -421,9 +421,9 @@ const LeaderList: React.FC = () => {
{t('leaderList.title')}
- } onClick={() => navigate('/leaders/add')} size={isMobile ? 'middle' : 'large'} style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px', fontSize: isMobile ? '14px' : '16px' }}>
- {t('leaderList.addLeader')}
-
+
+ } onClick={() => navigate('/leaders/add')} size={isMobile ? 'middle' : 'large'} style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px', fontSize: isMobile ? '14px' : '16px' }} />
+
@@ -440,7 +440,6 @@ const LeaderList: React.FC = () => {
dataSource={leaders}
renderItem={(leader) => {
const balance = balanceMap[leader.id]
- const isLoading = balanceLoading[leader.id]
return (
{
{/* 头部区域 - 名称和地址 */}
@@ -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' }}
>
-
+
{t('common.edit')}
@@ -557,7 +556,7 @@ const LeaderList: React.FC = () => {
}}
>
-
+
{t('leaderList.viewCopyTradings')}
@@ -576,7 +575,7 @@ const LeaderList: React.FC = () => {
}}
>
-
+
{t('leaderList.viewBacktests')}
diff --git a/frontend/src/pages/TemplateList.tsx b/frontend/src/pages/TemplateList.tsx
index ead9a4a..6a3dfce 100644
--- a/frontend/src/pages/TemplateList.tsx
+++ b/frontend/src/pages/TemplateList.tsx
@@ -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) => (
-
- }
- onClick={() => navigate(`/templates/edit/${record.id}`)}
- >
- {t('common.edit') || '编辑'}
-
- }
- onClick={() => handleCopy(record)}
- >
- {t('templateList.copy') || '复制'}
-
+
+
+ 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'}
+ >
+
+
+
+
+
+ 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'}
+ >
+
+
+
+
{
okText={t('common.confirm') || '确定'}
cancelText={t('common.cancel') || '取消'}
>
- }
- >
- {t('common.delete') || '删除'}
-
+
+ e.currentTarget.style.backgroundColor = '#fff1f0'}
+ onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
+ >
+
+
+
)
@@ -254,26 +289,29 @@ const TemplateList: React.FC = () => {
return (
-
-
-
{t('templateList.title') || '跟单模板管理'}
-
- setSearchText(e.target.value)}
- />
+
+
{t('templateList.title') || '跟单模板管理'}
+
+ setSearchText(e.target.value)}
+ />
+
}
onClick={() => navigate('/templates/add')}
- >
- {t('templateList.addTemplate') || '新增模板'}
-
-
-
+ size={isMobile ? 'middle' : 'large'}
+ style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px', fontSize: isMobile ? '14px' : '16px' }}
+ />
+
+
+
+
+
{isMobile ? (
// 移动端卡片布局
@@ -283,116 +321,133 @@ const TemplateList: React.FC = () => {
) : filteredTemplates.length === 0 ? (
-
- {t('templateList.noData') || '暂无模板数据'}
-
+
) : (
-
- {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'
- })
-
+
{
return (
- {/* 模板名称和模式 */}
-
-
+ {/* 头部区域 - 模板名称 */}
+
+
{template.templateName}
-
-
- {template.copyMode === 'RATIO' ? (t('templateList.ratioMode') || '比例模式') : (t('templateList.fixedAmountMode') || '固定金额模式')}
-
-
- {template.supportSell ? (t('templateList.supportSell') || '跟单卖出') : (t('templateList.notSupportSell') || '不跟单卖出')}
-
-
-
-
-
-
- {/* 跟单配置 */}
-
-
{t('templateList.copyConfig') || '跟单配置'}
-
+
{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`
}
-
- {/* 其他配置信息 */}
- {template.copyMode === 'RATIO' && (
-
-
{t('templateList.amountLimit') || '金额限制'}
-
- {template.maxOrderSize && (
-
{t('templateList.max') || '最大'}: {formatUSDC(template.maxOrderSize)} USDC
- )}
- {template.maxOrderSize && template.minOrderSize &&
| }
- {template.minOrderSize && (
-
{t('templateList.min') || '最小'}: {formatUSDC(template.minOrderSize)} USDC
- )}
- {!template.maxOrderSize && !template.minOrderSize &&
{t('templateList.notSet') || '未设置'}}
+
+ {/* 配置信息区域 */}
+
+
+
+
+ {t('templateList.supportSell') || '跟单卖出'}
+
+
+
+ {template.supportSell ? (t('common.yes') || '是') : (t('common.no') || '否')}
+
+
+
+
+
+ {t('templateList.maxDailyOrders') || '每日最大'}
+
+
+ {template.maxDailyOrders} {t('common.orders') || '单'}
+
+
+
+ {/* 金额限制区域(仅比例模式显示) */}
+ {template.copyMode === 'RATIO' && (
+
+ {t('templateList.amountLimit') || '金额限制'}:
+ {template.maxOrderSize && (
+ {t('templateList.max') || '最大'} {formatUSDC(template.maxOrderSize)} USDC
+ )}
+ {template.maxOrderSize && template.minOrderSize && | }
+ {template.minOrderSize && (
+ {t('templateList.min') || '最小'} {formatUSDC(template.minOrderSize)} USDC
+ )}
+ {!template.maxOrderSize && !template.minOrderSize && {t('templateList.notSet') || '未设置'}}
+
)}
-
-
-
{t('templateList.otherConfig') || '其他配置'}
-
- {t('templateList.maxDailyOrders') || '每日最大订单'}: {template.maxDailyOrders} | {t('templateList.priceTolerance') || '价格容忍度'}: {template.priceTolerance}%
-
-
-
+
{/* 创建时间 */}
-
-
- {t('common.createdAt') || '创建时间'}: {formattedDate}
-
+
+ {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'
+ })}
-
- {/* 操作按钮 */}
-
-
}
- onClick={() => navigate(`/templates/edit/${template.id}`)}
- style={{ flex: 1, minWidth: '80px' }}
- >
- {t('common.edit') || '编辑'}
-
-
}
- onClick={() => handleCopy(template)}
- style={{ flex: 1, minWidth: '80px' }}
- >
- {t('templateList.copy') || '复制'}
-
+
+ {/* 图标操作栏 */}
+
+
+ navigate(`/templates/edit/${template.id}`)}
+ style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}
+ >
+
+ {t('common.edit') || '编辑'}
+
+
+
+
+ handleCopy(template)}
+ style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}
+ >
+
+ {t('templateList.copy') || '复制'}
+
+
+
{
okText={t('common.confirm') || '确定'}
cancelText={t('common.cancel') || '取消'}
>
- }
- style={{ flex: 1, minWidth: '80px' }}
- >
- {t('common.delete') || '删除'}
-
+
+
+
+ {t('common.delete') || '删除'}
+
+
)
- })}
-
+ }}
+ />
)}
) : (
diff --git a/frontend/src/pages/UserList.tsx b/frontend/src/pages/UserList.tsx
index 9aa3844..e97bcf0 100644
--- a/frontend/src/pages/UserList.tsx
+++ b/frontend/src/pages/UserList.tsx
@@ -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 (
-
-
-
{t('userList.title') || '用户管理'}
-
- }
- onClick={() => setUpdateOwnPasswordModalVisible(true)}
- >
- {t('userList.updateMyPassword') || '修改我的密码'}
-
- }
- onClick={fetchUsers}
- loading={loading}
- >
- {t('common.refresh') || '刷新'}
-
- {isDefaultUser && (
+
+
{t('userList.title') || '用户管理'}
+
+ }
+ onClick={() => setUpdateOwnPasswordModalVisible(true)}
+ size={isMobile ? 'middle' : 'large'}
+ style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px' }}
+ >
+ {isMobile ? (t('userList.updateMyPassword') || '改密') : (t('userList.updateMyPassword') || '修改我的密码')}
+
+ }
+ onClick={fetchUsers}
+ loading={loading}
+ size={isMobile ? 'middle' : 'large'}
+ style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px' }}
+ >
+ {t('common.refresh') || '刷新'}
+
+ {isDefaultUser && (
+
}
onClick={() => setCreateModalVisible(true)}
- >
- {t('userList.addUser') || '新增用户'}
-
+ size={isMobile ? 'middle' : 'large'}
+ style={{ borderRadius: '8px', height: isMobile ? '40px' : '48px', fontSize: isMobile ? '14px' : '16px' }}
+ />
+
+ )}
+
+
+
+
+ {isMobile ? (
+ // 移动端卡片布局
+
+ {loading ? (
+
+
+
+ ) : users.length === 0 ? (
+
+ ) : (
+
(
+
+ {/* 头部区域 - 用户名 */}
+
+
+
+ {user.username}
+
+
+ ID: {user.id}
+
+
+
+ {/* 角色信息区域 */}
+
+
+
+
+ {t('userList.role') || '角色'}
+
+
+ {user.isDefault ? (t('userList.defaultAccount') || '默认账户') : (t('userList.normalUser') || '普通用户')}
+
+
+
+
+ {t('common.createdAt') || '创建时间'}
+
+
+ {new Date(user.createdAt).toLocaleDateString(i18n.language || 'zh-CN')}
+
+
+
+
+
+ {/* 操作区域(仅管理员可见) */}
+ {isDefaultUser && !user.isDefault && (
+
+
+ {
+ setSelectedUser(user)
+ setUpdatePasswordModalVisible(true)
+ }}
+ style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: 'pointer', padding: '4px 8px' }}
+ >
+
+ {t('userList.updatePassword') || '改密'}
+
+
+
+
handleDelete(user)}
+ okText={t('common.confirm') || '确定'}
+ cancelText={t('common.cancel') || '取消'}
+ >
+
+
+
+ {t('common.delete') || '删除'}
+
+
+
+
+ )}
+
+ )}
+ />
)}
-
-
- t('userList.total', { total }) || `共 ${total} 条`
- }}
- scroll={isMobile ? { x: 600 } : undefined}
- />
+
+ ) : (
+ t('userList.total', { total }) || `共 ${total} 条`
+ }}
+ scroll={isMobile ? { x: 600 } : undefined}
+ />
+ )}
{/* 创建用户弹窗 */}