feat(web): trade detail modal with prices, fees, timestamps

- Click any trade row in history table to open detail modal
- Modal shows 6 sections: 概览, 时间, 价差, 手续费, 多仓, 空仓
- Entries and exits displayed with 6 decimal precision
- Fee entry/exit and total fee displayed
- Open/close timestamps with full date-time format
- Duration, scale count, total amount, exit reason
- Orders sub-table if available
- Escape key and overlay click to close
This commit is contained in:
jackyu66git
2026-05-03 18:59:28 +08:00
parent 02b74f1ec0
commit 505137bcb0
6 changed files with 229 additions and 4 deletions
+115 -1
View File
@@ -471,7 +471,7 @@ async function loadTrades() {
const pnlCls = t.NetPnl > 0 ? 'text-green' : t.NetPnl < 0 ? 'text-red' : '';
const convCls = t.Convergence === '价差收敛' ? 'text-green' :
t.Convergence === '价差发散' ? 'text-red' : 'text-yellow';
return `<tr>
return `<tr class="trade-row" data-id="${t.ID}" onclick="openTradeDetail(${t.ID})">
<td class="text-dim">${t.ClosedAt ? new Date(t.ClosedAt).toLocaleTimeString('zh-CN', { hour12: false }) : '-'}</td>
<td><strong>${t.Coin}</strong></td>
<td>${t.Direction}</td>
@@ -489,6 +489,120 @@ async function loadTrades() {
}
}
// ---- Trade Detail Modal ----
function openTradeDetail(id) {
const modal = document.getElementById('trade-modal');
const body = document.getElementById('trade-detail-body');
modal.style.display = 'flex';
body.innerHTML = '<div class="loading">加载中...</div>';
fetch('/api/trade/' + id)
.then(r => r.json())
.then(data => {
const t = data.trade;
if (!t || !t.ID) {
body.innerHTML = '<div class="loading text-red">交易数据加载失败</div>';
return;
}
const opened = new Date(t.OpenedAt);
const closed = t.ClosedAt ? new Date(t.ClosedAt) : null;
const dur = closed ? Math.round((closed - opened) / 1000) + 's' : '-';
const pnlCls = t.NetPnl > 0 ? 'text-green' : t.NetPnl < 0 ? 'text-red' : '';
const feeEntry = t.FeeEntry != null ? t.FeeEntry.toFixed(3) + '%' : '-';
const feeExit = t.FeeExit != null ? t.FeeExit.toFixed(3) + '%' : '-';
const totalFee = t.FeeEntry != null && t.FeeExit != null
? (t.FeeEntry + t.FeeExit).toFixed(3) + '%' : '-';
const le = t.LongEntry != null ? t.LongEntry.toFixed(6) : '-';
const lx = t.LongExit != null ? t.LongExit.toFixed(6) : '-';
const se = t.ShortEntry != null ? t.ShortEntry.toFixed(6) : '-';
const sx = t.ShortExit != null ? t.ShortExit.toFixed(6) : '-';
const lpnl = t.LongPnl != null ? t.LongPnl.toFixed(4) + '%' : '-';
const spnl = t.ShortPnl != null ? t.ShortPnl.toFixed(4) + '%' : '-';
body.innerHTML = `<div class="detail-grid">
<div class="detail-section">
<h3>概览</h3>
<div class="detail-row"><span class="label">币种</span><span class="value"><strong>${t.Coin}</strong>/USDT</span></div>
<div class="detail-row"><span class="label">方向</span><span class="value">${t.Direction || '-'}</span></div>
<div class="detail-row"><span class="label">状态</span><span class="value">${t.Status === 'closed' ? '已平仓' : t.Status}</span></div>
<div class="detail-row"><span class="label">加仓次数</span><span class="value">${t.ScaleCount || 0} 次</span></div>
<div class="detail-row"><span class="label">总规模</span><span class="value">$${(t.AmountUSD || 0).toFixed(0)}</span></div>
</div>
<div class="detail-section">
<h3>时间</h3>
<div class="detail-row"><span class="label">开仓</span><span class="value">${opened.toLocaleString('zh-CN', { hour12: false })}</span></div>
<div class="detail-row"><span class="label">平仓</span><span class="value">${closed ? closed.toLocaleString('zh-CN', { hour12: false }) : '-'}</span></div>
<div class="detail-row"><span class="label">持仓时长</span><span class="value">${dur}</span></div>
</div>
<div class="detail-section">
<h3>价差</h3>
<div class="detail-row"><span class="label">入场价差</span><span class="value">${t.EntrySpread != null ? t.EntrySpread.toFixed(4) + '%' : '-'}</span></div>
<div class="detail-row"><span class="label">出场价差</span><span class="value">${t.ExitSpread != null ? t.ExitSpread.toFixed(4) + '%' : '-'}</span></div>
<div class="detail-row"><span class="label">收敛情况</span><span class="value ${t.Convergence === '价差收敛' ? 'text-green' : t.Convergence === '价差发散' ? 'text-red' : ''}">${t.Convergence || '-'}</span></div>
<div class="detail-row"><span class="label">平仓原因</span><span class="value">${t.ExitReason || '-'}</span></div>
</div>
<div class="detail-section">
<h3>手续费</h3>
<div class="detail-row"><span class="label">开仓费</span><span class="value">${feeEntry}</span></div>
<div class="detail-row"><span class="label">平仓费</span><span class="value">${feeExit}</span></div>
<div class="detail-row"><span class="label">总手续费</span><span class="value">${totalFee}</span></div>
</div>
<div class="detail-section">
<h3>多仓 ${t.LongExchange || '-'}</h3>
<div class="detail-row"><span class="label">入场价</span><span class="value">$${le}</span></div>
<div class="detail-row"><span class="label">出场价</span><span class="value">$${lx}</span></div>
<div class="detail-row"><span class="label">盈亏</span><span class="value ${t.LongPnl > 0 ? 'text-green' : t.LongPnl < 0 ? 'text-red' : ''}">${lpnl}</span></div>
</div>
<div class="detail-section">
<h3>空仓 ${t.ShortExchange || '-'}</h3>
<div class="detail-row"><span class="label">入场价</span><span class="value">$${se}</span></div>
<div class="detail-row"><span class="label">出场价</span><span class="value">$${sx}</span></div>
<div class="detail-row"><span class="label">盈亏</span><span class="value ${t.ShortPnl > 0 ? 'text-green' : t.ShortPnl < 0 ? 'text-red' : ''}">${spnl}</span></div>
</div>
<div class="detail-section detail-section-full">
<h3>净收益</h3>
<div class="detail-row" style="font-size:16px"><span class="label">总计</span><span class="value ${pnlCls}" style="font-weight:700">${t.NetPnl != null ? t.NetPnl.toFixed(4) + '%' : '-'}</span></div>
</div>
</div>`;
// Append orders table if available
if (data.orders && data.orders.length > 0) {
const ordersHtml = data.orders.map(o => {
const typeLabel = o.Type === 'entry' ? '开仓' : o.Type === 'exit' ? '平仓' : o.Type === 'scale' ? '加仓' : o.Type;
return `<tr><td>${typeLabel}</td><td>${o.Side === 'buy' ? '买' : '卖'}</td><td>${o.Exchange}</td><td>$${o.Price ? o.Price.toFixed(6) : '-'}</td><td>${o.Size || '-'}</td><td>${o.Fee ? o.Fee.toFixed(4) + '%' : '-'}</td><td>${o.OrderID ? o.OrderID.substring(0, 12) + '...' : '-'}</td></tr>`;
}).join('');
body.innerHTML += `<div class="detail-section detail-section-full" style="border-top:1px solid var(--border)">
<h3>订单明细 (${data.orders.length})</h3>
<table class="detail-orders">
<thead><tr><th>类型</th><th>方向</th><th>交易所</th><th>价格</th><th>数量</th><th>手续费</th><th>订单ID</th></tr></thead>
<tbody>${ordersHtml}</tbody>
</table>
</div>`;
}
})
.catch(err => {
body.innerHTML = '<div class="loading text-red">加载失败: ' + err.message + '</div>';
});
}
function closeTradeDetail() {
document.getElementById('trade-modal').style.display = 'none';
}
// Close modal on overlay click
document.addEventListener('click', function(e) {
const modal = document.getElementById('trade-modal');
if (e.target === modal) closeTradeDetail();
});
// Close on Escape
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeTradeDetail();
});
// ---- Init ----
function init() {
connectSSE();