/** * 实例数据看板:拉 /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 tableWrap(headers, rowsHtml) { return ( '
' + '' + "" + 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 (!sectionHasData(sec)) return ""; const items = sec.items || []; const count = Number(sec.count) || items.length; return ( '
' + '
' + "

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

" + '' + "
" + renderTable(key, items) + "
" ); } function bindClicks(el) { if (!el) return; 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(); } }); }); } 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) { const html = SECTION_ORDER.map(function (k) { return renderSection(k, data[k]); }).join(""); sections.innerHTML = html || '

当前无活跃监控与持仓

'; 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);