Improve dashboard responsive layout, collapsible risk section, and breakeven badge.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -166,6 +166,33 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
"symbol_is_main": bool(meta.get("is_main")),
|
||||
}
|
||||
|
||||
def _breakeven_locked(
|
||||
*,
|
||||
entry: Optional[float],
|
||||
stop_loss: Optional[float],
|
||||
direction: str,
|
||||
tick_size: Optional[float] = None,
|
||||
trailing_r_locked: int = 0,
|
||||
) -> bool:
|
||||
if int(trailing_r_locked or 0) >= 1:
|
||||
return True
|
||||
if entry is None or stop_loss is None:
|
||||
return False
|
||||
try:
|
||||
entry_f = float(entry)
|
||||
sl_f = float(stop_loss)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
if entry_f <= 0:
|
||||
return False
|
||||
tick = float(tick_size or 0) or max(abs(entry_f) * 1e-6, 0.01)
|
||||
buf = tick * max(2, get_trailing_be_tick_buffer(get_setting))
|
||||
d = (direction or "long").strip().lower()
|
||||
near = abs(sl_f - entry_f) <= buf + tick
|
||||
if d == "long":
|
||||
return near and sl_f >= entry_f - tick * 0.05
|
||||
return near and sl_f <= entry_f + tick * 0.05
|
||||
|
||||
def _schedule_recommend_refresh() -> None:
|
||||
from db_conn import DB_PATH
|
||||
|
||||
@@ -1208,6 +1235,13 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
"pending_orders": pending_for_row,
|
||||
"trailing_be": bool(mon.get("trailing_be")) if mon else False,
|
||||
"trailing_r_locked": int(mon.get("trailing_r_locked") or 0) if mon else 0,
|
||||
"breakeven_locked": _breakeven_locked(
|
||||
entry=entry,
|
||||
stop_loss=sl,
|
||||
direction=direction,
|
||||
tick_size=tick.get("tick_size"),
|
||||
trailing_r_locked=int(mon.get("trailing_r_locked") or 0) if mon else 0,
|
||||
),
|
||||
}
|
||||
|
||||
def _compose_pending_row(
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dash-risk-doc-ref {
|
||||
@@ -262,3 +263,187 @@
|
||||
padding: 0.35rem 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 风控折叠 / 平板两行 ---- */
|
||||
.dash-section-toggle {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.dash-section-toggle-label {
|
||||
margin-right: 0.15rem;
|
||||
}
|
||||
|
||||
.dash-toggle-icon {
|
||||
margin-left: auto;
|
||||
font-size: 0.68rem;
|
||||
color: var(--text-muted);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.dashboard-risk-card.is-expanded .dash-toggle-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
html:is([data-layout="phone"], [data-layout="tablet"], .layout-phone, .layout-tablet)
|
||||
.dashboard-risk-card:not(.is-expanded) .dash-risk-body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html[data-layout="tablet"] .dashboard-risk-grid,
|
||||
html.layout-tablet .dashboard-risk-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
overflow-x: visible;
|
||||
}
|
||||
|
||||
html[data-layout="tablet"] .dashboard-risk-item,
|
||||
html.layout-tablet .dashboard-risk-item {
|
||||
flex: none;
|
||||
min-width: 0;
|
||||
border-right: 1px solid var(--table-border);
|
||||
border-bottom: 1px solid var(--table-border);
|
||||
}
|
||||
|
||||
html[data-layout="tablet"] .dashboard-risk-item:nth-child(7n),
|
||||
html.layout-tablet .dashboard-risk-item:nth-child(7n) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
html[data-layout="tablet"] .dashboard-risk-item:nth-last-child(-n+6),
|
||||
html.layout-tablet .dashboard-risk-item:nth-last-child(-n+6) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
html:is([data-layout="phone"], .layout-phone) .dashboard-risk-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
overflow-x: visible;
|
||||
}
|
||||
|
||||
html:is([data-layout="phone"], .layout-phone) .dashboard-risk-item {
|
||||
flex: none;
|
||||
min-width: 0;
|
||||
border-right: 1px solid var(--table-border);
|
||||
border-bottom: 1px solid var(--table-border);
|
||||
}
|
||||
|
||||
html:is([data-layout="phone"], .layout-phone) .dashboard-risk-item:nth-child(2n) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
html:is([data-layout="phone"], .layout-phone) .dashboard-risk-item:nth-last-child(-n+1) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* ---- 持仓 / 关键位:桌面平板最多 3 行后滚动 ---- */
|
||||
html:not([data-layout="phone"]):not(.layout-phone) .dash-pos-table-wrap,
|
||||
html:not([data-layout="phone"]):not(.layout-phone) .dash-keys-table-wrap {
|
||||
max-height: 12rem;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
html:not([data-layout="phone"]):not(.layout-phone) .dash-pos-table-wrap thead th,
|
||||
html:not([data-layout="phone"]):not(.layout-phone) .dash-keys-table-wrap thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
background: var(--card-inner);
|
||||
}
|
||||
|
||||
/* ---- 手机简要列表 ---- */
|
||||
.dash-mobile-list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html:is([data-layout="phone"], .layout-phone) .dash-mobile-list {
|
||||
display: block;
|
||||
}
|
||||
|
||||
html:is([data-layout="phone"], .layout-phone) .dash-pos-table-wrap,
|
||||
html:is([data-layout="phone"], .layout-phone) .dash-keys-table-wrap {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dash-mobile-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border: 1px solid var(--table-border);
|
||||
border-radius: 10px;
|
||||
background: var(--card-inner);
|
||||
padding: 0.65rem 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.dash-mobile-item:hover,
|
||||
.dash-mobile-item:focus-visible {
|
||||
border-color: var(--accent);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 1px rgba(76, 194, 255, 0.15);
|
||||
}
|
||||
|
||||
.dash-mobile-item-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.45rem;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.dash-mobile-item-title {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dash-mobile-item-meta {
|
||||
font-size: 0.74rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.dash-mobile-item-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.35rem;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.dash-mobile-chevron {
|
||||
font-size: 0.72rem;
|
||||
color: var(--accent);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dash-mobile-empty {
|
||||
padding: 0.85rem 0.35rem;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.dash-be-badge {
|
||||
font-size: 0.66rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dash-detail-modal {
|
||||
max-width: 520px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dash-detail-modal .modal-grid .item.wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.dash-detail-modal .modal-actions {
|
||||
margin-top: 1rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
+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) {
|
||||
|
||||
@@ -32,22 +32,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card dashboard-section dashboard-risk-card">
|
||||
<h2 class="dashboard-risk-heading">
|
||||
风控说明
|
||||
<div class="card dashboard-section dashboard-risk-card" id="dash-risk-card">
|
||||
<h2 class="dashboard-risk-heading dash-section-toggle" id="dash-risk-toggle" role="button" tabindex="0" aria-expanded="false" aria-controls="dash-risk-body">
|
||||
<span class="dash-section-toggle-label">风控说明</span>
|
||||
{% if nav_items.risk_guide %}
|
||||
<a class="dash-risk-doc-link" href="{{ url_for('risk_guide') }}">完整说明</a>
|
||||
<a class="dash-risk-doc-link" href="{{ url_for('risk_guide') }}" onclick="event.stopPropagation()">完整说明</a>
|
||||
{% else %}
|
||||
<span class="text-muted dash-risk-doc-ref">· 详见 <code>docs/风控说明.md</code></span>
|
||||
{% endif %}
|
||||
<span class="dash-toggle-icon" aria-hidden="true">▼</span>
|
||||
</h2>
|
||||
<p class="dashboard-risk-reason" id="dash-risk-reason">加载中…</p>
|
||||
<div class="stat-grid stat-grid-summary dashboard-risk-grid" id="dash-risk-grid"></div>
|
||||
<div class="dash-risk-body" id="dash-risk-body">
|
||||
<p class="dashboard-risk-reason" id="dash-risk-reason">加载中…</p>
|
||||
<div class="stat-grid stat-grid-summary dashboard-risk-grid" id="dash-risk-grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card dashboard-section">
|
||||
<h2>持仓信息</h2>
|
||||
<div class="card-scroll">
|
||||
<div class="dash-mobile-list" id="dash-pos-mobile-list"></div>
|
||||
<div class="dash-pos-table-wrap card-scroll">
|
||||
<table class="dashboard-table" id="dash-positions-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -71,7 +75,8 @@
|
||||
|
||||
<div class="card dashboard-section">
|
||||
<h2>关键位监控</h2>
|
||||
<div class="card-scroll">
|
||||
<div class="dash-mobile-list" id="dash-keys-mobile-list"></div>
|
||||
<div class="dash-keys-table-wrap card-scroll">
|
||||
<table class="dashboard-table" id="dash-keys-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -116,6 +121,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-mask" id="dash-detail-modal" hidden>
|
||||
<div class="modal-box dash-detail-modal">
|
||||
<h3 id="dash-detail-title">详情</h3>
|
||||
<div class="modal-grid" id="dash-detail-grid"></div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn-primary" id="dash-detail-close">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% block extra_js %}
|
||||
<script src="{{ url_for('static', filename='js/dashboard.js') }}?v={{ asset_v }}"></script>
|
||||
|
||||
Reference in New Issue
Block a user