diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CopyTradingStatisticsDto.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CopyTradingStatisticsDto.kt index 4d10605..723e103 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CopyTradingStatisticsDto.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CopyTradingStatisticsDto.kt @@ -110,7 +110,7 @@ data class OrderTrackingRequest( val page: Int? = 1, val limit: Int? = 20, val marketId: String? = null, - val side: String? = null, + val marketTitle: String? = null, // 市场标题关键字筛选 val status: String? = null, val sellOrderId: String? = null, val buyOrderId: String? = null @@ -123,7 +123,9 @@ data class MarketGroupedOrdersRequest( val copyTradingId: Long, val type: String, // buy, sell, matched val page: Int? = 1, - val limit: Int? = 20 + val limit: Int? = 20, + val marketId: String? = null, + val marketTitle: String? = null ) /** diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyTradingStatisticsService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyTradingStatisticsService.kt index c47b837..3d09291 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyTradingStatisticsService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyTradingStatisticsService.kt @@ -134,12 +134,21 @@ class CopyTradingStatisticsService( private fun getBuyOrderList(request: OrderTrackingRequest): Pair, Long> { var orders = copyOrderTrackingRepository.findByCopyTradingId(request.copyTradingId) + // 批量获取市场信息(用于筛选) + val allMarketIds = orders.map { it.marketId }.distinct() + val markets = marketService.getMarkets(allMarketIds) + // 筛选 if (!request.marketId.isNullOrBlank()) { - orders = orders.filter { it.marketId == request.marketId } + // marketId 支持模糊匹配 + orders = orders.filter { it.marketId.contains(request.marketId!!, ignoreCase = true) } } - if (!request.side.isNullOrBlank()) { - orders = orders.filter { it.side == request.side } + if (!request.marketTitle.isNullOrBlank()) { + // marketTitle 关键字筛选 + orders = orders.filter { order -> + val market = markets[order.marketId] + market?.title?.contains(request.marketTitle!!, ignoreCase = true) == true + } } if (!request.status.isNullOrBlank()) { orders = orders.filter { it.status == request.status } @@ -157,10 +166,6 @@ class CopyTradingStatisticsService( val end = minOf(start + limit, orders.size) val pagedOrders = if (start < orders.size) orders.subList(start, end) else emptyList() - // 批量获取市场信息 - val marketIds = pagedOrders.map { it.marketId }.distinct() - val markets = marketService.getMarkets(marketIds) - // 转换为DTO val list = pagedOrders.map { order -> val amount = order.quantity.toSafeBigDecimal().multi(order.price) @@ -193,12 +198,21 @@ class CopyTradingStatisticsService( private fun getSellOrderList(request: OrderTrackingRequest): Pair, Long> { var records = sellMatchRecordRepository.findByCopyTradingId(request.copyTradingId) + // 批量获取市场信息(用于筛选) + val allMarketIds = records.map { it.marketId }.distinct() + val markets = marketService.getMarkets(allMarketIds) + // 筛选 if (!request.marketId.isNullOrBlank()) { - records = records.filter { it.marketId == request.marketId } + // marketId 支持模糊匹配 + records = records.filter { it.marketId.contains(request.marketId!!, ignoreCase = true) } } - if (!request.side.isNullOrBlank()) { - records = records.filter { it.side == request.side } + if (!request.marketTitle.isNullOrBlank()) { + // marketTitle 关键字筛选 + records = records.filter { record -> + val market = markets[record.marketId] + market?.title?.contains(request.marketTitle!!, ignoreCase = true) == true + } } val total = records.size.toLong() @@ -213,10 +227,6 @@ class CopyTradingStatisticsService( val end = minOf(start + limit, records.size) val pagedRecords = if (start < records.size) records.subList(start, end) else emptyList() - // 批量获取市场信息 - val marketIds = pagedRecords.map { it.marketId }.distinct() - val markets = marketService.getMarkets(marketIds) - // 转换为DTO val list = pagedRecords.map { record -> val amount = record.totalMatchedQuantity.toSafeBigDecimal().multi(record.sellPrice) @@ -247,6 +257,14 @@ class CopyTradingStatisticsService( private fun getMatchedOrderList(request: OrderTrackingRequest): Pair, Long> { val matchDetails = sellMatchDetailRepository.findByCopyTradingId(request.copyTradingId) + // 获取所有相关的卖出记录(用于筛选) + val matchRecordIds = matchDetails.map { it.matchRecordId }.distinct() + val matchRecords = matchRecordIds.mapNotNull { id -> + sellMatchRecordRepository.findById(id).orElse(null) + } + val marketIds = matchRecords.map { it.marketId }.distinct() + val markets = marketService.getMarkets(marketIds) + // 筛选 var filtered = matchDetails if (!request.sellOrderId.isNullOrBlank()) { @@ -260,6 +278,21 @@ class CopyTradingStatisticsService( if (!request.buyOrderId.isNullOrBlank()) { filtered = filtered.filter { it.buyOrderId == request.buyOrderId } } + if (!request.marketId.isNullOrBlank()) { + // marketId 支持模糊匹配 + filtered = filtered.filter { detail -> + val matchRecord = matchRecords.find { it.id == detail.matchRecordId } + matchRecord?.marketId?.contains(request.marketId!!, ignoreCase = true) == true + } + } + if (!request.marketTitle.isNullOrBlank()) { + // marketTitle 关键字筛选 + filtered = filtered.filter { detail -> + val matchRecord = matchRecords.find { it.id == detail.matchRecordId } + val market = matchRecord?.let { markets[it.marketId] } + market?.title?.contains(request.marketTitle!!, ignoreCase = true) == true + } + } val total = filtered.size.toLong() @@ -274,17 +307,17 @@ class CopyTradingStatisticsService( val pagedDetails = if (start < filtered.size) filtered.subList(start, end) else emptyList() // 获取匹配记录以获取市场ID - val matchRecordIds = pagedDetails.map { it.matchRecordId }.distinct() - val matchRecords = matchRecordIds.mapNotNull { id -> + val pagedMatchRecordIds = pagedDetails.map { it.matchRecordId }.distinct() + val pagedMatchRecords = pagedMatchRecordIds.mapNotNull { id -> sellMatchRecordRepository.findById(id).orElse(null) } - val marketIds = matchRecords.map { it.marketId }.distinct() - val markets = marketService.getMarkets(marketIds) + val pagedMarketIds = pagedMatchRecords.map { it.marketId }.distinct() + val pagedMarkets = marketService.getMarkets(pagedMarketIds) // 转换为DTO val list = pagedDetails.map { detail -> - val matchRecord = matchRecords.find { it.id == detail.matchRecordId } - val market = matchRecord?.let { markets[it.marketId] } + val matchRecord = pagedMatchRecords.find { it.id == detail.matchRecordId } + val market = matchRecord?.let { pagedMarkets[it.marketId] } MatchedOrderInfo( sellOrderId = matchRecord?.sellOrderId ?: "", buyOrderId = detail.buyOrderId, @@ -550,7 +583,23 @@ class CopyTradingStatisticsService( // 2. 获取所有买入订单 var orders = copyOrderTrackingRepository.findByCopyTradingId(request.copyTradingId) - // 3. 按市场ID分组 + // 3. 批量获取市场信息(用于筛选) + val allMarketIds = orders.map { it.marketId }.distinct() + val markets = marketService.getMarkets(allMarketIds) + + // 4. 筛选 + if (!request.marketId.isNullOrBlank()) { + // marketId 支持模糊匹配 + orders = orders.filter { it.marketId.contains(request.marketId!!, ignoreCase = true) } + } + if (!request.marketTitle.isNullOrBlank()) { + // marketTitle 关键字筛选 + orders = orders.filter { order -> + val market = markets[order.marketId] + market?.title?.contains(request.marketTitle!!, ignoreCase = true) == true + } + } + // 5. 按市场ID分组 val groups = mutableMapOf>() orders.forEach { order -> val marketId = order.marketId @@ -562,7 +611,6 @@ class CopyTradingStatisticsService( // 4. 转换为分组数据并计算统计信息 val marketIds = groups.keys.toList() - val markets = marketService.getMarkets(marketIds) val list = marketIds.map { marketId -> val marketOrders = groups[marketId] ?: mutableListOf() @@ -659,9 +707,25 @@ class CopyTradingStatisticsService( ?: return Result.failure(IllegalArgumentException("跟单关系不存在: ${request.copyTradingId}")) // 2. 获取所有卖出记录 - val sellRecords = sellMatchRecordRepository.findByCopyTradingId(request.copyTradingId) + var sellRecords = sellMatchRecordRepository.findByCopyTradingId(request.copyTradingId) - // 3. 按市场ID分组 + // 3. 批量获取市场信息(用于筛选) + val allMarketIds = sellRecords.map { it.marketId }.distinct() + val markets = marketService.getMarkets(allMarketIds) + + // 4. 筛选 + if (!request.marketId.isNullOrBlank()) { + // marketId 支持模糊匹配 + sellRecords = sellRecords.filter { it.marketId.contains(request.marketId!!, ignoreCase = true) } + } + if (!request.marketTitle.isNullOrBlank()) { + // marketTitle 关键字筛选 + sellRecords = sellRecords.filter { record -> + val market = markets[record.marketId] + market?.title?.contains(request.marketTitle!!, ignoreCase = true) == true + } + } + // 5. 按市场ID分组 val groups = mutableMapOf>() sellRecords.forEach { record -> val marketId = record.marketId @@ -673,7 +737,6 @@ class CopyTradingStatisticsService( // 4. 转换为分组数据并计算统计信息 val marketIds = groups.keys.toList() - val markets = marketService.getMarkets(marketIds) val list = marketIds.map { marketId -> val marketRecords = groups[marketId] ?: mutableListOf() diff --git a/frontend/src/locales/zh-CN/common.json b/frontend/src/locales/zh-CN/common.json index 5d89968..940ed7e 100644 --- a/frontend/src/locales/zh-CN/common.json +++ b/frontend/src/locales/zh-CN/common.json @@ -1089,6 +1089,7 @@ "price": "价格", "amount": "金额", "filterMarketId": "筛选市场ID", + "filterMarketTitle": "筛选市场标题", "filterSide": "筛选方向", "filterStatus": "筛选状态", "filterSellOrderId": "筛选卖出订单ID", diff --git a/frontend/src/pages/CopyTradingOrders/BuyOrdersTab.tsx b/frontend/src/pages/CopyTradingOrders/BuyOrdersTab.tsx index c392547..fcc895b 100644 --- a/frontend/src/pages/CopyTradingOrders/BuyOrdersTab.tsx +++ b/frontend/src/pages/CopyTradingOrders/BuyOrdersTab.tsx @@ -25,19 +25,44 @@ const BuyOrdersTab: React.FC = ({ copyTradingId, active = fal const [limit, setLimit] = useState(20) const [filters, setFilters] = useState<{ marketId?: string - side?: string + marketTitle?: string status?: string }>({}) - const [groupByMarket, setGroupByMarket] = useState(false) // 是否按市场分组 + // 从 localStorage 读取用户分组偏好,如果没有则默认为 false + const [groupByMarket, setGroupByMarket] = useState(() => { + const savedPreference = localStorage.getItem('copyTradingOrders_groupByMarket') + return savedPreference === 'true' + }) const [expandedMarkets, setExpandedMarkets] = useState>(new Set()) // 展开的市场ID集合 const [groupedOrders, setGroupedOrders] = useState([]) // 分组数据(从后端获取) const [groupedTotal, setGroupedTotal] = useState(0) // 分组总数(市场数量) + const handleMarketTitleChange = (value: string) => { + setFilters({ ...filters, marketTitle: value || undefined }) + } + + // 切换分组模式时,保存用户偏好到 localStorage + const handleToggleGroupByMarket = (checked: boolean) => { + setGroupByMarket(checked) + localStorage.setItem('copyTradingOrders_groupByMarket', String(checked)) + } + useEffect(() => { if (copyTradingId && active) { fetchOrders() } - }, [copyTradingId, active, page, limit, filters, groupByMarket]) + }, [copyTradingId, active, page, limit, filters.marketId, filters.status, groupByMarket]) + + // 防抖搜索 - marketTitle 和 marketId 变化时延迟0.5秒再搜索 + useEffect(() => { + if (!copyTradingId || !active) return + + const timer = setTimeout(() => { + fetchOrders() + }, 500) // 0.5秒延迟 + + return () => clearTimeout(timer) + }, [filters.marketTitle]) const fetchOrders = async () => { if (!copyTradingId) return @@ -45,12 +70,14 @@ const BuyOrdersTab: React.FC = ({ copyTradingId, active = fal setLoading(true) try { if (groupByMarket) { - // 调用分组接口 + // 调用分组接口(支持市场筛选) const request: MarketGroupedOrdersRequest = { copyTradingId: parseInt(copyTradingId), type: 'buy', page, - limit + limit, + marketId: filters.marketId, + marketTitle: filters.marketTitle } const response = await apiService.orderTracking.listGroupedByMarket(request) @@ -123,7 +150,7 @@ const BuyOrdersTab: React.FC = ({ copyTradingId, active = fal } } - const columns = [ + const columns = [ { title: t('copyTradingOrders.orderId') || '订单ID', dataIndex: 'orderId', @@ -208,16 +235,6 @@ const BuyOrdersTab: React.FC = ({ copyTradingId, active = fal ) } }, - { - title: t('copyTradingOrders.side') || '方向', - dataIndex: 'side', - key: 'side', - width: isMobile ? 60 : 80, - render: (side: string) => { - const displaySide = side === '0' ? 'YES' : side === '1' ? 'NO' : side - return {displaySide} - } - }, { title: t('copyTradingOrders.buyQuantity') || '买入数量', dataIndex: 'quantity', @@ -400,7 +417,6 @@ const BuyOrdersTab: React.FC = ({ copyTradingId, active = fal minute: '2-digit' }) const amount = (parseFloat(order.quantity) * parseFloat(order.price)).toString() - const displaySide = order.side === '0' ? 'YES' : order.side === '1' ? 'NO' : order.side return ( = ({ copyTradingId, active = fal )}
- {displaySide} {getStatusTag(order.status)}
@@ -485,18 +500,17 @@ const BuyOrdersTab: React.FC = ({ copyTradingId, active = fal ) : (
{orders.map((order) => { - const date = new Date(order.createdAt) - const formattedDate = date.toLocaleString('zh-CN', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit' - }) - const amount = (parseFloat(order.quantity) * parseFloat(order.price)).toString() - const displaySide = order.side === '0' ? 'YES' : order.side === '1' ? 'NO' : order.side + const date = new Date(order.createdAt) + const formattedDate = date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }) + const amount = (parseFloat(order.quantity) * parseFloat(order.price)).toString() - return ( + return ( = ({ copyTradingId, active = fal )}
- {displaySide} {getStatusTag(order.status)}
@@ -633,23 +646,20 @@ const BuyOrdersTab: React.FC = ({ copyTradingId, active = fal
setFilters({ ...filters, marketId: e.target.value || undefined })} /> - + style={{ width: isMobile ? '100%' : 200 }} + value={filters.marketTitle} + onChange={(e) => handleMarketTitleChange(e.target.value)} + /> handleMarketTitleChange(e.target.value)} + /> + = ({ copyTradingId, active = f const [limit, setLimit] = useState(20) const [filters, setFilters] = useState<{ marketId?: string - side?: string + marketTitle?: string + status?: string }>({}) - const [groupByMarket, setGroupByMarket] = useState(false) // 是否按市场分组 + // 从 localStorage 读取用户分组偏好,如果没有则默认为 false + const [groupByMarket, setGroupByMarket] = useState(() => { + const savedPreference = localStorage.getItem('copyTradingOrders_groupByMarket_sell') + return savedPreference === 'true' + }) const [expandedMarkets, setExpandedMarkets] = useState>(new Set()) // 展开的市场ID集合 const [groupedOrders, setGroupedOrders] = useState([]) // 分组数据(从后端获取) const [groupedTotal, setGroupedTotal] = useState(0) // 分组总数(市场数量) + const handleMarketTitleChange = (value: string) => { + setFilters({ ...filters, marketTitle: value || undefined }) + } + + // 切换分组模式时,保存用户偏好到 localStorage + const handleToggleGroupByMarket = (checked: boolean) => { + setGroupByMarket(checked) + localStorage.setItem('copyTradingOrders_groupByMarket_sell', String(checked)) + } + useEffect(() => { if (copyTradingId && active) { fetchOrders() } - }, [copyTradingId, active, page, limit, filters, groupByMarket]) + }, [copyTradingId, active, page, limit, filters.marketId, filters.status, groupByMarket]) + + // 防抖搜索 - marketTitle 变化时延迟0.5秒再搜索 + useEffect(() => { + if (!copyTradingId || !active) return + + const timer = setTimeout(() => { + fetchOrders() + }, 500) // 0.5秒延迟 + + return () => clearTimeout(timer) + }, [filters.marketTitle]) const fetchOrders = async () => { if (!copyTradingId) return @@ -49,7 +75,9 @@ const SellOrdersTab: React.FC = ({ copyTradingId, active = f copyTradingId: parseInt(copyTradingId), type: 'sell', page, - limit + limit, + marketId: filters.marketId, + marketTitle: filters.marketTitle } const response = await apiService.orderTracking.listGroupedByMarket(request) @@ -204,16 +232,6 @@ const SellOrdersTab: React.FC = ({ copyTradingId, active = f ) } }, - { - title: t('copyTradingOrders.side') || '方向', - dataIndex: 'side', - key: 'side', - width: isMobile ? 60 : 80, - render: (side: string) => { - const displaySide = side === '0' ? 'YES' : side === '1' ? 'NO' : side - return {displaySide} - } - }, { title: t('copyTradingOrders.sellQuantity') || '卖出数量', dataIndex: 'quantity', @@ -378,7 +396,6 @@ const SellOrdersTab: React.FC = ({ copyTradingId, active = f minute: '2-digit' }) const amount = (parseFloat(order.quantity) * parseFloat(order.price)).toString() - const displaySide = order.side === '0' ? 'YES' : order.side === '1' ? 'NO' : order.side return ( = ({ copyTradingId, active = f /> )}
-
- {displaySide} -
@@ -473,7 +487,6 @@ const SellOrdersTab: React.FC = ({ copyTradingId, active = f minute: '2-digit' }) const amount = (parseFloat(order.quantity) * parseFloat(order.price)).toString() - const displaySide = order.side === '0' ? 'YES' : order.side === '1' ? 'NO' : order.side return ( = ({ copyTradingId, active = f /> )}
-
- {displaySide} -
@@ -622,17 +632,24 @@ const SellOrdersTab: React.FC = ({ copyTradingId, active = f onChange={(e) => setFilters({ ...filters, marketId: e.target.value || undefined })} /> + handleMarketTitleChange(e.target.value)} + /> + @@ -641,7 +658,7 @@ const SellOrdersTab: React.FC = ({ copyTradingId, active = f } unCheckedChildren={} /> diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index fcff2ac..b60b390 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -670,6 +670,7 @@ export interface SellOrderInfo { price: string amount: string realizedPnl: string + status?: string // 卖出状态(filled, partially_matched, fully_matched) createdAt: number } @@ -710,7 +711,7 @@ export interface OrderTrackingRequest { page?: number limit?: number marketId?: string - side?: string + marketTitle?: string status?: string sellOrderId?: string buyOrderId?: string @@ -724,6 +725,8 @@ export interface MarketGroupedOrdersRequest { type: 'buy' | 'sell' page?: number limit?: number + marketId?: string + marketTitle?: string } /**