feat: 优化订单列表筛选功能,支持按市场标题搜索并改进分组体验
- 后端:MarketGroupedOrdersRequest 添加 marketId 和 marketTitle 字段 - 后端:买入/卖出订单分组接口支持市场 ID 模糊匹配和市场标题关键字筛选 - 前端:BuyOrdersTab 添加市场标题筛选,移除方向筛选,记录用户分组偏好 - 前端:SellOrdersTab 添加市场标题筛选,将方向筛选改为状态筛选,移除方向列 - 前端:MatchedOrdersTab 添加市场标题筛选和市场列显示 - 前端:所有搜索输入框添加 0.5 秒防抖优化 - 前端:分组偏好保存到 localStorage 以便跨会话持久化
This commit is contained in:
@@ -110,7 +110,7 @@ data class OrderTrackingRequest(
|
|||||||
val page: Int? = 1,
|
val page: Int? = 1,
|
||||||
val limit: Int? = 20,
|
val limit: Int? = 20,
|
||||||
val marketId: String? = null,
|
val marketId: String? = null,
|
||||||
val side: String? = null,
|
val marketTitle: String? = null, // 市场标题关键字筛选
|
||||||
val status: String? = null,
|
val status: String? = null,
|
||||||
val sellOrderId: String? = null,
|
val sellOrderId: String? = null,
|
||||||
val buyOrderId: String? = null
|
val buyOrderId: String? = null
|
||||||
@@ -123,7 +123,9 @@ data class MarketGroupedOrdersRequest(
|
|||||||
val copyTradingId: Long,
|
val copyTradingId: Long,
|
||||||
val type: String, // buy, sell, matched
|
val type: String, // buy, sell, matched
|
||||||
val page: Int? = 1,
|
val page: Int? = 1,
|
||||||
val limit: Int? = 20
|
val limit: Int? = 20,
|
||||||
|
val marketId: String? = null,
|
||||||
|
val marketTitle: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+88
-25
@@ -134,12 +134,21 @@ class CopyTradingStatisticsService(
|
|||||||
private fun getBuyOrderList(request: OrderTrackingRequest): Pair<List<BuyOrderInfo>, Long> {
|
private fun getBuyOrderList(request: OrderTrackingRequest): Pair<List<BuyOrderInfo>, Long> {
|
||||||
var orders = copyOrderTrackingRepository.findByCopyTradingId(request.copyTradingId)
|
var orders = copyOrderTrackingRepository.findByCopyTradingId(request.copyTradingId)
|
||||||
|
|
||||||
|
// 批量获取市场信息(用于筛选)
|
||||||
|
val allMarketIds = orders.map { it.marketId }.distinct()
|
||||||
|
val markets = marketService.getMarkets(allMarketIds)
|
||||||
|
|
||||||
// 筛选
|
// 筛选
|
||||||
if (!request.marketId.isNullOrBlank()) {
|
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()) {
|
if (!request.marketTitle.isNullOrBlank()) {
|
||||||
orders = orders.filter { it.side == request.side }
|
// marketTitle 关键字筛选
|
||||||
|
orders = orders.filter { order ->
|
||||||
|
val market = markets[order.marketId]
|
||||||
|
market?.title?.contains(request.marketTitle!!, ignoreCase = true) == true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!request.status.isNullOrBlank()) {
|
if (!request.status.isNullOrBlank()) {
|
||||||
orders = orders.filter { it.status == request.status }
|
orders = orders.filter { it.status == request.status }
|
||||||
@@ -157,10 +166,6 @@ class CopyTradingStatisticsService(
|
|||||||
val end = minOf(start + limit, orders.size)
|
val end = minOf(start + limit, orders.size)
|
||||||
val pagedOrders = if (start < orders.size) orders.subList(start, end) else emptyList()
|
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
|
// 转换为DTO
|
||||||
val list = pagedOrders.map { order ->
|
val list = pagedOrders.map { order ->
|
||||||
val amount = order.quantity.toSafeBigDecimal().multi(order.price)
|
val amount = order.quantity.toSafeBigDecimal().multi(order.price)
|
||||||
@@ -193,12 +198,21 @@ class CopyTradingStatisticsService(
|
|||||||
private fun getSellOrderList(request: OrderTrackingRequest): Pair<List<SellOrderInfo>, Long> {
|
private fun getSellOrderList(request: OrderTrackingRequest): Pair<List<SellOrderInfo>, Long> {
|
||||||
var records = sellMatchRecordRepository.findByCopyTradingId(request.copyTradingId)
|
var records = sellMatchRecordRepository.findByCopyTradingId(request.copyTradingId)
|
||||||
|
|
||||||
|
// 批量获取市场信息(用于筛选)
|
||||||
|
val allMarketIds = records.map { it.marketId }.distinct()
|
||||||
|
val markets = marketService.getMarkets(allMarketIds)
|
||||||
|
|
||||||
// 筛选
|
// 筛选
|
||||||
if (!request.marketId.isNullOrBlank()) {
|
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()) {
|
if (!request.marketTitle.isNullOrBlank()) {
|
||||||
records = records.filter { it.side == request.side }
|
// marketTitle 关键字筛选
|
||||||
|
records = records.filter { record ->
|
||||||
|
val market = markets[record.marketId]
|
||||||
|
market?.title?.contains(request.marketTitle!!, ignoreCase = true) == true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val total = records.size.toLong()
|
val total = records.size.toLong()
|
||||||
@@ -213,10 +227,6 @@ class CopyTradingStatisticsService(
|
|||||||
val end = minOf(start + limit, records.size)
|
val end = minOf(start + limit, records.size)
|
||||||
val pagedRecords = if (start < records.size) records.subList(start, end) else emptyList()
|
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
|
// 转换为DTO
|
||||||
val list = pagedRecords.map { record ->
|
val list = pagedRecords.map { record ->
|
||||||
val amount = record.totalMatchedQuantity.toSafeBigDecimal().multi(record.sellPrice)
|
val amount = record.totalMatchedQuantity.toSafeBigDecimal().multi(record.sellPrice)
|
||||||
@@ -247,6 +257,14 @@ class CopyTradingStatisticsService(
|
|||||||
private fun getMatchedOrderList(request: OrderTrackingRequest): Pair<List<MatchedOrderInfo>, Long> {
|
private fun getMatchedOrderList(request: OrderTrackingRequest): Pair<List<MatchedOrderInfo>, Long> {
|
||||||
val matchDetails = sellMatchDetailRepository.findByCopyTradingId(request.copyTradingId)
|
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
|
var filtered = matchDetails
|
||||||
if (!request.sellOrderId.isNullOrBlank()) {
|
if (!request.sellOrderId.isNullOrBlank()) {
|
||||||
@@ -260,6 +278,21 @@ class CopyTradingStatisticsService(
|
|||||||
if (!request.buyOrderId.isNullOrBlank()) {
|
if (!request.buyOrderId.isNullOrBlank()) {
|
||||||
filtered = filtered.filter { it.buyOrderId == request.buyOrderId }
|
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()
|
val total = filtered.size.toLong()
|
||||||
|
|
||||||
@@ -274,17 +307,17 @@ class CopyTradingStatisticsService(
|
|||||||
val pagedDetails = if (start < filtered.size) filtered.subList(start, end) else emptyList()
|
val pagedDetails = if (start < filtered.size) filtered.subList(start, end) else emptyList()
|
||||||
|
|
||||||
// 获取匹配记录以获取市场ID
|
// 获取匹配记录以获取市场ID
|
||||||
val matchRecordIds = pagedDetails.map { it.matchRecordId }.distinct()
|
val pagedMatchRecordIds = pagedDetails.map { it.matchRecordId }.distinct()
|
||||||
val matchRecords = matchRecordIds.mapNotNull { id ->
|
val pagedMatchRecords = pagedMatchRecordIds.mapNotNull { id ->
|
||||||
sellMatchRecordRepository.findById(id).orElse(null)
|
sellMatchRecordRepository.findById(id).orElse(null)
|
||||||
}
|
}
|
||||||
val marketIds = matchRecords.map { it.marketId }.distinct()
|
val pagedMarketIds = pagedMatchRecords.map { it.marketId }.distinct()
|
||||||
val markets = marketService.getMarkets(marketIds)
|
val pagedMarkets = marketService.getMarkets(pagedMarketIds)
|
||||||
|
|
||||||
// 转换为DTO
|
// 转换为DTO
|
||||||
val list = pagedDetails.map { detail ->
|
val list = pagedDetails.map { detail ->
|
||||||
val matchRecord = matchRecords.find { it.id == detail.matchRecordId }
|
val matchRecord = pagedMatchRecords.find { it.id == detail.matchRecordId }
|
||||||
val market = matchRecord?.let { markets[it.marketId] }
|
val market = matchRecord?.let { pagedMarkets[it.marketId] }
|
||||||
MatchedOrderInfo(
|
MatchedOrderInfo(
|
||||||
sellOrderId = matchRecord?.sellOrderId ?: "",
|
sellOrderId = matchRecord?.sellOrderId ?: "",
|
||||||
buyOrderId = detail.buyOrderId,
|
buyOrderId = detail.buyOrderId,
|
||||||
@@ -550,7 +583,23 @@ class CopyTradingStatisticsService(
|
|||||||
// 2. 获取所有买入订单
|
// 2. 获取所有买入订单
|
||||||
var orders = copyOrderTrackingRepository.findByCopyTradingId(request.copyTradingId)
|
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<String, MutableList<CopyOrderTracking>>()
|
val groups = mutableMapOf<String, MutableList<CopyOrderTracking>>()
|
||||||
orders.forEach { order ->
|
orders.forEach { order ->
|
||||||
val marketId = order.marketId
|
val marketId = order.marketId
|
||||||
@@ -562,7 +611,6 @@ class CopyTradingStatisticsService(
|
|||||||
|
|
||||||
// 4. 转换为分组数据并计算统计信息
|
// 4. 转换为分组数据并计算统计信息
|
||||||
val marketIds = groups.keys.toList()
|
val marketIds = groups.keys.toList()
|
||||||
val markets = marketService.getMarkets(marketIds)
|
|
||||||
|
|
||||||
val list = marketIds.map { marketId ->
|
val list = marketIds.map { marketId ->
|
||||||
val marketOrders = groups[marketId] ?: mutableListOf()
|
val marketOrders = groups[marketId] ?: mutableListOf()
|
||||||
@@ -659,9 +707,25 @@ class CopyTradingStatisticsService(
|
|||||||
?: return Result.failure(IllegalArgumentException("跟单关系不存在: ${request.copyTradingId}"))
|
?: return Result.failure(IllegalArgumentException("跟单关系不存在: ${request.copyTradingId}"))
|
||||||
|
|
||||||
// 2. 获取所有卖出记录
|
// 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<String, MutableList<SellMatchRecord>>()
|
val groups = mutableMapOf<String, MutableList<SellMatchRecord>>()
|
||||||
sellRecords.forEach { record ->
|
sellRecords.forEach { record ->
|
||||||
val marketId = record.marketId
|
val marketId = record.marketId
|
||||||
@@ -673,7 +737,6 @@ class CopyTradingStatisticsService(
|
|||||||
|
|
||||||
// 4. 转换为分组数据并计算统计信息
|
// 4. 转换为分组数据并计算统计信息
|
||||||
val marketIds = groups.keys.toList()
|
val marketIds = groups.keys.toList()
|
||||||
val markets = marketService.getMarkets(marketIds)
|
|
||||||
|
|
||||||
val list = marketIds.map { marketId ->
|
val list = marketIds.map { marketId ->
|
||||||
val marketRecords = groups[marketId] ?: mutableListOf()
|
val marketRecords = groups[marketId] ?: mutableListOf()
|
||||||
|
|||||||
@@ -1089,6 +1089,7 @@
|
|||||||
"price": "价格",
|
"price": "价格",
|
||||||
"amount": "金额",
|
"amount": "金额",
|
||||||
"filterMarketId": "筛选市场ID",
|
"filterMarketId": "筛选市场ID",
|
||||||
|
"filterMarketTitle": "筛选市场标题",
|
||||||
"filterSide": "筛选方向",
|
"filterSide": "筛选方向",
|
||||||
"filterStatus": "筛选状态",
|
"filterStatus": "筛选状态",
|
||||||
"filterSellOrderId": "筛选卖出订单ID",
|
"filterSellOrderId": "筛选卖出订单ID",
|
||||||
|
|||||||
@@ -25,19 +25,44 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
|
|||||||
const [limit, setLimit] = useState(20)
|
const [limit, setLimit] = useState(20)
|
||||||
const [filters, setFilters] = useState<{
|
const [filters, setFilters] = useState<{
|
||||||
marketId?: string
|
marketId?: string
|
||||||
side?: string
|
marketTitle?: string
|
||||||
status?: 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<Set<string>>(new Set()) // 展开的市场ID集合
|
const [expandedMarkets, setExpandedMarkets] = useState<Set<string>>(new Set()) // 展开的市场ID集合
|
||||||
const [groupedOrders, setGroupedOrders] = useState<MarketOrderGroup[]>([]) // 分组数据(从后端获取)
|
const [groupedOrders, setGroupedOrders] = useState<MarketOrderGroup[]>([]) // 分组数据(从后端获取)
|
||||||
const [groupedTotal, setGroupedTotal] = useState(0) // 分组总数(市场数量)
|
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(() => {
|
useEffect(() => {
|
||||||
if (copyTradingId && active) {
|
if (copyTradingId && active) {
|
||||||
fetchOrders()
|
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 () => {
|
const fetchOrders = async () => {
|
||||||
if (!copyTradingId) return
|
if (!copyTradingId) return
|
||||||
@@ -45,12 +70,14 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
if (groupByMarket) {
|
if (groupByMarket) {
|
||||||
// 调用分组接口
|
// 调用分组接口(支持市场筛选)
|
||||||
const request: MarketGroupedOrdersRequest = {
|
const request: MarketGroupedOrdersRequest = {
|
||||||
copyTradingId: parseInt(copyTradingId),
|
copyTradingId: parseInt(copyTradingId),
|
||||||
type: 'buy',
|
type: 'buy',
|
||||||
page,
|
page,
|
||||||
limit
|
limit,
|
||||||
|
marketId: filters.marketId,
|
||||||
|
marketTitle: filters.marketTitle
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await apiService.orderTracking.listGroupedByMarket(request)
|
const response = await apiService.orderTracking.listGroupedByMarket(request)
|
||||||
@@ -123,7 +150,7 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: t('copyTradingOrders.orderId') || '订单ID',
|
title: t('copyTradingOrders.orderId') || '订单ID',
|
||||||
dataIndex: 'orderId',
|
dataIndex: 'orderId',
|
||||||
@@ -208,16 +235,6 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ 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 <Tag style={{ fontSize: isMobile ? 11 : 12 }}>{displaySide}</Tag>
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: t('copyTradingOrders.buyQuantity') || '买入数量',
|
title: t('copyTradingOrders.buyQuantity') || '买入数量',
|
||||||
dataIndex: 'quantity',
|
dataIndex: 'quantity',
|
||||||
@@ -400,7 +417,6 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
|
|||||||
minute: '2-digit'
|
minute: '2-digit'
|
||||||
})
|
})
|
||||||
const amount = (parseFloat(order.quantity) * parseFloat(order.price)).toString()
|
const amount = (parseFloat(order.quantity) * parseFloat(order.price)).toString()
|
||||||
const displaySide = order.side === '0' ? 'YES' : order.side === '1' ? 'NO' : order.side
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@@ -436,7 +452,6 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
|
||||||
<Tag style={{ fontSize: '11px' }}>{displaySide}</Tag>
|
|
||||||
{getStatusTag(order.status)}
|
{getStatusTag(order.status)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -485,18 +500,17 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
|
|||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||||
{orders.map((order) => {
|
{orders.map((order) => {
|
||||||
const date = new Date(order.createdAt)
|
const date = new Date(order.createdAt)
|
||||||
const formattedDate = date.toLocaleString('zh-CN', {
|
const formattedDate = date.toLocaleString('zh-CN', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit'
|
minute: '2-digit'
|
||||||
})
|
})
|
||||||
const amount = (parseFloat(order.quantity) * parseFloat(order.price)).toString()
|
const amount = (parseFloat(order.quantity) * parseFloat(order.price)).toString()
|
||||||
const displaySide = order.side === '0' ? 'YES' : order.side === '1' ? 'NO' : order.side
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={order.orderId}
|
key={order.orderId}
|
||||||
style={{
|
style={{
|
||||||
@@ -529,7 +543,6 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
|
||||||
<Tag>{displaySide}</Tag>
|
|
||||||
{getStatusTag(order.status)}
|
{getStatusTag(order.status)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -633,23 +646,20 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
|
|||||||
<div>
|
<div>
|
||||||
<div style={{ marginBottom: 16, display: 'flex', gap: 16, flexWrap: 'wrap', alignItems: 'center' }}>
|
<div style={{ marginBottom: 16, display: 'flex', gap: 16, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('copyTradingOrders.filterMarketId') || '筛选市场ID'}
|
placeholder={t('copyTradingOrders.filterMarketId') || '筛选市场ID(支持模糊匹配)'}
|
||||||
allowClear
|
allowClear
|
||||||
style={{ width: isMobile ? '100%' : 200 }}
|
style={{ width: isMobile ? '100%' : 200 }}
|
||||||
value={filters.marketId}
|
value={filters.marketId}
|
||||||
onChange={(e) => setFilters({ ...filters, marketId: e.target.value || undefined })}
|
onChange={(e) => setFilters({ ...filters, marketId: e.target.value || undefined })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select
|
<Input
|
||||||
placeholder={t('copyTradingOrders.filterSide') || '筛选方向'}
|
placeholder={t('copyTradingOrders.filterMarketTitle') || '筛选市场标题'}
|
||||||
allowClear
|
allowClear
|
||||||
style={{ width: isMobile ? '100%' : 150 }}
|
style={{ width: isMobile ? '100%' : 200 }}
|
||||||
value={filters.side}
|
value={filters.marketTitle}
|
||||||
onChange={(value) => setFilters({ ...filters, side: value || undefined })}
|
onChange={(e) => handleMarketTitleChange(e.target.value)}
|
||||||
>
|
/>
|
||||||
<Option value="0">YES</Option>
|
|
||||||
<Option value="1">NO</Option>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
placeholder={t('copyTradingOrders.filterStatus') || '筛选状态'}
|
placeholder={t('copyTradingOrders.filterStatus') || '筛选状态'}
|
||||||
@@ -669,7 +679,7 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
|
|||||||
</span>
|
</span>
|
||||||
<Switch
|
<Switch
|
||||||
checked={groupByMarket}
|
checked={groupByMarket}
|
||||||
onChange={setGroupByMarket}
|
onChange={handleToggleGroupByMarket}
|
||||||
checkedChildren={<AppstoreOutlined />}
|
checkedChildren={<AppstoreOutlined />}
|
||||||
unCheckedChildren={<UnorderedListOutlined />}
|
unCheckedChildren={<UnorderedListOutlined />}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -24,13 +24,29 @@ const MatchedOrdersTab: React.FC<MatchedOrdersTabProps> = ({ copyTradingId, acti
|
|||||||
const [filters, setFilters] = useState<{
|
const [filters, setFilters] = useState<{
|
||||||
sellOrderId?: string
|
sellOrderId?: string
|
||||||
buyOrderId?: string
|
buyOrderId?: string
|
||||||
|
marketTitle?: string
|
||||||
}>({})
|
}>({})
|
||||||
|
|
||||||
|
const handleMarketTitleChange = (value: string) => {
|
||||||
|
setFilters({ ...filters, marketTitle: value || undefined })
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (copyTradingId && active) {
|
if (copyTradingId && active) {
|
||||||
fetchOrders()
|
fetchOrders()
|
||||||
}
|
}
|
||||||
}, [copyTradingId, active, page, limit, filters])
|
}, [copyTradingId, active, page, limit])
|
||||||
|
|
||||||
|
// 防抖搜索 - marketTitle 和 orderId 变化时延迟0.5秒再搜索
|
||||||
|
useEffect(() => {
|
||||||
|
if (!copyTradingId || !active) return
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
fetchOrders()
|
||||||
|
}, 500) // 0.5秒延迟
|
||||||
|
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}, [filters.marketTitle, filters.buyOrderId, filters.sellOrderId])
|
||||||
|
|
||||||
const fetchOrders = async () => {
|
const fetchOrders = async () => {
|
||||||
if (!copyTradingId) return
|
if (!copyTradingId) return
|
||||||
@@ -193,6 +209,14 @@ const MatchedOrdersTab: React.FC<MatchedOrdersTabProps> = ({ copyTradingId, acti
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ marginBottom: 16, display: 'flex', gap: 16, flexWrap: 'wrap' }}>
|
<div style={{ marginBottom: 16, display: 'flex', gap: 16, flexWrap: 'wrap' }}>
|
||||||
|
<Input
|
||||||
|
placeholder={t('copyTradingOrders.filterMarketTitle') || '筛选市场标题'}
|
||||||
|
allowClear
|
||||||
|
style={{ width: isMobile ? '100%' : 200 }}
|
||||||
|
value={filters.marketTitle}
|
||||||
|
onChange={(e) => handleMarketTitleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('copyTradingOrders.filterSellOrderId') || '筛选卖出订单ID'}
|
placeholder={t('copyTradingOrders.filterSellOrderId') || '筛选卖出订单ID'}
|
||||||
allowClear
|
allowClear
|
||||||
|
|||||||
@@ -25,18 +25,44 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
|
|||||||
const [limit, setLimit] = useState(20)
|
const [limit, setLimit] = useState(20)
|
||||||
const [filters, setFilters] = useState<{
|
const [filters, setFilters] = useState<{
|
||||||
marketId?: string
|
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<Set<string>>(new Set()) // 展开的市场ID集合
|
const [expandedMarkets, setExpandedMarkets] = useState<Set<string>>(new Set()) // 展开的市场ID集合
|
||||||
const [groupedOrders, setGroupedOrders] = useState<MarketOrderGroup[]>([]) // 分组数据(从后端获取)
|
const [groupedOrders, setGroupedOrders] = useState<MarketOrderGroup[]>([]) // 分组数据(从后端获取)
|
||||||
const [groupedTotal, setGroupedTotal] = useState(0) // 分组总数(市场数量)
|
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(() => {
|
useEffect(() => {
|
||||||
if (copyTradingId && active) {
|
if (copyTradingId && active) {
|
||||||
fetchOrders()
|
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 () => {
|
const fetchOrders = async () => {
|
||||||
if (!copyTradingId) return
|
if (!copyTradingId) return
|
||||||
@@ -49,7 +75,9 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
|
|||||||
copyTradingId: parseInt(copyTradingId),
|
copyTradingId: parseInt(copyTradingId),
|
||||||
type: 'sell',
|
type: 'sell',
|
||||||
page,
|
page,
|
||||||
limit
|
limit,
|
||||||
|
marketId: filters.marketId,
|
||||||
|
marketTitle: filters.marketTitle
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await apiService.orderTracking.listGroupedByMarket(request)
|
const response = await apiService.orderTracking.listGroupedByMarket(request)
|
||||||
@@ -204,16 +232,6 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ 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 <Tag style={{ fontSize: isMobile ? 11 : 12 }}>{displaySide}</Tag>
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: t('copyTradingOrders.sellQuantity') || '卖出数量',
|
title: t('copyTradingOrders.sellQuantity') || '卖出数量',
|
||||||
dataIndex: 'quantity',
|
dataIndex: 'quantity',
|
||||||
@@ -378,7 +396,6 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
|
|||||||
minute: '2-digit'
|
minute: '2-digit'
|
||||||
})
|
})
|
||||||
const amount = (parseFloat(order.quantity) * parseFloat(order.price)).toString()
|
const amount = (parseFloat(order.quantity) * parseFloat(order.price)).toString()
|
||||||
const displaySide = order.side === '0' ? 'YES' : order.side === '1' ? 'NO' : order.side
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@@ -413,9 +430,6 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
|
|
||||||
<Tag style={{ fontSize: '11px' }}>{displaySide}</Tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||||
@@ -473,7 +487,6 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
|
|||||||
minute: '2-digit'
|
minute: '2-digit'
|
||||||
})
|
})
|
||||||
const amount = (parseFloat(order.quantity) * parseFloat(order.price)).toString()
|
const amount = (parseFloat(order.quantity) * parseFloat(order.price)).toString()
|
||||||
const displaySide = order.side === '0' ? 'YES' : order.side === '1' ? 'NO' : order.side
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@@ -507,9 +520,6 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
|
|
||||||
<Tag>{displaySide}</Tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Divider style={{ margin: '12px 0' }} />
|
<Divider style={{ margin: '12px 0' }} />
|
||||||
@@ -622,17 +632,24 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
|
|||||||
onChange={(e) => setFilters({ ...filters, marketId: e.target.value || undefined })}
|
onChange={(e) => setFilters({ ...filters, marketId: e.target.value || undefined })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
placeholder={t('copyTradingOrders.filterMarketTitle') || '筛选市场标题'}
|
||||||
|
allowClear
|
||||||
|
style={{ width: isMobile ? '100%' : 200 }}
|
||||||
|
value={filters.marketTitle}
|
||||||
|
onChange={(e) => handleMarketTitleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
placeholder={t('copyTradingOrders.filterSide') || '筛选方向'}
|
placeholder={t('copyTradingOrders.filterStatus') || '筛选状态'}
|
||||||
allowClear
|
allowClear
|
||||||
style={{ width: isMobile ? '100%' : 150 }}
|
style={{ width: isMobile ? '100%' : 150 }}
|
||||||
value={filters.side}
|
value={filters.status}
|
||||||
onChange={(value) => setFilters({ ...filters, side: value || undefined })}
|
onChange={(value) => setFilters({ ...filters, status: value || undefined })}
|
||||||
>
|
>
|
||||||
<Option value="0">YES</Option>
|
<Option value="filled">{t('copyTradingOrders.statusFilled') || '未成交'}</Option>
|
||||||
<Option value="1">NO</Option>
|
<Option value="partially_matched">{t('copyTradingOrders.statusPartiallyMatched') || '部分成交'}</Option>
|
||||||
<Option value="YES">YES</Option>
|
<Option value="fully_matched">{t('copyTradingOrders.statusFullyMatched') || '完全成交'}</Option>
|
||||||
<Option value="NO">NO</Option>
|
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Space>
|
<Space>
|
||||||
@@ -641,7 +658,7 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
|
|||||||
</span>
|
</span>
|
||||||
<Switch
|
<Switch
|
||||||
checked={groupByMarket}
|
checked={groupByMarket}
|
||||||
onChange={setGroupByMarket}
|
onChange={handleToggleGroupByMarket}
|
||||||
checkedChildren={<AppstoreOutlined />}
|
checkedChildren={<AppstoreOutlined />}
|
||||||
unCheckedChildren={<UnorderedListOutlined />}
|
unCheckedChildren={<UnorderedListOutlined />}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -670,6 +670,7 @@ export interface SellOrderInfo {
|
|||||||
price: string
|
price: string
|
||||||
amount: string
|
amount: string
|
||||||
realizedPnl: string
|
realizedPnl: string
|
||||||
|
status?: string // 卖出状态(filled, partially_matched, fully_matched)
|
||||||
createdAt: number
|
createdAt: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,7 +711,7 @@ export interface OrderTrackingRequest {
|
|||||||
page?: number
|
page?: number
|
||||||
limit?: number
|
limit?: number
|
||||||
marketId?: string
|
marketId?: string
|
||||||
side?: string
|
marketTitle?: string
|
||||||
status?: string
|
status?: string
|
||||||
sellOrderId?: string
|
sellOrderId?: string
|
||||||
buyOrderId?: string
|
buyOrderId?: string
|
||||||
@@ -724,6 +725,8 @@ export interface MarketGroupedOrdersRequest {
|
|||||||
type: 'buy' | 'sell'
|
type: 'buy' | 'sell'
|
||||||
page?: number
|
page?: number
|
||||||
limit?: number
|
limit?: number
|
||||||
|
marketId?: string
|
||||||
|
marketTitle?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user