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 {
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 */
}

View File

@ -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 <div className="loading">加载中...</div>
return (
@ -310,36 +344,67 @@ const TradeList = () => {
{
stats && (
<div >
<div style={{ fontSize: '1.1rem' }}>整体统计</div>
<div style={{ fontSize: '1.1rem' }}>总交易数{stats.total_trades} </div>
<div style={{ fontSize: '1.1rem' }}>胜率{stats.win_rate.toFixed(2)}%</div>
<div style={{ fontSize: '1.1rem' }}>总盈亏{stats.total_pnl.toFixed(2)} USDT</div>
<div style={{ fontSize: '1.1rem' }}>平均盈亏{stats.avg_pnl.toFixed(2)} USDT</div>
<div style={{ fontSize: '1.1rem' }}>平均持仓时长分钟{stats.avg_duration_minutes ? Number(stats.avg_duration_minutes).toFixed(0) : 0}</div>
<div style={{ fontSize: '1.1rem' }}>平仓原因有意义交易
<div style={{ fontSize: '1.1rem' }}>
{(() => {
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(' / ') : '—'
})()}
</div>
<div style={{ fontSize: '1.1rem' }}>平均盈利 / 平均亏损期望 3:1{Number(stats.avg_win_loss_ratio || 0).toFixed(2)} : 1</div>
<div style={{ fontSize: '1.1rem' }}>总交易量名义{Number(stats.total_notional_usdt || 0).toFixed(2)} USDT</div>
<div className="stats-copy-section">
<div className="stats-header">
<h3>整体统计分析用</h3>
<button className="btn-copy" onClick={() => handleCopyStats(stats)}>
复制统计
</button>
</div>
<table className="stats-table">
<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 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(' / ') : '—'
})()}
</td>
</tr>
<tr>
<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>
)
}