feat: 优化订单列表筛选功能,支持按市场标题搜索并改进分组体验

- 后端:MarketGroupedOrdersRequest 添加 marketId 和 marketTitle 字段
- 后端:买入/卖出订单分组接口支持市场 ID 模糊匹配和市场标题关键字筛选
- 前端:BuyOrdersTab 添加市场标题筛选,移除方向筛选,记录用户分组偏好
- 前端:SellOrdersTab 添加市场标题筛选,将方向筛选改为状态筛选,移除方向列
- 前端:MatchedOrdersTab 添加市场标题筛选和市场列显示
- 前端:所有搜索输入框添加 0.5 秒防抖优化
- 前端:分组偏好保存到 localStorage 以便跨会话持久化
This commit is contained in:
WrBug
2026-01-12 10:01:20 +08:00
parent 90fa487d1b
commit 279806da2b
7 changed files with 220 additions and 100 deletions
+1
View File
@@ -1089,6 +1089,7 @@
"price": "价格",
"amount": "金额",
"filterMarketId": "筛选市场ID",
"filterMarketTitle": "筛选市场标题",
"filterSide": "筛选方向",
"filterStatus": "筛选状态",
"filterSellOrderId": "筛选卖出订单ID",
@@ -25,19 +25,44 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ 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<Set<string>>(new Set()) // 展开的市场ID集合
const [groupedOrders, setGroupedOrders] = useState<MarketOrderGroup[]>([]) // 分组数据(从后端获取)
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<BuyOrdersTabProps> = ({ 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<BuyOrdersTabProps> = ({ copyTradingId, active = fal
}
}
const columns = [
const columns = [
{
title: t('copyTradingOrders.orderId') || '订单ID',
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') || '买入数量',
dataIndex: 'quantity',
@@ -400,7 +417,6 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ 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 (
<Card
@@ -436,7 +452,6 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
)}
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
<Tag style={{ fontSize: '11px' }}>{displaySide}</Tag>
{getStatusTag(order.status)}
</div>
</div>
@@ -485,18 +500,17 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{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 (
<Card
key={order.orderId}
style={{
@@ -529,7 +543,6 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
)}
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
<Tag>{displaySide}</Tag>
{getStatusTag(order.status)}
</div>
</div>
@@ -633,23 +646,20 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
<div>
<div style={{ marginBottom: 16, display: 'flex', gap: 16, flexWrap: 'wrap', alignItems: 'center' }}>
<Input
placeholder={t('copyTradingOrders.filterMarketId') || '筛选市场ID'}
placeholder={t('copyTradingOrders.filterMarketId') || '筛选市场ID(支持模糊匹配)'}
allowClear
style={{ width: isMobile ? '100%' : 200 }}
value={filters.marketId}
onChange={(e) => setFilters({ ...filters, marketId: e.target.value || undefined })}
/>
<Select
placeholder={t('copyTradingOrders.filterSide') || '筛选方向'}
<Input
placeholder={t('copyTradingOrders.filterMarketTitle') || '筛选市场标题'}
allowClear
style={{ width: isMobile ? '100%' : 150 }}
value={filters.side}
onChange={(value) => setFilters({ ...filters, side: value || undefined })}
>
<Option value="0">YES</Option>
<Option value="1">NO</Option>
</Select>
style={{ width: isMobile ? '100%' : 200 }}
value={filters.marketTitle}
onChange={(e) => handleMarketTitleChange(e.target.value)}
/>
<Select
placeholder={t('copyTradingOrders.filterStatus') || '筛选状态'}
@@ -669,7 +679,7 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
</span>
<Switch
checked={groupByMarket}
onChange={setGroupByMarket}
onChange={handleToggleGroupByMarket}
checkedChildren={<AppstoreOutlined />}
unCheckedChildren={<UnorderedListOutlined />}
/>
@@ -24,13 +24,29 @@ const MatchedOrdersTab: React.FC<MatchedOrdersTabProps> = ({ copyTradingId, acti
const [filters, setFilters] = useState<{
sellOrderId?: string
buyOrderId?: string
marketTitle?: string
}>({})
const handleMarketTitleChange = (value: string) => {
setFilters({ ...filters, marketTitle: value || undefined })
}
useEffect(() => {
if (copyTradingId && active) {
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 () => {
if (!copyTradingId) return
@@ -193,6 +209,14 @@ const MatchedOrdersTab: React.FC<MatchedOrdersTabProps> = ({ copyTradingId, acti
return (
<div>
<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
placeholder={t('copyTradingOrders.filterSellOrderId') || '筛选卖出订单ID'}
allowClear
@@ -25,18 +25,44 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ 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<Set<string>>(new Set()) // 展开的市场ID集合
const [groupedOrders, setGroupedOrders] = useState<MarketOrderGroup[]>([]) // 分组数据(从后端获取)
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<SellOrdersTabProps> = ({ 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<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') || '卖出数量',
dataIndex: 'quantity',
@@ -378,7 +396,6 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ 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 (
<Card
@@ -413,9 +430,6 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
/>
)}
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
<Tag style={{ fontSize: '11px' }}>{displaySide}</Tag>
</div>
</div>
<div style={{ fontSize: '12px', color: '#666' }}>
@@ -473,7 +487,6 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ 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 (
<Card
@@ -507,9 +520,6 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
/>
)}
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
<Tag>{displaySide}</Tag>
</div>
</div>
<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 })}
/>
<Input
placeholder={t('copyTradingOrders.filterMarketTitle') || '筛选市场标题'}
allowClear
style={{ width: isMobile ? '100%' : 200 }}
value={filters.marketTitle}
onChange={(e) => handleMarketTitleChange(e.target.value)}
/>
<Select
placeholder={t('copyTradingOrders.filterSide') || '筛选方向'}
placeholder={t('copyTradingOrders.filterStatus') || '筛选状态'}
allowClear
style={{ width: isMobile ? '100%' : 150 }}
value={filters.side}
onChange={(value) => setFilters({ ...filters, side: value || undefined })}
value={filters.status}
onChange={(value) => setFilters({ ...filters, status: value || undefined })}
>
<Option value="0">YES</Option>
<Option value="1">NO</Option>
<Option value="YES">YES</Option>
<Option value="NO">NO</Option>
<Option value="filled">{t('copyTradingOrders.statusFilled') || '未成交'}</Option>
<Option value="partially_matched">{t('copyTradingOrders.statusPartiallyMatched') || '部分成交'}</Option>
<Option value="fully_matched">{t('copyTradingOrders.statusFullyMatched') || '完全成交'}</Option>
</Select>
<Space>
@@ -641,7 +658,7 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
</span>
<Switch
checked={groupByMarket}
onChange={setGroupByMarket}
onChange={handleToggleGroupByMarket}
checkedChildren={<AppstoreOutlined />}
unCheckedChildren={<UnorderedListOutlined />}
/>
+4 -1
View File
@@ -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
}
/**