Improve dashboard responsive layout, collapsible risk section, and breakeven badge.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+284
-5
@@ -15,14 +15,124 @@
|
||||
var updatedEl = document.getElementById('dash-updated');
|
||||
var riskReasonEl = document.getElementById('dash-risk-reason');
|
||||
var riskGridEl = document.getElementById('dash-risk-grid');
|
||||
var riskCardEl = document.getElementById('dash-risk-card');
|
||||
var riskToggleEl = document.getElementById('dash-risk-toggle');
|
||||
var riskBodyEl = document.getElementById('dash-risk-body');
|
||||
var posMobileList = document.getElementById('dash-pos-mobile-list');
|
||||
var keysMobileList = document.getElementById('dash-keys-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 positionSource = null;
|
||||
var posRowCache = {};
|
||||
var posRenderSig = '';
|
||||
var posMobileCache = {};
|
||||
var keysMobileCache = {};
|
||||
var lastPosRows = [];
|
||||
var lastKeyRows = [];
|
||||
var lastKeyIds = '';
|
||||
var lastCloseHeadId = null;
|
||||
var lastRiskPayload = null;
|
||||
|
||||
function isPhoneLayout() {
|
||||
var root = document.documentElement;
|
||||
return root.dataset.layout === 'phone' || root.classList.contains('layout-phone')
|
||||
|| root.dataset.mobile === '1';
|
||||
}
|
||||
|
||||
function isTabletLayout() {
|
||||
var root = document.documentElement;
|
||||
return root.dataset.layout === 'tablet' || root.classList.contains('layout-tablet');
|
||||
}
|
||||
|
||||
function shouldCollapseRiskDefault() {
|
||||
return isPhoneLayout() || isTabletLayout();
|
||||
}
|
||||
|
||||
function syncRiskCollapseUi() {
|
||||
if (!riskCardEl || !riskToggleEl) return;
|
||||
var expanded = riskCardEl.classList.contains('is-expanded');
|
||||
riskToggleEl.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
||||
}
|
||||
|
||||
function initRiskToggle() {
|
||||
if (!riskCardEl || !riskToggleEl) return;
|
||||
if (shouldCollapseRiskDefault()) {
|
||||
riskCardEl.classList.remove('is-expanded');
|
||||
} else {
|
||||
riskCardEl.classList.add('is-expanded');
|
||||
}
|
||||
syncRiskCollapseUi();
|
||||
function toggleRisk() {
|
||||
riskCardEl.classList.toggle('is-expanded');
|
||||
syncRiskCollapseUi();
|
||||
}
|
||||
riskToggleEl.addEventListener('click', function (ev) {
|
||||
if (ev.target && ev.target.closest('.dash-risk-doc-link')) return;
|
||||
toggleRisk();
|
||||
});
|
||||
riskToggleEl.addEventListener('keydown', function (ev) {
|
||||
if (ev.key === 'Enter' || ev.key === ' ') {
|
||||
ev.preventDefault();
|
||||
toggleRisk();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isBreakevenLocked(row) {
|
||||
if (!row) return false;
|
||||
if (row.breakeven_locked) return true;
|
||||
if ((row.trailing_r_locked || 0) >= 1) return true;
|
||||
if (row.stop_loss == null || row.entry_price == null) return false;
|
||||
var entry = Number(row.entry_price);
|
||||
var sl = Number(row.stop_loss);
|
||||
if (isNaN(entry) || isNaN(sl) || entry <= 0) return false;
|
||||
var tick = Number(row.tick_size) || Math.max(Math.abs(entry) * 1e-6, 0.01);
|
||||
var buf = tick * 2.5;
|
||||
var dir = (row.direction || 'long').toString().toLowerCase();
|
||||
if (Math.abs(sl - entry) > buf + tick) return false;
|
||||
return dir === 'short' ? sl <= entry + tick * 0.05 : sl >= entry - tick * 0.05;
|
||||
}
|
||||
|
||||
function breakevenBadgeHtml(row) {
|
||||
return isBreakevenLocked(row)
|
||||
? ' <span class="badge profit dash-be-badge">已保本</span>' : '';
|
||||
}
|
||||
|
||||
function openDetailModal(title, items) {
|
||||
if (!detailModal || !detailGridEl || !detailTitleEl) return;
|
||||
detailTitleEl.textContent = title || '详情';
|
||||
detailGridEl.innerHTML = (items || []).map(function (it) {
|
||||
var cls = it.wide ? ' item wide' : ' item';
|
||||
return (
|
||||
'<div class="' + cls.trim() + '">' +
|
||||
'<label>' + escHtml(it.label) + '</label>' +
|
||||
'<div>' + (it.html != null ? it.html : escHtml(it.value)) + '</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}).join('');
|
||||
detailModal.hidden = false;
|
||||
detailModal.classList.add('show');
|
||||
}
|
||||
|
||||
function closeDetailModal() {
|
||||
if (!detailModal) return;
|
||||
detailModal.hidden = true;
|
||||
detailModal.classList.remove('show');
|
||||
}
|
||||
|
||||
function initDetailModal() {
|
||||
if (detailCloseBtn) detailCloseBtn.addEventListener('click', closeDetailModal);
|
||||
if (detailModal) {
|
||||
detailModal.addEventListener('click', function (ev) {
|
||||
if (ev.target === detailModal) closeDetailModal();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function fmtNum(v, digits) {
|
||||
if (v === null || v === undefined || v === '') return '—';
|
||||
var n = Number(v);
|
||||
@@ -85,6 +195,7 @@
|
||||
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>';
|
||||
} else if (!name && code) {
|
||||
@@ -358,13 +469,18 @@
|
||||
if (!ids) {
|
||||
keysBody.innerHTML = '<tr><td colspan="8" class="text-muted">暂无关键位监控</td></tr>';
|
||||
lastKeyIds = '';
|
||||
lastKeyRows = [];
|
||||
if (keysMobileList) keysMobileList.innerHTML = '<div class="dash-mobile-empty">暂无关键位监控</div>';
|
||||
return;
|
||||
}
|
||||
if (ids !== lastKeyIds) {
|
||||
lastKeyIds = ids;
|
||||
lastKeyRows = keys || [];
|
||||
renderKeys(keys);
|
||||
if (isPhoneLayout()) renderKeysMobile(lastKeyRows);
|
||||
} else {
|
||||
patchKeys(keys);
|
||||
if (isPhoneLayout()) patchKeysMobile(keys);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,11 +504,160 @@
|
||||
});
|
||||
}
|
||||
|
||||
function posDetailItems(r) {
|
||||
return [
|
||||
{ label: '品种', value: (r.symbol_name || r.symbol || '') + (r.symbol_code ? ' ' + r.symbol_code : '') },
|
||||
{ label: '方向', html: directionBadgeHtml(r) },
|
||||
{ label: '手数', value: String(r.lots) },
|
||||
{ label: '均价', value: fmtNum(r.entry_price) },
|
||||
{ label: '现价', value: r.current_price != null ? fmtNum(r.current_price) : '—' },
|
||||
{ label: '浮盈亏', html: '<span class="' + pnlClass(r.float_pnl) + '">' + fmtPnl(r.float_pnl) + '</span>' },
|
||||
{ label: '保证金', value: r.margin != null ? fmtMoney(r.margin) : '—' },
|
||||
{ label: '止损', value: slText(r) },
|
||||
{ label: '止盈', value: tpText(r) },
|
||||
];
|
||||
}
|
||||
|
||||
function keyDetailItems(k) {
|
||||
return [
|
||||
{ label: '品种', value: (k.symbol_name || k.symbol || k.symbol_code || '') },
|
||||
{ label: '类型', value: k.monitor_type || '—' },
|
||||
{ label: '周期', value: k.bar_period || '—' },
|
||||
{ label: '上沿', value: fmtNum(k.upper) },
|
||||
{ label: '下沿', value: fmtNum(k.lower) },
|
||||
{ label: '现价', value: k.price != null ? fmtNum(k.price) : '—' },
|
||||
{ label: '距上沿', value: k.dist_upper != null ? fmtNum(k.dist_upper) : '—' },
|
||||
{ label: '距下沿', value: k.dist_lower != null ? fmtNum(k.dist_lower) : '—' },
|
||||
];
|
||||
}
|
||||
|
||||
function renderPositionsMobile(rows) {
|
||||
if (!posMobileList) return;
|
||||
if (!rows.length) {
|
||||
posMobileList.innerHTML = '<div class="dash-mobile-empty">暂无持仓</div>';
|
||||
posMobileCache = {};
|
||||
return;
|
||||
}
|
||||
posMobileCache = {};
|
||||
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) : '—');
|
||||
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>' +
|
||||
'<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>'
|
||||
);
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function patchPositionsMobile(rows) {
|
||||
if (!posMobileList || !rows) return;
|
||||
rows.forEach(function (r) {
|
||||
var key = r.key || r.position_key;
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderKeysMobile(keys) {
|
||||
if (!keysMobileList) return;
|
||||
if (!keys || !keys.length) {
|
||||
keysMobileList.innerHTML = '<div class="dash-mobile-empty">暂无关键位监控</div>';
|
||||
keysMobileCache = {};
|
||||
return;
|
||||
}
|
||||
keysMobileCache = {};
|
||||
keysMobileList.innerHTML = keys.map(function (k) {
|
||||
keysMobileCache[String(k.id)] = k;
|
||||
var title = symbolCellHtml(k);
|
||||
var meta = escHtml(k.monitor_type || '—') + ' · ' + escHtml(k.bar_period || '—') +
|
||||
' · 现价 ' + (k.price != null ? fmtNum(k.price) : '—');
|
||||
var dist = '距上 ' + (k.dist_upper != null ? fmtNum(k.dist_upper) : '—') +
|
||||
' / 距下 ' + (k.dist_lower != null ? fmtNum(k.dist_lower) : '—');
|
||||
return (
|
||||
'<button type="button" class="dash-mobile-item" data-key-id="' + k.id + '">' +
|
||||
'<div class="dash-mobile-item-head"><div class="dash-mobile-item-title">' + title + '</div></div>' +
|
||||
'<div class="dash-mobile-item-meta">' + meta + '</div>' +
|
||||
'<div class="dash-mobile-item-foot">' +
|
||||
'<span class="text-muted">' + escHtml(dist) + '</span>' +
|
||||
'<span class="dash-mobile-chevron">查看详情 ›</span>' +
|
||||
'</div></button>'
|
||||
);
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function patchKeysMobile(keys) {
|
||||
if (!keysMobileList || !keys) return;
|
||||
keys.forEach(function (k) {
|
||||
keysMobileCache[String(k.id)] = k;
|
||||
var btn = keysMobileList.querySelector('[data-key-id="' + k.id + '"]');
|
||||
if (!btn) return;
|
||||
var metaEl = btn.querySelector('.dash-mobile-item-meta');
|
||||
var distEl = btn.querySelector('.dash-mobile-item-foot span:first-child');
|
||||
if (metaEl) {
|
||||
metaEl.innerHTML = escHtml(k.monitor_type || '—') + ' · ' + escHtml(k.bar_period || '—') +
|
||||
' · 现价 ' + (k.price != null ? fmtNum(k.price) : '—');
|
||||
}
|
||||
if (distEl) {
|
||||
distEl.textContent = '距上 ' + (k.dist_upper != null ? fmtNum(k.dist_upper) : '—') +
|
||||
' / 距下 ' + (k.dist_lower != null ? fmtNum(k.dist_lower) : '—');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initMobileLists() {
|
||||
if (posMobileList) {
|
||||
posMobileList.addEventListener('click', function (ev) {
|
||||
var btn = ev.target.closest('[data-pos-key]');
|
||||
if (!btn) return;
|
||||
var key = btn.getAttribute('data-pos-key');
|
||||
var row = posMobileCache[key];
|
||||
if (!row) return;
|
||||
var name = row.symbol_name || row.symbol || row.symbol_code || '持仓';
|
||||
openDetailModal(name, posDetailItems(row));
|
||||
});
|
||||
}
|
||||
if (keysMobileList) {
|
||||
keysMobileList.addEventListener('click', function (ev) {
|
||||
var btn = ev.target.closest('[data-key-id]');
|
||||
if (!btn) return;
|
||||
var k = keysMobileCache[btn.getAttribute('data-key-id')];
|
||||
if (!k) return;
|
||||
openDetailModal(k.symbol_name || k.symbol || '关键位', keyDetailItems(k));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function renderPositions(rows) {
|
||||
lastPosRows = rows || [];
|
||||
if (isPhoneLayout()) {
|
||||
renderPositionsMobile(lastPosRows);
|
||||
}
|
||||
if (!posBody) return;
|
||||
if (!rows.length) {
|
||||
posBody.innerHTML = '<tr><td colspan="9" class="text-muted">暂无持仓</td></tr>';
|
||||
posRowCache = {};
|
||||
posRenderSig = '';
|
||||
return;
|
||||
}
|
||||
posRowCache = {};
|
||||
@@ -427,7 +692,13 @@
|
||||
function patchPositionQuotes(quotes) {
|
||||
if (!quotes) return;
|
||||
quotes.forEach(function (q) {
|
||||
var row = findPosRow(q.key || q.position_key);
|
||||
var key = q.key || q.position_key;
|
||||
if (isPhoneLayout() && posMobileCache[key]) {
|
||||
var cached = posMobileCache[key];
|
||||
if (q.mark_price != null) cached.current_price = q.mark_price;
|
||||
if (q.float_pnl != null) cached.float_pnl = q.float_pnl;
|
||||
}
|
||||
var row = findPosRow(key);
|
||||
if (!row) return;
|
||||
var markEl = row.querySelector('.dash-p-mark');
|
||||
var pnlEl = row.querySelector('.dash-p-pnl');
|
||||
@@ -437,6 +708,9 @@
|
||||
pnlEl.className = 'dash-p-pnl ' + pnlClass(q.float_pnl);
|
||||
}
|
||||
});
|
||||
if (isPhoneLayout()) patchPositionsMobile(Object.keys(posMobileCache).map(function (k) {
|
||||
return posMobileCache[k];
|
||||
}));
|
||||
}
|
||||
|
||||
function applyPositionsData(data) {
|
||||
@@ -452,11 +726,12 @@
|
||||
equityEl.textContent = fmtMoney(data.capital);
|
||||
}
|
||||
var rows = positionRows(data);
|
||||
var keys = rows.map(function (r) {
|
||||
return r.key || r.position_key || ((r.symbol_code || '') + ':' + (r.direction || ''));
|
||||
var sig = rows.map(function (r) {
|
||||
var key = r.key || r.position_key || ((r.symbol_code || '') + ':' + (r.direction || ''));
|
||||
return key + '|' + (isBreakevenLocked(r) ? '1' : '0') + '|' + slText(r) + '|' + tpText(r) + '|' + String(r.lots);
|
||||
}).sort().join('|');
|
||||
var cachedKeys = Object.keys(posRowCache).sort().join('|');
|
||||
if (keys !== cachedKeys) {
|
||||
if (sig !== posRenderSig) {
|
||||
posRenderSig = sig;
|
||||
renderPositions(rows);
|
||||
} else {
|
||||
rows.forEach(function (r) {
|
||||
@@ -481,6 +756,7 @@
|
||||
if (slEl) slEl.textContent = slText(r);
|
||||
if (tpEl) tpEl.textContent = tpText(r);
|
||||
});
|
||||
if (isPhoneLayout()) patchPositionsMobile(rows);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,4 +832,7 @@
|
||||
|
||||
startPolling();
|
||||
connectPositionStream();
|
||||
initRiskToggle();
|
||||
initDetailModal();
|
||||
initMobileLists();
|
||||
})();
|
||||
|
||||
+17
-1
@@ -1024,7 +1024,23 @@
|
||||
var name = row.symbol_name || row.symbol || '';
|
||||
var code = row.symbol_code || '';
|
||||
var mainBadge = row.symbol_is_main ? ' <span class="badge planned pos-main-badge">主力</span>' : '';
|
||||
var inner = name + mainBadge;
|
||||
var beBadge = (function () {
|
||||
if (row.breakeven_locked) return ' <span class="badge profit dash-be-badge">已保本</span>';
|
||||
if ((row.trailing_r_locked || 0) >= 1) return ' <span class="badge profit dash-be-badge">已保本</span>';
|
||||
if (row.stop_loss == null || row.entry_price == null) return '';
|
||||
var entry = Number(row.entry_price);
|
||||
var sl = Number(row.stop_loss);
|
||||
if (isNaN(entry) || isNaN(sl)) return '';
|
||||
var tick = Number(row.tick_size) || Math.max(Math.abs(entry) * 1e-6, 0.01);
|
||||
var buf = tick * 2.5;
|
||||
var dir = (row.direction || 'long').toString().toLowerCase();
|
||||
if (Math.abs(sl - entry) > buf + tick) return '';
|
||||
if (dir === 'short' ? sl <= entry + tick * 0.05 : sl >= entry - tick * 0.05) {
|
||||
return ' <span class="badge profit dash-be-badge">已保本</span>';
|
||||
}
|
||||
return '';
|
||||
}());
|
||||
var inner = name + mainBadge + beBadge;
|
||||
if (code && String(name).toLowerCase() !== String(code).toLowerCase()) {
|
||||
inner += ' <span class="text-accent">' + code + '</span>';
|
||||
} else if (!name && code) {
|
||||
|
||||
Reference in New Issue
Block a user