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 <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-17 12:14:03 +08:00
parent c7aae67881
commit cd2dfac2ba
5 changed files with 217 additions and 55 deletions
+204 -43
View File
@@ -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 '<span class="' + cls + '">' + sign + n.toFixed(2) + "U</span>";
@@ -53,46 +54,197 @@
location.href = path;
}
function renderItems(items) {
if (!items || !items.length) {
return '<p class="inst-dash-empty muted">暂无</p>';
}
function tableWrap(headers, rowsHtml) {
return (
'<div class="inst-dash-list">' +
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 (
'<button type="button" class="inst-dash-item" data-dash-tab="' +
escapeHtml(it.tab || "") +
'">' +
'<div class="inst-dash-item-title">' +
escapeHtml(it.title || "-") +
"</div>" +
(it.subtitle
? '<div class="inst-dash-item-sub muted">' + escapeHtml(it.subtitle) + "</div>"
: "") +
(meta.length ? '<div class="inst-dash-item-meta">' + meta.join(" · ") + "</div>" : "") +
"</button>"
);
})
.join("") +
"</div>"
'<div class="inst-dash-table-wrap">' +
'<table class="inst-dash-table">' +
"<thead><tr>" +
headers.map(function (h) {
return "<th>" + escapeHtml(h) + "</th>";
}).join("") +
"</tr></thead>" +
"<tbody>" +
rowsHtml +
"</tbody></table></div>"
);
}
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 (
"<tr" +
rowClickAttrs(it.tab || "trade") +
">" +
"<td>" +
escapeHtml(it.symbol || "-") +
"</td>" +
"<td>" +
escapeHtml(it.direction_label || it.direction || "-") +
"</td>" +
"<td>" +
escapeHtml(it.subtitle || "—") +
"</td>" +
"<td>" +
fmtNum(it.entry) +
"</td>" +
"<td>" +
fmtNum(it.stop_loss) +
"</td>" +
"<td>" +
fmtNum(it.take_profit) +
"</td>" +
"</tr>"
);
})
.join("");
return tableWrap(["合约", "方向", "类型", "入场", "止损", "止盈"], rows);
}
function renderKeysTable(items) {
const rows = items
.map(function (it) {
return (
"<tr" +
rowClickAttrs(it.tab || "key_monitor") +
">" +
"<td>" +
escapeHtml(it.symbol || "-") +
"</td>" +
"<td>" +
escapeHtml(it.direction_label || it.direction || "-") +
"</td>" +
"<td>" +
escapeHtml(it.subtitle || "—") +
"</td>" +
"<td>" +
fmtNum(it.upper) +
"</td>" +
"<td>" +
fmtNum(it.lower) +
"</td>" +
"</tr>"
);
})
.join("");
return tableWrap(["合约", "方向", "信号", "上沿", "下沿"], rows);
}
function renderStrategyTable(items) {
const rows = items
.map(function (it) {
const kindLabel = it.kind === "roll" ? "顺势加仓" : it.kind === "trend" ? "趋势回调" : "策略";
return (
"<tr" +
rowClickAttrs(it.tab || "strategy") +
">" +
"<td>" +
escapeHtml(kindLabel) +
"</td>" +
"<td>" +
escapeHtml(it.symbol || "-") +
"</td>" +
"<td>" +
escapeHtml(it.direction_label || it.direction || "-") +
"</td>" +
"<td>" +
escapeHtml(it.status || "—") +
"</td>" +
"<td>" +
fmtNum(it.entry) +
"</td>" +
"</tr>"
);
})
.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 (
"<tr" +
rowClickAttrs(it.tab || "options") +
">" +
"<td>" +
escapeHtml(it.inst_id || it.title || "-") +
"</td>" +
"<td>" +
escapeHtml(opt) +
"</td>" +
"<td>" +
fmtNum(it.pos) +
"</td>" +
"<td>" +
fmtPnl(it.pnl) +
"</td>" +
"</tr>"
);
})
.join("");
return tableWrap(["合约", "类型", "张数", "盈亏"], rows);
}
function renderHedgeTable(items) {
const rows = items
.map(function (it) {
return (
"<tr" +
rowClickAttrs(it.tab || "hedge_plan") +
">" +
"<td>#" +
escapeHtml(it.id != null ? it.id : "—") +
"</td>" +
"<td>" +
escapeHtml(it.underlying || "-") +
"</td>" +
"<td>" +
escapeHtml(it.plan_type || "—") +
"</td>" +
"<td>" +
escapeHtml(it.status || "—") +
"</td>" +
"<td>" +
escapeHtml(it.subtitle || "—") +
"</td>" +
"</tr>"
);
})
.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 (
'<section class="inst-dash-section" data-dash-section="' +
escapeHtml(key) +
@@ -107,16 +259,23 @@
escapeHtml(sec.tab || "") +
'">打开</button>' +
"</div>" +
renderItems(sec.items || []) +
renderTable(key, items) +
"</section>"
);
}
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 || '<p class="inst-dash-empty muted">当前无活跃监控与持仓</p>';
bindClicks(sections);
}
if (status) status.textContent = "";