feat: 优化前端UI和功能
- 修复仓位轮询检查逻辑,增加2分钟延迟判断 - 统一订单页面为Tab切换模式,使用Modal展示 - 优化移动端统计UI,改为两列线性布局 - 简化过滤类型标签,移除括号说明 - 账户管理列表优化:钱包地址显示前6后4,移除API凭证和活跃订单列 - 修复复制按钮功能,添加降级方案和Toast提示 - 修复菜单层级问题,订单下拉直接显示已成交和已过滤订单 - 添加编辑功能Modal化 - 完善多语言支持
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
# 🎉 v1.0.1 版本发布公告
|
||||
|
||||
## 📅 发布日期
|
||||
|
||||
2025年12月(具体日期待定)
|
||||
|
||||
## 🚀 主要更新
|
||||
|
||||
### 性能优化
|
||||
|
||||
#### 📊 订单簿请求优化
|
||||
- 优化订单簿获取逻辑,仅在需要时请求,避免不必要的 API 调用
|
||||
- 当未配置需要订单簿的过滤条件(价差、订单深度)时,跳过订单簿获取
|
||||
- 订单簿只请求一次,在过滤检查中复用,提高性能
|
||||
|
||||
#### 💰 卖出价格计算优化
|
||||
- 卖出改为市价卖出:优先使用订单簿的 bestBid(最高买入价)
|
||||
- 如果 bestBid 获取失败,自动降级使用 Leader 的交易价格
|
||||
- 卖出价格固定按 90% 计算,不再使用价格容忍度配置
|
||||
- 提高卖出订单的成交率
|
||||
|
||||
#### ⚙️ 价格容忍度默认值
|
||||
- 如果价格容忍度配置为 0,自动使用默认值 5%
|
||||
- 确保买入订单能够正常应用价格调整,提高成交率
|
||||
|
||||
### 代码优化
|
||||
|
||||
#### 🎯 使用枚举优化过滤逻辑
|
||||
- 新增 `FilterResult` 数据类和 `FilterStatus` 枚举
|
||||
- 使用类型安全的枚举替代字符串判断,提高代码可维护性
|
||||
- 移除无用字段 `isBuyOrder` 参数
|
||||
- 优化 `checkFilters` 方法返回值,使用数据类封装结果
|
||||
|
||||
#### 📝 代码结构优化
|
||||
- 减少 if-else 嵌套,使用 Kotlin 链式调用和空安全操作符
|
||||
- 优化卖出价格计算逻辑,使用 `runCatching` 和 `?.let` 简化代码
|
||||
- 提取公共方法 `calculateFallbackSellPrice`,提高代码复用性
|
||||
|
||||
### 功能改进
|
||||
|
||||
#### 🗑️ 移除最小订单簿深度功能
|
||||
- 移除 `minOrderbookDepth`(最小订单簿深度)配置项
|
||||
- 简化过滤逻辑,只保留 `minOrderDepth`(最小订单深度)
|
||||
- 更新前端界面,移除相关配置项和提示文案
|
||||
|
||||
#### 📈 优化最小订单深度逻辑
|
||||
- 修改 `minOrderDepth` 检查逻辑,检查所有方向(买盘+卖盘)的总深度
|
||||
- 不再区分买卖方向,提供更全面的市场流动性评估
|
||||
|
||||
#### 🎯 市场结算判断优化
|
||||
- 添加市场已关闭时的结算判断逻辑
|
||||
- 如果市场已关闭且该 outcome 赢了(价格 >= 0.99),返回价格为 1
|
||||
- 如果市场已关闭且该 outcome 输了(价格 <= 0.01),返回价格为 0
|
||||
- 当没有仓位但有未完成订单时,正确判断市场结算状态并设置价格
|
||||
|
||||
### Bug 修复
|
||||
|
||||
- ✅ 修复盈亏和持仓价值计算逻辑未实现的问题
|
||||
- ✅ 修复过滤检查中订单簿可能重复请求的问题
|
||||
- ✅ 修复健康检查路径不一致问题(从 `/api/health` 改为 `/api/system/health`)
|
||||
|
||||
## 📦 如何更新
|
||||
|
||||
### Docker 部署
|
||||
|
||||
```bash
|
||||
# 拉取最新镜像
|
||||
docker pull wrbug/polyhermes:v1.0.1
|
||||
|
||||
# 或使用 latest 标签
|
||||
docker pull wrbug/polyhermes:latest
|
||||
|
||||
# 重启容器
|
||||
docker-compose down
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 源码部署
|
||||
|
||||
```bash
|
||||
# 拉取最新代码
|
||||
git fetch origin
|
||||
git checkout v1.0.1
|
||||
|
||||
# 重新构建
|
||||
cd backend
|
||||
./gradlew bootJar
|
||||
|
||||
# 重启服务
|
||||
# 根据您的部署方式重启服务
|
||||
```
|
||||
|
||||
## 📚 文档更新
|
||||
|
||||
- 更新前端多语言文案,优化提示信息
|
||||
- 移除最小订单簿深度相关的文档说明
|
||||
|
||||
## 🔗 相关链接
|
||||
|
||||
- **GitHub Release**: https://github.com/WrBug/PolyHermes/releases/tag/v1.0.1
|
||||
- **完整更新日志**: https://github.com/WrBug/PolyHermes/compare/v1.0.0...v1.0.1
|
||||
- **Docker Hub**: https://hub.docker.com/r/wrbug/polyhermes
|
||||
|
||||
## ⚠️ 重要提醒
|
||||
|
||||
**请务必使用官方 Docker 镜像源,避免财产损失!**
|
||||
|
||||
### ✅ 官方 Docker Hub 镜像
|
||||
|
||||
**官方镜像地址**:`wrbug/polyhermes`
|
||||
|
||||
```bash
|
||||
# ✅ 正确:使用官方镜像
|
||||
docker pull wrbug/polyhermes:v1.0.1
|
||||
|
||||
# ❌ 错误:不要使用其他来源的镜像
|
||||
# 任何非官方来源的镜像都可能包含恶意代码,导致您的私钥和资产被盗
|
||||
```
|
||||
|
||||
### 🔗 官方渠道
|
||||
|
||||
请通过以下**唯一官方渠道**获取 PolyHermes:
|
||||
|
||||
* **GitHub 仓库**:https://github.com/WrBug/PolyHermes
|
||||
* **Twitter**:@quant_tr
|
||||
* **Telegram 群组**:加入群组
|
||||
|
||||
---
|
||||
|
||||
**⭐ 如果这个项目对您有帮助,请给个 Star 支持一下!**
|
||||
|
||||
**💬 如有问题或建议,欢迎在 GitHub Issues 中反馈。**
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# v1.0.1
|
||||
|
||||
## 🚀 性能优化
|
||||
|
||||
### 📊 订单簿请求优化
|
||||
* 优化订单簿获取逻辑,仅在需要时请求,避免不必要的 API 调用
|
||||
* 当未配置需要订单簿的过滤条件(价差、订单深度)时,跳过订单簿获取
|
||||
* 订单簿只请求一次,在过滤检查中复用,提高性能
|
||||
|
||||
### 💰 卖出价格计算优化
|
||||
* 卖出改为市价卖出:优先使用订单簿的 bestBid(最高买入价)
|
||||
* 如果 bestBid 获取失败,自动降级使用 Leader 的交易价格
|
||||
* 卖出价格固定按 90% 计算,不再使用价格容忍度配置
|
||||
* 提高卖出订单的成交率
|
||||
|
||||
### ⚙️ 价格容忍度默认值
|
||||
* 如果价格容忍度配置为 0,自动使用默认值 5%
|
||||
* 确保买入订单能够正常应用价格调整,提高成交率
|
||||
|
||||
## 🔧 代码优化
|
||||
|
||||
### 🎯 使用枚举优化过滤逻辑
|
||||
* 新增 `FilterResult` 数据类和 `FilterStatus` 枚举
|
||||
* 使用类型安全的枚举替代字符串判断,提高代码可维护性
|
||||
* 移除无用字段 `isBuyOrder` 参数
|
||||
* 优化 `checkFilters` 方法返回值,使用数据类封装结果
|
||||
|
||||
### 📝 代码结构优化
|
||||
* 减少 if-else 嵌套,使用 Kotlin 链式调用和空安全操作符
|
||||
* 优化卖出价格计算逻辑,使用 `runCatching` 和 `?.let` 简化代码
|
||||
* 提取公共方法 `calculateFallbackSellPrice`,提高代码复用性
|
||||
|
||||
## 🗑️ 功能移除
|
||||
|
||||
### 移除最小订单簿深度功能
|
||||
* 移除 `minOrderbookDepth`(最小订单簿深度)配置项
|
||||
* 简化过滤逻辑,只保留 `minOrderDepth`(最小订单深度)
|
||||
* 更新前端界面,移除相关配置项和提示文案
|
||||
|
||||
### 优化最小订单深度逻辑
|
||||
* 修改 `minOrderDepth` 检查逻辑,检查所有方向(买盘+卖盘)的总深度
|
||||
* 不再区分买卖方向,提供更全面的市场流动性评估
|
||||
|
||||
## 🐛 Bug 修复
|
||||
|
||||
* 修复盈亏和持仓价值计算逻辑未实现的问题
|
||||
* 修复过滤检查中订单簿可能重复请求的问题
|
||||
|
||||
## 📚 文档更新
|
||||
|
||||
* 更新前端多语言文案,优化提示信息
|
||||
* 移除最小订单簿深度相关的文档说明
|
||||
|
||||
**Full Changelog**: https://github.com/WrBug/polymarket-bot/compare/v1.0.0...v1.0.1
|
||||
|
||||
+37
-8
@@ -192,7 +192,7 @@ class PositionCheckService(
|
||||
|
||||
/**
|
||||
* 逻辑1:处理待赎回仓位
|
||||
* 按照以下逻辑处理:
|
||||
https://clob.polymarket.com * 按照以下逻辑处理:
|
||||
* 1. 无待赎回仓位:跳过
|
||||
* 2. (未配置apikey || autoredeem==false) && 有待赎回的仓位:发送通知事件
|
||||
* 3. (已配置) && 有待赎回的仓位:处理订单逻辑
|
||||
@@ -392,11 +392,30 @@ class PositionCheckService(
|
||||
}
|
||||
} else {
|
||||
// 有仓位,按订单下单顺序(FIFO)更新状态
|
||||
// 如果仓位数量 >= 订单数量总和,所有订单完全成交
|
||||
// 如果仓位数量 < 订单数量总和,按FIFO顺序部分成交
|
||||
// 计算逻辑:
|
||||
// 1. 总订单数量 = 所有未卖出订单的剩余数量总和
|
||||
// 2. 已成交数量 = 总订单数量 - 仓位数量(因为还有仓位,说明部分订单已卖出)
|
||||
// 3. 如果已成交数量 = 0,说明订单还没有卖出,不修改订单状态
|
||||
// 4. 如果已成交数量 > 0,按FIFO顺序匹配订单
|
||||
val positionQuantity = position.quantity.toSafeBigDecimal()
|
||||
|
||||
// 计算总订单数量
|
||||
val totalOrderQuantity = orders.fold(BigDecimal.ZERO) { sum, order ->
|
||||
sum.add(order.remainingQuantity.toSafeBigDecimal())
|
||||
}
|
||||
|
||||
// 计算已成交数量
|
||||
val soldQuantity = totalOrderQuantity.subtract(positionQuantity)
|
||||
|
||||
// 如果已成交数量 <= 0,说明订单还没有卖出,不修改订单状态
|
||||
if (soldQuantity <= BigDecimal.ZERO) {
|
||||
logger.debug("仓位数量 >= 订单数量总和,订单尚未卖出: marketId=$marketId, outcomeIndex=$outcomeIndex, positionQuantity=$positionQuantity, totalOrderQuantity=$totalOrderQuantity")
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果已成交数量 > 0,按FIFO顺序匹配订单
|
||||
val currentPrice = getCurrentMarketPrice(marketId, outcomeIndex)
|
||||
updateOrdersAsSoldByFIFO(orders, positionQuantity, currentPrice,
|
||||
updateOrdersAsSoldByFIFO(orders, soldQuantity, currentPrice,
|
||||
copyTrading.id, marketId, outcomeIndex)
|
||||
}
|
||||
}
|
||||
@@ -629,12 +648,22 @@ class PositionCheckService(
|
||||
|
||||
/**
|
||||
* 按 FIFO 顺序更新订单状态为已卖出
|
||||
* 仓位数量小于订单数量总和时,按订单下单顺序更新
|
||||
* 同时创建卖出记录和匹配明细,用于统计
|
||||
* @param orders 订单列表(已按创建时间排序,FIFO)
|
||||
* @param soldQuantity 已成交数量(总订单数量 - 仓位数量)
|
||||
* @param sellPrice 卖出价格
|
||||
* @param copyTradingId 跟单配置ID
|
||||
* @param marketId 市场ID
|
||||
* @param outcomeIndex 结果索引
|
||||
*
|
||||
* 逻辑说明:
|
||||
* 1. 按订单创建时间顺序(FIFO)处理
|
||||
* 2. 如果订单剩余数量 <= 已成交数量,订单完全成交
|
||||
* 3. 如果订单剩余数量 > 已成交数量,订单部分成交
|
||||
* 4. 同时创建卖出记录和匹配明细,用于统计
|
||||
*/
|
||||
private suspend fun updateOrdersAsSoldByFIFO(
|
||||
orders: List<CopyOrderTracking>,
|
||||
availableQuantity: BigDecimal,
|
||||
soldQuantity: BigDecimal,
|
||||
sellPrice: BigDecimal,
|
||||
copyTradingId: Long,
|
||||
marketId: String,
|
||||
@@ -646,7 +675,7 @@ class PositionCheckService(
|
||||
|
||||
try {
|
||||
// 订单已经按 createdAt ASC 排序(FIFO)
|
||||
var remaining = availableQuantity
|
||||
var remaining = soldQuantity
|
||||
var totalMatchedQuantity = BigDecimal.ZERO
|
||||
var totalRealizedPnl = BigDecimal.ZERO
|
||||
val matchDetails = mutableListOf<SellMatchDetail>()
|
||||
|
||||
@@ -252,6 +252,7 @@ function App() {
|
||||
<Route path="/copy-trading/add" element={<ProtectedRoute><CopyTradingAdd /></ProtectedRoute>} />
|
||||
<Route path="/copy-trading/edit/:id" element={<ProtectedRoute><CopyTradingEdit /></ProtectedRoute>} />
|
||||
<Route path="/copy-trading/statistics/:copyTradingId" element={<ProtectedRoute><CopyTradingStatistics /></ProtectedRoute>} />
|
||||
{/* 保留旧路由以保持向后兼容 */}
|
||||
<Route path="/copy-trading/orders/buy/:copyTradingId" element={<ProtectedRoute><CopyTradingBuyOrders /></ProtectedRoute>} />
|
||||
<Route path="/copy-trading/orders/sell/:copyTradingId" element={<ProtectedRoute><CopyTradingSellOrders /></ProtectedRoute>} />
|
||||
<Route path="/copy-trading/orders/matched/:copyTradingId" element={<ProtectedRoute><CopyTradingMatchedOrders /></ProtectedRoute>} />
|
||||
|
||||
@@ -868,10 +868,11 @@
|
||||
"totalPnl": "Total P&L",
|
||||
"statistics": "Statistics",
|
||||
"orders": "Orders",
|
||||
"matchedOrders": "Matched Orders",
|
||||
"ordersButton": "Orders",
|
||||
"viewStatistics": "View Statistics",
|
||||
"buyOrders": "Buy Orders",
|
||||
"sellOrders": "Sell Orders",
|
||||
"matchedOrders": "Matched Orders",
|
||||
"filteredOrders": "Filtered Orders",
|
||||
"filterWallet": "Filter Wallet",
|
||||
"filterTemplate": "Filter Template",
|
||||
@@ -966,5 +967,72 @@
|
||||
"collapse": "Collapse",
|
||||
"refresh": "Refresh",
|
||||
"list": "List"
|
||||
},
|
||||
"copyTradingOrders": {
|
||||
"title": "Order List",
|
||||
"buyOrders": "Buy Orders",
|
||||
"sellOrders": "Sell Orders",
|
||||
"matchedOrders": "Matched Orders",
|
||||
"filteredOrders": "Filtered Orders",
|
||||
"statistics": "Copy Trading Statistics",
|
||||
"orderId": "Order ID",
|
||||
"leaderTradeId": "Leader Trade ID",
|
||||
"market": "Market",
|
||||
"marketId": "Market ID",
|
||||
"side": "Side",
|
||||
"buyQuantity": "Buy Quantity",
|
||||
"buyPrice": "Buy Price",
|
||||
"buyAmount": "Buy Amount",
|
||||
"sellQuantity": "Sell Quantity",
|
||||
"sellPrice": "Sell Price",
|
||||
"sellAmount": "Sell Amount",
|
||||
"matchedQuantity": "Matched",
|
||||
"remainingQuantity": "Remaining",
|
||||
"sellStatus": "Sell Status",
|
||||
"status": "Status",
|
||||
"statusFilled": "Filled",
|
||||
"statusPartiallySold": "Partially Sold",
|
||||
"statusFullySold": "Fully Sold",
|
||||
"realizedPnl": "Realized PnL",
|
||||
"createdAt": "Created At",
|
||||
"matchedAt": "Matched At",
|
||||
"sellOrderId": "Sell Order ID",
|
||||
"buyOrderId": "Buy Order ID",
|
||||
"priceInfo": "Price Info",
|
||||
"buy": "Buy",
|
||||
"sell": "Sell",
|
||||
"buyInfo": "Buy Info",
|
||||
"sellInfo": "Sell Info",
|
||||
"matchInfo": "Match Info",
|
||||
"matched": "Matched",
|
||||
"remaining": "Remaining",
|
||||
"quantity": "Quantity",
|
||||
"price": "Price",
|
||||
"amount": "Amount",
|
||||
"filterMarketId": "Filter Market ID",
|
||||
"filterSide": "Filter Side",
|
||||
"filterStatus": "Filter Status",
|
||||
"filterSellOrderId": "Filter Sell Order ID",
|
||||
"filterBuyOrderId": "Filter Buy Order ID",
|
||||
"noBuyOrders": "No buy orders",
|
||||
"noSellOrders": "No sell orders",
|
||||
"noMatchedOrders": "No matched orders",
|
||||
"fetchBuyOrdersFailed": "Failed to fetch buy orders",
|
||||
"fetchSellOrdersFailed": "Failed to fetch sell orders",
|
||||
"fetchMatchedOrdersFailed": "Failed to fetch matched orders",
|
||||
"fetchStatisticsFailed": "Failed to fetch statistics",
|
||||
"noStatistics": "No statistics available",
|
||||
"totalBuyOrders": "Total Buy Orders",
|
||||
"totalSellOrders": "Total Sell Orders",
|
||||
"totalMatchedOrders": "Total Matched Orders",
|
||||
"totalBuyAmount": "Total Buy Amount",
|
||||
"totalSellAmount": "Total Sell Amount",
|
||||
"totalPnl": "Total PnL",
|
||||
"totalRealizedPnl": "Total Realized PnL",
|
||||
"totalUnrealizedPnl": "Total Unrealized PnL",
|
||||
"winRate": "Win Rate",
|
||||
"averagePnl": "Average PnL",
|
||||
"maxPnl": "Max PnL",
|
||||
"minPnl": "Min PnL"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -785,10 +785,11 @@
|
||||
"totalPnl": "总盈亏",
|
||||
"statistics": "统计",
|
||||
"orders": "订单",
|
||||
"matchedOrders": "已成交订单",
|
||||
"ordersButton": "订单",
|
||||
"viewStatistics": "查看统计",
|
||||
"buyOrders": "买入订单",
|
||||
"sellOrders": "卖出订单",
|
||||
"matchedOrders": "匹配关系",
|
||||
"filteredOrders": "已过滤订单",
|
||||
"filterWallet": "筛选钱包",
|
||||
"filterTemplate": "筛选模板",
|
||||
@@ -883,5 +884,72 @@
|
||||
"collapse": "收起",
|
||||
"refresh": "刷新",
|
||||
"list": "列表"
|
||||
},
|
||||
"copyTradingOrders": {
|
||||
"title": "订单列表",
|
||||
"buyOrders": "买入订单",
|
||||
"sellOrders": "卖出订单",
|
||||
"matchedOrders": "匹配关系",
|
||||
"filteredOrders": "已过滤订单",
|
||||
"statistics": "跟单关系统计",
|
||||
"orderId": "订单ID",
|
||||
"leaderTradeId": "Leader 交易ID",
|
||||
"market": "市场",
|
||||
"marketId": "市场ID",
|
||||
"side": "方向",
|
||||
"buyQuantity": "买入数量",
|
||||
"buyPrice": "买入价格",
|
||||
"buyAmount": "买入金额",
|
||||
"sellQuantity": "卖出数量",
|
||||
"sellPrice": "卖出价格",
|
||||
"sellAmount": "卖出金额",
|
||||
"matchedQuantity": "已匹配",
|
||||
"remainingQuantity": "剩余",
|
||||
"sellStatus": "卖出状态",
|
||||
"status": "状态",
|
||||
"statusFilled": "已完成",
|
||||
"statusPartiallySold": "部分成交",
|
||||
"statusFullySold": "全部成交",
|
||||
"realizedPnl": "已实现盈亏",
|
||||
"createdAt": "创建时间",
|
||||
"matchedAt": "匹配时间",
|
||||
"sellOrderId": "卖出订单ID",
|
||||
"buyOrderId": "买入订单ID",
|
||||
"priceInfo": "价格信息",
|
||||
"buy": "买入",
|
||||
"sell": "卖出",
|
||||
"buyInfo": "买入信息",
|
||||
"sellInfo": "卖出信息",
|
||||
"matchInfo": "匹配信息",
|
||||
"matched": "已匹配",
|
||||
"remaining": "剩余",
|
||||
"quantity": "数量",
|
||||
"price": "价格",
|
||||
"amount": "金额",
|
||||
"filterMarketId": "筛选市场ID",
|
||||
"filterSide": "筛选方向",
|
||||
"filterStatus": "筛选状态",
|
||||
"filterSellOrderId": "筛选卖出订单ID",
|
||||
"filterBuyOrderId": "筛选买入订单ID",
|
||||
"noBuyOrders": "暂无买入订单",
|
||||
"noSellOrders": "暂无卖出订单",
|
||||
"noMatchedOrders": "暂无匹配关系",
|
||||
"fetchBuyOrdersFailed": "获取买入订单列表失败",
|
||||
"fetchSellOrdersFailed": "获取卖出订单列表失败",
|
||||
"fetchMatchedOrdersFailed": "获取匹配关系列表失败",
|
||||
"fetchStatisticsFailed": "获取统计信息失败",
|
||||
"noStatistics": "暂无统计数据",
|
||||
"totalBuyOrders": "总买入订单数",
|
||||
"totalSellOrders": "总卖出订单数",
|
||||
"totalMatchedOrders": "总匹配订单数",
|
||||
"totalBuyAmount": "总买入金额",
|
||||
"totalSellAmount": "总卖出金额",
|
||||
"totalPnl": "总盈亏",
|
||||
"totalRealizedPnl": "总已实现盈亏",
|
||||
"totalUnrealizedPnl": "总未实现盈亏",
|
||||
"winRate": "胜率",
|
||||
"averagePnl": "平均盈亏",
|
||||
"maxPnl": "最大盈亏",
|
||||
"minPnl": "最小盈亏"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -868,10 +868,11 @@
|
||||
"totalPnl": "總盈虧",
|
||||
"statistics": "統計",
|
||||
"orders": "訂單",
|
||||
"matchedOrders": "已成交訂單",
|
||||
"ordersButton": "訂單",
|
||||
"viewStatistics": "查看統計",
|
||||
"buyOrders": "買入訂單",
|
||||
"sellOrders": "賣出訂單",
|
||||
"matchedOrders": "匹配關係",
|
||||
"filteredOrders": "被過濾訂單",
|
||||
"filterWallet": "篩選錢包",
|
||||
"filterTemplate": "篩選模板",
|
||||
@@ -966,5 +967,72 @@
|
||||
"collapse": "收起",
|
||||
"refresh": "刷新",
|
||||
"list": "列表"
|
||||
},
|
||||
"copyTradingOrders": {
|
||||
"title": "訂單列表",
|
||||
"buyOrders": "買入訂單",
|
||||
"sellOrders": "賣出訂單",
|
||||
"matchedOrders": "匹配關係",
|
||||
"filteredOrders": "已過濾訂單",
|
||||
"statistics": "跟單關系統計",
|
||||
"orderId": "訂單ID",
|
||||
"leaderTradeId": "Leader 交易ID",
|
||||
"market": "市場",
|
||||
"marketId": "市場ID",
|
||||
"side": "方向",
|
||||
"buyQuantity": "買入數量",
|
||||
"buyPrice": "買入價格",
|
||||
"buyAmount": "買入金額",
|
||||
"sellQuantity": "賣出數量",
|
||||
"sellPrice": "賣出價格",
|
||||
"sellAmount": "賣出金額",
|
||||
"matchedQuantity": "已匹配",
|
||||
"remainingQuantity": "剩餘",
|
||||
"sellStatus": "賣出狀態",
|
||||
"status": "狀態",
|
||||
"statusFilled": "已完成",
|
||||
"statusPartiallySold": "部分成交",
|
||||
"statusFullySold": "全部成交",
|
||||
"realizedPnl": "已實現盈虧",
|
||||
"createdAt": "創建時間",
|
||||
"matchedAt": "匹配時間",
|
||||
"sellOrderId": "賣出訂單ID",
|
||||
"buyOrderId": "買入訂單ID",
|
||||
"priceInfo": "價格信息",
|
||||
"buy": "買入",
|
||||
"sell": "賣出",
|
||||
"buyInfo": "買入信息",
|
||||
"sellInfo": "賣出信息",
|
||||
"matchInfo": "匹配信息",
|
||||
"matched": "已匹配",
|
||||
"remaining": "剩餘",
|
||||
"quantity": "數量",
|
||||
"price": "價格",
|
||||
"amount": "金額",
|
||||
"filterMarketId": "篩選市場ID",
|
||||
"filterSide": "篩選方向",
|
||||
"filterStatus": "篩選狀態",
|
||||
"filterSellOrderId": "篩選賣出訂單ID",
|
||||
"filterBuyOrderId": "篩選買入訂單ID",
|
||||
"noBuyOrders": "暫無買入訂單",
|
||||
"noSellOrders": "暫無賣出訂單",
|
||||
"noMatchedOrders": "暫無匹配關係",
|
||||
"fetchBuyOrdersFailed": "獲取買入訂單列表失敗",
|
||||
"fetchSellOrdersFailed": "獲取賣出訂單列表失敗",
|
||||
"fetchMatchedOrdersFailed": "獲取匹配關係列表失敗",
|
||||
"fetchStatisticsFailed": "獲取統計信息失敗",
|
||||
"noStatistics": "暫無統計數據",
|
||||
"totalBuyOrders": "總買入訂單數",
|
||||
"totalSellOrders": "總賣出訂單數",
|
||||
"totalMatchedOrders": "總匹配訂單數",
|
||||
"totalBuyAmount": "總買入金額",
|
||||
"totalSellAmount": "總賣出金額",
|
||||
"totalPnl": "總盈虧",
|
||||
"totalRealizedPnl": "總已實現盈虧",
|
||||
"totalUnrealizedPnl": "總未實現盈虧",
|
||||
"winRate": "勝率",
|
||||
"averagePnl": "平均盈虧",
|
||||
"maxPnl": "最大盈虧",
|
||||
"minPnl": "最小盈虧"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,11 +74,54 @@ const AccountList: React.FC = () => {
|
||||
}
|
||||
|
||||
const handleCopy = (text: string) => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
message.success(t('accountList.copySuccess'))
|
||||
}).catch(() => {
|
||||
message.error(t('accountList.copyFailed'))
|
||||
})
|
||||
if (!text) {
|
||||
message.warning(t('accountList.copyFailed') || '复制失败:地址为空')
|
||||
return
|
||||
}
|
||||
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
message.success({
|
||||
content: t('accountList.copySuccess') || '已复制到剪贴板',
|
||||
duration: 2
|
||||
})
|
||||
}).catch((err) => {
|
||||
console.error('复制失败:', err)
|
||||
// 降级方案:使用传统方法
|
||||
fallbackCopyTextToClipboard(text)
|
||||
})
|
||||
} else {
|
||||
// 降级方案:使用传统方法
|
||||
fallbackCopyTextToClipboard(text)
|
||||
}
|
||||
}
|
||||
|
||||
const fallbackCopyTextToClipboard = (text: string) => {
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = text
|
||||
textArea.style.position = 'fixed'
|
||||
textArea.style.left = '-999999px'
|
||||
textArea.style.top = '-999999px'
|
||||
document.body.appendChild(textArea)
|
||||
textArea.focus()
|
||||
textArea.select()
|
||||
|
||||
try {
|
||||
const successful = document.execCommand('copy')
|
||||
if (successful) {
|
||||
message.success({
|
||||
content: t('accountList.copySuccess') || '已复制到剪贴板',
|
||||
duration: 2
|
||||
})
|
||||
} else {
|
||||
message.error(t('accountList.copyFailed') || '复制失败')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err)
|
||||
message.error(t('accountList.copyFailed') || '复制失败')
|
||||
} finally {
|
||||
document.body.removeChild(textArea)
|
||||
}
|
||||
}
|
||||
|
||||
const handleShowDetail = async (account: Account) => {
|
||||
@@ -210,46 +253,45 @@ const AccountList: React.FC = () => {
|
||||
title: t('accountList.walletAddress'),
|
||||
dataIndex: 'walletAddress',
|
||||
key: 'walletAddress',
|
||||
render: (text: string) => (
|
||||
<Space>
|
||||
<span style={{ fontFamily: 'monospace', fontSize: '12px' }}>{text}</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCopy(text)}
|
||||
title={t('accountList.walletAddress')}
|
||||
/>
|
||||
</Space>
|
||||
)
|
||||
render: (text: string) => {
|
||||
const formatted = text ? `${text.slice(0, 6)}...${text.slice(-4)}` : '-'
|
||||
return (
|
||||
<Space>
|
||||
<span style={{ fontFamily: 'monospace', fontSize: '12px' }}>{formatted}</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleCopy(text)
|
||||
}}
|
||||
title={t('accountList.walletAddress')}
|
||||
/>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('accountList.proxyAddress'),
|
||||
dataIndex: 'proxyAddress',
|
||||
key: 'proxyAddress',
|
||||
render: (address: string) => (
|
||||
<Space>
|
||||
<span style={{ fontFamily: 'monospace', fontSize: '12px' }}>{address}</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCopy(address)}
|
||||
title={t('accountList.proxyAddress')}
|
||||
/>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('accountList.apiCredentials'),
|
||||
key: 'apiCredentials',
|
||||
render: (_: any, record: Account) => {
|
||||
const allConfigured = record.apiKeyConfigured && record.apiSecretConfigured && record.apiPassphraseConfigured
|
||||
const partialConfigured = record.apiKeyConfigured || record.apiSecretConfigured || record.apiPassphraseConfigured
|
||||
render: (address: string) => {
|
||||
const formatted = address ? `${address.slice(0, 6)}...${address.slice(-4)}` : '-'
|
||||
return (
|
||||
<Tag color={allConfigured ? 'success' : partialConfigured ? 'warning' : 'default'}>
|
||||
{allConfigured ? t('accountList.fullConfig') : partialConfigured ? t('accountList.partialConfig') : t('accountList.notConfigured')}
|
||||
</Tag>
|
||||
<Space>
|
||||
<span style={{ fontFamily: 'monospace', fontSize: '12px' }}>{formatted}</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleCopy(address)
|
||||
}}
|
||||
title={t('accountList.proxyAddress')}
|
||||
/>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -266,17 +308,6 @@ const AccountList: React.FC = () => {
|
||||
return balance && balance !== '-' && typeof balance === 'string' ? `${formatUSDC(balance)} USDC` : '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('accountList.activeOrders'),
|
||||
dataIndex: 'activeOrders',
|
||||
key: 'activeOrders',
|
||||
render: (_: any, record: Account) => {
|
||||
if (record.activeOrders !== undefined && record.activeOrders !== null) {
|
||||
return <Tag color={record.activeOrders > 0 ? 'orange' : 'default'}>{record.activeOrders}</Tag>
|
||||
}
|
||||
return <span style={{ color: '#999' }}>-</span>
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('accountList.action'),
|
||||
key: 'action',
|
||||
@@ -323,9 +354,6 @@ const AccountList: React.FC = () => {
|
||||
title: t('accountList.accountName'),
|
||||
key: 'info',
|
||||
render: (_: any, record: Account) => {
|
||||
const allConfigured = record.apiKeyConfigured && record.apiSecretConfigured && record.apiPassphraseConfigured
|
||||
const partialConfigured = record.apiKeyConfigured || record.apiSecretConfigured || record.apiPassphraseConfigured
|
||||
|
||||
return (
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<div style={{
|
||||
@@ -344,31 +372,32 @@ const AccountList: React.FC = () => {
|
||||
lineHeight: '1.4'
|
||||
}}>
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
<strong>{t('accountList.walletAddress')}:</strong> {record.walletAddress}
|
||||
<strong>{t('accountList.walletAddress')}:</strong> {record.walletAddress ? `${record.walletAddress.slice(0, 6)}...${record.walletAddress.slice(-4)}` : '-'}
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCopy(record.walletAddress)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleCopy(record.walletAddress)
|
||||
}}
|
||||
style={{ marginLeft: '4px', padding: '0 4px' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{t('accountList.proxyAddress')}:</strong> {record.proxyAddress}
|
||||
<strong>{t('accountList.proxyAddress')}:</strong> {record.proxyAddress ? `${record.proxyAddress.slice(0, 6)}...${record.proxyAddress.slice(-4)}` : '-'}
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCopy(record.proxyAddress)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleCopy(record.proxyAddress)
|
||||
}}
|
||||
style={{ marginLeft: '4px', padding: '0 4px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: '8px', display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
|
||||
<Tag color={allConfigured ? 'success' : partialConfigured ? 'warning' : 'default'} style={{ margin: 0 }}>
|
||||
{allConfigured ? t('accountList.fullConfig') : partialConfigured ? t('accountList.partialConfig') : t('accountList.notConfigured')}
|
||||
</Tag>
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
@@ -391,18 +420,6 @@ const AccountList: React.FC = () => {
|
||||
{t('accountList.available')}: {formatUSDC(balanceMap[record.id].available)} USDC | {t('accountList.position')}: {formatUSDC(balanceMap[record.id].position)} USDC
|
||||
</div>
|
||||
)}
|
||||
{(record.activeOrders !== undefined && record.activeOrders !== null) && (
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
marginTop: '4px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
{t('accountList.activeOrders')}: <Tag color={record.activeOrders > 0 ? 'orange' : 'default'} style={{ margin: 0 }}>{record.activeOrders}</Tag>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -598,7 +615,10 @@ const AccountList: React.FC = () => {
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCopy(detailAccount.walletAddress || '')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleCopy(detailAccount.walletAddress || '')
|
||||
}}
|
||||
title={t('accountList.walletAddress')}
|
||||
/>
|
||||
</Space>
|
||||
@@ -618,7 +638,10 @@ const AccountList: React.FC = () => {
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => handleCopy(detailAccount.proxyAddress || '')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleCopy(detailAccount.proxyAddress || '')
|
||||
}}
|
||||
title={t('accountList.proxyAddress')}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
@@ -9,6 +9,10 @@ import { useAccountStore } from '../store/accountStore'
|
||||
import type { CopyTrading, Leader, CopyTradingStatistics } from '../types'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { formatUSDC } from '../utils'
|
||||
import CopyTradingOrdersModal from './CopyTradingOrders/index'
|
||||
import StatisticsModal from './CopyTradingOrders/StatisticsModal'
|
||||
import FilteredOrdersModal from './CopyTradingOrders/FilteredOrdersModal'
|
||||
import EditModal from './CopyTradingOrders/EditModal'
|
||||
|
||||
const { Option } = Select
|
||||
|
||||
@@ -28,6 +32,17 @@ const CopyTradingList: React.FC = () => {
|
||||
enabled?: boolean
|
||||
}>({})
|
||||
|
||||
// Modal 状态
|
||||
const [ordersModalOpen, setOrdersModalOpen] = useState(false)
|
||||
const [ordersModalCopyTradingId, setOrdersModalCopyTradingId] = useState<string>('')
|
||||
const [ordersModalTab, setOrdersModalTab] = useState<'buy' | 'sell' | 'matched'>('buy')
|
||||
const [statisticsModalOpen, setStatisticsModalOpen] = useState(false)
|
||||
const [statisticsModalCopyTradingId, setStatisticsModalCopyTradingId] = useState<string>('')
|
||||
const [filteredOrdersModalOpen, setFilteredOrdersModalOpen] = useState(false)
|
||||
const [filteredOrdersModalCopyTradingId, setFilteredOrdersModalCopyTradingId] = useState<string>('')
|
||||
const [editModalOpen, setEditModalOpen] = useState(false)
|
||||
const [editModalCopyTradingId, setEditModalCopyTradingId] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccounts()
|
||||
fetchLeaders()
|
||||
@@ -265,47 +280,24 @@ const CopyTradingList: React.FC = () => {
|
||||
fixed: 'right' as const,
|
||||
render: (_: any, record: CopyTrading) => {
|
||||
const menuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: 'buyOrders',
|
||||
label: t('copyTradingList.buyOrders') || '买入订单',
|
||||
icon: <UnorderedListOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/orders/buy/${record.id}`)
|
||||
},
|
||||
{
|
||||
key: 'sellOrders',
|
||||
label: t('copyTradingList.sellOrders') || '卖出订单',
|
||||
icon: <UnorderedListOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/orders/sell/${record.id}`)
|
||||
},
|
||||
{
|
||||
key: 'matchedOrders',
|
||||
label: t('copyTradingList.matchedOrders') || '匹配关系',
|
||||
label: t('copyTradingList.matchedOrders') || '已成交订单',
|
||||
icon: <UnorderedListOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/orders/matched/${record.id}`)
|
||||
onClick: () => {
|
||||
setOrdersModalCopyTradingId(record.id.toString())
|
||||
setOrdersModalTab('buy')
|
||||
setOrdersModalOpen(true)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'filteredOrders',
|
||||
label: t('copyTradingList.filteredOrders') || '已过滤订单',
|
||||
icon: <UnorderedListOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/filtered-orders/${record.id}`)
|
||||
},
|
||||
{
|
||||
type: 'divider'
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: (
|
||||
<Popconfirm
|
||||
title={t('copyTradingList.deleteConfirm') || '确定要删除这个跟单关系吗?'}
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText={t('common.confirm') || '确定'}
|
||||
cancelText={t('common.cancel') || '取消'}
|
||||
onCancel={(e) => e?.stopPropagation()}
|
||||
>
|
||||
<span style={{ color: '#ff4d4f' }}>{t('common.delete') || '删除'}</span>
|
||||
</Popconfirm>
|
||||
),
|
||||
danger: true
|
||||
onClick: () => {
|
||||
setFilteredOrdersModalCopyTradingId(record.id.toString())
|
||||
setFilteredOrdersModalOpen(true)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -317,7 +309,10 @@ const CopyTradingList: React.FC = () => {
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => navigate(`/copy-trading/edit/${record.id}`)}
|
||||
onClick={() => {
|
||||
setEditModalCopyTradingId(record.id.toString())
|
||||
setEditModalOpen(true)
|
||||
}}
|
||||
>
|
||||
{t('common.edit') || '编辑'}
|
||||
</Button>
|
||||
@@ -325,7 +320,10 @@ const CopyTradingList: React.FC = () => {
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<BarChartOutlined />}
|
||||
onClick={() => navigate(`/copy-trading/statistics/${record.id}`)}
|
||||
onClick={() => {
|
||||
setStatisticsModalCopyTradingId(record.id.toString())
|
||||
setStatisticsModalOpen(true)
|
||||
}}
|
||||
>
|
||||
{t('copyTradingList.statistics') || '统计'}
|
||||
</Button>
|
||||
@@ -554,7 +552,10 @@ const CopyTradingList: React.FC = () => {
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => navigate(`/copy-trading/edit/${record.id}`)}
|
||||
onClick={() => {
|
||||
setEditModalCopyTradingId(record.id.toString())
|
||||
setEditModalOpen(true)
|
||||
}}
|
||||
style={{ flex: 1, minWidth: '80px' }}
|
||||
>
|
||||
{t('common.edit') || '编辑'}
|
||||
@@ -562,7 +563,10 @@ const CopyTradingList: React.FC = () => {
|
||||
<Button
|
||||
size="small"
|
||||
icon={<BarChartOutlined />}
|
||||
onClick={() => navigate(`/copy-trading/statistics/${record.id}`)}
|
||||
onClick={() => {
|
||||
setStatisticsModalCopyTradingId(record.id.toString())
|
||||
setStatisticsModalOpen(true)
|
||||
}}
|
||||
style={{ flex: 1, minWidth: '80px' }}
|
||||
>
|
||||
{t('copyTradingList.statistics') || '统计'}
|
||||
@@ -570,29 +574,24 @@ const CopyTradingList: React.FC = () => {
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'buyOrders',
|
||||
label: t('copyTradingList.buyOrders') || '买入订单',
|
||||
icon: <UnorderedListOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/orders/buy/${record.id}`)
|
||||
},
|
||||
{
|
||||
key: 'sellOrders',
|
||||
label: t('copyTradingList.sellOrders') || '卖出订单',
|
||||
icon: <UnorderedListOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/orders/sell/${record.id}`)
|
||||
},
|
||||
{
|
||||
key: 'matchedOrders',
|
||||
label: t('copyTradingList.matchedOrders') || '匹配关系',
|
||||
label: t('copyTradingList.matchedOrders') || '已成交订单',
|
||||
icon: <UnorderedListOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/orders/matched/${record.id}`)
|
||||
onClick: () => {
|
||||
setOrdersModalCopyTradingId(record.id.toString())
|
||||
setOrdersModalTab('buy')
|
||||
setOrdersModalOpen(true)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'filteredOrders',
|
||||
label: t('copyTradingList.filteredOrders') || '已过滤订单',
|
||||
icon: <UnorderedListOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/filtered-orders/${record.id}`)
|
||||
onClick: () => {
|
||||
setFilteredOrdersModalCopyTradingId(record.id.toString())
|
||||
setFilteredOrdersModalOpen(true)
|
||||
}
|
||||
}
|
||||
]
|
||||
}}
|
||||
@@ -643,6 +642,32 @@ const CopyTradingList: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Modal 组件 */}
|
||||
<CopyTradingOrdersModal
|
||||
open={ordersModalOpen}
|
||||
onClose={() => setOrdersModalOpen(false)}
|
||||
copyTradingId={ordersModalCopyTradingId}
|
||||
defaultTab={ordersModalTab}
|
||||
/>
|
||||
<StatisticsModal
|
||||
open={statisticsModalOpen}
|
||||
onClose={() => setStatisticsModalOpen(false)}
|
||||
copyTradingId={statisticsModalCopyTradingId}
|
||||
/>
|
||||
<FilteredOrdersModal
|
||||
open={filteredOrdersModalOpen}
|
||||
onClose={() => setFilteredOrdersModalOpen(false)}
|
||||
copyTradingId={filteredOrdersModalCopyTradingId}
|
||||
/>
|
||||
<EditModal
|
||||
open={editModalOpen}
|
||||
onClose={() => setEditModalOpen(false)}
|
||||
copyTradingId={editModalCopyTradingId}
|
||||
onSuccess={() => {
|
||||
fetchCopyTradings()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Table, Tag, Select, Input, Button, Card, Divider, Spin } from 'antd'
|
||||
import { apiService } from '../../services/api'
|
||||
import { formatUSDC } from '../../utils'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { BuyOrderInfo, OrderTrackingRequest, OrderTrackingListResponse } from '../../types'
|
||||
|
||||
const { Option } = Select
|
||||
|
||||
interface BuyOrdersTabProps {
|
||||
copyTradingId: string
|
||||
}
|
||||
|
||||
const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId }) => {
|
||||
const { t } = useTranslation()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [orders, setOrders] = useState<BuyOrderInfo[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [limit, setLimit] = useState(20)
|
||||
const [filters, setFilters] = useState<{
|
||||
marketId?: string
|
||||
side?: string
|
||||
status?: string
|
||||
}>({})
|
||||
|
||||
useEffect(() => {
|
||||
if (copyTradingId) {
|
||||
fetchOrders()
|
||||
}
|
||||
}, [copyTradingId, page, limit, filters])
|
||||
|
||||
const fetchOrders = async () => {
|
||||
if (!copyTradingId) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const request: OrderTrackingRequest = {
|
||||
copyTradingId: parseInt(copyTradingId),
|
||||
type: 'buy',
|
||||
page,
|
||||
limit,
|
||||
...filters
|
||||
}
|
||||
|
||||
const response = await apiService.orderTracking.list(request)
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
const data = response.data.data as OrderTrackingListResponse
|
||||
setOrders((data.list || []) as BuyOrderInfo[])
|
||||
setTotal(data.total || 0)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取买入订单列表失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusTag = (status: string) => {
|
||||
const statusMap: Record<string, { color: string; text: string }> = {
|
||||
filled: { color: 'processing', text: t('copyTradingOrders.statusFilled') || '已完成' },
|
||||
partially_matched: { color: 'warning', text: t('copyTradingOrders.statusPartiallySold') || '部分成交' },
|
||||
fully_matched: { color: 'success', text: t('copyTradingOrders.statusFullySold') || '全部成交' }
|
||||
}
|
||||
const config = statusMap[status] || { color: 'default', text: status }
|
||||
return <Tag color={config.color}>{config.text}</Tag>
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('copyTradingOrders.orderId') || '订单ID',
|
||||
dataIndex: 'orderId',
|
||||
key: 'orderId',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (text: string) => (
|
||||
<span style={{ fontFamily: 'monospace', fontSize: isMobile ? 11 : 12 }}>
|
||||
{isMobile
|
||||
? `${text.slice(0, 6)}...${text.slice(-4)}`
|
||||
: `${text.slice(0, 8)}...${text.slice(-6)}`
|
||||
}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('copyTradingOrders.leaderTradeId') || 'Leader 交易ID',
|
||||
dataIndex: 'leaderTradeId',
|
||||
key: 'leaderTradeId',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (text: string) => (
|
||||
<span style={{ fontFamily: 'monospace', fontSize: isMobile ? 11 : 12 }}>
|
||||
{isMobile
|
||||
? `${text.slice(0, 6)}...${text.slice(-4)}`
|
||||
: `${text.slice(0, 8)}...${text.slice(-6)}`
|
||||
}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('copyTradingOrders.market') || '市场',
|
||||
dataIndex: 'marketId',
|
||||
key: 'marketId',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (text: string) => (
|
||||
<span style={{ fontFamily: 'monospace', fontSize: isMobile ? 11 : 12 }}>
|
||||
{isMobile
|
||||
? `${text.slice(0, 6)}...${text.slice(-4)}`
|
||||
: `${text.slice(0, 8)}...${text.slice(-6)}`
|
||||
}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
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',
|
||||
key: 'quantity',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (value: string) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{formatUSDC(value)}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('copyTradingOrders.buyPrice') || '买入价格',
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (value: string) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{formatUSDC(value)}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('copyTradingOrders.buyAmount') || '买入金额',
|
||||
key: 'amount',
|
||||
width: isMobile ? 100 : 120,
|
||||
render: (_: any, record: BuyOrderInfo) => {
|
||||
const amount = (parseFloat(record.quantity) * parseFloat(record.price)).toString()
|
||||
return (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>
|
||||
{isMobile ? formatUSDC(amount) : `${formatUSDC(amount)} USDC`}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('copyTradingOrders.matchedQuantity') || '已匹配',
|
||||
dataIndex: 'matchedQuantity',
|
||||
key: 'matchedQuantity',
|
||||
width: isMobile ? 70 : 90,
|
||||
render: (value: string) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{formatUSDC(value)}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('copyTradingOrders.remainingQuantity') || '剩余',
|
||||
dataIndex: 'remainingQuantity',
|
||||
key: 'remainingQuantity',
|
||||
width: isMobile ? 70 : 90,
|
||||
render: (value: string) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{formatUSDC(value)}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('copyTradingOrders.sellStatus') || '卖出状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (status: string) => getStatusTag(status)
|
||||
},
|
||||
{
|
||||
title: t('copyTradingOrders.createdAt') || '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: isMobile ? 120 : 160,
|
||||
render: (timestamp: number) => (
|
||||
<span style={{ fontSize: isMobile ? 11 : 12 }}>
|
||||
{isMobile
|
||||
? new Date(timestamp).toLocaleDateString('zh-CN')
|
||||
: new Date(timestamp).toLocaleString('zh-CN')
|
||||
}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, display: 'flex', gap: 16, flexWrap: 'wrap' }}>
|
||||
<Input
|
||||
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') || '筛选方向'}
|
||||
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>
|
||||
<Option value="YES">YES</Option>
|
||||
<Option value="NO">NO</Option>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
placeholder={t('copyTradingOrders.filterStatus') || '筛选状态'}
|
||||
allowClear
|
||||
style={{ width: isMobile ? '100%' : 150 }}
|
||||
value={filters.status}
|
||||
onChange={(value) => setFilters({ ...filters, status: value || undefined })}
|
||||
>
|
||||
<Option value="filled">{t('copyTradingOrders.statusFilled') || '已完成'}</Option>
|
||||
<Option value="partially_matched">{t('copyTradingOrders.statusPartiallySold') || '部分成交'}</Option>
|
||||
<Option value="fully_matched">{t('copyTradingOrders.statusFullySold') || '全部成交'}</Option>
|
||||
</Select>
|
||||
|
||||
<Button onClick={fetchOrders}>{t('common.search') || '查询'}</Button>
|
||||
</div>
|
||||
|
||||
{isMobile ? (
|
||||
<div>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : orders.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
|
||||
{t('copyTradingOrders.noBuyOrders') || '暂无买入订单'}
|
||||
</div>
|
||||
) : (
|
||||
<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
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={order.orderId}
|
||||
style={{
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
|
||||
border: '1px solid #e8e8e8'
|
||||
}}
|
||||
bodyStyle={{ padding: '16px' }}
|
||||
>
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '8px',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
{order.orderId.slice(0, 8)}...{order.orderId.slice(-6)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
|
||||
<Tag>{displaySide}</Tag>
|
||||
{getStatusTag(order.status)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('copyTradingOrders.buyInfo') || '买入信息'}</div>
|
||||
<div style={{ fontSize: '14px', fontWeight: '500' }}>
|
||||
{t('copyTradingOrders.quantity') || '数量'}: {formatUSDC(order.quantity)} | {t('copyTradingOrders.price') || '价格'}: {formatUSDC(order.price)}
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', fontWeight: '500', marginTop: '4px' }}>
|
||||
{t('copyTradingOrders.amount') || '金额'}: {formatUSDC(amount)} USDC
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('copyTradingOrders.matchInfo') || '匹配信息'}</div>
|
||||
<div style={{ fontSize: '13px', color: '#333' }}>
|
||||
{t('copyTradingOrders.matched') || '已匹配'}: {formatUSDC(order.matchedQuantity)} | {t('copyTradingOrders.remaining') || '剩余'}: {formatUSDC(order.remainingQuantity)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('copyTradingOrders.leaderTradeId') || 'Leader 交易ID'}</div>
|
||||
<div style={{ fontSize: '12px', color: '#999', fontFamily: 'monospace' }}>
|
||||
{order.leaderTradeId.slice(0, 8)}...{order.leaderTradeId.slice(-6)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('copyTradingOrders.marketId') || '市场ID'}</div>
|
||||
<div style={{ fontSize: '12px', color: '#999', fontFamily: 'monospace' }}>
|
||||
{order.marketId.slice(0, 8)}...{order.marketId.slice(-6)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#999' }}>
|
||||
{t('copyTradingOrders.createdAt') || '创建时间'}: {formattedDate}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={orders}
|
||||
rowKey="orderId"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: limit,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `${t('common.total') || '共'} ${total} ${t('common.items') || '条'}`,
|
||||
onChange: (newPage, newLimit) => {
|
||||
setPage(newPage)
|
||||
setLimit(newLimit)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BuyOrdersTab
|
||||
|
||||
@@ -0,0 +1,481 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Modal, Form, Button, message, Radio, InputNumber, Divider, Spin, Select, Input, Space, Switch } from 'antd'
|
||||
import { SaveOutlined } from '@ant-design/icons'
|
||||
import { apiService } from '../../services/api'
|
||||
import type { CopyTrading, CopyTradingUpdateRequest } from '../../types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const { Option } = Select
|
||||
|
||||
interface EditModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
copyTradingId: string
|
||||
onSuccess?: () => void
|
||||
}
|
||||
|
||||
const EditModal: React.FC<EditModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
copyTradingId,
|
||||
onSuccess
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [form] = Form.useForm()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [fetching, setFetching] = useState(true)
|
||||
const [copyTrading, setCopyTrading] = useState<CopyTrading | null>(null)
|
||||
const [copyMode, setCopyMode] = useState<'RATIO' | 'FIXED'>('RATIO')
|
||||
const [originalEnabled, setOriginalEnabled] = useState<boolean>(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (open && copyTradingId) {
|
||||
fetchCopyTrading(parseInt(copyTradingId))
|
||||
}
|
||||
}, [open, copyTradingId])
|
||||
|
||||
const fetchCopyTrading = async (copyTradingId: number) => {
|
||||
setFetching(true)
|
||||
try {
|
||||
const response = await apiService.copyTrading.list({})
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
const found = response.data.data.list.find((ct: CopyTrading) => ct.id === copyTradingId)
|
||||
if (found) {
|
||||
setCopyTrading(found)
|
||||
setCopyMode(found.copyMode)
|
||||
setOriginalEnabled(found.enabled)
|
||||
form.setFieldsValue({
|
||||
accountId: found.accountId,
|
||||
leaderId: found.leaderId,
|
||||
copyMode: found.copyMode,
|
||||
copyRatio: found.copyRatio ? parseFloat(found.copyRatio) * 100 : 100,
|
||||
fixedAmount: found.fixedAmount ? parseFloat(found.fixedAmount) : undefined,
|
||||
maxOrderSize: found.maxOrderSize ? parseFloat(found.maxOrderSize) : undefined,
|
||||
minOrderSize: found.minOrderSize ? parseFloat(found.minOrderSize) : undefined,
|
||||
maxDailyLoss: found.maxDailyLoss ? parseFloat(found.maxDailyLoss) : undefined,
|
||||
maxDailyOrders: found.maxDailyOrders,
|
||||
priceTolerance: found.priceTolerance ? parseFloat(found.priceTolerance) : undefined,
|
||||
delaySeconds: found.delaySeconds,
|
||||
pollIntervalSeconds: found.pollIntervalSeconds,
|
||||
useWebSocket: found.useWebSocket,
|
||||
websocketReconnectInterval: found.websocketReconnectInterval,
|
||||
websocketMaxRetries: found.websocketMaxRetries,
|
||||
supportSell: found.supportSell,
|
||||
minOrderDepth: found.minOrderDepth ? parseFloat(found.minOrderDepth) : undefined,
|
||||
maxSpread: found.maxSpread ? parseFloat(found.maxSpread) : undefined,
|
||||
minPrice: found.minPrice ? parseFloat(found.minPrice) : undefined,
|
||||
maxPrice: found.maxPrice ? parseFloat(found.maxPrice) : undefined,
|
||||
configName: found.configName || '',
|
||||
pushFailedOrders: found.pushFailedOrders ?? false
|
||||
})
|
||||
} else {
|
||||
message.error(t('copyTradingEdit.fetchFailed') || '跟单配置不存在')
|
||||
onClose()
|
||||
}
|
||||
} else {
|
||||
message.error(response.data.msg || t('copyTradingEdit.fetchFailed') || '获取跟单配置失败')
|
||||
onClose()
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('copyTradingEdit.fetchFailed') || '获取跟单配置失败')
|
||||
onClose()
|
||||
} finally {
|
||||
setFetching(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCopyModeChange = (mode: 'RATIO' | 'FIXED') => {
|
||||
setCopyMode(mode)
|
||||
}
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
if (values.copyMode === 'FIXED') {
|
||||
if (!values.fixedAmount || Number(values.fixedAmount) < 1) {
|
||||
message.error('固定金额必须 >= 1')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (values.copyMode === 'RATIO' && values.minOrderSize !== undefined && values.minOrderSize !== null && Number(values.minOrderSize) < 1) {
|
||||
message.error('最小金额必须 >= 1')
|
||||
return
|
||||
}
|
||||
|
||||
if (!copyTradingId) {
|
||||
message.error('配置ID不存在')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const request: CopyTradingUpdateRequest = {
|
||||
copyTradingId: parseInt(copyTradingId),
|
||||
enabled: originalEnabled,
|
||||
copyMode: values.copyMode,
|
||||
copyRatio: values.copyMode === 'RATIO' && values.copyRatio ? (values.copyRatio / 100).toString() : undefined,
|
||||
fixedAmount: values.copyMode === 'FIXED' ? values.fixedAmount?.toString() : undefined,
|
||||
maxOrderSize: values.maxOrderSize?.toString(),
|
||||
minOrderSize: values.minOrderSize?.toString(),
|
||||
maxDailyLoss: values.maxDailyLoss?.toString(),
|
||||
maxDailyOrders: values.maxDailyOrders,
|
||||
priceTolerance: values.priceTolerance?.toString(),
|
||||
delaySeconds: values.delaySeconds,
|
||||
pollIntervalSeconds: values.pollIntervalSeconds,
|
||||
useWebSocket: values.useWebSocket,
|
||||
websocketReconnectInterval: values.websocketReconnectInterval,
|
||||
websocketMaxRetries: values.websocketMaxRetries,
|
||||
supportSell: values.supportSell,
|
||||
minOrderDepth: values.minOrderDepth?.toString(),
|
||||
maxSpread: values.maxSpread?.toString(),
|
||||
minPrice: values.minPrice?.toString(),
|
||||
maxPrice: values.maxPrice?.toString(),
|
||||
configName: values.configName?.trim() || undefined,
|
||||
pushFailedOrders: values.pushFailedOrders
|
||||
}
|
||||
|
||||
const response = await apiService.copyTrading.update(request)
|
||||
|
||||
if (response.data.code === 0) {
|
||||
message.success(t('copyTradingEdit.saveSuccess') || '更新跟单配置成功')
|
||||
onClose()
|
||||
if (onSuccess) {
|
||||
onSuccess()
|
||||
}
|
||||
} else {
|
||||
message.error(response.data.msg || t('copyTradingEdit.saveFailed') || '更新跟单配置失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('copyTradingEdit.saveFailed') || '更新跟单配置失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('copyTradingEdit.title') || '编辑跟单配置'}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width="90%"
|
||||
style={{ top: 20 }}
|
||||
bodyStyle={{ padding: '24px', maxHeight: 'calc(100vh - 100px)', overflow: 'auto' }}
|
||||
>
|
||||
{fetching ? (
|
||||
<div style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : !copyTrading ? (
|
||||
<div style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<p>{t('copyTradingEdit.fetchFailed') || '跟单配置不存在'}</p>
|
||||
</div>
|
||||
) : (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.configName') || '配置名'}
|
||||
name="configName"
|
||||
rules={[
|
||||
{ required: true, message: t('copyTradingEdit.configNameRequired') || '请输入配置名' },
|
||||
{ whitespace: true, message: t('copyTradingEdit.configNameRequired') || '配置名不能为空' }
|
||||
]}
|
||||
tooltip={t('copyTradingEdit.configNameTooltip') || '为跟单配置设置一个名称,便于识别和管理'}
|
||||
>
|
||||
<Input
|
||||
placeholder={t('copyTradingEdit.configNamePlaceholder') || '例如:跟单配置1'}
|
||||
maxLength={255}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingAdd.selectWallet') || t('copyTradingEdit.selectWallet') || '钱包'}
|
||||
name="accountId"
|
||||
>
|
||||
<Select disabled>
|
||||
<Option value={copyTrading.accountId}>
|
||||
{copyTrading.accountName || `账户 ${copyTrading.accountId}`} ({copyTrading.walletAddress.slice(0, 6)}...{copyTrading.walletAddress.slice(-4)})
|
||||
</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingAdd.selectLeader') || t('copyTradingEdit.selectLeader') || 'Leader'}
|
||||
name="leaderId"
|
||||
>
|
||||
<Select disabled>
|
||||
<Option value={copyTrading.leaderId}>
|
||||
{copyTrading.leaderName || `Leader ${copyTrading.leaderId}`} ({copyTrading.leaderAddress.slice(0, 6)}...{copyTrading.leaderAddress.slice(-4)})
|
||||
</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Divider>{t('copyTradingEdit.basicConfig') || '基础配置'}</Divider>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.copyMode') || '跟单金额模式'}
|
||||
name="copyMode"
|
||||
tooltip={t('copyTradingEdit.copyModeTooltip') || '选择跟单金额的计算方式'}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Radio.Group onChange={(e) => handleCopyModeChange(e.target.value)}>
|
||||
<Radio value="RATIO">{t('copyTradingEdit.ratioMode') || '比例模式'}</Radio>
|
||||
<Radio value="FIXED">{t('copyTradingEdit.fixedAmountMode') || '固定金额模式'}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{copyMode === 'RATIO' && (
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.copyRatio') || '跟单比例'}
|
||||
name="copyRatio"
|
||||
tooltip={t('copyTradingEdit.copyRatioTooltip') || '跟单比例表示跟单金额相对于 Leader 订单金额的百分比'}
|
||||
>
|
||||
<InputNumber
|
||||
min={10}
|
||||
max={1000}
|
||||
step={1}
|
||||
precision={0}
|
||||
style={{ width: '100%' }}
|
||||
addonAfter="%"
|
||||
placeholder={t('copyTradingEdit.copyRatioPlaceholder') || '例如:100 表示 100%(1:1 跟单)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{copyMode === 'FIXED' && (
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.fixedAmount') || '固定跟单金额 (USDC)'}
|
||||
name="fixedAmount"
|
||||
rules={[
|
||||
{ required: true, message: t('copyTradingEdit.fixedAmountRequired') || '请输入固定跟单金额' },
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
const amount = Number(value)
|
||||
if (isNaN(amount)) {
|
||||
return Promise.reject(new Error(t('copyTradingEdit.invalidNumber') || '请输入有效的数字'))
|
||||
}
|
||||
if (amount < 1) {
|
||||
return Promise.reject(new Error(t('copyTradingEdit.fixedAmountMin') || '固定金额必须 >= 1'))
|
||||
}
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingEdit.fixedAmountPlaceholder') || '固定金额,不随 Leader 订单大小变化,必须 >= 1'}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{copyMode === 'RATIO' && (
|
||||
<>
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.maxOrderSize') || '单笔订单最大金额 (USDC)'}
|
||||
name="maxOrderSize"
|
||||
tooltip={t('copyTradingEdit.maxOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最大金额上限'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0.0001}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingEdit.maxOrderSizePlaceholder') || '仅在比例模式下生效(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.minOrderSize') || '单笔订单最小金额 (USDC)'}
|
||||
name="minOrderSize"
|
||||
tooltip={t('copyTradingEdit.minOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最小金额下限,必须 >= 1'}
|
||||
rules={[
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (typeof value === 'number' && value < 1) {
|
||||
return Promise.reject(new Error(t('copyTradingEdit.minOrderSizeMin') || '最小金额必须 >= 1'))
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingEdit.minOrderSizePlaceholder') || '仅在比例模式下生效,必须 >= 1(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.maxDailyLoss') || '每日最大亏损限制 (USDC)'}
|
||||
name="maxDailyLoss"
|
||||
tooltip={t('copyTradingEdit.maxDailyLossTooltip') || '限制每日最大亏损金额,用于风险控制'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingEdit.maxDailyLossPlaceholder') || '默认 10000 USDC(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.maxDailyOrders') || '每日最大跟单订单数'}
|
||||
name="maxDailyOrders"
|
||||
tooltip={t('copyTradingEdit.maxDailyOrdersTooltip') || '限制每日最多跟单的订单数量'}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
step={1}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingEdit.maxDailyOrdersPlaceholder') || '默认 100(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.priceTolerance') || '价格容忍度 (%)'}
|
||||
name="priceTolerance"
|
||||
tooltip={t('copyTradingEdit.priceToleranceTooltip') || '允许跟单价格在 Leader 价格基础上的调整范围'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.1}
|
||||
precision={2}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingEdit.priceTolerancePlaceholder') || '默认 5%(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.delaySeconds') || '跟单延迟 (秒)'}
|
||||
name="delaySeconds"
|
||||
tooltip={t('copyTradingEdit.delaySecondsTooltip') || '跟单延迟时间,0 表示立即跟单'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={1}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingEdit.delaySecondsPlaceholder') || '默认 0(立即跟单)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.minOrderDepth') || '最小订单深度 (USDC)'}
|
||||
name="minOrderDepth"
|
||||
tooltip={t('copyTradingEdit.minOrderDepthTooltip') || '检查订单簿的总订单金额(买盘+卖盘),确保市场有足够的流动性。不填写则不启用此过滤'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingEdit.minOrderDepthPlaceholder') || '例如:100(可选,不填写表示不启用)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.maxSpread') || '最大价差(绝对价格)'}
|
||||
name="maxSpread"
|
||||
tooltip={t('copyTradingEdit.maxSpreadTooltip') || '最大价差(绝对价格)。避免在价差过大的市场跟单。不填写则不启用此过滤'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingEdit.maxSpreadPlaceholder') || '例如:0.05(5美分,可选,不填写表示不启用)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Divider>{t('copyTradingEdit.priceRangeFilter') || '价格区间过滤'}</Divider>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.priceRange') || '价格区间'}
|
||||
name="priceRange"
|
||||
tooltip={t('copyTradingEdit.priceRangeTooltip') || '配置价格区间,仅在指定价格区间内的订单才会下单。例如:0.11-0.89 表示区间在0.11和0.89之间;-0.89 表示0.89以下都可以;0.11- 表示0.11以上都可以'}
|
||||
>
|
||||
<Input.Group compact style={{ display: 'flex' }}>
|
||||
<Form.Item name="minPrice" noStyle>
|
||||
<InputNumber
|
||||
min={0.01}
|
||||
max={0.99}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '50%' }}
|
||||
placeholder={t('copyTradingEdit.minPricePlaceholder') || '最低价(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
<span style={{ display: 'inline-block', width: '20px', textAlign: 'center', lineHeight: '32px' }}>-</span>
|
||||
<Form.Item name="maxPrice" noStyle>
|
||||
<InputNumber
|
||||
min={0.01}
|
||||
max={0.99}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '50%' }}
|
||||
placeholder={t('copyTradingEdit.maxPricePlaceholder') || '最高价(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Input.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Divider>{t('copyTradingEdit.advancedSettings') || '高级设置'}</Divider>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.supportSell') || '跟单卖出'}
|
||||
name="supportSell"
|
||||
tooltip={t('copyTradingEdit.supportSellTooltip') || '是否跟单 Leader 的卖出订单'}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.pushFailedOrders') || '推送失败订单'}
|
||||
name="pushFailedOrders"
|
||||
tooltip={t('copyTradingEdit.pushFailedOrdersTooltip') || '开启后,失败的订单会推送到 Telegram'}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
icon={<SaveOutlined />}
|
||||
loading={loading}
|
||||
>
|
||||
{t('copyTradingEdit.save') || '保存'}
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
{t('common.cancel') || '取消'}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditModal
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Modal, Table, Tag, Select, Card, Divider, Spin } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { apiService } from '../../services/api'
|
||||
import type { FilteredOrder, FilteredOrderListResponse } from '../../types'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { formatUSDC } from '../../utils'
|
||||
|
||||
const { Option } = Select
|
||||
|
||||
interface FilteredOrdersModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
copyTradingId: string
|
||||
}
|
||||
|
||||
const FilteredOrdersModal: React.FC<FilteredOrdersModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
copyTradingId
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [filteredOrders, setFilteredOrders] = useState<FilteredOrder[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [limit] = useState(20)
|
||||
const [filterType, setFilterType] = useState<string | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (open && copyTradingId) {
|
||||
fetchFilteredOrders()
|
||||
}
|
||||
}, [open, copyTradingId, page, filterType])
|
||||
|
||||
const fetchFilteredOrders = async () => {
|
||||
if (!copyTradingId) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await apiService.copyTrading.getFilteredOrders({
|
||||
copyTradingId: parseInt(copyTradingId),
|
||||
filterType: filterType,
|
||||
page: page,
|
||||
limit: limit
|
||||
})
|
||||
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
const data: FilteredOrderListResponse = response.data.data
|
||||
setFilteredOrders(data.list || [])
|
||||
setTotal(data.total || 0)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取被过滤订单列表失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getFilterTypeTag = (filterType: string) => {
|
||||
const typeMap: Record<string, { color: string; text: string }> = {
|
||||
ORDER_DEPTH: { color: 'orange', text: t('filteredOrdersList.filterTypes.orderDepth') || '订单深度不足' },
|
||||
SPREAD: { color: 'red', text: t('filteredOrdersList.filterTypes.spread') || '价差过大' },
|
||||
ORDERBOOK_DEPTH: { color: 'purple', text: t('filteredOrdersList.filterTypes.orderbookDepth') || '订单簿深度不足' },
|
||||
PRICE_VALIDITY: { color: 'blue', text: t('filteredOrdersList.filterTypes.priceValidity') || '价格不合理' },
|
||||
MARKET_STATUS: { color: 'default', text: t('filteredOrdersList.filterTypes.marketStatus') || '市场状态不可交易' },
|
||||
ORDERBOOK_ERROR: { color: 'default', text: t('filteredOrdersList.filterTypes.orderbookError') || '订单簿获取失败' },
|
||||
ORDERBOOK_EMPTY: { color: 'default', text: t('filteredOrdersList.filterTypes.orderbookEmpty') || '订单簿为空' },
|
||||
PRICE_RANGE: { color: 'purple', text: t('filteredOrdersList.filterTypes.priceRange') || '价格区间不符' }
|
||||
}
|
||||
const config = typeMap[filterType] || { color: 'default', text: filterType }
|
||||
return <Tag color={config.color}>{config.text}</Tag>
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('filteredOrdersList.market') || '市场',
|
||||
dataIndex: 'marketTitle',
|
||||
key: 'marketTitle',
|
||||
width: isMobile ? 150 : 200,
|
||||
ellipsis: true,
|
||||
render: (text: string, record: FilteredOrder) => {
|
||||
const marketTitle = text || record.marketId.slice(0, 10) + '...'
|
||||
return <span style={{ fontSize: isMobile ? 12 : 14 }}>{marketTitle}</span>
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('filteredOrdersList.side') || '方向',
|
||||
dataIndex: 'side',
|
||||
key: 'side',
|
||||
width: isMobile ? 60 : 80,
|
||||
render: (side: string) => (
|
||||
<Tag color={side === 'BUY' ? 'green' : 'red'}>
|
||||
{side === 'BUY' ? (t('order.buy') || '买入') : (t('order.sell') || '卖出')}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('filteredOrdersList.price') || '价格',
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (value: string) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{formatUSDC(value)}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('filteredOrdersList.size') || '数量',
|
||||
dataIndex: 'size',
|
||||
key: 'size',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (value: string) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{formatUSDC(value)}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('filteredOrdersList.filterType') || '过滤类型',
|
||||
dataIndex: 'filterType',
|
||||
key: 'filterType',
|
||||
width: isMobile ? 120 : 150,
|
||||
render: (filterType: string) => getFilterTypeTag(filterType)
|
||||
},
|
||||
{
|
||||
title: t('filteredOrdersList.filterReason') || '过滤原因',
|
||||
dataIndex: 'filterReason',
|
||||
key: 'filterReason',
|
||||
width: isMobile ? 150 : 250,
|
||||
ellipsis: true,
|
||||
render: (text: string) => (
|
||||
<span style={{ fontSize: isMobile ? 11 : 12 }} title={text}>{text}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('filteredOrdersList.createdAt') || '时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: isMobile ? 120 : 160,
|
||||
render: (timestamp: number) => {
|
||||
const date = new Date(timestamp)
|
||||
const format = isMobile
|
||||
? `${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
|
||||
return (
|
||||
<span style={{ fontSize: isMobile ? 11 : 12 }}>
|
||||
{format}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('copyTradingOrders.filteredOrders') || '已过滤订单'}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width="90%"
|
||||
style={{ top: 20 }}
|
||||
bodyStyle={{ padding: '24px', maxHeight: 'calc(100vh - 100px)', overflow: 'auto' }}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Select
|
||||
placeholder={t('filteredOrdersList.filterByType') || '按类型筛选'}
|
||||
allowClear
|
||||
style={{ width: isMobile ? '100%' : 200 }}
|
||||
value={filterType}
|
||||
onChange={(value) => setFilterType(value || undefined)}
|
||||
>
|
||||
<Option value="ORDER_DEPTH">{t('filteredOrdersList.filterTypes.orderDepth') || '订单深度不足'}</Option>
|
||||
<Option value="SPREAD">{t('filteredOrdersList.filterTypes.spread') || '价差过大'}</Option>
|
||||
<Option value="ORDERBOOK_DEPTH">{t('filteredOrdersList.filterTypes.orderbookDepth') || '订单簿深度不足'}</Option>
|
||||
<Option value="PRICE_VALIDITY">{t('filteredOrdersList.filterTypes.priceValidity') || '价格不合理'}</Option>
|
||||
<Option value="MARKET_STATUS">{t('filteredOrdersList.filterTypes.marketStatus') || '市场状态不可交易'}</Option>
|
||||
<Option value="ORDERBOOK_ERROR">{t('filteredOrdersList.filterTypes.orderbookError') || '订单簿获取失败'}</Option>
|
||||
<Option value="ORDERBOOK_EMPTY">{t('filteredOrdersList.filterTypes.orderbookEmpty') || '订单簿为空'}</Option>
|
||||
<Option value="PRICE_RANGE">{t('filteredOrdersList.filterTypes.priceRange') || '价格区间不符'}</Option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{isMobile ? (
|
||||
<div>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : filteredOrders.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
|
||||
{t('filteredOrdersList.noFilteredOrders') || '暂无已过滤订单'}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{filteredOrders.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 marketTitle = order.marketTitle || order.marketId.slice(0, 10) + '...'
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={order.id}
|
||||
style={{
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
|
||||
border: '1px solid #e8e8e8'
|
||||
}}
|
||||
bodyStyle={{ padding: '16px' }}
|
||||
>
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '8px',
|
||||
color: '#1890ff'
|
||||
}}>
|
||||
{marketTitle}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
|
||||
<Tag color={order.side === 'BUY' ? 'green' : 'red'}>
|
||||
{order.side === 'BUY' ? (t('order.buy') || '买入') : (t('order.sell') || '卖出')}
|
||||
</Tag>
|
||||
{getFilterTypeTag(order.filterType)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('filteredOrdersList.orderDetails') || '订单详情'}</div>
|
||||
<div style={{ fontSize: '13px', color: '#333' }}>
|
||||
{t('filteredOrdersList.price') || '价格'}: {formatUSDC(order.price)} | {t('filteredOrdersList.size') || '数量'}: {formatUSDC(order.size)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('filteredOrdersList.filterReason') || '过滤原因'}</div>
|
||||
<div style={{ fontSize: '12px', color: '#333' }} title={order.filterReason}>
|
||||
{order.filterReason}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#999' }}>
|
||||
{t('filteredOrdersList.createdAt') || '时间'}: {formattedDate}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={filteredOrders}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: limit,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `${t('common.total') || '共'} ${total} ${t('common.items') || '条'}`,
|
||||
onChange: (newPage) => {
|
||||
setPage(newPage)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default FilteredOrdersModal
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Table, Input, Button, Card, Divider, Spin } from 'antd'
|
||||
import { apiService } from '../../services/api'
|
||||
import { formatUSDC } from '../../utils'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { MatchedOrderInfo, OrderTrackingRequest, OrderTrackingListResponse } from '../../types'
|
||||
|
||||
interface MatchedOrdersTabProps {
|
||||
copyTradingId: string
|
||||
}
|
||||
|
||||
const MatchedOrdersTab: React.FC<MatchedOrdersTabProps> = ({ copyTradingId }) => {
|
||||
const { t } = useTranslation()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [orders, setOrders] = useState<MatchedOrderInfo[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [limit, setLimit] = useState(20)
|
||||
const [filters, setFilters] = useState<{
|
||||
sellOrderId?: string
|
||||
buyOrderId?: string
|
||||
}>({})
|
||||
|
||||
useEffect(() => {
|
||||
if (copyTradingId) {
|
||||
fetchOrders()
|
||||
}
|
||||
}, [copyTradingId, page, limit, filters])
|
||||
|
||||
const fetchOrders = async () => {
|
||||
if (!copyTradingId) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const request: OrderTrackingRequest = {
|
||||
copyTradingId: parseInt(copyTradingId),
|
||||
type: 'matched',
|
||||
page,
|
||||
limit,
|
||||
...filters
|
||||
}
|
||||
|
||||
const response = await apiService.orderTracking.list(request)
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
const data = response.data.data as OrderTrackingListResponse
|
||||
setOrders((data.list || []) as MatchedOrderInfo[])
|
||||
setTotal(data.total || 0)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取匹配关系列表失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getPnlColor = (value: string): string => {
|
||||
const num = parseFloat(value)
|
||||
if (isNaN(num)) return '#666'
|
||||
return num >= 0 ? '#3f8600' : '#cf1322'
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('copyTradingOrders.sellOrderId') || '卖出订单ID',
|
||||
dataIndex: 'sellOrderId',
|
||||
key: 'sellOrderId',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (text: string) => (
|
||||
<span style={{ fontFamily: 'monospace', fontSize: isMobile ? 11 : 12 }}>
|
||||
{isMobile
|
||||
? `${text.slice(0, 6)}...${text.slice(-4)}`
|
||||
: `${text.slice(0, 8)}...${text.slice(-6)}`
|
||||
}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('copyTradingOrders.buyOrderId') || '买入订单ID',
|
||||
dataIndex: 'buyOrderId',
|
||||
key: 'buyOrderId',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (text: string) => (
|
||||
<span style={{ fontFamily: 'monospace', fontSize: isMobile ? 11 : 12 }}>
|
||||
{isMobile
|
||||
? `${text.slice(0, 6)}...${text.slice(-4)}`
|
||||
: `${text.slice(0, 8)}...${text.slice(-6)}`
|
||||
}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('copyTradingOrders.matchedQuantity') || '匹配数量',
|
||||
dataIndex: 'matchedQuantity',
|
||||
key: 'matchedQuantity',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (value: string) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{formatUSDC(value)}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('copyTradingOrders.buyPrice') || '买入价格',
|
||||
dataIndex: 'buyPrice',
|
||||
key: 'buyPrice',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (value: string) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{formatUSDC(value)}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('copyTradingOrders.sellPrice') || '卖出价格',
|
||||
dataIndex: 'sellPrice',
|
||||
key: 'sellPrice',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (value: string) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{formatUSDC(value)}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('copyTradingOrders.realizedPnl') || '盈亏',
|
||||
dataIndex: 'realizedPnl',
|
||||
key: 'realizedPnl',
|
||||
width: isMobile ? 100 : 120,
|
||||
render: (value: string) => (
|
||||
<span style={{
|
||||
color: getPnlColor(value),
|
||||
fontWeight: 500,
|
||||
fontSize: isMobile ? 12 : 14
|
||||
}}>
|
||||
{isMobile ? formatUSDC(value) : `${formatUSDC(value)} USDC`}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('copyTradingOrders.matchedAt') || '匹配时间',
|
||||
dataIndex: 'matchedAt',
|
||||
key: 'matchedAt',
|
||||
width: isMobile ? 120 : 160,
|
||||
render: (timestamp: number) => (
|
||||
<span style={{ fontSize: isMobile ? 11 : 12 }}>
|
||||
{isMobile
|
||||
? new Date(timestamp).toLocaleDateString('zh-CN')
|
||||
: new Date(timestamp).toLocaleString('zh-CN')
|
||||
}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, display: 'flex', gap: 16, flexWrap: 'wrap' }}>
|
||||
<Input
|
||||
placeholder={t('copyTradingOrders.filterSellOrderId') || '筛选卖出订单ID'}
|
||||
allowClear
|
||||
style={{ width: isMobile ? '100%' : 200 }}
|
||||
value={filters.sellOrderId}
|
||||
onChange={(e) => setFilters({ ...filters, sellOrderId: e.target.value || undefined })}
|
||||
/>
|
||||
|
||||
<Input
|
||||
placeholder={t('copyTradingOrders.filterBuyOrderId') || '筛选买入订单ID'}
|
||||
allowClear
|
||||
style={{ width: isMobile ? '100%' : 200 }}
|
||||
value={filters.buyOrderId}
|
||||
onChange={(e) => setFilters({ ...filters, buyOrderId: e.target.value || undefined })}
|
||||
/>
|
||||
|
||||
<Button onClick={fetchOrders}>{t('common.search') || '查询'}</Button>
|
||||
</div>
|
||||
|
||||
{isMobile ? (
|
||||
<div>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : orders.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
|
||||
{t('copyTradingOrders.noMatchedOrders') || '暂无匹配关系'}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{orders.map((order) => {
|
||||
const date = new Date(order.matchedAt)
|
||||
const formattedDate = date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={`${order.sellOrderId}-${order.buyOrderId}-${order.matchedAt}`}
|
||||
style={{
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
|
||||
border: '1px solid #e8e8e8'
|
||||
}}
|
||||
bodyStyle={{ padding: '16px' }}
|
||||
>
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('copyTradingOrders.sellOrderId') || '卖出订单ID'}</div>
|
||||
<div style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: '500',
|
||||
fontFamily: 'monospace',
|
||||
marginBottom: '8px'
|
||||
}}>
|
||||
{order.sellOrderId.slice(0, 8)}...{order.sellOrderId.slice(-6)}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('copyTradingOrders.buyOrderId') || '买入订单ID'}</div>
|
||||
<div style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: '500',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
{order.buyOrderId.slice(0, 8)}...{order.buyOrderId.slice(-6)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('copyTradingOrders.matchedQuantity') || '匹配数量'}</div>
|
||||
<div style={{ fontSize: '14px', fontWeight: '500' }}>
|
||||
{formatUSDC(order.matchedQuantity)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('copyTradingOrders.priceInfo') || '价格信息'}</div>
|
||||
<div style={{ fontSize: '13px', color: '#333' }}>
|
||||
{t('copyTradingOrders.buy') || '买入'}: {formatUSDC(order.buyPrice)} | {t('copyTradingOrders.sell') || '卖出'}: {formatUSDC(order.sellPrice)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('copyTradingOrders.realizedPnl') || '盈亏'}</div>
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: getPnlColor(order.realizedPnl)
|
||||
}}>
|
||||
{formatUSDC(order.realizedPnl)} USDC
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#999' }}>
|
||||
{t('copyTradingOrders.matchedAt') || '匹配时间'}: {formattedDate}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={orders}
|
||||
rowKey={(record) => `${record.sellOrderId}-${record.buyOrderId}-${record.matchedAt}`}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: limit,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `${t('common.total') || '共'} ${total} ${t('common.items') || '条'}`,
|
||||
onChange: (newPage, newLimit) => {
|
||||
setPage(newPage)
|
||||
setLimit(newLimit)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MatchedOrdersTab
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Table, Tag, Select, Input, Button, Card, Divider, Spin } from 'antd'
|
||||
import { apiService } from '../../services/api'
|
||||
import { formatUSDC } from '../../utils'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { SellOrderInfo, OrderTrackingRequest, OrderTrackingListResponse } from '../../types'
|
||||
|
||||
const { Option } = Select
|
||||
|
||||
interface SellOrdersTabProps {
|
||||
copyTradingId: string
|
||||
}
|
||||
|
||||
const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId }) => {
|
||||
const { t } = useTranslation()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [orders, setOrders] = useState<SellOrderInfo[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [limit, setLimit] = useState(20)
|
||||
const [filters, setFilters] = useState<{
|
||||
marketId?: string
|
||||
side?: string
|
||||
}>({})
|
||||
|
||||
useEffect(() => {
|
||||
if (copyTradingId) {
|
||||
fetchOrders()
|
||||
}
|
||||
}, [copyTradingId, page, limit, filters])
|
||||
|
||||
const fetchOrders = async () => {
|
||||
if (!copyTradingId) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const request: OrderTrackingRequest = {
|
||||
copyTradingId: parseInt(copyTradingId),
|
||||
type: 'sell',
|
||||
page,
|
||||
limit,
|
||||
...filters
|
||||
}
|
||||
|
||||
const response = await apiService.orderTracking.list(request)
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
const data = response.data.data as OrderTrackingListResponse
|
||||
setOrders((data.list || []) as SellOrderInfo[])
|
||||
setTotal(data.total || 0)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取卖出订单列表失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getPnlColor = (value: string): string => {
|
||||
const num = parseFloat(value)
|
||||
if (isNaN(num)) return '#666'
|
||||
return num >= 0 ? '#3f8600' : '#cf1322'
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('copyTradingOrders.orderId') || '订单ID',
|
||||
dataIndex: 'orderId',
|
||||
key: 'orderId',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (text: string) => (
|
||||
<span style={{ fontFamily: 'monospace', fontSize: isMobile ? 11 : 12 }}>
|
||||
{isMobile
|
||||
? `${text.slice(0, 6)}...${text.slice(-4)}`
|
||||
: `${text.slice(0, 8)}...${text.slice(-6)}`
|
||||
}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('copyTradingOrders.leaderTradeId') || 'Leader 交易ID',
|
||||
dataIndex: 'leaderTradeId',
|
||||
key: 'leaderTradeId',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (text: string) => (
|
||||
<span style={{ fontFamily: 'monospace', fontSize: isMobile ? 11 : 12 }}>
|
||||
{isMobile
|
||||
? `${text.slice(0, 6)}...${text.slice(-4)}`
|
||||
: `${text.slice(0, 8)}...${text.slice(-6)}`
|
||||
}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('copyTradingOrders.market') || '市场',
|
||||
dataIndex: 'marketId',
|
||||
key: 'marketId',
|
||||
width: isMobile ? 100 : 150,
|
||||
render: (text: string) => (
|
||||
<span style={{ fontFamily: 'monospace', fontSize: isMobile ? 11 : 12 }}>
|
||||
{isMobile
|
||||
? `${text.slice(0, 6)}...${text.slice(-4)}`
|
||||
: `${text.slice(0, 8)}...${text.slice(-6)}`
|
||||
}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
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',
|
||||
key: 'quantity',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (value: string) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{formatUSDC(value)}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('copyTradingOrders.sellPrice') || '卖出价格',
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (value: string) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{formatUSDC(value)}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('copyTradingOrders.sellAmount') || '卖出金额',
|
||||
key: 'amount',
|
||||
width: isMobile ? 100 : 120,
|
||||
render: (_: any, record: SellOrderInfo) => {
|
||||
const amount = (parseFloat(record.quantity) * parseFloat(record.price)).toString()
|
||||
return (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>
|
||||
{isMobile ? formatUSDC(amount) : `${formatUSDC(amount)} USDC`}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('copyTradingOrders.realizedPnl') || '已实现盈亏',
|
||||
dataIndex: 'realizedPnl',
|
||||
key: 'realizedPnl',
|
||||
width: isMobile ? 100 : 120,
|
||||
render: (value: string) => (
|
||||
<span style={{
|
||||
color: getPnlColor(value),
|
||||
fontWeight: 500,
|
||||
fontSize: isMobile ? 12 : 14
|
||||
}}>
|
||||
{isMobile ? formatUSDC(value) : `${formatUSDC(value)} USDC`}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('copyTradingOrders.createdAt') || '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: isMobile ? 120 : 160,
|
||||
render: (timestamp: number) => (
|
||||
<span style={{ fontSize: isMobile ? 11 : 12 }}>
|
||||
{isMobile
|
||||
? new Date(timestamp).toLocaleDateString('zh-CN')
|
||||
: new Date(timestamp).toLocaleString('zh-CN')
|
||||
}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, display: 'flex', gap: 16, flexWrap: 'wrap' }}>
|
||||
<Input
|
||||
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') || '筛选方向'}
|
||||
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>
|
||||
<Option value="YES">YES</Option>
|
||||
<Option value="NO">NO</Option>
|
||||
</Select>
|
||||
|
||||
<Button onClick={fetchOrders}>{t('common.search') || '查询'}</Button>
|
||||
</div>
|
||||
|
||||
{isMobile ? (
|
||||
<div>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : orders.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
|
||||
{t('copyTradingOrders.noSellOrders') || '暂无卖出订单'}
|
||||
</div>
|
||||
) : (
|
||||
<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
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={order.orderId}
|
||||
style={{
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
|
||||
border: '1px solid #e8e8e8'
|
||||
}}
|
||||
bodyStyle={{ padding: '16px' }}
|
||||
>
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '8px',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
{order.orderId.slice(0, 8)}...{order.orderId.slice(-6)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
|
||||
<Tag>{displaySide}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('copyTradingOrders.sellInfo') || '卖出信息'}</div>
|
||||
<div style={{ fontSize: '14px', fontWeight: '500' }}>
|
||||
{t('copyTradingOrders.quantity') || '数量'}: {formatUSDC(order.quantity)} | {t('copyTradingOrders.price') || '价格'}: {formatUSDC(order.price)}
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', fontWeight: '500', marginTop: '4px' }}>
|
||||
{t('copyTradingOrders.amount') || '金额'}: {formatUSDC(amount)} USDC
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('copyTradingOrders.realizedPnl') || '已实现盈亏'}</div>
|
||||
<div style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: getPnlColor(order.realizedPnl)
|
||||
}}>
|
||||
{formatUSDC(order.realizedPnl)} USDC
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('copyTradingOrders.leaderTradeId') || 'Leader 交易ID'}</div>
|
||||
<div style={{ fontSize: '12px', color: '#999', fontFamily: 'monospace' }}>
|
||||
{order.leaderTradeId.slice(0, 8)}...{order.leaderTradeId.slice(-6)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('copyTradingOrders.marketId') || '市场ID'}</div>
|
||||
<div style={{ fontSize: '12px', color: '#999', fontFamily: 'monospace' }}>
|
||||
{order.marketId.slice(0, 8)}...{order.marketId.slice(-6)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#999' }}>
|
||||
{t('copyTradingOrders.createdAt') || '创建时间'}: {formattedDate}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={orders}
|
||||
rowKey="orderId"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: limit,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `${t('common.total') || '共'} ${total} ${t('common.items') || '条'}`,
|
||||
onChange: (newPage, newLimit) => {
|
||||
setPage(newPage)
|
||||
setLimit(newLimit)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SellOrdersTab
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Modal, Row, Col, Statistic, Spin, message, Card } from 'antd'
|
||||
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'
|
||||
import { apiService } from '../../services/api'
|
||||
import { formatUSDC } from '../../utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import type { CopyTradingStatistics } from '../../types'
|
||||
|
||||
interface StatisticsModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
copyTradingId: string
|
||||
}
|
||||
|
||||
const StatisticsModal: React.FC<StatisticsModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
copyTradingId
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [statistics, setStatistics] = useState<CopyTradingStatistics | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (open && copyTradingId) {
|
||||
fetchStatistics()
|
||||
}
|
||||
}, [open, copyTradingId])
|
||||
|
||||
const fetchStatistics = async () => {
|
||||
if (!copyTradingId) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await apiService.statistics.detail({ copyTradingId: parseInt(copyTradingId) })
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setStatistics(response.data.data)
|
||||
} else {
|
||||
message.error(response.data.msg || t('copyTradingOrders.fetchStatisticsFailed') || '获取统计信息失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('copyTradingOrders.fetchStatisticsFailed') || '获取统计信息失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getPnlColor = (value: string): string => {
|
||||
const num = parseFloat(value)
|
||||
if (isNaN(num)) return '#666'
|
||||
return num >= 0 ? '#3f8600' : '#cf1322'
|
||||
}
|
||||
|
||||
const getPnlIcon = (value: string) => {
|
||||
const num = parseFloat(value)
|
||||
if (isNaN(num)) return null
|
||||
return num >= 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />
|
||||
}
|
||||
|
||||
const formatPercent = (value: string): string => {
|
||||
const num = parseFloat(value)
|
||||
if (isNaN(num)) return '-'
|
||||
return `${num >= 0 ? '+' : ''}${num.toFixed(2)}%`
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('copyTradingOrders.statistics') || '跟单关系统计'}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width="90%"
|
||||
style={{ top: 20 }}
|
||||
bodyStyle={{ padding: '24px', maxHeight: 'calc(100vh - 100px)', overflow: 'auto' }}
|
||||
>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : !statistics ? (
|
||||
<div style={{ textAlign: 'center', padding: '50px' }}>
|
||||
<p>{t('copyTradingOrders.noStatistics') || '暂无统计数据'}</p>
|
||||
</div>
|
||||
) : isMobile ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<div style={{ fontSize: '14px', color: '#666', flex: '0 0 auto', marginRight: '12px' }}>
|
||||
{t('copyTradingOrders.totalBuyOrders') || '总买入订单数'}
|
||||
</div>
|
||||
<div style={{ fontSize: '16px', fontWeight: '500', color: '#333', flex: '1', textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '4px' }}>
|
||||
<ArrowUpOutlined style={{ color: '#1890ff', fontSize: '14px' }} />
|
||||
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{statistics.totalBuyOrders}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<div style={{ fontSize: '14px', color: '#666', flex: '0 0 auto', marginRight: '12px' }}>
|
||||
{t('copyTradingOrders.totalSellOrders') || '总卖出订单数'}
|
||||
</div>
|
||||
<div style={{ fontSize: '16px', fontWeight: '500', color: '#333', flex: '1', textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '4px' }}>
|
||||
<ArrowDownOutlined style={{ color: '#ff4d4f', fontSize: '14px' }} />
|
||||
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{statistics.totalSellOrders}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<div style={{ fontSize: '14px', color: '#666', flex: '0 0 auto', marginRight: '12px' }}>
|
||||
{t('copyTradingOrders.totalMatchedOrders') || '总匹配订单数'}
|
||||
</div>
|
||||
<div style={{ fontSize: '16px', fontWeight: '500', color: '#333', flex: '1', textAlign: 'right' }}>
|
||||
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{statistics.totalMatchedOrders || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<div style={{ fontSize: '14px', color: '#666', flex: '0 0 auto', marginRight: '12px' }}>
|
||||
{t('copyTradingOrders.totalBuyAmount') || '总买入金额'}
|
||||
</div>
|
||||
<div style={{ fontSize: '16px', fontWeight: '500', color: '#333', flex: '1', textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '4px' }}>
|
||||
<ArrowUpOutlined style={{ color: '#1890ff', fontSize: '14px' }} />
|
||||
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{formatUSDC(statistics.totalBuyAmount)} USDC</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<div style={{ fontSize: '14px', color: '#666', flex: '0 0 auto', marginRight: '12px' }}>
|
||||
{t('copyTradingOrders.totalSellAmount') || '总卖出金额'}
|
||||
</div>
|
||||
<div style={{ fontSize: '16px', fontWeight: '500', color: '#333', flex: '1', textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '4px' }}>
|
||||
<ArrowDownOutlined style={{ color: '#ff4d4f', fontSize: '14px' }} />
|
||||
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{formatUSDC(statistics.totalSellAmount)} USDC</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<div style={{ fontSize: '14px', color: '#666', flex: '0 0 auto', marginRight: '12px' }}>
|
||||
{t('copyTradingOrders.totalPnl') || '总盈亏'}
|
||||
</div>
|
||||
<div style={{ fontSize: '16px', fontWeight: 'bold', color: getPnlColor(statistics.totalPnl), flex: '1', textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '4px' }}>
|
||||
{getPnlIcon(statistics.totalPnl)}
|
||||
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{formatUSDC(statistics.totalPnl)} USDC</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<div style={{ fontSize: '14px', color: '#666', flex: '0 0 auto', marginRight: '12px' }}>
|
||||
{t('copyTradingOrders.totalRealizedPnl') || '总已实现盈亏'}
|
||||
</div>
|
||||
<div style={{ fontSize: '16px', fontWeight: '500', color: getPnlColor(statistics.totalRealizedPnl), flex: '1', textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '4px' }}>
|
||||
{getPnlIcon(statistics.totalRealizedPnl)}
|
||||
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{formatUSDC(statistics.totalRealizedPnl)} USDC</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<div style={{ fontSize: '14px', color: '#666', flex: '0 0 auto', marginRight: '12px' }}>
|
||||
{t('copyTradingOrders.totalUnrealizedPnl') || '总未实现盈亏'}
|
||||
</div>
|
||||
<div style={{ fontSize: '16px', fontWeight: '500', color: getPnlColor(statistics.totalUnrealizedPnl), flex: '1', textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '4px' }}>
|
||||
{getPnlIcon(statistics.totalUnrealizedPnl)}
|
||||
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{formatUSDC(statistics.totalUnrealizedPnl)} USDC</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<div style={{ fontSize: '14px', color: '#666', flex: '0 0 auto', marginRight: '12px' }}>
|
||||
{t('copyTradingOrders.winRate') || '胜率'}
|
||||
</div>
|
||||
<div style={{ fontSize: '16px', fontWeight: '500', color: '#333', flex: '1', textAlign: 'right' }}>
|
||||
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{formatPercent(statistics.winRate)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<div style={{ fontSize: '14px', color: '#666', flex: '0 0 auto', marginRight: '12px' }}>
|
||||
{t('copyTradingOrders.averagePnl') || '平均盈亏'}
|
||||
</div>
|
||||
<div style={{ fontSize: '16px', fontWeight: '500', color: getPnlColor(statistics.averagePnl), flex: '1', textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '4px' }}>
|
||||
{getPnlIcon(statistics.averagePnl)}
|
||||
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{formatUSDC(statistics.averagePnl)} USDC</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<div style={{ fontSize: '14px', color: '#666', flex: '0 0 auto', marginRight: '12px' }}>
|
||||
{t('copyTradingOrders.maxPnl') || '最大盈亏'}
|
||||
</div>
|
||||
<div style={{ fontSize: '16px', fontWeight: '500', color: getPnlColor(statistics.maxPnl), flex: '1', textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '4px' }}>
|
||||
{getPnlIcon(statistics.maxPnl)}
|
||||
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{formatUSDC(statistics.maxPnl)} USDC</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px' }}>
|
||||
<div style={{ fontSize: '14px', color: '#666', flex: '0 0 auto', marginRight: '12px' }}>
|
||||
{t('copyTradingOrders.minPnl') || '最小盈亏'}
|
||||
</div>
|
||||
<div style={{ fontSize: '16px', fontWeight: '500', color: getPnlColor(statistics.minPnl), flex: '1', textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '4px' }}>
|
||||
{getPnlIcon(statistics.minPnl)}
|
||||
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{formatUSDC(statistics.minPnl)} USDC</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Statistic
|
||||
title={t('copyTradingOrders.totalBuyOrders') || '总买入订单数'}
|
||||
value={statistics.totalBuyOrders}
|
||||
prefix={<ArrowUpOutlined style={{ color: '#1890ff' }} />}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Statistic
|
||||
title={t('copyTradingOrders.totalSellOrders') || '总卖出订单数'}
|
||||
value={statistics.totalSellOrders}
|
||||
prefix={<ArrowDownOutlined style={{ color: '#ff4d4f' }} />}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Statistic
|
||||
title={t('copyTradingOrders.totalMatchedOrders') || '总匹配订单数'}
|
||||
value={statistics.totalMatchedOrders || 0}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Statistic
|
||||
title={t('copyTradingOrders.totalBuyAmount') || '总买入金额'}
|
||||
value={formatUSDC(statistics.totalBuyAmount)}
|
||||
suffix="USDC"
|
||||
prefix={<ArrowUpOutlined style={{ color: '#1890ff' }} />}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Statistic
|
||||
title={t('copyTradingOrders.totalSellAmount') || '总卖出金额'}
|
||||
value={formatUSDC(statistics.totalSellAmount)}
|
||||
suffix="USDC"
|
||||
prefix={<ArrowDownOutlined style={{ color: '#ff4d4f' }} />}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Statistic
|
||||
title={t('copyTradingOrders.totalPnl') || '总盈亏'}
|
||||
value={formatUSDC(statistics.totalPnl)}
|
||||
suffix="USDC"
|
||||
valueStyle={{ color: getPnlColor(statistics.totalPnl) }}
|
||||
prefix={getPnlIcon(statistics.totalPnl)}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Statistic
|
||||
title={t('copyTradingOrders.totalRealizedPnl') || '总已实现盈亏'}
|
||||
value={formatUSDC(statistics.totalRealizedPnl)}
|
||||
suffix="USDC"
|
||||
valueStyle={{ color: getPnlColor(statistics.totalRealizedPnl) }}
|
||||
prefix={getPnlIcon(statistics.totalRealizedPnl)}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Statistic
|
||||
title={t('copyTradingOrders.totalUnrealizedPnl') || '总未实现盈亏'}
|
||||
value={formatUSDC(statistics.totalUnrealizedPnl)}
|
||||
suffix="USDC"
|
||||
valueStyle={{ color: getPnlColor(statistics.totalUnrealizedPnl) }}
|
||||
prefix={getPnlIcon(statistics.totalUnrealizedPnl)}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Statistic
|
||||
title={t('copyTradingOrders.winRate') || '胜率'}
|
||||
value={formatPercent(statistics.winRate)}
|
||||
suffix="%"
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Statistic
|
||||
title={t('copyTradingOrders.averagePnl') || '平均盈亏'}
|
||||
value={formatUSDC(statistics.averagePnl)}
|
||||
suffix="USDC"
|
||||
valueStyle={{ color: getPnlColor(statistics.averagePnl) }}
|
||||
prefix={getPnlIcon(statistics.averagePnl)}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Statistic
|
||||
title={t('copyTradingOrders.maxPnl') || '最大盈亏'}
|
||||
value={formatUSDC(statistics.maxPnl)}
|
||||
suffix="USDC"
|
||||
valueStyle={{ color: getPnlColor(statistics.maxPnl) }}
|
||||
prefix={getPnlIcon(statistics.maxPnl)}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Statistic
|
||||
title={t('copyTradingOrders.minPnl') || '最小盈亏'}
|
||||
value={formatUSDC(statistics.minPnl)}
|
||||
suffix="USDC"
|
||||
valueStyle={{ color: getPnlColor(statistics.minPnl) }}
|
||||
prefix={getPnlIcon(statistics.minPnl)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatisticsModal
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useParams, useSearchParams } from 'react-router-dom'
|
||||
import { Modal, Tabs } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import BuyOrdersTab from './BuyOrdersTab'
|
||||
import SellOrdersTab from './SellOrdersTab'
|
||||
import MatchedOrdersTab from './MatchedOrdersTab'
|
||||
|
||||
type TabType = 'buy' | 'sell' | 'matched'
|
||||
|
||||
interface CopyTradingOrdersModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
copyTradingId: string
|
||||
defaultTab?: TabType
|
||||
}
|
||||
|
||||
const CopyTradingOrdersModal: React.FC<CopyTradingOrdersModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
copyTradingId,
|
||||
defaultTab = 'buy'
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [activeTab, setActiveTab] = useState<TabType>(defaultTab)
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setActiveTab(defaultTab)
|
||||
}
|
||||
}, [open, defaultTab])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('copyTradingOrders.title') || '订单列表'}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
width="90%"
|
||||
style={{ top: 20 }}
|
||||
bodyStyle={{ padding: '24px', maxHeight: 'calc(100vh - 100px)', overflow: 'auto' }}
|
||||
>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={(key) => setActiveTab(key as TabType)}
|
||||
items={[
|
||||
{
|
||||
key: 'buy',
|
||||
label: t('copyTradingOrders.buyOrders') || '买入订单',
|
||||
children: <BuyOrdersTab copyTradingId={copyTradingId} />
|
||||
},
|
||||
{
|
||||
key: 'sell',
|
||||
label: t('copyTradingOrders.sellOrders') || '卖出订单',
|
||||
children: <SellOrdersTab copyTradingId={copyTradingId} />
|
||||
},
|
||||
{
|
||||
key: 'matched',
|
||||
label: t('copyTradingOrders.matchedOrders') || '匹配关系',
|
||||
children: <MatchedOrdersTab copyTradingId={copyTradingId} />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyTradingOrdersModal
|
||||
|
||||
Reference in New Issue
Block a user