Files
crypto_monitor/lib/common/static/instance_dashboard.js
T
dekun c7aae67881 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>
2026-07-17 12:09:29 +08:00

205 lines
6.1 KiB
JavaScript

/**
* 实例数据看板:拉 /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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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);