feat: 优化订单列表筛选功能,支持按市场标题搜索并改进分组体验
- 后端:MarketGroupedOrdersRequest 添加 marketId 和 marketTitle 字段 - 后端:买入/卖出订单分组接口支持市场 ID 模糊匹配和市场标题关键字筛选 - 前端:BuyOrdersTab 添加市场标题筛选,移除方向筛选,记录用户分组偏好 - 前端:SellOrdersTab 添加市场标题筛选,将方向筛选改为状态筛选,移除方向列 - 前端:MatchedOrdersTab 添加市场标题筛选和市场列显示 - 前端:所有搜索输入框添加 0.5 秒防抖优化 - 前端:分组偏好保存到 localStorage 以便跨会话持久化
This commit is contained in:
@@ -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 />}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user