优化订单记录的页面

This commit is contained in:
薇薇安 2026-02-03 13:41:45 +08:00
parent 8bc6c1ecc4
commit c713e7d27e

View File

@ -9,6 +9,7 @@ const TradeList = () => {
const [trades, setTrades] = useState([])
const [stats, setStats] = useState(null)
const [loading, setLoading] = useState(true)
const [showStatsTable, setShowStatsTable] = useState(false) //
//
const [period, setPeriod] = useState('today') // '1d', '7d', '30d', 'today', 'week', 'month', null
@ -136,13 +137,50 @@ const TradeList = () => {
return row
})
// CSV helper
const convertToCSV = (objArray) => {
const array = typeof objArray !== 'object' ? JSON.parse(objArray) : objArray;
let str = '';
if (array.length === 0) return '';
// Header
const headers = Object.keys(array[0]);
str += headers.join(',') + '\r\n';
// Rows
for (let i = 0; i < array.length; i++) {
let line = '';
for (const index in array[i]) {
if (line !== '') line += ',';
let value = array[i][index];
if (value === null || value === undefined) {
value = '';
} else {
value = String(value);
}
// Escape quotes and wrap in quotes if necessary
// Excel needs double quotes to be escaped as ""
if (value.search(/("|,|\n|\r)/g) >= 0) {
value = '"' + value.replace(/"/g, '""') + '"';
}
line += value;
}
str += line + '\r\n';
}
return str;
}
//
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-')
const filename = `交易记录_${timestamp}.json`
const filename = `交易记录_${timestamp}.csv`
//
const dataStr = JSON.stringify(exportData, null, 2)
const dataBlob = new Blob([dataStr], { type: 'application/json' })
// (CSV with BOM for Excel)
const csvStr = convertToCSV(exportData)
const bom = '\uFEFF'
const dataBlob = new Blob([bom + csvStr], { type: 'text/csv;charset=utf-8;' })
const url = URL.createObjectURL(dataBlob)
const link = document.createElement('a')
link.href = url
@ -335,8 +373,8 @@ const TradeList = () => {
重置
</button>
{trades.length > 0 && (
<button className="btn-export" onClick={handleExport} title="导出完整数据(含入场/离场原因、入场思路等),便于后续分析">
导出数据 ({trades.length})
<button className="btn-export" onClick={handleExport} title="导出Excel/CSV(含入场/离场原因、入场思路等),便于后续分析">
导出 Excel ({trades.length})
</button>
)}
</div>
@ -344,73 +382,8 @@ const TradeList = () => {
{
stats && (
<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>
)
}
{
stats && (
<div className="stats-section">
{/* 卡片式统计(始终显示) */}
<div className="stats-summary">
<div className="stat-card">
<div className="stat-label">总交易数</div>
@ -500,6 +473,84 @@ const TradeList = () => {
</div>
)}
</div>
{/* 折叠控制按钮 */}
<div className="stats-toggle-row">
<button
className="btn-toggle"
onClick={() => setShowStatsTable(!showStatsTable)}
title="查看可复制的详细统计表格"
>
{showStatsTable ? '▲ 收起详细统计' : '▼ 展开详细统计(分析用)'}
</button>
</div>
{/* 详细统计表格(可折叠) */}
{showStatsTable && (
<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>
)}
</div>
)
}