feat(leader): Leader 管理展示跟单/回测数量并支持跳转筛选
- 后端:LeaderDto 增加 backtestCount,LeaderService 统计回测数并返回 - Leader 列表:展示跟单数、回测数,点击跳转跟单配置/回测页并带 leaderId 筛选 - 跟单配置页:从 URL 读取 leaderId 并应用筛选,Leader 下拉改用 LeaderSelect - 回测页:支持 Leader 筛选,页面加载时请求 Leader 列表,筛选与详情统一显示规则 - LeaderSelect:支持 style,回测/跟单筛选统一使用 - 多语言:补充 leaderList 相关 key - 移动端:Leader 管理卡片内按钮增加内边距 padding 8px 16px Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -54,6 +54,7 @@ data class LeaderDto(
|
|||||||
val remark: String? = null, // Leader 备注(可选)
|
val remark: String? = null, // Leader 备注(可选)
|
||||||
val website: String? = null, // Leader 网站(可选)
|
val website: String? = null, // Leader 网站(可选)
|
||||||
val copyTradingCount: Long = 0, // 跟单关系数量
|
val copyTradingCount: Long = 0, // 跟单关系数量
|
||||||
|
val backtestCount: Long = 0, // 回测数量
|
||||||
val totalOrders: Long? = null, // 总订单数(可选)
|
val totalOrders: Long? = null, // 总订单数(可选)
|
||||||
val totalPnl: String? = null, // 总盈亏(可选)
|
val totalPnl: String? = null, // 总盈亏(可选)
|
||||||
val createdAt: Long,
|
val createdAt: Long,
|
||||||
|
|||||||
+8
-3
@@ -3,6 +3,7 @@ package com.wrbug.polymarketbot.service.copytrading.leaders
|
|||||||
import com.wrbug.polymarketbot.dto.*
|
import com.wrbug.polymarketbot.dto.*
|
||||||
import com.wrbug.polymarketbot.entity.Leader
|
import com.wrbug.polymarketbot.entity.Leader
|
||||||
import com.wrbug.polymarketbot.repository.AccountRepository
|
import com.wrbug.polymarketbot.repository.AccountRepository
|
||||||
|
import com.wrbug.polymarketbot.repository.BacktestTaskRepository
|
||||||
import com.wrbug.polymarketbot.repository.CopyTradingRepository
|
import com.wrbug.polymarketbot.repository.CopyTradingRepository
|
||||||
import com.wrbug.polymarketbot.repository.LeaderRepository
|
import com.wrbug.polymarketbot.repository.LeaderRepository
|
||||||
import com.wrbug.polymarketbot.service.common.BlockchainService
|
import com.wrbug.polymarketbot.service.common.BlockchainService
|
||||||
@@ -20,6 +21,7 @@ class LeaderService(
|
|||||||
private val leaderRepository: LeaderRepository,
|
private val leaderRepository: LeaderRepository,
|
||||||
private val accountRepository: AccountRepository,
|
private val accountRepository: AccountRepository,
|
||||||
private val copyTradingRepository: CopyTradingRepository,
|
private val copyTradingRepository: CopyTradingRepository,
|
||||||
|
private val backtestTaskRepository: BacktestTaskRepository,
|
||||||
private val blockchainService: BlockchainService
|
private val blockchainService: BlockchainService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -157,7 +159,8 @@ class LeaderService(
|
|||||||
|
|
||||||
val leaderDtos = leaders.map { leader ->
|
val leaderDtos = leaders.map { leader ->
|
||||||
val copyTradingCount = copyTradingRepository.countByLeaderId(leader.id!!)
|
val copyTradingCount = copyTradingRepository.countByLeaderId(leader.id!!)
|
||||||
toDto(leader, copyTradingCount)
|
val backtestCount = backtestTaskRepository.findByLeaderId(leader.id).size.toLong()
|
||||||
|
toDto(leader, copyTradingCount, backtestCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
Result.success(
|
Result.success(
|
||||||
@@ -181,7 +184,8 @@ class LeaderService(
|
|||||||
?: return Result.failure(IllegalArgumentException("Leader 不存在"))
|
?: return Result.failure(IllegalArgumentException("Leader 不存在"))
|
||||||
|
|
||||||
val copyTradingCount = copyTradingRepository.countByLeaderId(leaderId)
|
val copyTradingCount = copyTradingRepository.countByLeaderId(leaderId)
|
||||||
Result.success(toDto(leader, copyTradingCount))
|
val backtestCount = backtestTaskRepository.findByLeaderId(leaderId).size.toLong()
|
||||||
|
Result.success(toDto(leader, copyTradingCount, backtestCount))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.error("查询 Leader 详情失败", e)
|
logger.error("查询 Leader 详情失败", e)
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
@@ -225,7 +229,7 @@ class LeaderService(
|
|||||||
/**
|
/**
|
||||||
* 转换为 DTO
|
* 转换为 DTO
|
||||||
*/
|
*/
|
||||||
private fun toDto(leader: Leader, copyTradingCount: Long = 0): LeaderDto {
|
private fun toDto(leader: Leader, copyTradingCount: Long = 0, backtestCount: Long = 0): LeaderDto {
|
||||||
return LeaderDto(
|
return LeaderDto(
|
||||||
id = leader.id!!,
|
id = leader.id!!,
|
||||||
leaderAddress = leader.leaderAddress,
|
leaderAddress = leader.leaderAddress,
|
||||||
@@ -234,6 +238,7 @@ class LeaderService(
|
|||||||
remark = leader.remark,
|
remark = leader.remark,
|
||||||
website = leader.website,
|
website = leader.website,
|
||||||
copyTradingCount = copyTradingCount,
|
copyTradingCount = copyTradingCount,
|
||||||
|
backtestCount = backtestCount,
|
||||||
createdAt = leader.createdAt,
|
createdAt = leader.createdAt,
|
||||||
updatedAt = leader.updatedAt
|
updatedAt = leader.updatedAt
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface LeaderSelectProps {
|
|||||||
showSearch?: boolean
|
showSearch?: boolean
|
||||||
allowClear?: boolean
|
allowClear?: boolean
|
||||||
notFoundContent?: React.ReactNode
|
notFoundContent?: React.ReactNode
|
||||||
|
style?: React.CSSProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
const LeaderSelect: React.FC<LeaderSelectProps> = ({
|
const LeaderSelect: React.FC<LeaderSelectProps> = ({
|
||||||
@@ -25,7 +26,8 @@ const LeaderSelect: React.FC<LeaderSelectProps> = ({
|
|||||||
disabled,
|
disabled,
|
||||||
showSearch = true,
|
showSearch = true,
|
||||||
allowClear = false,
|
allowClear = false,
|
||||||
notFoundContent
|
notFoundContent,
|
||||||
|
style
|
||||||
}) => {
|
}) => {
|
||||||
// 处理选择变化
|
// 处理选择变化
|
||||||
const handleChange = (val: number | undefined) => {
|
const handleChange = (val: number | undefined) => {
|
||||||
@@ -46,6 +48,7 @@ const LeaderSelect: React.FC<LeaderSelectProps> = ({
|
|||||||
showSearch={showSearch}
|
showSearch={showSearch}
|
||||||
allowClear={allowClear}
|
allowClear={allowClear}
|
||||||
notFoundContent={notFoundContent}
|
notFoundContent={notFoundContent}
|
||||||
|
style={style}
|
||||||
optionFilterProp="label"
|
optionFilterProp="label"
|
||||||
optionLabelProp="label"
|
optionLabelProp="label"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -434,7 +434,10 @@
|
|||||||
"openWebsite": "Open Website",
|
"openWebsite": "Open Website",
|
||||||
"all": "All",
|
"all": "All",
|
||||||
"copyTradingCount": "Copy Trading Count",
|
"copyTradingCount": "Copy Trading Count",
|
||||||
|
"backtestCount": "Backtest Count",
|
||||||
"copyTradingRelations": "{{count}} copy trading relations",
|
"copyTradingRelations": "{{count}} copy trading relations",
|
||||||
|
"viewCopyTradings": "View Copy Tradings",
|
||||||
|
"viewBacktests": "View Backtests",
|
||||||
"createdAt": "Created At",
|
"createdAt": "Created At",
|
||||||
"noData": "No Leader data",
|
"noData": "No Leader data",
|
||||||
"fetchFailed": "Failed to get Leader list",
|
"fetchFailed": "Failed to get Leader list",
|
||||||
@@ -442,7 +445,9 @@
|
|||||||
"deleteFailed": "Failed to delete Leader",
|
"deleteFailed": "Failed to delete Leader",
|
||||||
"deleteConfirm": "Are you sure you want to delete this Leader?",
|
"deleteConfirm": "Are you sure you want to delete this Leader?",
|
||||||
"deleteConfirmDesc": "This Leader has {{count}} copy trading relations, please delete them first",
|
"deleteConfirmDesc": "This Leader has {{count}} copy trading relations, please delete them first",
|
||||||
"openDetailFailed": "Failed to open detail"
|
"openDetailFailed": "Failed to open detail",
|
||||||
|
"noCopyTradings": "No copy trading configs",
|
||||||
|
"noBacktests": "No backtests"
|
||||||
},
|
},
|
||||||
"leaderAdd": {
|
"leaderAdd": {
|
||||||
"title": "Add Leader",
|
"title": "Add Leader",
|
||||||
|
|||||||
@@ -434,7 +434,10 @@
|
|||||||
"openWebsite": "打开网页",
|
"openWebsite": "打开网页",
|
||||||
"all": "全部",
|
"all": "全部",
|
||||||
"copyTradingCount": "跟单数",
|
"copyTradingCount": "跟单数",
|
||||||
|
"backtestCount": "回测数",
|
||||||
"copyTradingRelations": "{{count}} 个跟单关系",
|
"copyTradingRelations": "{{count}} 个跟单关系",
|
||||||
|
"viewCopyTradings": "查看跟单配置",
|
||||||
|
"viewBacktests": "查看回测",
|
||||||
"createdAt": "创建时间",
|
"createdAt": "创建时间",
|
||||||
"noData": "暂无 Leader 数据",
|
"noData": "暂无 Leader 数据",
|
||||||
"fetchFailed": "获取 Leader 列表失败",
|
"fetchFailed": "获取 Leader 列表失败",
|
||||||
@@ -442,7 +445,9 @@
|
|||||||
"deleteFailed": "删除 Leader 失败",
|
"deleteFailed": "删除 Leader 失败",
|
||||||
"deleteConfirm": "确定要删除这个 Leader 吗?",
|
"deleteConfirm": "确定要删除这个 Leader 吗?",
|
||||||
"deleteConfirmDesc": "该 Leader 还有 {{count}} 个跟单关系,请先删除跟单关系",
|
"deleteConfirmDesc": "该 Leader 还有 {{count}} 个跟单关系,请先删除跟单关系",
|
||||||
"openDetailFailed": "打开详情失败"
|
"openDetailFailed": "打开详情失败",
|
||||||
|
"noCopyTradings": "暂无跟单配置",
|
||||||
|
"noBacktests": "暂无回测"
|
||||||
},
|
},
|
||||||
"leaderAdd": {
|
"leaderAdd": {
|
||||||
"title": "添加 Leader",
|
"title": "添加 Leader",
|
||||||
|
|||||||
@@ -434,7 +434,10 @@
|
|||||||
"openWebsite": "打開網頁",
|
"openWebsite": "打開網頁",
|
||||||
"all": "全部",
|
"all": "全部",
|
||||||
"copyTradingCount": "跟單數",
|
"copyTradingCount": "跟單數",
|
||||||
|
"backtestCount": "回測數",
|
||||||
"copyTradingRelations": "{{count}} 個跟單關係",
|
"copyTradingRelations": "{{count}} 個跟單關係",
|
||||||
|
"viewCopyTradings": "查看跟單配置",
|
||||||
|
"viewBacktests": "查看回測",
|
||||||
"createdAt": "創建時間",
|
"createdAt": "創建時間",
|
||||||
"noData": "暫無 Leader 數據",
|
"noData": "暫無 Leader 數據",
|
||||||
"fetchFailed": "獲取 Leader 列表失敗",
|
"fetchFailed": "獲取 Leader 列表失敗",
|
||||||
@@ -442,7 +445,9 @@
|
|||||||
"deleteFailed": "刪除 Leader 失敗",
|
"deleteFailed": "刪除 Leader 失敗",
|
||||||
"deleteConfirm": "確定要刪除這個 Leader 嗎?",
|
"deleteConfirm": "確定要刪除這個 Leader 嗎?",
|
||||||
"deleteConfirmDesc": "該 Leader 還有 {{count}} 個跟單關係,請先刪除跟單關係",
|
"deleteConfirmDesc": "該 Leader 還有 {{count}} 個跟單關係,請先刪除跟單關係",
|
||||||
"openDetailFailed": "打開詳情失敗"
|
"openDetailFailed": "打開詳情失敗",
|
||||||
|
"noCopyTradings": "暫無跟單配置",
|
||||||
|
"noBacktests": "暫無回測"
|
||||||
},
|
},
|
||||||
"leaderAdd": {
|
"leaderAdd": {
|
||||||
"title": "添加 Leader",
|
"title": "添加 Leader",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react'
|
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 } from 'antd'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { PlusOutlined, ReloadOutlined, DeleteOutlined, StopOutlined, EyeOutlined, RedoOutlined, CopyOutlined, SyncOutlined } from '@ant-design/icons'
|
import { PlusOutlined, ReloadOutlined, DeleteOutlined, StopOutlined, EyeOutlined, RedoOutlined, CopyOutlined, SyncOutlined } from '@ant-design/icons'
|
||||||
@@ -13,6 +14,7 @@ import LeaderSelect from '../components/LeaderSelect'
|
|||||||
|
|
||||||
const BacktestList: React.FC = () => {
|
const BacktestList: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [tasks, setTasks] = useState<BacktestTaskDto[]>([])
|
const [tasks, setTasks] = useState<BacktestTaskDto[]>([])
|
||||||
@@ -20,7 +22,14 @@ const BacktestList: React.FC = () => {
|
|||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [size] = useState(10)
|
const [size] = useState(10)
|
||||||
const [statusFilter, setStatusFilter] = useState<string | undefined>()
|
const [statusFilter, setStatusFilter] = useState<string | undefined>()
|
||||||
const [leaderIdFilter] = useState<number | undefined>()
|
const [leaderIdFilter, setLeaderIdFilter] = useState<number | undefined>(() => {
|
||||||
|
const leaderIdParam = searchParams.get('leaderId')
|
||||||
|
if (leaderIdParam) {
|
||||||
|
const id = parseInt(leaderIdParam, 10)
|
||||||
|
return isNaN(id) ? undefined : id
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
const [sortBy, setSortBy] = useState<'profitAmount' | 'profitRate' | 'createdAt'>('createdAt')
|
const [sortBy, setSortBy] = useState<'profitAmount' | 'profitRate' | 'createdAt'>('createdAt')
|
||||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc')
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc')
|
||||||
|
|
||||||
@@ -80,6 +89,15 @@ const BacktestList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从 URL 读取 leaderId 并应用筛选(如从 Leader 管理页跳转过来)
|
||||||
|
useEffect(() => {
|
||||||
|
const leaderIdParam = searchParams.get('leaderId')
|
||||||
|
if (leaderIdParam) {
|
||||||
|
const id = parseInt(leaderIdParam, 10)
|
||||||
|
setLeaderIdFilter(isNaN(id) ? undefined : id)
|
||||||
|
}
|
||||||
|
}, [searchParams])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTasks()
|
fetchTasks()
|
||||||
}, [page, statusFilter, leaderIdFilter, sortBy, sortOrder])
|
}, [page, statusFilter, leaderIdFilter, sortBy, sortOrder])
|
||||||
@@ -200,22 +218,20 @@ const BacktestList: React.FC = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取 Leader 列表
|
// 获取 Leader 列表(页面加载时请求,供筛选和创建任务使用)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (createModalVisible) {
|
const fetchLeaders = async () => {
|
||||||
const fetchLeaders = async () => {
|
try {
|
||||||
try {
|
const response = await apiService.leaders.list({})
|
||||||
const response = await apiService.leaders.list({})
|
if (response.data.code === 0 && response.data.data) {
|
||||||
if (response.data.code === 0 && response.data.data) {
|
setLeaders(response.data.data.list || [])
|
||||||
setLeaders(response.data.data.list || [])
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch leaders:', error)
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch leaders:', error)
|
||||||
}
|
}
|
||||||
fetchLeaders()
|
|
||||||
}
|
}
|
||||||
}, [createModalVisible])
|
fetchLeaders()
|
||||||
|
}, [])
|
||||||
|
|
||||||
// 打开创建 modal
|
// 打开创建 modal
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
@@ -487,7 +503,7 @@ const BacktestList: React.FC = () => {
|
|||||||
dataIndex: 'leaderName',
|
dataIndex: 'leaderName',
|
||||||
key: 'leaderName',
|
key: 'leaderName',
|
||||||
width: isMobile ? 100 : 150,
|
width: isMobile ? 100 : 150,
|
||||||
render: (_: any, record: BacktestTaskDto) => record.leaderName || record.leaderAddress?.substring(0, 8) + '...' || '-'
|
render: (_: any, record: BacktestTaskDto) => record.leaderName || `Leader ${record.leaderId}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('backtest.initialBalance'),
|
title: t('backtest.initialBalance'),
|
||||||
@@ -656,6 +672,14 @@ const BacktestList: React.FC = () => {
|
|||||||
<Row justify="space-between" align="middle" gutter={[16, 16]}>
|
<Row justify="space-between" align="middle" gutter={[16, 16]}>
|
||||||
<Col xs={24} sm={24} md={12} lg={16}>
|
<Col xs={24} sm={24} md={12} lg={16}>
|
||||||
<Space size="middle" direction={isMobile ? 'vertical' : 'horizontal'} style={{ width: isMobile ? '100%' : 'auto' }}>
|
<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
|
<Select
|
||||||
style={{ width: isMobile ? '100%' : 150 }}
|
style={{ width: isMobile ? '100%' : 150 }}
|
||||||
placeholder={t('backtest.status')}
|
placeholder={t('backtest.status')}
|
||||||
@@ -1068,7 +1092,7 @@ const BacktestList: React.FC = () => {
|
|||||||
<Descriptions column={isMobile ? 1 : 2} bordered size="small">
|
<Descriptions column={isMobile ? 1 : 2} bordered size="small">
|
||||||
<Descriptions.Item label={t('backtest.taskName')}>{detailTask.taskName}</Descriptions.Item>
|
<Descriptions.Item label={t('backtest.taskName')}>{detailTask.taskName}</Descriptions.Item>
|
||||||
<Descriptions.Item label={t('backtest.leader')}>
|
<Descriptions.Item label={t('backtest.leader')}>
|
||||||
{detailTask.leaderName || detailTask.leaderAddress}
|
{detailTask.leaderName || `Leader ${detailTask.leaderId}`}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label={t('backtest.initialBalance')}>
|
<Descriptions.Item label={t('backtest.initialBalance')}>
|
||||||
{formatUSDC(detailTask.initialBalance)} USDC
|
{formatUSDC(detailTask.initialBalance)} USDC
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react'
|
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 { 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 { PlusOutlined, DeleteOutlined, BarChartOutlined, UnorderedListOutlined, ArrowUpOutlined, ArrowDownOutlined, EditOutlined } from '@ant-design/icons'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -13,11 +14,13 @@ import StatisticsModal from './CopyTradingOrders/StatisticsModal'
|
|||||||
import FilteredOrdersModal from './CopyTradingOrders/FilteredOrdersModal'
|
import FilteredOrdersModal from './CopyTradingOrders/FilteredOrdersModal'
|
||||||
import EditModal from './CopyTradingOrders/EditModal'
|
import EditModal from './CopyTradingOrders/EditModal'
|
||||||
import AddModal from './CopyTradingOrders/AddModal'
|
import AddModal from './CopyTradingOrders/AddModal'
|
||||||
|
import LeaderSelect from '../components/LeaderSelect'
|
||||||
|
|
||||||
const { Option } = Select
|
const { Option } = Select
|
||||||
|
|
||||||
const CopyTradingList: React.FC = () => {
|
const CopyTradingList: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||||
const { accounts, fetchAccounts } = useAccountStore()
|
const { accounts, fetchAccounts } = useAccountStore()
|
||||||
const [copyTradings, setCopyTradings] = useState<CopyTrading[]>([])
|
const [copyTradings, setCopyTradings] = useState<CopyTrading[]>([])
|
||||||
@@ -29,7 +32,14 @@ const CopyTradingList: React.FC = () => {
|
|||||||
accountId?: number
|
accountId?: number
|
||||||
leaderId?: number
|
leaderId?: number
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
}>({})
|
}>(() => {
|
||||||
|
const leaderIdParam = searchParams.get('leaderId')
|
||||||
|
if (leaderIdParam) {
|
||||||
|
const leaderId = parseInt(leaderIdParam, 10)
|
||||||
|
if (!isNaN(leaderId)) return { leaderId }
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
|
||||||
// Modal 状态
|
// Modal 状态
|
||||||
const [ordersModalOpen, setOrdersModalOpen] = useState(false)
|
const [ordersModalOpen, setOrdersModalOpen] = useState(false)
|
||||||
@@ -43,6 +53,17 @@ const CopyTradingList: React.FC = () => {
|
|||||||
const [editModalCopyTradingId, setEditModalCopyTradingId] = useState<string>('')
|
const [editModalCopyTradingId, setEditModalCopyTradingId] = useState<string>('')
|
||||||
const [addModalOpen, setAddModalOpen] = useState(false)
|
const [addModalOpen, setAddModalOpen] = useState(false)
|
||||||
|
|
||||||
|
// 从 URL 读取 leaderId 并应用筛选(如从 Leader 管理页跳转过来)
|
||||||
|
useEffect(() => {
|
||||||
|
const leaderIdParam = searchParams.get('leaderId')
|
||||||
|
if (leaderIdParam) {
|
||||||
|
const leaderId = parseInt(leaderIdParam, 10)
|
||||||
|
if (!isNaN(leaderId)) {
|
||||||
|
setFilters(prev => ({ ...prev, leaderId }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [searchParams])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAccounts()
|
fetchAccounts()
|
||||||
fetchLeaders()
|
fetchLeaders()
|
||||||
@@ -390,19 +411,14 @@ const CopyTradingList: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Select
|
<LeaderSelect
|
||||||
placeholder={t('copyTradingList.filterLeader') || '筛选 Leader'}
|
placeholder={t('copyTradingList.filterLeader') || '筛选 Leader'}
|
||||||
allowClear
|
allowClear
|
||||||
style={{ width: isMobile ? '100%' : 200 }}
|
style={{ width: isMobile ? '100%' : 200 }}
|
||||||
value={filters.leaderId}
|
value={filters.leaderId}
|
||||||
onChange={(value) => setFilters({ ...filters, leaderId: value || undefined })}
|
onChange={(value) => setFilters({ ...filters, leaderId: value || undefined })}
|
||||||
>
|
leaders={leaders}
|
||||||
{leaders.map(leader => (
|
/>
|
||||||
<Option key={leader.id} value={leader.id}>
|
|
||||||
{leader.leaderName || `Leader ${leader.id}`}
|
|
||||||
</Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
placeholder="筛选状态"
|
placeholder="筛选状态"
|
||||||
|
|||||||
@@ -260,7 +260,34 @@ const LeaderList: React.FC = () => {
|
|||||||
dataIndex: 'copyTradingCount',
|
dataIndex: 'copyTradingCount',
|
||||||
key: 'copyTradingCount',
|
key: 'copyTradingCount',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (count: number) => <Tag color="cyan">{count}</Tag>
|
render: (count: number, record: Leader) => (
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
onClick={() => navigate(`/copy-trading?leaderId=${record.id}`)}
|
||||||
|
disabled={count === 0}
|
||||||
|
style={{ padding: 0 }}
|
||||||
|
>
|
||||||
|
<Tag color="cyan">{count}</Tag>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('leaderList.backtestCount'),
|
||||||
|
dataIndex: 'backtestCount',
|
||||||
|
key: 'backtestCount',
|
||||||
|
width: 100,
|
||||||
|
render: (count: number, record: Leader) => (
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
onClick={() => navigate(`/backtest?leaderId=${record.id}`)}
|
||||||
|
disabled={count === 0}
|
||||||
|
style={{ padding: 0 }}
|
||||||
|
>
|
||||||
|
<Tag color="purple">{count}</Tag>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('common.actions'),
|
title: t('common.actions'),
|
||||||
@@ -345,7 +372,24 @@ const LeaderList: React.FC = () => {
|
|||||||
<Divider style={{ margin: '12px 0' }} />
|
<Divider style={{ margin: '12px 0' }} />
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: '8px', marginBottom: '12px', flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: '8px', marginBottom: '12px', flexWrap: 'wrap' }}>
|
||||||
<Tag color="cyan">{leader.copyTradingCount} {t('leaderList.copyTradingCount')}</Tag>
|
<Button
|
||||||
|
type="default"
|
||||||
|
size="small"
|
||||||
|
onClick={() => navigate(`/copy-trading?leaderId=${leader.id}`)}
|
||||||
|
disabled={leader.copyTradingCount === 0}
|
||||||
|
style={{ borderRadius: '6px', padding: '8px 16px' }}
|
||||||
|
>
|
||||||
|
{t('leaderList.viewCopyTradings')} ({leader.copyTradingCount})
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
size="small"
|
||||||
|
onClick={() => navigate(`/backtest?leaderId=${leader.id}`)}
|
||||||
|
disabled={leader.backtestCount === 0}
|
||||||
|
style={{ borderRadius: '6px', padding: '8px 16px' }}
|
||||||
|
>
|
||||||
|
{t('leaderList.viewBacktests')} ({leader.backtestCount})
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{leader.remark && (
|
{leader.remark && (
|
||||||
@@ -356,15 +400,15 @@ const LeaderList: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||||
<Button type="primary" size="small" icon={<EyeOutlined />} onClick={() => handleShowDetail(leader)} style={{ flex: 1, minWidth: '80px', borderRadius: '6px' }}>
|
<Button type="primary" size="small" icon={<EyeOutlined />} onClick={() => handleShowDetail(leader)} style={{ flex: 1, minWidth: '80px', borderRadius: '6px', padding: '8px 16px' }}>
|
||||||
{t('common.viewDetail')}
|
{t('common.viewDetail')}
|
||||||
</Button>
|
</Button>
|
||||||
{leader.website && (
|
{leader.website && (
|
||||||
<Button type="default" size="small" icon={<GlobalOutlined />} onClick={() => window.open(leader.website, '_blank', 'noopener,noreferrer')} style={{ flex: 1, minWidth: '80px', borderRadius: '6px' }}>
|
<Button type="default" size="small" icon={<GlobalOutlined />} onClick={() => window.open(leader.website, '_blank', 'noopener,noreferrer')} style={{ flex: 1, minWidth: '80px', borderRadius: '6px', padding: '8px 16px' }}>
|
||||||
{t('leaderList.openWebsite')}
|
{t('leaderList.openWebsite')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button type="default" size="small" icon={<EditOutlined />} onClick={() => navigate(`/leaders/edit?id=${leader.id}`)} style={{ flex: 1, minWidth: '80px', borderRadius: '6px' }}>
|
<Button type="default" size="small" icon={<EditOutlined />} onClick={() => navigate(`/leaders/edit?id=${leader.id}`)} style={{ flex: 1, minWidth: '80px', borderRadius: '6px', padding: '8px 16px' }}>
|
||||||
{t('common.edit')}
|
{t('common.edit')}
|
||||||
</Button>
|
</Button>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
@@ -374,7 +418,7 @@ const LeaderList: React.FC = () => {
|
|||||||
okText={t('common.confirm')}
|
okText={t('common.confirm')}
|
||||||
cancelText={t('common.cancel')}
|
cancelText={t('common.cancel')}
|
||||||
>
|
>
|
||||||
<Button type="primary" danger size="small" icon={<DeleteOutlined />} style={{ flex: 1, minWidth: '80px', borderRadius: '6px' }}>
|
<Button type="primary" danger size="small" icon={<DeleteOutlined />} style={{ flex: 1, minWidth: '80px', borderRadius: '6px', padding: '8px 16px' }}>
|
||||||
{t('common.delete')}
|
{t('common.delete')}
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export interface Leader {
|
|||||||
remark?: string // Leader 备注(可选)
|
remark?: string // Leader 备注(可选)
|
||||||
website?: string // Leader 网站(可选)
|
website?: string // Leader 网站(可选)
|
||||||
copyTradingCount: number
|
copyTradingCount: number
|
||||||
|
backtestCount: number // 回测数量
|
||||||
totalOrders?: number
|
totalOrders?: number
|
||||||
totalPnl?: string
|
totalPnl?: string
|
||||||
createdAt: number
|
createdAt: number
|
||||||
@@ -978,3 +979,27 @@ export interface NodeCheckResult {
|
|||||||
responseTimeMs?: number
|
responseTimeMs?: number
|
||||||
blockNumber?: string
|
blockNumber?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回测任务 DTO
|
||||||
|
*/
|
||||||
|
export interface BacktestTaskDto {
|
||||||
|
id: number
|
||||||
|
taskName: string
|
||||||
|
leaderId: number
|
||||||
|
leaderName?: string
|
||||||
|
leaderAddress?: string
|
||||||
|
initialBalance: string
|
||||||
|
finalBalance?: string
|
||||||
|
profitAmount?: string
|
||||||
|
profitRate?: string
|
||||||
|
backtestDays: number
|
||||||
|
startTime: number
|
||||||
|
endTime?: number
|
||||||
|
status: string // PENDING/RUNNING/COMPLETED/STOPPED/FAILED
|
||||||
|
progress: number
|
||||||
|
totalTrades: number
|
||||||
|
createdAt: number
|
||||||
|
executionStartedAt?: number
|
||||||
|
executionFinishedAt?: number
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user