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")),
|
"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:
|
def _schedule_recommend_refresh() -> None:
|
||||||
from db_conn import DB_PATH
|
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,
|
"pending_orders": pending_for_row,
|
||||||
"trailing_be": bool(mon.get("trailing_be")) if mon else False,
|
"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,
|
"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(
|
def _compose_pending_row(
|
||||||
|
|||||||
@@ -80,6 +80,7 @@
|
|||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dash-risk-doc-ref {
|
.dash-risk-doc-ref {
|
||||||
@@ -262,3 +263,187 @@
|
|||||||
padding: 0.35rem 0.4rem;
|
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 updatedEl = document.getElementById('dash-updated');
|
||||||
var riskReasonEl = document.getElementById('dash-risk-reason');
|
var riskReasonEl = document.getElementById('dash-risk-reason');
|
||||||
var riskGridEl = document.getElementById('dash-risk-grid');
|
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 pollTimer = null;
|
||||||
var positionSource = null;
|
var positionSource = null;
|
||||||
var posRowCache = {};
|
var posRowCache = {};
|
||||||
|
var posRenderSig = '';
|
||||||
|
var posMobileCache = {};
|
||||||
|
var keysMobileCache = {};
|
||||||
|
var lastPosRows = [];
|
||||||
|
var lastKeyRows = [];
|
||||||
var lastKeyIds = '';
|
var lastKeyIds = '';
|
||||||
var lastCloseHeadId = null;
|
var lastCloseHeadId = null;
|
||||||
var lastRiskPayload = 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) {
|
function fmtNum(v, digits) {
|
||||||
if (v === null || v === undefined || v === '') return '—';
|
if (v === null || v === undefined || v === '') return '—';
|
||||||
var n = Number(v);
|
var n = Number(v);
|
||||||
@@ -85,6 +195,7 @@
|
|||||||
titleInner += ' <span class="dash-symbol-ex text-muted">' + escHtml(exchange) + '</span>';
|
titleInner += ' <span class="dash-symbol-ex text-muted">' + escHtml(exchange) + '</span>';
|
||||||
}
|
}
|
||||||
titleInner += mainBadge;
|
titleInner += mainBadge;
|
||||||
|
titleInner += breakevenBadgeHtml(row);
|
||||||
if (code && String(name).toLowerCase() !== String(code).toLowerCase()) {
|
if (code && String(name).toLowerCase() !== String(code).toLowerCase()) {
|
||||||
titleInner += ' <span class="text-accent">' + escHtml(code) + '</span>';
|
titleInner += ' <span class="text-accent">' + escHtml(code) + '</span>';
|
||||||
} else if (!name && code) {
|
} else if (!name && code) {
|
||||||
@@ -358,13 +469,18 @@
|
|||||||
if (!ids) {
|
if (!ids) {
|
||||||
keysBody.innerHTML = '<tr><td colspan="8" class="text-muted">暂无关键位监控</td></tr>';
|
keysBody.innerHTML = '<tr><td colspan="8" class="text-muted">暂无关键位监控</td></tr>';
|
||||||
lastKeyIds = '';
|
lastKeyIds = '';
|
||||||
|
lastKeyRows = [];
|
||||||
|
if (keysMobileList) keysMobileList.innerHTML = '<div class="dash-mobile-empty">暂无关键位监控</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ids !== lastKeyIds) {
|
if (ids !== lastKeyIds) {
|
||||||
lastKeyIds = ids;
|
lastKeyIds = ids;
|
||||||
|
lastKeyRows = keys || [];
|
||||||
renderKeys(keys);
|
renderKeys(keys);
|
||||||
|
if (isPhoneLayout()) renderKeysMobile(lastKeyRows);
|
||||||
} else {
|
} else {
|
||||||
patchKeys(keys);
|
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) {
|
function renderPositions(rows) {
|
||||||
|
lastPosRows = rows || [];
|
||||||
|
if (isPhoneLayout()) {
|
||||||
|
renderPositionsMobile(lastPosRows);
|
||||||
|
}
|
||||||
if (!posBody) return;
|
if (!posBody) return;
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
posBody.innerHTML = '<tr><td colspan="9" class="text-muted">暂无持仓</td></tr>';
|
posBody.innerHTML = '<tr><td colspan="9" class="text-muted">暂无持仓</td></tr>';
|
||||||
posRowCache = {};
|
posRowCache = {};
|
||||||
|
posRenderSig = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
posRowCache = {};
|
posRowCache = {};
|
||||||
@@ -427,7 +692,13 @@
|
|||||||
function patchPositionQuotes(quotes) {
|
function patchPositionQuotes(quotes) {
|
||||||
if (!quotes) return;
|
if (!quotes) return;
|
||||||
quotes.forEach(function (q) {
|
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;
|
if (!row) return;
|
||||||
var markEl = row.querySelector('.dash-p-mark');
|
var markEl = row.querySelector('.dash-p-mark');
|
||||||
var pnlEl = row.querySelector('.dash-p-pnl');
|
var pnlEl = row.querySelector('.dash-p-pnl');
|
||||||
@@ -437,6 +708,9 @@
|
|||||||
pnlEl.className = 'dash-p-pnl ' + pnlClass(q.float_pnl);
|
pnlEl.className = 'dash-p-pnl ' + pnlClass(q.float_pnl);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (isPhoneLayout()) patchPositionsMobile(Object.keys(posMobileCache).map(function (k) {
|
||||||
|
return posMobileCache[k];
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyPositionsData(data) {
|
function applyPositionsData(data) {
|
||||||
@@ -452,11 +726,12 @@
|
|||||||
equityEl.textContent = fmtMoney(data.capital);
|
equityEl.textContent = fmtMoney(data.capital);
|
||||||
}
|
}
|
||||||
var rows = positionRows(data);
|
var rows = positionRows(data);
|
||||||
var keys = rows.map(function (r) {
|
var sig = rows.map(function (r) {
|
||||||
return r.key || r.position_key || ((r.symbol_code || '') + ':' + (r.direction || ''));
|
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('|');
|
}).sort().join('|');
|
||||||
var cachedKeys = Object.keys(posRowCache).sort().join('|');
|
if (sig !== posRenderSig) {
|
||||||
if (keys !== cachedKeys) {
|
posRenderSig = sig;
|
||||||
renderPositions(rows);
|
renderPositions(rows);
|
||||||
} else {
|
} else {
|
||||||
rows.forEach(function (r) {
|
rows.forEach(function (r) {
|
||||||
@@ -481,6 +756,7 @@
|
|||||||
if (slEl) slEl.textContent = slText(r);
|
if (slEl) slEl.textContent = slText(r);
|
||||||
if (tpEl) tpEl.textContent = tpText(r);
|
if (tpEl) tpEl.textContent = tpText(r);
|
||||||
});
|
});
|
||||||
|
if (isPhoneLayout()) patchPositionsMobile(rows);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,4 +832,7 @@
|
|||||||
|
|
||||||
startPolling();
|
startPolling();
|
||||||
connectPositionStream();
|
connectPositionStream();
|
||||||
|
initRiskToggle();
|
||||||
|
initDetailModal();
|
||||||
|
initMobileLists();
|
||||||
})();
|
})();
|
||||||
|
|||||||
+17
-1
@@ -1024,7 +1024,23 @@
|
|||||||
var name = row.symbol_name || row.symbol || '';
|
var name = row.symbol_name || row.symbol || '';
|
||||||
var code = row.symbol_code || '';
|
var code = row.symbol_code || '';
|
||||||
var mainBadge = row.symbol_is_main ? ' <span class="badge planned pos-main-badge">主力</span>' : '';
|
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()) {
|
if (code && String(name).toLowerCase() !== String(code).toLowerCase()) {
|
||||||
inner += ' <span class="text-accent">' + code + '</span>';
|
inner += ' <span class="text-accent">' + code + '</span>';
|
||||||
} else if (!name && code) {
|
} else if (!name && code) {
|
||||||
|
|||||||
@@ -32,22 +32,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card dashboard-section dashboard-risk-card">
|
<div class="card dashboard-section dashboard-risk-card" id="dash-risk-card">
|
||||||
<h2 class="dashboard-risk-heading">
|
<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 %}
|
{% 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 %}
|
{% else %}
|
||||||
<span class="text-muted dash-risk-doc-ref">· 详见 <code>docs/风控说明.md</code></span>
|
<span class="text-muted dash-risk-doc-ref">· 详见 <code>docs/风控说明.md</code></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<span class="dash-toggle-icon" aria-hidden="true">▼</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p class="dashboard-risk-reason" id="dash-risk-reason">加载中…</p>
|
<div class="dash-risk-body" id="dash-risk-body">
|
||||||
<div class="stat-grid stat-grid-summary dashboard-risk-grid" id="dash-risk-grid"></div>
|
<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>
|
||||||
|
|
||||||
<div class="card dashboard-section">
|
<div class="card dashboard-section">
|
||||||
<h2>持仓信息</h2>
|
<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">
|
<table class="dashboard-table" id="dash-positions-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -71,7 +75,8 @@
|
|||||||
|
|
||||||
<div class="card dashboard-section">
|
<div class="card dashboard-section">
|
||||||
<h2>关键位监控</h2>
|
<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">
|
<table class="dashboard-table" id="dash-keys-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -116,6 +121,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 %}
|
{% endblock %}
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script src="{{ url_for('static', filename='js/dashboard.js') }}?v={{ asset_v }}"></script>
|
<script src="{{ url_for('static', filename='js/dashboard.js') }}?v={{ asset_v }}"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user