From fd25821e3948afe1cd2d707c8ff7192e8d6fed85 Mon Sep 17 00:00:00 2001 From: WrBug Date: Sat, 31 Jan 2026 07:46:16 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E5=9B=9E=E6=B5=8B?= =?UTF-8?q?=E5=8A=9F=E8=83=BDUI=E5=92=8C=E4=BF=AE=E5=A4=8D=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 主要修改 ### UI 优化 - 将回测功能移到跟单交易菜单下作为子菜单 - 创建回测改为使用 Modal 方式(移除独立页面) - 优化导航路径,使用 useNavigate 替代 window.location.href ### 功能修复 - 修复 API 路径问题:移除重复的 /api 前缀(/api/backtest -> /backtest) - 修复 copyRatio 输入框单位:改为百分比输入(100 表示 100%),与跟单配置保持一致 - 修复回测列表中 leader 列显示为空的问题:优先显示 leaderName,为空时显示 leaderAddress ### 多语言完善 - 补充缺失的多语言 key: - common.ascending / common.descending / common.day - backtest.sortBy / backtest.sortOrder / backtest.createdAt - backtest.copyRatioTooltip / backtest.copyRatioPlaceholder - 以及其他表单验证相关的 key - 为中文简体、中文繁体、英文三种语言都添加了完整的翻译 ## 修改的文件 - frontend/src/App.tsx - 移除 BacktestCreate 路由 - frontend/src/components/Layout.tsx - 调整菜单结构 - frontend/src/pages/BacktestList.tsx - Modal 创建、修复 leader 显示 - frontend/src/pages/BacktestDetail.tsx - 优化导航 - frontend/src/pages/BacktestCreate.tsx - 同步 copyRatio 修改 - frontend/src/services/api.ts - 修复 API 路径 - frontend/src/locales/* - 补充多语言配置 --- frontend/src/App.tsx | 2 - frontend/src/components/Layout.tsx | 14 +- frontend/src/locales/en/common.json | 26 +- frontend/src/locales/zh-CN/common.json | 26 +- frontend/src/locales/zh-TW/common.json | 26 +- frontend/src/pages/BacktestCreate.tsx | 30 +- frontend/src/pages/BacktestDetail.tsx | 7 +- frontend/src/pages/BacktestList.tsx | 410 ++++++++++++++++++++++++- frontend/src/services/api.ts | 12 +- 9 files changed, 514 insertions(+), 39 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1fbc712..3e3cd49 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -33,7 +33,6 @@ import ApiHealthStatus from './pages/ApiHealthStatus' import RpcNodeSettings from './pages/RpcNodeSettings' import Announcements from './pages/Announcements' import BacktestList from './pages/BacktestList' -import BacktestCreate from './pages/BacktestCreate' import BacktestDetail from './pages/BacktestDetail' import { wsManager } from './services/websocket' import type { OrderPushMessage } from './types' @@ -258,7 +257,6 @@ function App() { } /> } /> } /> - } /> } /> } /> } /> diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 38d34b9..ddae261 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -72,7 +72,7 @@ const Layout: React.FC = ({ children }) => { const getInitialOpenKeys = (): string[] => { const path = location.pathname const keys: string[] = [] - if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading')) { + if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading') || path.startsWith('/backtest')) { keys.push('/copy-trading-management') } if (path.startsWith('/system-settings')) { @@ -87,7 +87,7 @@ const Layout: React.FC = ({ children }) => { useEffect(() => { const path = location.pathname const keys: string[] = [] - if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading')) { + if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading') || path.startsWith('/backtest')) { keys.push('/copy-trading-management') } if (path.startsWith('/system-settings')) { @@ -149,6 +149,11 @@ const Layout: React.FC = ({ children }) => { key: '/templates', icon: , label: t('menu.templates') + }, + { + key: '/backtest', + icon: , + label: t('menu.backtest') || '回测' } ] }, @@ -157,11 +162,6 @@ const Layout: React.FC = ({ children }) => { icon: , label: t('menu.positions') }, - { - key: '/backtest', - icon: , - label: t('menu.backtest') || '回测' - }, { key: '/statistics', icon: , diff --git a/frontend/src/locales/en/common.json b/frontend/src/locales/en/common.json index 5b38b59..7bd22cb 100644 --- a/frontend/src/locales/en/common.json +++ b/frontend/src/locales/en/common.json @@ -34,7 +34,10 @@ "previous": "Previous", "next": "Next", "page": "Page", - "pageOf": "Page" + "pageOf": "Page", + "ascending": "Ascending", + "descending": "Descending", + "day": "day" }, "account": { "title": "Account Management", @@ -1326,6 +1329,25 @@ "starting": "Starting...", "stopping": "Stopping...", "errorOccurred": "Error occurred", - "retry": "Retry" + "retry": "Retry", + "taskNameRequired": "Please enter task name", + "leaderRequired": "Please select Leader", + "initialBalanceRequired": "Please enter initial balance", + "initialBalanceInvalid": "Initial balance must be greater than 0", + "backtestDaysRequired": "Please enter backtest days", + "backtestDaysInvalid": "Backtest days must be between 1-15", + "copyRatioRequired": "Please enter copy ratio", + "copyRatioInvalid": "Copy ratio must be between 0.01-10000", + "copyRatioPlaceholder": "For example: 100 means 100% (1:1 copy), default 100%", + "copyRatioTooltip": "Copy ratio represents the percentage of copy amount relative to Leader order amount. For example: 100% means 1:1 copy, 50% means half position copy, 200% means double copy", + "fixedAmountRequired": "Please enter fixed amount", + "fixedAmountInvalid": "Fixed amount must be greater than 0", + "advancedFilters": "Advanced Filters", + "keywordsPlaceholder": "Please enter keywords, press Enter to add", + "delaySecondsHint": "Delay execution to simulate real copy trading delay", + "supportSellHint": "Whether to follow Leader's sell operations", + "sortBy": "Sort By", + "sortOrder": "Sort Order", + "createdAt": "Created At" } } \ No newline at end of file diff --git a/frontend/src/locales/zh-CN/common.json b/frontend/src/locales/zh-CN/common.json index 7688396..8bee86c 100644 --- a/frontend/src/locales/zh-CN/common.json +++ b/frontend/src/locales/zh-CN/common.json @@ -34,7 +34,10 @@ "pageOf": "第", "success": "成功", "failed": "失败", - "close": "关闭" + "close": "关闭", + "ascending": "升序", + "descending": "降序", + "day": "天" }, "login": { "title": "登录", @@ -1326,6 +1329,25 @@ "starting": "启动中...", "stopping": "停止中...", "errorOccurred": "发生错误", - "retry": "重试" + "retry": "重试", + "taskNameRequired": "请输入任务名称", + "leaderRequired": "请选择 Leader", + "initialBalanceRequired": "请输入初始资金", + "initialBalanceInvalid": "初始资金必须大于 0", + "backtestDaysRequired": "请输入回测天数", + "backtestDaysInvalid": "回测天数必须在 1-15 之间", + "copyRatioRequired": "请输入跟单比例", + "copyRatioInvalid": "跟单比例必须在 0.01-10000 之间", + "copyRatioPlaceholder": "例如:100 表示 100%(1:1 跟单),默认 100%", + "copyRatioTooltip": "跟单比例表示跟单金额相对于 Leader 订单金额的百分比。例如:100% 表示 1:1 跟单,50% 表示半仓跟单,200% 表示双倍跟单", + "fixedAmountRequired": "请输入固定金额", + "fixedAmountInvalid": "固定金额必须大于 0", + "advancedFilters": "高级过滤", + "keywordsPlaceholder": "请输入关键字,按回车添加", + "delaySecondsHint": "延迟执行模拟真实跟单延迟", + "supportSellHint": "是否跟随 Leader 的卖出操作", + "sortBy": "排序字段", + "sortOrder": "排序顺序", + "createdAt": "创建时间" } } \ No newline at end of file diff --git a/frontend/src/locales/zh-TW/common.json b/frontend/src/locales/zh-TW/common.json index b2dec19..42d8b8b 100644 --- a/frontend/src/locales/zh-TW/common.json +++ b/frontend/src/locales/zh-TW/common.json @@ -34,7 +34,10 @@ "previous": "上一頁", "next": "下一頁", "page": "頁", - "pageOf": "第" + "pageOf": "第", + "ascending": "升序", + "descending": "降序", + "day": "天" }, "account": { "title": "賬戶管理", @@ -1326,6 +1329,25 @@ "starting": "啟動中...", "stopping": "停止中...", "errorOccurred": "發生錯誤", - "retry": "重試" + "retry": "重試", + "taskNameRequired": "請輸入任務名稱", + "leaderRequired": "請選擇 Leader", + "initialBalanceRequired": "請輸入初始資金", + "initialBalanceInvalid": "初始資金必須大於 0", + "backtestDaysRequired": "請輸入回測天數", + "backtestDaysInvalid": "回測天數必須在 1-15 之間", + "copyRatioRequired": "請輸入跟單比例", + "copyRatioInvalid": "跟單比例必須在 0.01-10000 之間", + "copyRatioPlaceholder": "例如:100 表示 100%(1:1 跟單),預設 100%", + "copyRatioTooltip": "跟單比例表示跟單金額相對於 Leader 訂單金額的百分比。例如:100% 表示 1:1 跟單,50% 表示半倉跟單,200% 表示雙倍跟單", + "fixedAmountRequired": "請輸入固定金額", + "fixedAmountInvalid": "固定金額必須大於 0", + "advancedFilters": "進階過濾", + "keywordsPlaceholder": "請輸入關鍵字,按回車添加", + "delaySecondsHint": "延遲執行模擬真實跟單延遲", + "supportSellHint": "是否跟隨 Leader 的賣出操作", + "sortBy": "排序欄位", + "sortOrder": "排序順序", + "createdAt": "創建時間" } } \ No newline at end of file diff --git a/frontend/src/pages/BacktestCreate.tsx b/frontend/src/pages/BacktestCreate.tsx index 76e0540..72b6c6e 100644 --- a/frontend/src/pages/BacktestCreate.tsx +++ b/frontend/src/pages/BacktestCreate.tsx @@ -45,7 +45,7 @@ const BacktestCreate: React.FC = () => { initialBalance: values.initialBalance, backtestDays: values.backtestDays, copyMode: values.copyMode || 'RATIO', - copyRatio: values.copyMode === 'RATIO' ? values.copyRatio : undefined, + copyRatio: values.copyMode === 'RATIO' && values.copyRatio ? (values.copyRatio / 100).toString() : undefined, fixedAmount: values.copyMode === 'FIXED' ? values.fixedAmount : undefined, maxOrderSize: values.maxOrderSize, minOrderSize: values.minOrderSize, @@ -88,7 +88,7 @@ const BacktestCreate: React.FC = () => { useEffect(() => { form.setFieldsValue({ copyMode: 'RATIO', - copyRatio: 1.0, + copyRatio: 100, // 默认 100%(显示为百分比) maxOrderSize: 1000, minOrderSize: 1, maxDailyLoss: 500, @@ -117,7 +117,7 @@ const BacktestCreate: React.FC = () => { onFinish={handleSubmit} initialValues={{ copyMode: 'RATIO', - copyRatio: 1.0, + copyRatio: 100, // 默认 100%(显示为百分比) maxOrderSize: 1000, minOrderSize: 1, maxDailyLoss: 500, @@ -212,18 +212,32 @@ const BacktestCreate: React.FC = () => { { + const parsed = parseFloat(value || '0') + if (parsed > 10000) return 10000 + return parsed + }} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + if (num > 10000) return '10000' + return num.toString().replace(/\.0+$/, '') + }} /> )} diff --git a/frontend/src/pages/BacktestDetail.tsx b/frontend/src/pages/BacktestDetail.tsx index 754166a..1012cc4 100644 --- a/frontend/src/pages/BacktestDetail.tsx +++ b/frontend/src/pages/BacktestDetail.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react' -import { useParams } from 'react-router-dom' +import { useParams, useNavigate } from 'react-router-dom' import { Card, Descriptions, Button, Tag, Space, Table, message, Row, Col, Statistic, Spin } from 'antd' import { ArrowLeftOutlined, ReloadOutlined, DeleteOutlined, StopOutlined } from '@ant-design/icons' import { useTranslation } from 'react-i18next' @@ -10,6 +10,7 @@ import BacktestChart from './BacktestChart' const BacktestDetail: React.FC = () => { const { t } = useTranslation() + const navigate = useNavigate() const { id } = useParams<{ id: string }>() const [loading, setLoading] = useState(false) const [task, setTask] = useState(null) @@ -102,7 +103,7 @@ const BacktestDetail: React.FC = () => { // 返回 const handleBack = () => { - window.location.href = '/backtest/list' + navigate('/backtest') } // 停止任务 @@ -136,7 +137,7 @@ const BacktestDetail: React.FC = () => { const response = await backtestService.delete({ id: parseInt(id!) }) if (response.data.code === 0) { message.success(t('backtest.deleteSuccess')) - window.location.href = '/backtest/list' + navigate('/backtest') } else { message.error(response.data.msg || t('backtest.deleteFailed')) } diff --git a/frontend/src/pages/BacktestList.tsx b/frontend/src/pages/BacktestList.tsx index 5f759ab..a26da01 100644 --- a/frontend/src/pages/BacktestList.tsx +++ b/frontend/src/pages/BacktestList.tsx @@ -1,13 +1,18 @@ import { useState, useEffect } from 'react' -import { Table, Card, Button, Select, Tag, Space, Modal, message, Row, Col } from 'antd' +import { Table, Card, Button, Select, Tag, Space, Modal, message, Row, Col, Form, Input, InputNumber, Switch } from 'antd' import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router-dom' import { PlusOutlined, ReloadOutlined, DeleteOutlined, StopOutlined, EyeOutlined } from '@ant-design/icons' import { formatUSDC } from '../utils' -import { backtestService } from '../services/api' -import type { BacktestTaskDto, BacktestListRequest } from '../types/backtest' +import { backtestService, apiService } from '../services/api' +import type { BacktestTaskDto, BacktestListRequest, BacktestCreateRequest } from '../types/backtest' +import type { Leader } from '../types' + +const { Option } = Select const BacktestList: React.FC = () => { const { t } = useTranslation() + const navigate = useNavigate() const [loading, setLoading] = useState(false) const [tasks, setTasks] = useState([]) const [total, setTotal] = useState(0) @@ -17,6 +22,13 @@ const BacktestList: React.FC = () => { const [leaderIdFilter] = useState() const [sortBy, setSortBy] = useState<'profitAmount' | 'profitRate' | 'createdAt'>('createdAt') const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc') + + // 创建回测 modal 相关状态 + const [createModalVisible, setCreateModalVisible] = useState(false) + const [createForm] = Form.useForm() + const [createLoading, setCreateLoading] = useState(false) + const [leaders, setLeaders] = useState([]) + const [copyMode, setCopyMode] = useState<'RATIO' | 'FIXED'>('RATIO') // 获取回测任务列表 const fetchTasks = async () => { @@ -100,14 +112,94 @@ const BacktestList: React.FC = () => { }) } + // 获取 Leader 列表 + useEffect(() => { + if (createModalVisible) { + const fetchLeaders = async () => { + try { + const response = await apiService.leaders.list({}) + if (response.data.code === 0 && response.data.data) { + setLeaders(response.data.data.list || []) + } + } catch (error) { + console.error('Failed to fetch leaders:', error) + } + } + fetchLeaders() + } + }, [createModalVisible]) + // 查看详情 const handleViewDetail = (id: number) => { - window.location.href = `/backtest/detail?id=${id}` + navigate(`/backtest/detail/${id}`) } - // 创建新任务 + // 打开创建 modal const handleCreate = () => { - window.location.href = '/backtest/create' + setCreateModalVisible(true) + createForm.resetFields() + createForm.setFieldsValue({ + copyMode: 'RATIO', + copyRatio: 100, // 默认 100%(显示为百分比) + maxOrderSize: 1000, + minOrderSize: 1, + maxDailyLoss: 500, + maxDailyOrders: 50, + priceTolerance: 5, + delaySeconds: 0, + supportSell: true, + keywordFilterMode: 'DISABLED', + backtestDays: 7 + }) + setCopyMode('RATIO') + } + + // 提交创建回测任务 + const handleCreateSubmit = async () => { + try { + const values = await createForm.validateFields() + setCreateLoading(true) + + const request: BacktestCreateRequest = { + taskName: values.taskName, + leaderId: values.leaderId, + initialBalance: values.initialBalance, + backtestDays: values.backtestDays, + copyMode: values.copyMode || 'RATIO', + copyRatio: values.copyMode === 'RATIO' && values.copyRatio ? (values.copyRatio / 100).toString() : undefined, + fixedAmount: values.copyMode === 'FIXED' ? values.fixedAmount : undefined, + maxOrderSize: values.maxOrderSize, + minOrderSize: values.minOrderSize, + maxDailyLoss: values.maxDailyLoss, + maxDailyOrders: values.maxDailyOrders, + priceTolerance: values.priceTolerance, + delaySeconds: values.delaySeconds, + supportSell: values.supportSell, + minOrderDepth: values.minOrderDepth, + maxSpread: values.maxSpread, + minPrice: values.minPrice, + maxPrice: values.maxPrice, + maxPositionValue: values.maxPositionValue, + keywordFilterMode: values.keywordFilterMode, + keywords: values.keywords, + maxMarketEndDate: values.maxMarketEndDate + } + + const response = await backtestService.create(request) + if (response.data.code === 0) { + message.success(t('backtest.createSuccess')) + setCreateModalVisible(false) + createForm.resetFields() + fetchTasks() + } else { + message.error(response.data.msg || t('backtest.createFailed')) + } + } catch (error) { + console.error('Failed to create backtest task:', error) + message.error(t('backtest.createFailed')) + } finally { + setCreateLoading(false) + } } // 状态标签颜色 @@ -145,7 +237,8 @@ const BacktestList: React.FC = () => { title: t('backtest.leader'), dataIndex: 'leaderName', key: 'leaderName', - width: 150 + width: 150, + render: (_: any, record: BacktestTaskDto) => record.leaderName || record.leaderAddress || '-' }, { title: t('backtest.initialBalance'), @@ -356,6 +449,309 @@ const BacktestList: React.FC = () => { /> + + {/* 创建回测任务 Modal */} + { + setCreateModalVisible(false) + createForm.resetFields() + }} + onOk={handleCreateSubmit} + okText={t('common.save')} + cancelText={t('common.cancel')} + width={800} + confirmLoading={createLoading} + destroyOnClose + style={{ top: 20 }} + bodyStyle={{ maxHeight: 'calc(100vh - 200px)', overflowY: 'auto' }} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + {/* 跟单配置 */} +
+

{t('backtest.config')}

+ + + + + + {copyMode === 'RATIO' && ( + + { + const parsed = parseFloat(value || '0') + if (parsed > 10000) return 10000 + return parsed + }} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + if (num > 10000) return '10000' + return num.toString().replace(/\.0+$/, '') + }} + /> + + )} + + {copyMode === 'FIXED' && ( + + + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {t('backtest.delaySecondsHint') || '延迟执行模拟真实跟单延迟'} + + + + + {t('backtest.supportSellHint') || '是否跟随 Leader 的卖出操作'} + + +

{t('backtest.advancedFilters')}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +