From cd2dfac2ba18b340d3375d5b4858a7a8aed91752 Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 17 Jul 2026 12:14:03 +0800 Subject: [PATCH] Hide empty dashboard sections and render data as tables. Orders/keys/strategy/options/hedge only appear when they have rows, shown in table layout. Co-authored-by: Cursor --- lib/common/static/instance_dashboard.js | 247 ++++++++++++++++---- lib/common/static/instance_page.css | 15 +- lib/instance/templates/dashboard_panel.html | 2 +- lib/instance/templates/embed_shell.html | 4 +- lib/instance/templates/index.html | 4 +- 5 files changed, 217 insertions(+), 55 deletions(-) diff --git a/lib/common/static/instance_dashboard.js b/lib/common/static/instance_dashboard.js index 5f905e4..9a0896f 100644 --- a/lib/common/static/instance_dashboard.js +++ b/lib/common/static/instance_dashboard.js @@ -1,5 +1,6 @@ /** - * 实例数据看板:拉 /api/instance/dashboard 渲染只读区块. + * 实例数据看板:拉 /api/instance/dashboard 渲染只读表格. + * 各区块无数据时不展示;有数据按表格展示. */ (function (global) { const SECTION_ORDER = ["orders", "keys", "strategy", "options", "hedge_plan"]; @@ -28,9 +29,9 @@ } function fmtPnl(v) { - if (v == null || v === "") return ""; + if (v == null || v === "") return "—"; const n = Number(v); - if (!Number.isFinite(n)) return ""; + if (!Number.isFinite(n)) return "—"; const cls = n > 0 ? "pos-pnl-profit" : n < 0 ? "pos-pnl-loss" : ""; const sign = n > 0 ? "+" : ""; return '' + sign + n.toFixed(2) + "U"; @@ -53,46 +54,197 @@ location.href = path; } - function renderItems(items) { - if (!items || !items.length) { - return '

暂无

'; - } + function tableWrap(headers, rowsHtml) { return ( - '
' + - items - .map(function (it) { - const meta = []; - if (it.entry != null) meta.push("入场 " + fmtNum(it.entry)); - if (it.stop_loss != null) meta.push("止损 " + fmtNum(it.stop_loss)); - if (it.take_profit != null) meta.push("止盈 " + fmtNum(it.take_profit)); - if (it.upper != null || it.lower != null) { - meta.push("上 " + fmtNum(it.upper) + " / 下 " + fmtNum(it.lower)); - } - const pnlHtml = fmtPnl(it.pnl); - if (pnlHtml) meta.push(pnlHtml); - return ( - '" - ); - }) - .join("") + - "
" + '
' + + '' + + "" + + headers.map(function (h) { + return ""; + }).join("") + + "" + + "" + + rowsHtml + + "
" + escapeHtml(h) + "
" ); } + function rowClickAttrs(tab) { + return ' class="inst-dash-row" data-dash-tab="' + escapeHtml(tab || "") + '" role="link" tabindex="0"'; + } + + function renderOrdersTable(items) { + const rows = items + .map(function (it) { + return ( + "" + + "" + + escapeHtml(it.symbol || "-") + + "" + + "" + + escapeHtml(it.direction_label || it.direction || "-") + + "" + + "" + + escapeHtml(it.subtitle || "—") + + "" + + "" + + fmtNum(it.entry) + + "" + + "" + + fmtNum(it.stop_loss) + + "" + + "" + + fmtNum(it.take_profit) + + "" + + "" + ); + }) + .join(""); + return tableWrap(["合约", "方向", "类型", "入场", "止损", "止盈"], rows); + } + + function renderKeysTable(items) { + const rows = items + .map(function (it) { + return ( + "" + + "" + + escapeHtml(it.symbol || "-") + + "" + + "" + + escapeHtml(it.direction_label || it.direction || "-") + + "" + + "" + + escapeHtml(it.subtitle || "—") + + "" + + "" + + fmtNum(it.upper) + + "" + + "" + + fmtNum(it.lower) + + "" + + "" + ); + }) + .join(""); + return tableWrap(["合约", "方向", "信号", "上沿", "下沿"], rows); + } + + function renderStrategyTable(items) { + const rows = items + .map(function (it) { + const kindLabel = it.kind === "roll" ? "顺势加仓" : it.kind === "trend" ? "趋势回调" : "策略"; + return ( + "" + + "" + + escapeHtml(kindLabel) + + "" + + "" + + escapeHtml(it.symbol || "-") + + "" + + "" + + escapeHtml(it.direction_label || it.direction || "-") + + "" + + "" + + escapeHtml(it.status || "—") + + "" + + "" + + fmtNum(it.entry) + + "" + + "" + ); + }) + .join(""); + return tableWrap(["类型", "合约", "方向", "状态", "入场"], rows); + } + + function renderOptionsTable(items) { + const rows = items + .map(function (it) { + const opt = + String(it.opt_type || "").toUpperCase() === "C" + ? "Call" + : String(it.opt_type || "").toUpperCase() === "P" + ? "Put" + : it.opt_type || "—"; + return ( + "" + + "" + + escapeHtml(it.inst_id || it.title || "-") + + "" + + "" + + escapeHtml(opt) + + "" + + "" + + fmtNum(it.pos) + + "" + + "" + + fmtPnl(it.pnl) + + "" + + "" + ); + }) + .join(""); + return tableWrap(["合约", "类型", "张数", "盈亏"], rows); + } + + function renderHedgeTable(items) { + const rows = items + .map(function (it) { + return ( + "" + + "#" + + escapeHtml(it.id != null ? it.id : "—") + + "" + + "" + + escapeHtml(it.underlying || "-") + + "" + + "" + + escapeHtml(it.plan_type || "—") + + "" + + "" + + escapeHtml(it.status || "—") + + "" + + "" + + escapeHtml(it.subtitle || "—") + + "" + + "" + ); + }) + .join(""); + return tableWrap(["ID", "标的", "计划类型", "状态", "说明"], rows); + } + + function renderTable(key, items) { + if (key === "orders") return renderOrdersTable(items); + if (key === "keys") return renderKeysTable(items); + if (key === "strategy") return renderStrategyTable(items); + if (key === "options") return renderOptionsTable(items); + if (key === "hedge_plan") return renderHedgeTable(items); + return ""; + } + + function sectionHasData(sec) { + if (!sec) return false; + const count = Number(sec.count); + if (Number.isFinite(count) && count > 0) return true; + return Array.isArray(sec.items) && sec.items.length > 0; + } + function renderSection(key, sec) { - if (!sec) return ""; - if ((key === "options" || key === "hedge_plan") && !sec.visible) return ""; - const count = Number(sec.count) || 0; + if (!sectionHasData(sec)) return ""; + const items = sec.items || []; + const count = Number(sec.count) || items.length; return ( '
打开' + "" + - renderItems(sec.items || []) + + renderTable(key, items) + "
" ); } function bindClicks(el) { if (!el) return; - el.querySelectorAll("[data-dash-tab]").forEach(function (btn) { - btn.addEventListener("click", function () { - goTab(btn.getAttribute("data-dash-tab")); + el.querySelectorAll("[data-dash-tab]").forEach(function (node) { + const handler = function () { + goTab(node.getAttribute("data-dash-tab")); + }; + node.addEventListener("click", handler); + node.addEventListener("keydown", function (ev) { + if (ev.key === "Enter" || ev.key === " ") { + ev.preventDefault(); + handler(); + } }); }); } @@ -140,9 +299,11 @@ } if (updated) updated.textContent = "更新 " + (data.updated_at || "—"); if (sections) { - sections.innerHTML = SECTION_ORDER.map(function (k) { + const html = SECTION_ORDER.map(function (k) { return renderSection(k, data[k]); }).join(""); + sections.innerHTML = + html || '

当前无活跃监控与持仓

'; bindClicks(sections); } if (status) status.textContent = ""; diff --git a/lib/common/static/instance_page.css b/lib/common/static/instance_page.css index d41025e..00bca12 100644 --- a/lib/common/static/instance_page.css +++ b/lib/common/static/instance_page.css @@ -279,10 +279,11 @@ .inst-dash-section-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px} .inst-dash-section-head h3{margin:0;font-size:.95rem;color:#dbe4ff} .inst-dash-count{display:inline-block;min-width:1.4em;padding:1px 7px;margin-left:4px;border-radius:999px;background:#1f3a5a;color:#8fc8ff;font-size:.75rem;font-weight:600} -.inst-dash-empty{margin:0;padding:12px;text-align:center;border:1px dashed #2a3348;border-radius:8px;font-size:.84rem} -.inst-dash-list{display:flex;flex-direction:column;gap:8px} -.inst-dash-item{display:block;width:100%;text-align:left;padding:10px 12px;border-radius:8px;border:1px solid #2a3150;background:#1a2034;color:#eaeaea;cursor:pointer} -.inst-dash-item:hover{border-color:#3d4f78;background:#1e2740} -.inst-dash-item-title{font-weight:600;font-size:.9rem;color:#dbe4ff} -.inst-dash-item-sub{font-size:.78rem;margin-top:3px} -.inst-dash-item-meta{font-size:.78rem;margin-top:6px;color:#a8b0d8;font-variant-numeric:tabular-nums} +.inst-dash-empty{margin:0;padding:14px;text-align:center;border:1px dashed #2a3348;border-radius:8px;font-size:.84rem} +.inst-dash-table-wrap{overflow:auto;border:1px solid #2a3150;border-radius:8px} +.inst-dash-table{width:100%;border-collapse:collapse;font-size:.84rem} +.inst-dash-table th,.inst-dash-table td{padding:8px 10px;text-align:left;border-bottom:1px solid #25253b;white-space:nowrap} +.inst-dash-table th{color:#a9a9ff;background:#151a2a;font-weight:600} +.inst-dash-table tbody tr:last-child td{border-bottom:none} +.inst-dash-table tbody tr.inst-dash-row{cursor:pointer} +.inst-dash-table tbody tr.inst-dash-row:hover{background:#1e2740} diff --git a/lib/instance/templates/dashboard_panel.html b/lib/instance/templates/dashboard_panel.html index 2447aea..a47bafe 100644 --- a/lib/instance/templates/dashboard_panel.html +++ b/lib/instance/templates/dashboard_panel.html @@ -3,7 +3,7 @@

数据看板

-

本户活跃监控总览 · 只读 · 期权/对冲有数据才显示

+

本户活跃监控总览 · 只读 · 无数据的区块不显示 · 有数据按表格展示

diff --git a/lib/instance/templates/embed_shell.html b/lib/instance/templates/embed_shell.html index 55b34ff..4e63144 100644 --- a/lib/instance/templates/embed_shell.html +++ b/lib/instance/templates/embed_shell.html @@ -6,7 +6,7 @@ - + @@ -110,7 +110,7 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj {% include 'embed_boot_scripts.html' %} - + diff --git a/lib/instance/templates/index.html b/lib/instance/templates/index.html index ba4cd9d..804e1bb 100644 --- a/lib/instance/templates/index.html +++ b/lib/instance/templates/index.html @@ -16,7 +16,7 @@ {{ pwa_app_name }} - + @@ -1992,7 +1992,7 @@ tickOrderHoldDurations(); setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }}); - +