This commit is contained in:
薇薇安 2026-02-03 13:27:52 +08:00
parent c23db4aba0
commit 8bc6c1ecc4
2 changed files with 219 additions and 291 deletions

View File

@ -200,14 +200,15 @@
} }
.btn-secondary { .btn-secondary {
background: #6c757d; background: #e0e0e0;
color: white; color: #333;
} }
.btn-secondary:hover { .btn-secondary:hover {
background: #5a6268; background: #d5d5d5;
} }
/* 导出按钮样式 */
.btn-export { .btn-export {
padding: 0.75rem 1.5rem; padding: 0.75rem 1.5rem;
border: none; border: none;
@ -216,10 +217,9 @@
font-size: 0.9rem; font-size: 0.9rem;
font-weight: 500; font-weight: 500;
transition: all 0.3s; transition: all 0.3s;
touch-action: manipulation; background: #4CAF50;
min-height: 44px;
background: #28a745;
color: white; color: white;
min-height: 44px;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
@ -230,329 +230,192 @@
} }
.btn-export:hover { .btn-export:hover {
background: #218838; background: #43A047;
}
.no-data {
text-align: center;
padding: 3rem;
color: #999;
font-size: 1.1rem;
} }
.stats-summary { .stats-summary {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 0.75rem; gap: 1rem;
margin-bottom: 1rem; margin-bottom: 2rem;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.stats-summary { .stats-summary {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-template-columns: repeat(4, 1fr);
gap: 1rem;
margin-bottom: 2rem;
} }
} }
.stat-card { .stat-card {
background: #f8f9fa; background: #f8f9fa;
padding: 1.5rem; padding: 1rem;
border-radius: 8px; border-radius: 8px;
text-align: center; text-align: center;
} }
.stat-label { .stat-label {
font-size: 0.9rem;
color: #666; color: #666;
font-size: 0.9rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.stat-value { .stat-value {
font-size: 1.5rem; font-size: 1.25rem;
font-weight: bold; font-weight: 600;
color: #2c3e50; color: #333;
} }
.stat-value.positive { .stat-value.positive {
color: #27ae60; color: #4CAF50;
} }
.stat-value.negative { .stat-value.negative {
color: #e74c3c; color: #F44336;
} }
/* 表格横向滚动:避免整页过宽,内容区域可左右滑动 */
.table-wrapper { .table-wrapper {
width: 100%;
max-width: 100%;
overflow-x: auto; overflow-x: auto;
margin-top: 1rem;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
margin-bottom: 1rem;
border-radius: 8px; border-radius: 8px;
border: 1px solid #e9ecef; border: 1px solid #eee;
}
.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;
} }
.trades-table { .trades-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
display: none; font-size: 0.9rem;
min-width: 1100px; /* 表格最小宽度,超出时由 table-wrapper 横向滚动 */ min-width: 1000px; /* 保证在小屏幕上也能横向滚动 */
} }
@media (min-width: 768px) { .trades-table th,
.trades-table { .trades-table td {
display: table; padding: 1rem;
} text-align: left;
border-bottom: 1px solid #eee;
} }
.trades-table th { .trades-table th {
background-color: #34495e; background: #f8f9fa;
color: white; font-weight: 600;
padding: 0.5rem 0.4rem; color: #444;
text-align: left;
font-weight: 500;
font-size: 0.8rem;
white-space: nowrap; 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 { .trades-table tr:hover {
background-color: #f8f9fa; background-color: #f8f9fa;
} }
/* 移动端卡片式布局 */ .pnl-positive {
.trades-cards { color: #4CAF50;
display: flex; font-weight: 500;
flex-direction: column;
gap: 1rem;
margin-top: 1rem;
} }
@media (min-width: 768px) { .pnl-negative {
.trades-cards { color: #F44336;
display: none; 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; background: #f8f9fa;
padding: 1rem;
border-radius: 8px; 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; border: 1px solid #e9ecef;
} }
.trade-card-header { .stats-table td {
display: flex; padding: 0.75rem 1rem;
justify-content: space-between; border-bottom: 1px solid #f1f3f5;
align-items: center; font-size: 0.95rem;
margin-bottom: 0.75rem; color: #333;
padding-bottom: 0.75rem;
border-bottom: 1px solid #dee2e6;
} }
.trade-card-symbol { .stats-table tr:last-child td {
font-weight: bold; border-bottom: none;
font-size: 1.1rem;
color: #2c3e50;
} }
.trade-card-side { .stats-table td:first-child {
padding: 0.25rem 0.75rem; width: 200px;
border-radius: 12px; background: #f8f9fa;
font-size: 0.85rem;
font-weight: 500; font-weight: 500;
color: #495057;
border-right: 1px solid #f1f3f5;
} }
.trade-card-side.buy { .stats-table td:nth-child(2) {
background-color: #d4edda; font-family: 'SF Mono', 'Roboto Mono', monospace; /* Monospace for numbers alignment if needed */
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;
} }

View File

@ -153,6 +153,40 @@ const TradeList = () => {
URL.revokeObjectURL(url) 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 <div className="loading">加载中...</div> if (loading) return <div className="loading">加载中...</div>
return ( return (
@ -310,15 +344,38 @@ const TradeList = () => {
{ {
stats && ( stats && (
<div > <div className="stats-copy-section">
<div style={{ fontSize: '1.1rem' }}>整体统计</div> <div className="stats-header">
<div style={{ fontSize: '1.1rem' }}>总交易数{stats.total_trades} </div> <h3>整体统计分析用</h3>
<div style={{ fontSize: '1.1rem' }}>胜率{stats.win_rate.toFixed(2)}%</div> <button className="btn-copy" onClick={() => handleCopyStats(stats)}>
<div style={{ fontSize: '1.1rem' }}>总盈亏{stats.total_pnl.toFixed(2)} USDT</div> 复制统计
<div style={{ fontSize: '1.1rem' }}>平均盈亏{stats.avg_pnl.toFixed(2)} USDT</div> </button>
<div style={{ fontSize: '1.1rem' }}>平均持仓时长分钟{stats.avg_duration_minutes ? Number(stats.avg_duration_minutes).toFixed(0) : 0}</div> </div>
<div style={{ fontSize: '1.1rem' }}>平仓原因有意义交易 <table className="stats-table">
<div style={{ fontSize: '1.1rem' }}> <tbody>
<tr>
<td>总交易数</td>
<td>{stats.total_trades}</td>
</tr>
<tr>
<td>胜率</td>
<td>{stats.win_rate.toFixed(2)}%</td>
</tr>
<tr>
<td>总盈亏</td>
<td>{stats.total_pnl.toFixed(2)} USDT</td>
</tr>
<tr>
<td>平均盈亏</td>
<td>{stats.avg_pnl.toFixed(2)} USDT</td>
</tr>
<tr>
<td>平均持仓时长</td>
<td>{stats.avg_duration_minutes ? Number(stats.avg_duration_minutes).toFixed(0) : 0} 分钟</td>
</tr>
<tr>
<td>平仓原因分布</td>
<td>
{(() => { {(() => {
const m = stats.exit_reason_counts || {} const m = stats.exit_reason_counts || {}
const stopLoss = Number(m.stop_loss || 0) const stopLoss = Number(m.stop_loss || 0)
@ -336,10 +393,18 @@ const TradeList = () => {
if (other) parts.push(`其他 ${other}`) if (other) parts.push(`其他 ${other}`)
return parts.length ? parts.join(' / ') : '—' return parts.length ? parts.join(' / ') : '—'
})()} })()}
</div> </td>
<div style={{ fontSize: '1.1rem' }}>平均盈利 / 平均亏损期望 3:1{Number(stats.avg_win_loss_ratio || 0).toFixed(2)} : 1</div> </tr>
<div style={{ fontSize: '1.1rem' }}>总交易量名义{Number(stats.total_notional_usdt || 0).toFixed(2)} USDT</div> <tr>
</div> <td>盈亏比 (期望 3:1)</td>
<td>{Number(stats.avg_win_loss_ratio || 0).toFixed(2)} : 1</td>
</tr>
<tr>
<td>总交易量 (名义)</td>
<td>{Number(stats.total_notional_usdt || 0).toFixed(2)} USDT</td>
</tr>
</tbody>
</table>
</div> </div>
) )
} }