Add instance data dashboard (default off).
Read-only overview of live orders, key monitors, and strategy; show options/hedge only when present. Toggle via system settings nav prefs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* 实例数据看板:拉 /api/instance/dashboard 渲染只读区块.
|
||||
*/
|
||||
(function (global) {
|
||||
const SECTION_ORDER = ["orders", "keys", "strategy", "options", "hedge_plan"];
|
||||
let loading = false;
|
||||
let timer = null;
|
||||
|
||||
function root() {
|
||||
const active = document.querySelector('.embed-tab-pane.is-active-pane [data-inst-dashboard="1"]');
|
||||
if (active) return active;
|
||||
return document.getElementById("instance-dashboard");
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s == null ? "" : s)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function fmtNum(v) {
|
||||
if (v == null || v === "") return "—";
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n)) return escapeHtml(v);
|
||||
return String(n);
|
||||
}
|
||||
|
||||
function fmtPnl(v) {
|
||||
if (v == null || v === "") return "";
|
||||
const n = Number(v);
|
||||
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>";
|
||||
}
|
||||
|
||||
function goTab(tab) {
|
||||
if (!tab) return;
|
||||
if (global.InstanceEmbed && typeof global.InstanceEmbed.loadTab === "function") {
|
||||
global.InstanceEmbed.loadTab(tab);
|
||||
return;
|
||||
}
|
||||
const pathMap = {
|
||||
trade: "/trade",
|
||||
key_monitor: "/key_monitor",
|
||||
strategy: "/strategy",
|
||||
options: "/options",
|
||||
hedge_plan: "/hedge-plan",
|
||||
};
|
||||
const path = pathMap[tab] || "/" + tab;
|
||||
location.href = path;
|
||||
}
|
||||
|
||||
function renderItems(items) {
|
||||
if (!items || !items.length) {
|
||||
return '<p class="inst-dash-empty muted">暂无</p>';
|
||||
}
|
||||
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>"
|
||||
);
|
||||
}
|
||||
|
||||
function renderSection(key, sec) {
|
||||
if (!sec) return "";
|
||||
if ((key === "options" || key === "hedge_plan") && !sec.visible) return "";
|
||||
const count = Number(sec.count) || 0;
|
||||
return (
|
||||
'<section class="inst-dash-section" data-dash-section="' +
|
||||
escapeHtml(key) +
|
||||
'">' +
|
||||
'<div class="inst-dash-section-head">' +
|
||||
"<h3>" +
|
||||
escapeHtml(sec.title || key) +
|
||||
' <span class="inst-dash-count">' +
|
||||
count +
|
||||
"</span></h3>" +
|
||||
'<button type="button" class="btn-sm inst-dash-goto" data-dash-tab="' +
|
||||
escapeHtml(sec.tab || "") +
|
||||
'">打开</button>' +
|
||||
"</div>" +
|
||||
renderItems(sec.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"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function load(opts) {
|
||||
const el = root();
|
||||
if (!el) return;
|
||||
const status = el.querySelector("#inst-dash-status") || document.getElementById("inst-dash-status");
|
||||
const sections = el.querySelector("#inst-dash-sections") || document.getElementById("inst-dash-sections");
|
||||
const updated = el.querySelector("#inst-dash-updated") || document.getElementById("inst-dash-updated");
|
||||
if (loading) return;
|
||||
loading = true;
|
||||
if (status && !(opts && opts.silent)) status.textContent = "加载中…";
|
||||
try {
|
||||
const res = await fetch("/api/instance/dashboard", { credentials: "same-origin" });
|
||||
const data = await res.json().catch(function () {
|
||||
return {};
|
||||
});
|
||||
if (!res.ok || !data.ok) {
|
||||
throw new Error(data.msg || res.statusText || "加载失败");
|
||||
}
|
||||
if (updated) updated.textContent = "更新 " + (data.updated_at || "—");
|
||||
if (sections) {
|
||||
sections.innerHTML = SECTION_ORDER.map(function (k) {
|
||||
return renderSection(k, data[k]);
|
||||
}).join("");
|
||||
bindClicks(sections);
|
||||
}
|
||||
if (status) status.textContent = "";
|
||||
} catch (e) {
|
||||
if (status) status.textContent = e.message || "加载失败";
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function stopAuto() {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startAuto() {
|
||||
stopAuto();
|
||||
timer = setInterval(function () {
|
||||
const el = root();
|
||||
if (!el) return;
|
||||
const pane = el.closest(".embed-tab-pane");
|
||||
if (pane && !pane.classList.contains("is-active-pane")) return;
|
||||
load({ silent: true });
|
||||
}, 15000);
|
||||
}
|
||||
|
||||
function init(force) {
|
||||
const el = root();
|
||||
if (!el) return;
|
||||
if (!force && el.getAttribute("data-dash-booted") === "1") {
|
||||
load({ silent: true });
|
||||
startAuto();
|
||||
return;
|
||||
}
|
||||
el.setAttribute("data-dash-booted", "1");
|
||||
const btn = el.querySelector("#inst-dash-refresh") || document.getElementById("inst-dash-refresh");
|
||||
if (btn && !btn.getAttribute("data-bound")) {
|
||||
btn.setAttribute("data-bound", "1");
|
||||
btn.addEventListener("click", function () {
|
||||
load({});
|
||||
});
|
||||
}
|
||||
load({});
|
||||
startAuto();
|
||||
}
|
||||
|
||||
function refreshSoft(opts) {
|
||||
load(Object.assign({ silent: true }, opts || {}));
|
||||
}
|
||||
|
||||
global.InstanceDashboard = {
|
||||
init: init,
|
||||
refreshSoft: refreshSoft,
|
||||
load: load,
|
||||
stopAuto: stopAuto,
|
||||
};
|
||||
})(window);
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
(function (global) {
|
||||
const TAB_PATH = {
|
||||
dashboard: "/dashboard",
|
||||
key_monitor: "/key_monitor",
|
||||
trade: "/trade",
|
||||
strategy: "/strategy",
|
||||
@@ -99,6 +100,9 @@
|
||||
if (!revisit && tab === "key_monitor" && global.KeyMonitorForm && typeof global.KeyMonitorForm.init === "function") {
|
||||
global.KeyMonitorForm.init();
|
||||
}
|
||||
if (tab === "dashboard" && global.InstanceDashboard && typeof global.InstanceDashboard.init === "function") {
|
||||
global.InstanceDashboard.init(!!revisit);
|
||||
}
|
||||
if (!revisit && tab === "strategy" && typeof global.initStrategyRollForm === "function") {
|
||||
global.initStrategyRollForm();
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
if (tab === "options" && global.OptionsPanelLive && typeof global.OptionsPanelLive.refreshSoft === "function") {
|
||||
global.OptionsPanelLive.refreshSoft(options);
|
||||
}
|
||||
if (tab === "dashboard" && global.InstanceDashboard && typeof global.InstanceDashboard.refreshSoft === "function") {
|
||||
global.InstanceDashboard.refreshSoft(options);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleRefresh(opts) {
|
||||
|
||||
@@ -269,3 +269,20 @@
|
||||
.stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
|
||||
#embed-page-root{min-height:120px;position:relative}
|
||||
.embed-tab-pane[hidden]{display:none!important}
|
||||
.inst-dash-card{grid-column:1/-1}
|
||||
.inst-dash-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:8px}
|
||||
.inst-dash-desc{margin:0;font-size:.82rem}
|
||||
.inst-dash-head-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||||
.inst-dash-status{min-height:1.2em;margin:0 0 10px}
|
||||
.inst-dash-sections{display:flex;flex-direction:column;gap:14px}
|
||||
.inst-dash-section{padding:12px;background:#141923;border:1px solid #2a3150;border-radius:10px}
|
||||
.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}
|
||||
|
||||
@@ -19,8 +19,18 @@
|
||||
return data;
|
||||
}
|
||||
|
||||
/** 默认关闭的导航开关:缺失时按 false,不能用 !== false */
|
||||
const NAV_DEFAULT_OFF = { show_nav_dashboard: true };
|
||||
|
||||
function navPrefShow(display, key) {
|
||||
if (!key) return true;
|
||||
if (NAV_DEFAULT_OFF[key]) return display[key] === true;
|
||||
return display[key] !== false;
|
||||
}
|
||||
|
||||
function applyDisplayToNav(display) {
|
||||
const map = {
|
||||
dashboard: "show_nav_dashboard",
|
||||
strategy: "show_nav_strategy",
|
||||
strategy_records: "show_nav_strategy_records",
|
||||
records: "show_nav_records",
|
||||
@@ -37,7 +47,7 @@
|
||||
const tab = a.getAttribute("data-embed-tab") || (a.getAttribute("href") || "").replace(/^\//, "").split("?")[0];
|
||||
const key = map[tab];
|
||||
if (!key) return;
|
||||
const show = display[key] !== false;
|
||||
const show = navPrefShow(display, key);
|
||||
a.classList.toggle("nav-hidden", !show);
|
||||
a.style.display = show ? "" : "none";
|
||||
});
|
||||
@@ -47,6 +57,7 @@
|
||||
function pageNavAllowed(tab) {
|
||||
const d = DISPLAY();
|
||||
const map = {
|
||||
dashboard: "show_nav_dashboard",
|
||||
strategy: "show_nav_strategy",
|
||||
strategy_records: "show_nav_strategy_records",
|
||||
records: "show_nav_records",
|
||||
@@ -61,7 +72,7 @@
|
||||
};
|
||||
const key = map[tab];
|
||||
if (!key) return true;
|
||||
return d[key] !== false;
|
||||
return navPrefShow(d, key);
|
||||
}
|
||||
|
||||
function displayPrefsRoot() {
|
||||
@@ -129,7 +140,9 @@
|
||||
const cb = document.createElement("input");
|
||||
cb.type = "checkbox";
|
||||
cb.dataset.prefKey = item.key;
|
||||
cb.checked = display[item.key] !== false;
|
||||
cb.checked = NAV_DEFAULT_OFF[item.key]
|
||||
? display[item.key] === true
|
||||
: display[item.key] !== false;
|
||||
label.appendChild(cb);
|
||||
label.appendChild(document.createTextNode(" " + item.label));
|
||||
grid.appendChild(label);
|
||||
|
||||
Reference in New Issue
Block a user