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 7ac4d1c..324135d 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CopyTradingStatisticsDto.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CopyTradingStatisticsDto.kt @@ -41,6 +41,8 @@ data class BuyOrderInfo( val leaderTradeId: String, val marketId: String, val marketTitle: String? = null, // 市场名称 + val marketSlug: String? = null, // 市场 slug(用于构建 URL) + val marketCategory: String? = null, // 市场分类(sports, crypto 等) val side: String, val quantity: String, val price: String, @@ -59,6 +61,8 @@ data class SellOrderInfo( val leaderTradeId: String, val marketId: String, val marketTitle: String? = null, // 市场名称 + val marketSlug: String? = null, // 市场 slug(用于构建 URL) + val marketCategory: String? = null, // 市场分类(sports, crypto 等) val side: String, val quantity: String, val price: String, @@ -75,6 +79,8 @@ data class MatchedOrderInfo( val buyOrderId: String, val marketId: String? = null, // 市场ID(从买入订单获取) val marketTitle: String? = null, // 市场名称 + val marketSlug: String? = null, // 市场 slug(用于构建 URL) + val marketCategory: String? = null, // 市场分类(sports, crypto 等) val matchedQuantity: String, val buyPrice: String, val sellPrice: String, 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 e2c23cb..cb44d9a 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 @@ -179,6 +179,8 @@ class CopyTradingStatisticsService( leaderTradeId = order.leaderBuyTradeId, marketId = order.marketId, marketTitle = market?.title, + marketSlug = market?.slug, + marketCategory = market?.category, side = order.side, quantity = order.quantity.toString(), price = order.price.toString(), @@ -232,6 +234,8 @@ class CopyTradingStatisticsService( leaderTradeId = record.leaderSellTradeId, marketId = record.marketId, marketTitle = market?.title, + marketSlug = market?.slug, + marketCategory = market?.category, side = record.side, quantity = record.totalMatchedQuantity.toString(), price = record.sellPrice.toString(), @@ -293,6 +297,8 @@ class CopyTradingStatisticsService( buyOrderId = detail.buyOrderId, marketId = matchRecord?.marketId, marketTitle = market?.title, + marketSlug = market?.slug, + marketCategory = market?.category, matchedQuantity = detail.matchedQuantity.toString(), buyPrice = detail.buyPrice.toString(), sellPrice = detail.sellPrice.toString(), diff --git a/frontend/src/locales/en/common.json b/frontend/src/locales/en/common.json index d2a9af9..1d83e94 100644 --- a/frontend/src/locales/en/common.json +++ b/frontend/src/locales/en/common.json @@ -1053,6 +1053,15 @@ "noBuyOrders": "No buy orders", "noSellOrders": "No sell orders", "noMatchedOrders": "No matched orders", + "groupByMarket": "Group by Market", + "expandAll": "Expand All", + "collapseAll": "Collapse All", + "allFullyMatched": "All Fully Matched", + "partiallyMatched": "Partially Matched", + "orderCount": "Order Count", + "totalAmount": "Total Amount", + "totalPnl": "Total PnL", + "statusBreakdown": "Status", "fetchBuyOrdersFailed": "Failed to fetch buy orders", "fetchSellOrdersFailed": "Failed to fetch sell orders", "fetchMatchedOrdersFailed": "Failed to fetch matched orders", diff --git a/frontend/src/locales/zh-CN/common.json b/frontend/src/locales/zh-CN/common.json index 8b7015c..522a329 100644 --- a/frontend/src/locales/zh-CN/common.json +++ b/frontend/src/locales/zh-CN/common.json @@ -1053,6 +1053,15 @@ "noBuyOrders": "暂无买入订单", "noSellOrders": "暂无卖出订单", "noMatchedOrders": "暂无匹配关系", + "groupByMarket": "按市场分组", + "expandAll": "展开全部", + "collapseAll": "折叠全部", + "allFullyMatched": "全部成交", + "partiallyMatched": "部分成交", + "orderCount": "订单数", + "totalAmount": "总金额", + "totalPnl": "总盈亏", + "statusBreakdown": "状态", "fetchBuyOrdersFailed": "获取买入订单列表失败", "fetchSellOrdersFailed": "获取卖出订单列表失败", "fetchMatchedOrdersFailed": "获取匹配关系列表失败", diff --git a/frontend/src/locales/zh-TW/common.json b/frontend/src/locales/zh-TW/common.json index 5ee567f..7868027 100644 --- a/frontend/src/locales/zh-TW/common.json +++ b/frontend/src/locales/zh-TW/common.json @@ -1053,6 +1053,15 @@ "noBuyOrders": "暫無買入訂單", "noSellOrders": "暫無賣出訂單", "noMatchedOrders": "暫無匹配關係", + "groupByMarket": "按市場分組", + "expandAll": "展開全部", + "collapseAll": "折疊全部", + "allFullyMatched": "全部成交", + "partiallyMatched": "部分成交", + "orderCount": "訂單數", + "totalAmount": "總金額", + "totalPnl": "總盈虧", + "statusBreakdown": "狀態", "fetchBuyOrdersFailed": "獲取買入訂單列表失敗", "fetchSellOrdersFailed": "獲取賣出訂單列表失敗", "fetchMatchedOrdersFailed": "獲取匹配關係列表失敗", diff --git a/frontend/src/pages/CopyTradingOrders/BuyOrdersTab.tsx b/frontend/src/pages/CopyTradingOrders/BuyOrdersTab.tsx index 08fd9e6..ca87a01 100644 --- a/frontend/src/pages/CopyTradingOrders/BuyOrdersTab.tsx +++ b/frontend/src/pages/CopyTradingOrders/BuyOrdersTab.tsx @@ -1,14 +1,14 @@ -import { useEffect, useState } from 'react' -import { Table, Tag, Select, Input, Button, Card, Divider, Spin, message } from 'antd' +import { useEffect, useState, useMemo } from 'react' +import { Table, Tag, Select, Input, Button, Card, Divider, Spin, message, Switch, Space } from 'antd' import { apiService } from '../../services/api' -import { formatUSDC, isAutoGeneratedOrderId, copyToClipboard } from '../../utils' +import { formatUSDC, isAutoGeneratedOrderId, copyToClipboard, getPolymarketUrl } from '../../utils' import { useMediaQuery } from 'react-responsive' import { useTranslation } from 'react-i18next' import type { BuyOrderInfo, OrderTrackingRequest, OrderTrackingListResponse } from '../../types' const { Option } = Select -import { ReloadOutlined, CopyOutlined } from '@ant-design/icons' +import { ReloadOutlined, CopyOutlined, DownOutlined, UpOutlined, AppstoreOutlined, UnorderedListOutlined } from '@ant-design/icons' interface BuyOrdersTabProps { copyTradingId: string @@ -28,6 +28,8 @@ const BuyOrdersTab: React.FC = ({ copyTradingId, active = fal side?: string status?: string }>({}) + const [groupByMarket, setGroupByMarket] = useState(false) // 是否按市场分组 + const [expandedMarkets, setExpandedMarkets] = useState>(new Set()) // 展开的市场ID集合 useEffect(() => { if (copyTradingId && active) { @@ -80,6 +82,90 @@ const BuyOrdersTab: React.FC = ({ copyTradingId, active = fal } } + // 分组数据结构 + interface GroupedOrders { + marketId: string + marketTitle?: string + marketSlug?: string + marketCategory?: string + orders: BuyOrderInfo[] + stats: { + count: number + totalAmount: string + fullyMatched: boolean // 是否全部成交 + fullyMatchedCount: number // 完全成交的订单数 + partiallyMatchedCount: number // 部分成交的订单数 + filledCount: number // 未成交的订单数 + } + } + + // 按市场分组订单 + const groupedOrders = useMemo(() => { + if (!groupByMarket) return [] + + const groups = new Map() + + // 按市场ID分组 + orders.forEach(order => { + const key = order.marketId + if (!groups.has(key)) { + groups.set(key, []) + } + groups.get(key)!.push(order) + }) + + // 转换为分组数据并计算统计信息 + return Array.from(groups.entries()).map(([marketId, marketOrders]) => { + // 计算总金额 + const totalAmount = marketOrders.reduce((sum, order) => { + const amount = parseFloat(order.quantity) * parseFloat(order.price) + return sum + amount + }, 0) + + // 统计订单状态 + const fullyMatchedCount = marketOrders.filter(o => o.status === 'fully_matched').length + const partiallyMatchedCount = marketOrders.filter(o => o.status === 'partially_matched').length + const filledCount = marketOrders.filter(o => o.status === 'filled').length + const fullyMatched = fullyMatchedCount === marketOrders.length + + return { + marketId, + marketTitle: marketOrders[0]?.marketTitle, + marketSlug: marketOrders[0]?.marketSlug, + marketCategory: marketOrders[0]?.marketCategory, + orders: marketOrders, + stats: { + count: marketOrders.length, + totalAmount: totalAmount.toString(), + fullyMatched, + fullyMatchedCount, + partiallyMatchedCount, + filledCount + } + } + }).sort((a, b) => b.stats.count - a.stats.count) // 按订单数量降序排序 + }, [orders, groupByMarket]) + + // 切换市场展开/折叠 + const toggleMarket = (marketId: string) => { + const newExpanded = new Set(expandedMarkets) + if (newExpanded.has(marketId)) { + newExpanded.delete(marketId) + } else { + newExpanded.add(marketId) + } + setExpandedMarkets(newExpanded) + } + + // 展开/折叠全部 + const toggleAllMarkets = () => { + if (expandedMarkets.size === groupedOrders.length) { + setExpandedMarkets(new Set()) + } else { + setExpandedMarkets(new Set(groupedOrders.map(g => g.marketId))) + } + } + const columns = [ { title: t('copyTradingOrders.orderId') || '订单ID', @@ -129,21 +215,41 @@ const BuyOrdersTab: React.FC = ({ copyTradingId, active = fal dataIndex: 'marketId', key: 'marketId', width: isMobile ? 120 : 200, - render: (text: string, record: BuyOrderInfo) => ( -
- {record.marketTitle ? ( - - {record.marketTitle} + render: (text: string, record: BuyOrderInfo) => { + const marketUrl = getPolymarketUrl(record.marketSlug, record.marketCategory, record.marketId) + return ( +
+ {record.marketTitle ? ( + marketUrl ? ( + + {record.marketTitle} + + ) : ( + + {record.marketTitle} + + ) + ) : null} + + {isMobile + ? `${text.slice(0, 6)}...${text.slice(-4)}` + : `${text.slice(0, 8)}...${text.slice(-6)}` + } - ) : null} - - {isMobile - ? `${text.slice(0, 6)}...${text.slice(-4)}` - : `${text.slice(0, 8)}...${text.slice(-6)}` - } - -
- ) +
+ ) + } }, { title: t('copyTradingOrders.side') || '方向', @@ -227,44 +333,193 @@ const BuyOrdersTab: React.FC = ({ copyTradingId, active = fal } ] - return ( -
-
- setFilters({ ...filters, marketId: e.target.value || undefined })} - /> + // 初始化展开状态(桌面端默认全部展开) + useEffect(() => { + if (groupByMarket && groupedOrders.length > 0 && expandedMarkets.size === 0 && !isMobile) { + setExpandedMarkets(new Set(groupedOrders.map(g => g.marketId))) + } + }, [groupByMarket, groupedOrders.length, isMobile]) - + // 渲染分组视图 + const renderGroupedView = () => { + if (groupedOrders.length === 0) { + return ( +
+ {t('copyTradingOrders.noBuyOrders') || '暂无买入订单'} +
+ ) + } - + return ( +
+ {groupedOrders.map((group) => { + const isExpanded = expandedMarkets.has(group.marketId) + const marketDisplayName = group.marketTitle || group.marketId.slice(0, 8) + '...' + group.marketId.slice(-6) + const marketUrl = getPolymarketUrl(group.marketSlug, group.marketCategory, group.marketId) - + return ( + + {/* 分组头部 */} +
toggleMarket(group.marketId)} + > +
+
+ {marketUrl ? ( + e.stopPropagation()} + style={{ + fontSize: isMobile ? '14px' : '16px', + fontWeight: 'bold', + color: '#1890ff', + textDecoration: 'none', + cursor: 'pointer' + }} + > + {marketDisplayName} + + ) : ( + + {marketDisplayName} + + )} + {group.stats.fullyMatched ? ( + {t('copyTradingOrders.allFullyMatched') || '全部成交'} + ) : ( + + {t('copyTradingOrders.partiallyMatched') || '部分成交'} ({group.stats.fullyMatchedCount}/{group.stats.count}) + + )} +
+
+ {t('copyTradingOrders.orderCount') || '订单数'}: {group.stats.count} + {t('copyTradingOrders.totalAmount') || '总金额'}: {formatUSDC(group.stats.totalAmount)} USDC + + {t('copyTradingOrders.statusBreakdown') || '状态'}: + {group.stats.fullyMatchedCount > 0 && ` ${t('copyTradingOrders.statusFullySold') || '全部成交'} ${group.stats.fullyMatchedCount}`} + {group.stats.partiallyMatchedCount > 0 && ` ${t('copyTradingOrders.statusPartiallySold') || '部分成交'} ${group.stats.partiallyMatchedCount}`} + {group.stats.filledCount > 0 && ` ${t('copyTradingOrders.statusFilled') || '未成交'} ${group.stats.filledCount}`} + +
+
+
+ + {/* 订单列表 */} + {isExpanded && ( + <> + + {isMobile ? ( +
+ {group.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 + + return ( + +
+
+ {order.orderId.slice(0, 8)}...{order.orderId.slice(-6)} + {!isAutoGeneratedOrderId(order.orderId) && ( +
+
+ {displaySide} + {getStatusTag(order.status)} +
+
+ +
+
{t('copyTradingOrders.quantity') || '数量'}: {formatUSDC(order.quantity)} | {t('copyTradingOrders.price') || '价格'}: {formatUSDC(order.price)}
+
{t('copyTradingOrders.amount') || '金额'}: {formatUSDC(amount)} USDC
+
{t('copyTradingOrders.matched') || '已匹配'}: {formatUSDC(order.matchedQuantity)} | {t('copyTradingOrders.remaining') || '剩余'}: {formatUSDC(order.remainingQuantity)}
+
{formattedDate}
+
+
+ ) + })} +
+ ) : ( + + )} + + )} + + ) + })} + ) + } - {isMobile ? ( + // 渲染列表视图 + const renderListView = () => { + if (isMobile) { + return (
{loading ? (
@@ -355,9 +610,31 @@ const BuyOrdersTab: React.FC = ({ copyTradingId, active = fal
{t('copyTradingOrders.market') || '市场'}
{order.marketTitle ? ( -
- {order.marketTitle} -
+ (() => { + const marketUrl = getPolymarketUrl(order.marketSlug, order.marketCategory, order.marketId) + return marketUrl ? ( + + {order.marketTitle} + + ) : ( +
+ {order.marketTitle} +
+ ) + })() ) : null}
{order.marketId.slice(0, 8)}...{order.marketId.slice(-6)} @@ -375,7 +652,9 @@ const BuyOrdersTab: React.FC = ({ copyTradingId, active = fal
)}
- ) : ( + ) + } else { + return (
= ({ copyTradingId, active = fal } }} /> + ) + } + } + + return ( +
+
+ setFilters({ ...filters, marketId: e.target.value || undefined })} + /> + + + + + + + + {t('copyTradingOrders.groupByMarket') || '按市场分组'}: + + } + unCheckedChildren={} + /> + + + {groupByMarket && groupedOrders.length > 0 && ( + + )} + + +
+ + {loading ? ( +
+ +
+ ) : groupByMarket ? ( + renderGroupedView() + ) : ( + renderListView() )}
) diff --git a/frontend/src/pages/CopyTradingOrders/SellOrdersTab.tsx b/frontend/src/pages/CopyTradingOrders/SellOrdersTab.tsx index 268adb8..5c63d6b 100644 --- a/frontend/src/pages/CopyTradingOrders/SellOrdersTab.tsx +++ b/frontend/src/pages/CopyTradingOrders/SellOrdersTab.tsx @@ -1,14 +1,14 @@ -import { useEffect, useState } from 'react' -import { Table, Tag, Select, Input, Button, Card, Divider, Spin, message } from 'antd' +import { useEffect, useState, useMemo } from 'react' +import { Table, Tag, Select, Input, Button, Card, Divider, Spin, message, Switch, Space } from 'antd' import { apiService } from '../../services/api' -import { formatUSDC, isAutoGeneratedOrderId, copyToClipboard } from '../../utils' +import { formatUSDC, isAutoGeneratedOrderId, copyToClipboard, getPolymarketUrl } from '../../utils' import { useMediaQuery } from 'react-responsive' import { useTranslation } from 'react-i18next' import type { SellOrderInfo, OrderTrackingRequest, OrderTrackingListResponse } from '../../types' const { Option } = Select -import { ReloadOutlined, CopyOutlined } from '@ant-design/icons' +import { ReloadOutlined, CopyOutlined, DownOutlined, UpOutlined, AppstoreOutlined, UnorderedListOutlined } from '@ant-design/icons' interface SellOrdersTabProps { copyTradingId: string @@ -27,6 +27,8 @@ const SellOrdersTab: React.FC = ({ copyTradingId, active = f marketId?: string side?: string }>({}) + const [groupByMarket, setGroupByMarket] = useState(false) // 是否按市场分组 + const [expandedMarkets, setExpandedMarkets] = useState>(new Set()) // 展开的市场ID集合 useEffect(() => { if (copyTradingId && active) { @@ -75,6 +77,91 @@ const SellOrdersTab: React.FC = ({ copyTradingId, active = f } } + // 分组数据结构 + interface GroupedOrders { + marketId: string + marketTitle?: string + marketSlug?: string + marketCategory?: string + orders: SellOrderInfo[] + stats: { + count: number + totalAmount: string + totalPnl: string // 总盈亏 + fullyMatched: boolean // 卖出订单都是已成交的,这里始终为 true + } + } + + // 按市场分组订单 + const groupedOrders = useMemo(() => { + if (!groupByMarket) return [] + + const groups = new Map() + + // 按市场ID分组 + orders.forEach(order => { + const key = order.marketId + if (!groups.has(key)) { + groups.set(key, []) + } + groups.get(key)!.push(order) + }) + + // 转换为分组数据并计算统计信息 + return Array.from(groups.entries()).map(([marketId, marketOrders]) => { + // 计算总金额和总盈亏 + const totalAmount = marketOrders.reduce((sum, order) => { + const amount = parseFloat(order.quantity) * parseFloat(order.price) + return sum + amount + }, 0) + + const totalPnl = marketOrders.reduce((sum, order) => { + return sum + parseFloat(order.realizedPnl) + }, 0) + + return { + marketId, + marketTitle: marketOrders[0]?.marketTitle, + marketSlug: marketOrders[0]?.marketSlug, + marketCategory: marketOrders[0]?.marketCategory, + orders: marketOrders, + stats: { + count: marketOrders.length, + totalAmount: totalAmount.toString(), + totalPnl: totalPnl.toString(), + fullyMatched: true // 卖出订单都是已成交的 + } + } + }).sort((a, b) => b.stats.count - a.stats.count) // 按订单数量降序排序 + }, [orders, groupByMarket]) + + // 切换市场展开/折叠 + const toggleMarket = (marketId: string) => { + const newExpanded = new Set(expandedMarkets) + if (newExpanded.has(marketId)) { + newExpanded.delete(marketId) + } else { + newExpanded.add(marketId) + } + setExpandedMarkets(newExpanded) + } + + // 展开/折叠全部 + const toggleAllMarkets = () => { + if (expandedMarkets.size === groupedOrders.length) { + setExpandedMarkets(new Set()) + } else { + setExpandedMarkets(new Set(groupedOrders.map(g => g.marketId))) + } + } + + // 初始化展开状态(桌面端默认全部展开) + useEffect(() => { + if (groupByMarket && groupedOrders.length > 0 && expandedMarkets.size === 0 && !isMobile) { + setExpandedMarkets(new Set(groupedOrders.map(g => g.marketId))) + } + }, [groupByMarket, groupedOrders.length, isMobile]) + const columns = [ { title: t('copyTradingOrders.orderId') || '订单ID', @@ -124,21 +211,41 @@ const SellOrdersTab: React.FC = ({ copyTradingId, active = f dataIndex: 'marketId', key: 'marketId', width: isMobile ? 120 : 200, - render: (text: string, record: SellOrderInfo) => ( -
- {record.marketTitle ? ( - - {record.marketTitle} + render: (text: string, record: SellOrderInfo) => { + const marketUrl = getPolymarketUrl(record.marketSlug, record.marketCategory, record.marketId) + return ( +
+ {record.marketTitle ? ( + marketUrl ? ( + + {record.marketTitle} + + ) : ( + + {record.marketTitle} + + ) + ) : null} + + {isMobile + ? `${text.slice(0, 6)}...${text.slice(-4)}` + : `${text.slice(0, 8)}...${text.slice(-6)}` + } - ) : null} - - {isMobile - ? `${text.slice(0, 6)}...${text.slice(-4)}` - : `${text.slice(0, 8)}...${text.slice(-6)}` - } - -
- ) +
+ ) + } }, { title: t('copyTradingOrders.side') || '方向', @@ -212,34 +319,179 @@ const SellOrdersTab: React.FC = ({ copyTradingId, active = f } ] - return ( -
-
- setFilters({ ...filters, marketId: e.target.value || undefined })} - /> + // 渲染分组视图 + const renderGroupedView = () => { + if (groupedOrders.length === 0) { + return ( +
+ {t('copyTradingOrders.noSellOrders') || '暂无卖出订单'} +
+ ) + } - + return ( +
+ {groupedOrders.map((group) => { + const isExpanded = expandedMarkets.has(group.marketId) + const marketDisplayName = group.marketTitle || group.marketId.slice(0, 8) + '...' + group.marketId.slice(-6) + const marketUrl = getPolymarketUrl(group.marketSlug, group.marketCategory, group.marketId) + const pnlColor = getPnlColor(group.stats.totalPnl) - + return ( + + {/* 分组头部 */} +
toggleMarket(group.marketId)} + > +
+ +
+ {t('copyTradingOrders.orderCount') || '订单数'}: {group.stats.count} + {t('copyTradingOrders.totalAmount') || '总金额'}: {formatUSDC(group.stats.totalAmount)} USDC + + {t('copyTradingOrders.totalPnl') || '总盈亏'}: {formatUSDC(group.stats.totalPnl)} USDC + +
+
+
+ + {/* 订单列表 */} + {isExpanded && ( + <> + + {isMobile ? ( +
+ {group.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 + + return ( + +
+
+ {order.orderId.slice(0, 8)}...{order.orderId.slice(-6)} + {!isAutoGeneratedOrderId(order.orderId) && ( +
+
+ {displaySide} +
+
+ +
+
{t('copyTradingOrders.quantity') || '数量'}: {formatUSDC(order.quantity)} | {t('copyTradingOrders.price') || '价格'}: {formatUSDC(order.price)}
+
{t('copyTradingOrders.amount') || '金额'}: {formatUSDC(amount)} USDC
+
+ {t('copyTradingOrders.realizedPnl') || '已实现盈亏'}: {formatUSDC(order.realizedPnl)} USDC +
+
{formattedDate}
+
+
+ ) + })} +
+ ) : ( +
+ )} + + )} + + ) + })} + ) + } - {isMobile ? ( + // 渲染列表视图 + const renderListView = () => { + if (isMobile) { + return (
{loading ? (
@@ -333,9 +585,31 @@ const SellOrdersTab: React.FC = ({ copyTradingId, active = f
{t('copyTradingOrders.market') || '市场'}
{order.marketTitle ? ( -
- {order.marketTitle} -
+ (() => { + const marketUrl = getPolymarketUrl(order.marketSlug, order.marketCategory, order.marketId) + return marketUrl ? ( + + {order.marketTitle} + + ) : ( +
+ {order.marketTitle} +
+ ) + })() ) : null}
{order.marketId.slice(0, 8)}...{order.marketId.slice(-6)} @@ -353,7 +627,9 @@ const SellOrdersTab: React.FC = ({ copyTradingId, active = f
)}
- ) : ( + ) + } else { + return (
= ({ copyTradingId, active = f } }} /> + ) + } + } + + return ( +
+
+ setFilters({ ...filters, marketId: e.target.value || undefined })} + /> + + + + + + {t('copyTradingOrders.groupByMarket') || '按市场分组'}: + + } + unCheckedChildren={} + /> + + + {groupByMarket && groupedOrders.length > 0 && ( + + )} + + +
+ + {loading ? ( +
+ +
+ ) : groupByMarket ? ( + renderGroupedView() + ) : ( + renderListView() )}
) diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 7af66c8..a5d6be0 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -628,6 +628,8 @@ export interface BuyOrderInfo { leaderTradeId: string marketId: string marketTitle?: string // 市场名称 + marketSlug?: string // 市场 slug(用于构建 URL) + marketCategory?: string // 市场分类(sports, crypto 等) side: string quantity: string price: string @@ -646,6 +648,8 @@ export interface SellOrderInfo { leaderTradeId: string marketId: string marketTitle?: string // 市场名称 + marketSlug?: string // 市场 slug(用于构建 URL) + marketCategory?: string // 市场分类(sports, crypto 等) side: string quantity: string price: string @@ -662,6 +666,8 @@ export interface MatchedOrderInfo { buyOrderId: string marketId?: string // 市场ID marketTitle?: string // 市场名称 + marketSlug?: string // 市场 slug(用于构建 URL) + marketCategory?: string // 市场分类(sports, crypto 等) matchedQuantity: string buyPrice: string sellPrice: string diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts index c53f4ff..e73116d 100644 --- a/frontend/src/utils/index.ts +++ b/frontend/src/utils/index.ts @@ -94,6 +94,37 @@ export const isAutoGeneratedOrderId = (orderId: string | undefined | null): bool return orderId.startsWith('AUTO_') || orderId.startsWith('AUTO_FIFO_') } +/** + * 构建 Polymarket 市场 URL + * 对于 spread 市场(sports 分类),跳转到 moneyline 页面 + * @param marketSlug - 市场 slug + * @param marketCategory - 市场分类(sports, crypto 等) + * @param marketId - 市场ID(作为后备) + * @returns Polymarket URL,如果无法构建则返回 null + */ +export const getPolymarketUrl = ( + marketSlug?: string | null, + marketCategory?: string | null, + marketId?: string | null +): string | null => { + // 优先使用 slug + if (marketSlug) { + // 如果是 sports 分类,跳转到 moneyline 页面(spread 市场) + if (marketCategory?.toLowerCase() === 'sports') { + return `https://polymarket.com/event/${marketSlug}/moneyline` + } + // 其他分类跳转到普通市场页面 + return `https://polymarket.com/event/${marketSlug}` + } + + // 如果没有 slug,使用 marketId(作为后备) + if (marketId && marketId.startsWith('0x')) { + return `https://polymarket.com/condition/${marketId}` + } + + return null +} + /** * 复制文本到剪贴板 * @param text - 要复制的文本