From 8bc6c1ecc4d5bc82e987fe504b79404123e25f73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Tue, 3 Feb 2026 13:27:52 +0800 Subject: [PATCH] a --- frontend/src/components/TradeList.css | 387 +++++++++----------------- frontend/src/components/TradeList.jsx | 123 ++++++-- 2 files changed, 219 insertions(+), 291 deletions(-) diff --git a/frontend/src/components/TradeList.css b/frontend/src/components/TradeList.css index 64b9bae..0a23685 100644 --- a/frontend/src/components/TradeList.css +++ b/frontend/src/components/TradeList.css @@ -200,14 +200,15 @@ } .btn-secondary { - background: #6c757d; - color: white; + background: #e0e0e0; + color: #333; } .btn-secondary:hover { - background: #5a6268; + background: #d5d5d5; } +/* 导出按钮样式 */ .btn-export { padding: 0.75rem 1.5rem; border: none; @@ -216,10 +217,9 @@ font-size: 0.9rem; font-weight: 500; transition: all 0.3s; - touch-action: manipulation; - min-height: 44px; - background: #28a745; + background: #4CAF50; color: white; + min-height: 44px; } @media (min-width: 768px) { @@ -230,329 +230,192 @@ } .btn-export:hover { - background: #218838; -} - -.no-data { - text-align: center; - padding: 3rem; - color: #999; - font-size: 1.1rem; + background: #43A047; } .stats-summary { display: grid; grid-template-columns: repeat(2, 1fr); - gap: 0.75rem; - margin-bottom: 1rem; + gap: 1rem; + margin-bottom: 2rem; } @media (min-width: 768px) { .stats-summary { - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 1rem; - margin-bottom: 2rem; + grid-template-columns: repeat(4, 1fr); } } .stat-card { background: #f8f9fa; - padding: 1.5rem; + padding: 1rem; border-radius: 8px; text-align: center; } .stat-label { - font-size: 0.9rem; color: #666; + font-size: 0.9rem; margin-bottom: 0.5rem; } .stat-value { - font-size: 1.5rem; - font-weight: bold; - color: #2c3e50; + font-size: 1.25rem; + font-weight: 600; + color: #333; } .stat-value.positive { - color: #27ae60; + color: #4CAF50; } .stat-value.negative { - color: #e74c3c; + color: #F44336; } -/* 表格横向滚动:避免整页过宽,内容区域可左右滑动 */ .table-wrapper { - width: 100%; - max-width: 100%; overflow-x: auto; - margin-top: 1rem; -webkit-overflow-scrolling: touch; + margin-bottom: 1rem; border-radius: 8px; - border: 1px solid #e9ecef; -} - -.table-wrapper::-webkit-scrollbar { - height: 8px; -} - -.table-wrapper::-webkit-scrollbar-track { - background: #f1f1f1; - border-radius: 4px; -} - -.table-wrapper::-webkit-scrollbar-thumb { - background: #888; - border-radius: 4px; -} - -.table-wrapper::-webkit-scrollbar-thumb:hover { - background: #555; + border: 1px solid #eee; } .trades-table { width: 100%; border-collapse: collapse; - display: none; - min-width: 1100px; /* 表格最小宽度,超出时由 table-wrapper 横向滚动 */ + font-size: 0.9rem; + min-width: 1000px; /* 保证在小屏幕上也能横向滚动 */ } -@media (min-width: 768px) { - .trades-table { - display: table; - } +.trades-table th, +.trades-table td { + padding: 1rem; + text-align: left; + border-bottom: 1px solid #eee; } .trades-table th { - background-color: #34495e; - color: white; - padding: 0.5rem 0.4rem; - text-align: left; - font-weight: 500; - font-size: 0.8rem; + background: #f8f9fa; + font-weight: 600; + color: #444; white-space: nowrap; - position: sticky; - top: 0; - z-index: 10; -} - -.trades-table td { - padding: 0.5rem 0.4rem; - border-bottom: 1px solid #eee; - font-size: 0.8rem; - white-space: nowrap; -} - -/* 列宽适度收紧,减少横向占用,仍保证可读 */ -.trades-table th:nth-child(1), -.trades-table td:nth-child(1) { - min-width: 52px; -} - -.trades-table th:nth-child(2), -.trades-table td:nth-child(2) { - min-width: 88px; -} - -.trades-table th:nth-child(3), -.trades-table td:nth-child(3) { - min-width: 52px; -} - -.trades-table th:nth-child(4), -.trades-table td:nth-child(4) { - min-width: 78px; -} - -.trades-table th:nth-child(5), -.trades-table td:nth-child(5) { - min-width: 82px; -} - -.trades-table th:nth-child(6), -.trades-table td:nth-child(6) { - min-width: 82px; -} - -.trades-table th:nth-child(7), -.trades-table td:nth-child(7) { - min-width: 78px; -} - -.trades-table th:nth-child(8), -.trades-table td:nth-child(8) { - min-width: 78px; -} - -.trades-table th:nth-child(9), -.trades-table td:nth-child(9) { - min-width: 88px; -} - -.trades-table th:nth-child(10), -.trades-table td:nth-child(10) { - min-width: 88px; -} - -.trades-table th:nth-child(11), -.trades-table td:nth-child(11) { - min-width: 72px; -} - -.trades-table th:nth-child(12), -.trades-table td:nth-child(12) { - min-width: 88px; -} - -.trades-table th:nth-child(13), -.trades-table td:nth-child(13) { - min-width: 160px; - white-space: normal; - word-break: break-all; -} - -.trades-table th:nth-child(14), -.trades-table td:nth-child(14) { - min-width: 120px; -} - -.trades-table th:nth-child(15), -.trades-table td:nth-child(15) { - min-width: 120px; } .trades-table tr:hover { background-color: #f8f9fa; } -/* 移动端卡片式布局 */ -.trades-cards { - display: flex; - flex-direction: column; - gap: 1rem; - margin-top: 1rem; +.pnl-positive { + color: #4CAF50; + font-weight: 500; } -@media (min-width: 768px) { - .trades-cards { - display: none; - } +.pnl-negative { + color: #F44336; + font-weight: 500; } -.trade-card { +.status-badge { + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.8rem; + font-weight: 500; +} + +.status-open { + background: #E3F2FD; + color: #2196F3; +} + +.status-closed { + background: #E8F5E9; + color: #4CAF50; +} + +.status-cancelled { + background: #FFEBEE; + color: #F44336; +} + +.no-data { + text-align: center; + padding: 3rem; + color: #999; background: #f8f9fa; - padding: 1rem; border-radius: 8px; +} + +.loading { + text-align: center; + padding: 3rem; + color: #666; +} + +/* 统计复制区域优化 */ +.stats-copy-section { + background: #f1f3f5; + border: 1px solid #dee2e6; + border-radius: 8px; + padding: 1.5rem; + margin-bottom: 2rem; +} + +.stats-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.stats-header h3 { + margin: 0; + font-size: 1.1rem; + color: #333; +} + +.btn-copy { + padding: 0.4rem 0.8rem; + background: #fff; + border: 1px solid #ced4da; + border-radius: 4px; + color: #495057; + cursor: pointer; + font-size: 0.85rem; + transition: all 0.2s; +} + +.btn-copy:hover { + background: #e9ecef; + border-color: #adb5bd; +} + +.stats-table { + width: 100%; + border-collapse: collapse; + background: white; border: 1px solid #e9ecef; } -.trade-card-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0.75rem; - padding-bottom: 0.75rem; - border-bottom: 1px solid #dee2e6; +.stats-table td { + padding: 0.75rem 1rem; + border-bottom: 1px solid #f1f3f5; + font-size: 0.95rem; + color: #333; } -.trade-card-symbol { - font-weight: bold; - font-size: 1.1rem; - color: #2c3e50; +.stats-table tr:last-child td { + border-bottom: none; } -.trade-card-side { - padding: 0.25rem 0.75rem; - border-radius: 12px; - font-size: 0.85rem; +.stats-table td:first-child { + width: 200px; + background: #f8f9fa; font-weight: 500; + color: #495057; + border-right: 1px solid #f1f3f5; } -.trade-card-side.buy { - background-color: #d4edda; - color: #155724; -} - -.trade-card-side.sell { - background-color: #f8d7da; - color: #721c24; -} - -.trade-card-body { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 0.75rem; - margin-bottom: 0.75rem; -} - -.trade-card-field { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.trade-card-label { - font-size: 0.85rem; - color: #666; -} - -.trade-card-value { - font-weight: 500; - color: #2c3e50; -} - -.order-id { - font-family: 'Courier New', monospace; - font-size: 0.9em; - color: #666; -} - -.trade-card-footer { - display: flex; - justify-content: space-between; - align-items: center; - padding-top: 0.75rem; - border-top: 1px solid #dee2e6; - font-size: 0.9rem; -} - -.buy { - color: #27ae60; - font-weight: bold; -} - -.sell { - color: #e74c3c; - font-weight: bold; -} - -.positive { - color: #27ae60; -} - -.negative { - color: #e74c3c; -} - -.status { - padding: 0.25rem 0.75rem; - border-radius: 12px; - font-size: 0.85rem; - font-weight: 500; -} - -.status.open { - background-color: #3498db; - color: white; -} - -.status.closed { - background-color: #95a5a6; - color: white; -} - -.status.cancelled { - background-color: #e74c3c; - color: white; +.stats-table td:nth-child(2) { + font-family: 'SF Mono', 'Roboto Mono', monospace; /* Monospace for numbers alignment if needed */ } diff --git a/frontend/src/components/TradeList.jsx b/frontend/src/components/TradeList.jsx index b586d08..88f4d78 100644 --- a/frontend/src/components/TradeList.jsx +++ b/frontend/src/components/TradeList.jsx @@ -153,6 +153,40 @@ const TradeList = () => { URL.revokeObjectURL(url) } + // 复制统计数据到剪贴板 + const handleCopyStats = (statsData) => { + if (!statsData) return + + const m = statsData.exit_reason_counts || {} + const exitReasonText = [ + m.stop_loss ? `止损 ${m.stop_loss}` : '', + m.take_profit ? `止盈 ${m.take_profit}` : '', + m.trailing_stop ? `移动止损 ${m.trailing_stop}` : '', + m.manual ? `手动 ${m.manual}` : '', + m.sync ? `同步 ${m.sync}` : '', + m.unknown ? `其他 ${m.unknown}` : '' + ].filter(Boolean).join(' / ') || '—' + + const lines = [ + `总交易数\t${statsData.total_trades}`, + `胜率\t${statsData.win_rate.toFixed(2)}%`, + `总盈亏\t${statsData.total_pnl.toFixed(2)} USDT`, + `平均盈亏\t${statsData.avg_pnl.toFixed(2)} USDT`, + `平均持仓时长\t${statsData.avg_duration_minutes ? Number(statsData.avg_duration_minutes).toFixed(0) : 0} 分钟`, + `平仓原因分布\t${exitReasonText}`, + `盈亏比\t${Number(statsData.avg_win_loss_ratio || 0).toFixed(2)} : 1`, + `总交易量\t${Number(statsData.total_notional_usdt || 0).toFixed(2)} USDT` + ] + + const text = lines.join('\n') + navigator.clipboard.writeText(text).then(() => { + alert('统计数据已复制到剪贴板(可直接粘贴到Excel)') + }).catch(err => { + console.error('Copy failed:', err) + alert('复制失败,请手动复制') + }) + } + if (loading) return
加载中...
return ( @@ -310,36 +344,67 @@ const TradeList = () => { { stats && ( -
-
整体统计
-
总交易数:{stats.total_trades}
-
胜率:{stats.win_rate.toFixed(2)}%
-
总盈亏:{stats.total_pnl.toFixed(2)} USDT
-
平均盈亏:{stats.avg_pnl.toFixed(2)} USDT
-
平均持仓时长(分钟):{stats.avg_duration_minutes ? Number(stats.avg_duration_minutes).toFixed(0) : 0}
-
平仓原因(有意义交易): -
- {(() => { - const m = stats.exit_reason_counts || {} - const stopLoss = Number(m.stop_loss || 0) - const takeProfit = Number(m.take_profit || 0) - const trailing = Number(m.trailing_stop || 0) - const manual = Number(m.manual || 0) - const sync = Number(m.sync || 0) - const other = Number(m.unknown || 0) - const parts = [] - if (stopLoss) parts.push(`止损 ${stopLoss}`) - if (takeProfit) parts.push(`止盈 ${takeProfit}`) - if (trailing) parts.push(`移动止损 ${trailing}`) - if (manual) parts.push(`手动 ${manual}`) - if (sync) parts.push(`同步 ${sync}`) - if (other) parts.push(`其他 ${other}`) - return parts.length ? parts.join(' / ') : '—' - })()} -
-
平均盈利 / 平均亏损(期望 3:1):{Number(stats.avg_win_loss_ratio || 0).toFixed(2)} : 1
-
总交易量(名义):{Number(stats.total_notional_usdt || 0).toFixed(2)} USDT
+
+
+

整体统计(分析用)

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
总交易数{stats.total_trades}
胜率{stats.win_rate.toFixed(2)}%
总盈亏{stats.total_pnl.toFixed(2)} USDT
平均盈亏{stats.avg_pnl.toFixed(2)} USDT
平均持仓时长{stats.avg_duration_minutes ? Number(stats.avg_duration_minutes).toFixed(0) : 0} 分钟
平仓原因分布 + {(() => { + const m = stats.exit_reason_counts || {} + const stopLoss = Number(m.stop_loss || 0) + const takeProfit = Number(m.take_profit || 0) + const trailing = Number(m.trailing_stop || 0) + const manual = Number(m.manual || 0) + const sync = Number(m.sync || 0) + const other = Number(m.unknown || 0) + const parts = [] + if (stopLoss) parts.push(`止损 ${stopLoss}`) + if (takeProfit) parts.push(`止盈 ${takeProfit}`) + if (trailing) parts.push(`移动止损 ${trailing}`) + if (manual) parts.push(`手动 ${manual}`) + if (sync) parts.push(`同步 ${sync}`) + if (other) parts.push(`其他 ${other}`) + return parts.length ? parts.join(' / ') : '—' + })()} +
盈亏比 (期望 3:1){Number(stats.avg_win_loss_ratio || 0).toFixed(2)} : 1
总交易量 (名义){Number(stats.total_notional_usdt || 0).toFixed(2)} USDT
) }