Fix dashboard mobile load issues and simplify card layout.

Reduce poll pressure on phone/tablet, cache key prices, and handle live API errors gracefully. Rework mobile position and close cards with inline direction, compact P/L line, and detail modal.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-29 23:10:20 +08:00
parent d1ad0f9253
commit 2f5b5c4aae
6 changed files with 225 additions and 34 deletions
+167 -22
View File
@@ -20,17 +20,21 @@
var riskBodyEl = document.getElementById('dash-risk-body');
var posMobileList = document.getElementById('dash-pos-mobile-list');
var keysMobileList = document.getElementById('dash-keys-mobile-list');
var closesMobileList = document.getElementById('dash-closes-mobile-list');
var detailModal = document.getElementById('dash-detail-modal');
var detailTitleEl = document.getElementById('dash-detail-title');
var detailGridEl = document.getElementById('dash-detail-grid');
var detailCloseBtn = document.getElementById('dash-detail-close');
var pollTimer = null;
var pollInFlight = false;
var pollFailStreak = 0;
var positionSource = null;
var posRowCache = {};
var posRenderSig = '';
var posMobileCache = {};
var keysMobileCache = {};
var closesMobileCache = {};
var lastPosRows = [];
var lastKeyRows = [];
var lastKeyIds = '';
@@ -181,6 +185,77 @@
return '—';
}
function fmtMobileLots(v) {
var n = Number(v);
if (isNaN(n)) return '—';
if (Math.abs(n - Math.round(n)) < 0.001) return Math.round(n) + '手';
return fmtNum(n) + '手';
}
function fmtMobilePrice(v) {
if (v == null || v === '') return '—';
var n = Number(v);
if (isNaN(n)) return '—';
if (Math.abs(n - Math.round(n)) < 0.001) return String(Math.round(n));
return fmtNum(n);
}
function fmtMobilePnlNum(v) {
if (v == null || v === '') return '—';
var n = Number(v);
if (isNaN(n)) return '—';
var abs = Math.abs(n);
var s = Math.abs(abs - Math.round(abs)) < 0.01 ? String(Math.round(abs)) : fmtNum(abs);
if (n > 0) return s;
if (n < 0) return '-' + s;
return '0';
}
function mobilePnlSpanHtml(v) {
return '<span class="' + pnlClass(v) + '">' + escHtml(fmtMobilePnlNum(v)) + '</span>';
}
function mobileSummaryHtml(lots, priceLabel, price, pnl) {
return fmtMobileLots(lots) + ' ' + priceLabel + ' ' + fmtMobilePrice(price) +
' 盈亏 ' + mobilePnlSpanHtml(pnl);
}
function mobileSymbolTitleHtml(row) {
var name = row.symbol_name || row.symbol || '';
var code = row.symbol_code || '';
if (!code && row.symbol && String(row.symbol).toLowerCase() !== String(name).toLowerCase()) {
code = row.symbol;
}
var exchange = row.symbol_exchange || '';
var mainBadge = row.symbol_is_main
? ' <span class="badge planned dash-main-badge">主力</span>' : '';
var titleInner = escHtml(name);
if (exchange) {
titleInner += ' <span class="dash-symbol-ex text-muted">' + escHtml(exchange) + '</span>';
}
titleInner += mainBadge;
titleInner += breakevenBadgeHtml(row);
if (code && String(name).toLowerCase() !== String(code).toLowerCase()) {
titleInner += ' <span class="text-accent">' + escHtml(code) + '</span> ' + directionBadgeHtml(row);
} else if (!name && code) {
titleInner = (exchange
? '<span class="dash-symbol-ex text-muted">' + escHtml(exchange) + '</span> '
: '') + '<span class="text-accent">' + escHtml(code) + '</span> ' + directionBadgeHtml(row);
} else {
titleInner += ' ' + directionBadgeHtml(row);
}
return titleInner;
}
function mobileItemFootHtml(summaryHtml) {
return (
'<div class="dash-mobile-item-foot">' +
'<span class="dash-mobile-item-summary">' + summaryHtml + '</span>' +
'<span class="dash-mobile-chevron">查看详情 </span>' +
'</div>'
);
}
function symbolCellHtml(row) {
var name = row.symbol_name || row.symbol || '';
var code = row.symbol_code || '';
@@ -440,12 +515,40 @@
});
}
function renderClosesMobile(closes) {
if (!closesMobileList) return;
if (!closes || !closes.length) {
closesMobileList.innerHTML = '<div class="dash-mobile-empty">暂无平仓记录</div>';
closesMobileCache = {};
return;
}
closesMobileCache = {};
closesMobileList.innerHTML = closes.map(function (c) {
var net = c.pnl_net != null ? c.pnl_net : c.pnl;
closesMobileCache[String(c.id)] = c;
var summary = mobileSummaryHtml(c.lots, '平仓', c.close_price, net);
return (
'<button type="button" class="dash-mobile-item" data-close-id="' + c.id + '">' +
'<div class="dash-mobile-item-head">' +
'<div class="dash-mobile-item-title">' + mobileSymbolTitleHtml(c) + '</div>' +
'</div>' +
mobileItemFootHtml(summary) +
'</button>'
);
}).join('');
}
function renderCloses(closes) {
if (!closesBody) return;
if (!closes || !closes.length) {
closesBody.innerHTML = '<tr><td colspan="8" class="text-muted">暂无平仓记录</td></tr>';
if (isPhoneLayout() && closesMobileList) {
closesMobileList.innerHTML = '<div class="dash-mobile-empty">暂无平仓记录</div>';
}
closesMobileCache = {};
return;
}
if (isPhoneLayout()) renderClosesMobile(closes);
closesBody.innerHTML = closes.map(function (c) {
var pc = pnlClass(c.pnl_net != null ? c.pnl_net : c.pnl);
return (
@@ -489,6 +592,10 @@
if (!closes || !closes.length) {
closesBody.innerHTML = '<tr><td colspan="8" class="text-muted">暂无平仓记录</td></tr>';
lastCloseHeadId = null;
if (isPhoneLayout() && closesMobileList) {
closesMobileList.innerHTML = '<div class="dash-mobile-empty">暂无平仓记录</div>';
}
closesMobileCache = {};
return;
}
var headId = closes[0].id;
@@ -531,6 +638,22 @@
];
}
function closeDetailItems(c) {
var net = c.pnl_net != null ? c.pnl_net : c.pnl;
return [
{ label: '品种', value: (c.symbol_name || c.symbol || '') + (c.symbol_code ? ' ' + c.symbol_code : '') },
{ label: '方向', html: directionBadgeHtml(c) },
{ label: '手数', value: String(c.lots) },
{ label: '开仓价', value: fmtNum(c.entry_price) },
{ label: '平仓价', value: fmtNum(c.close_price) },
{ label: '盈亏', html: '<span class="' + pnlClass(c.pnl) + '">' + fmtPnl(c.pnl) + '</span>' },
{ label: '净盈亏', html: '<span class="' + pnlClass(net) + '">' + fmtPnl(net) + '</span>' },
{ label: '手续费', value: c.fee != null ? fmtMoney(c.fee) : '—' },
{ label: '平仓时间', value: c.close_time || '—' },
{ label: '结果', value: c.result || '—' },
];
}
function renderPositionsMobile(rows) {
if (!posMobileList) return;
if (!rows.length) {
@@ -542,19 +665,16 @@
posMobileList.innerHTML = rows.map(function (r) {
var key = r.key || r.position_key || ((r.symbol_code || '') + ':' + (r.direction || ''));
posMobileCache[key] = r;
var title = symbolCellHtml(r) + ' ' + directionBadgeHtml(r);
var meta = fmtNum(r.lots) + ' 手 · 现价 ' +
(r.current_price != null ? fmtNum(r.current_price) : '—');
var summary = mobileSummaryHtml(
r.lots, '现价', r.current_price, r.float_pnl
);
return (
'<button type="button" class="dash-mobile-item" data-pos-key="' + escHtml(key) + '">' +
'<div class="dash-mobile-item-head">' +
'<div class="dash-mobile-item-title">' + title + '</div>' +
'<div class="dash-mobile-item-title">' + mobileSymbolTitleHtml(r) + '</div>' +
'</div>' +
'<div class="dash-mobile-item-meta">' + escHtml(meta) + '</div>' +
'<div class="dash-mobile-item-foot">' +
'<span class="' + pnlClass(r.float_pnl) + '">' + fmtPnl(r.float_pnl) + '</span>' +
'<span class="dash-mobile-chevron">查看详情 </span>' +
'</div></button>'
mobileItemFootHtml(summary) +
'</button>'
);
}).join('');
}
@@ -566,15 +686,11 @@
posMobileCache[key] = r;
var btn = posMobileList.querySelector('[data-pos-key="' + key + '"]');
if (!btn) return;
var metaEl = btn.querySelector('.dash-mobile-item-meta');
var pnlEl = btn.querySelector('.dash-mobile-item-foot span:first-child');
if (metaEl) {
metaEl.textContent = fmtNum(r.lots) + ' 手 · 现价 ' +
(r.current_price != null ? fmtNum(r.current_price) : '—');
}
if (pnlEl) {
pnlEl.textContent = fmtPnl(r.float_pnl);
pnlEl.className = pnlClass(r.float_pnl);
var summaryEl = btn.querySelector('.dash-mobile-item-summary');
if (summaryEl) {
summaryEl.innerHTML = mobileSummaryHtml(
r.lots, '现价', r.current_price, r.float_pnl
);
}
});
}
@@ -646,6 +762,15 @@
openDetailModal(k.symbol_name || k.symbol || '关键位', keyDetailItems(k));
});
}
if (closesMobileList) {
closesMobileList.addEventListener('click', function (ev) {
var btn = ev.target.closest('[data-close-id]');
if (!btn) return;
var c = closesMobileCache[btn.getAttribute('data-close-id')];
if (!c) return;
openDetailModal(c.symbol_name || c.symbol || '平仓记录', closeDetailItems(c));
});
}
}
function renderPositions(rows) {
@@ -760,21 +885,40 @@
}
}
function pollIntervalMs() {
if (isPhoneLayout() || isTabletLayout()) return 3000;
return 2000;
}
function pollDashboard() {
if (pollInFlight) return;
pollInFlight = true;
fetch('/api/dashboard/live')
.then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function (data) {
if (!data.ok) return;
pollFailStreak = 0;
if (!data.ok) {
if (updatedEl) updatedEl.textContent = data.error || '看板数据暂不可用';
return;
}
applyAccount(data);
applyRisk(data.risk, data.account);
applyKeys(data.keys || []);
applyCloses(data.closes || []);
})
.catch(function () {
if (updatedEl) updatedEl.textContent = '看板数据加载失败';
pollFailStreak += 1;
if (updatedEl) {
updatedEl.textContent = pollFailStreak >= 3
? '看板连接失败,正在重试…'
: '看板数据加载失败';
}
})
.finally(function () {
pollInFlight = false;
});
}
@@ -800,14 +944,15 @@
positionSource.close();
positionSource = null;
}
setTimeout(connectPositionStream, 3000);
var delay = (isPhoneLayout() || isTabletLayout()) ? 8000 : 3000;
setTimeout(connectPositionStream, delay);
};
}
function startPolling() {
if (pollTimer) clearInterval(pollTimer);
pollDashboard();
pollTimer = setInterval(pollDashboard, 1000);
pollTimer = setInterval(pollDashboard, pollIntervalMs());
}
function stopPolling() {