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:
+115
-1
@@ -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();
|
||||
|
||||
@@ -116,6 +116,19 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Trade Detail Modal -->
|
||||
<div id="trade-modal" class="modal-overlay" style="display:none">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>📋 交易详情</h2>
|
||||
<button class="modal-close" onclick="closeTradeDetail()">✕</button>
|
||||
</div>
|
||||
<div id="trade-detail-body">
|
||||
<div class="loading">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/app.js"></script>
|
||||
|
||||
@@ -166,3 +166,74 @@ tr:hover td { background: rgba(88, 166, 255, 0.05); }
|
||||
header { flex-direction: column; gap: 8px; }
|
||||
.stats-row { justify-content: center; }
|
||||
}
|
||||
|
||||
/* Trade Detail Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 40px 16px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.modal-content {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
max-width: 700px;
|
||||
width: 100%;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
||||
}
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.modal-header h2 { font-size: 16px; margin: 0; padding: 0; border: none; color: var(--text); }
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-dim);
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
.modal-close:hover { background: rgba(255,255,255,0.1); color: var(--text); }
|
||||
#trade-detail-body { padding: 0; }
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0;
|
||||
}
|
||||
.detail-section {
|
||||
padding: 14px 20px;
|
||||
border-bottom: 1px solid rgba(48,54,61,0.4);
|
||||
}
|
||||
.detail-section:last-child { border-bottom: none; }
|
||||
.detail-section-full { grid-column: 1 / -1; }
|
||||
.detail-section h3 {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 3px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
.detail-row .label { color: var(--text-dim); }
|
||||
.detail-row .value { font-weight: 500; }
|
||||
.detail-orders { width: 100%; font-size: 12px; }
|
||||
.detail-orders th { background: var(--bg); font-size: 10px; }
|
||||
.detail-orders td { padding: 4px 6px; }
|
||||
|
||||
Reference in New Issue
Block a user