/** * 实例数据看板:拉 /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, """); } 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 '' + sign + n.toFixed(2) + "U"; } 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 '

暂无

'; } 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("") + "
" ); } function renderSection(key, sec) { if (!sec) return ""; if ((key === "options" || key === "hedge_plan") && !sec.visible) return ""; const count = Number(sec.count) || 0; return ( '
' + '
' + "

" + escapeHtml(sec.title || key) + ' ' + count + "

" + '' + "
" + renderItems(sec.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")); }); }); } 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);