Files
crypto_monitor_user/lib/common/static/instance_dashboard.js
T
dekun 53863559f4 Initialize crypto_monitor_user (user edition) from monitor codebase.
Retarget git remote, install path, and deploy docs from crypto_monitor to crypto_monitor_user.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 16:18:13 +08:00

495 lines
14 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 fmtPnlPlain(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" : "";
return '<span class="' + cls + '">' + n.toFixed(2) + "</span>";
}
function dirCell(it) {
const d = String(it.direction || "").toLowerCase();
const label = it.direction_label || (d === "short" ? "做空" : d === "long" ? "做多" : "-");
const cls = d === "short" ? "inst-dash-dir-short" : d === "long" ? "inst-dash-dir-long" : "";
return '<td class="' + cls + '">' + escapeHtml(label) + "</td>";
}
function fmtExpiry(ms) {
const n = Number(ms);
if (!Number.isFinite(n) || n <= 0) return "—";
let fallback = "—";
try {
const d = new Date(n);
if (!Number.isNaN(d.getTime())) {
const pad = function (x) {
return String(x).padStart(2, "0");
};
fallback =
d.getFullYear() +
"-" +
pad(d.getMonth() + 1) +
"-" +
pad(d.getDate()) +
" " +
pad(d.getHours()) +
":" +
pad(d.getMinutes());
}
} catch (_) {}
return (
'<span class="opt-expiry-cd" data-opt-exp-ms="' +
escapeHtml(String(Math.floor(n))) +
'">' +
escapeHtml(fallback) +
"</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 tableWrap(headers, rowsHtml) {
return (
'<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 mergeOrderLive(items, orderPrices) {
const map = {};
(orderPrices || []).forEach(function (p) {
if (p && p.id != null) map[String(p.id)] = p;
});
return (items || []).map(function (it) {
const live = map[String(it.id)] || {};
const mark =
live.exchange_mark_price != null
? live.exchange_mark_price
: live.price != null
? live.price
: it.mark_price;
const contracts =
live.contracts != null
? live.contracts
: live.order_amount != null
? live.order_amount
: it.contracts;
const entry =
live.avg_entry_price != null
? live.avg_entry_price
: it.entry;
return Object.assign({}, it, {
entry: entry,
mark_price: mark,
mark_display: live.price_display || null,
contracts: contracts,
tp_profit: live.reward_at_tp_usdt != null ? live.reward_at_tp_usdt : it.tp_profit,
float_pnl: live.float_pnl != null ? live.float_pnl : it.float_pnl,
});
});
}
function renderOrdersTable(items) {
const rows = items
.map(function (it) {
const sym = it.symbol || "-";
const mark =
it.mark_display != null && it.mark_display !== ""
? escapeHtml(it.mark_display)
: fmtNum(it.mark_price);
const tpProfit =
it.tp_profit != null && Number.isFinite(Number(it.tp_profit))
? '<span class="pos-tp-profit">' + Number(it.tp_profit).toFixed(2) + "U</span>"
: "—";
return (
"<tr" +
rowClickAttrs(it.tab || "trade") +
">" +
'<td class="td-symbol"><span class="inst-dash-sym-link">' +
escapeHtml(sym) +
"</span></td>" +
dirCell(it) +
"<td>" +
fmtNum(it.entry) +
"</td>" +
"<td>" +
mark +
"</td>" +
"<td>" +
fmtNum(it.contracts) +
"</td>" +
"<td>" +
tpProfit +
"</td>" +
"<td>" +
fmtPnlPlain(it.float_pnl) +
"</td>" +
"<td>—</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>" +
dirCell(it) +
"<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>" +
dirCell(it) +
"<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 = it.opt_type_label ||
(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(it.source_label || "纯期权") +
"</td>" +
"<td>" +
escapeHtml(opt) +
"</td>" +
"<td>" +
fmtNum(it.pos) +
"</td>" +
"<td>" +
fmtExpiry(it.exp_time_ms) +
"</td>" +
"<td>" +
escapeHtml(it.target_monitor || "—") +
"</td>" +
"<td>" +
fmtPnl(it.pnl) +
"</td>" +
"</tr>"
);
})
.join("");
return tableWrap(["合约", "来源", "类型", "张数", "到期时间", "目标监控", "盈亏"], rows);
}
function renderHedgeTable(items) {
const rows = items
.map(function (it) {
const stCls = it.status_active ? "inst-dash-status-active" : "";
const stText = it.status_label || (it.status_active ? "进行中" : it.status || "—");
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_label || it.plan_type || "—") +
"</td>" +
'<td class="' +
stCls +
'">' +
escapeHtml(stText) +
"</td>" +
"<td>" +
escapeHtml(it.contracts_summary || 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 (!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) +
'">' +
'<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>" +
renderTable(key, items) +
"</section>"
);
}
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 [dashRes, priceRes] = await Promise.all([
fetch("/api/instance/dashboard", { credentials: "same-origin" }),
fetch("/api/price_snapshot", { credentials: "same-origin" }).catch(function () {
return null;
}),
]);
const data = await dashRes.json().catch(function () {
return {};
});
if (!dashRes.ok || !data.ok) {
throw new Error(data.msg || dashRes.statusText || "加载失败");
}
let orderPrices = [];
if (priceRes && priceRes.ok) {
try {
const snap = await priceRes.json();
orderPrices = snap.order_prices || [];
} catch (_) {}
}
if (data.orders && Array.isArray(data.orders.items)) {
data.orders.items = mergeOrderLive(data.orders.items, orderPrices);
data.orders.count = data.orders.items.length;
}
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 || '<p class="inst-dash-empty muted">当前无活跃监控与持仓</p>';
bindClicks(sections);
if (global.OptionsExpiryCountdown) {
if (typeof global.OptionsExpiryCountdown.tick === "function") {
global.OptionsExpiryCountdown.tick(sections);
}
if (typeof global.OptionsExpiryCountdown.ensureTimer === "function") {
global.OptionsExpiryCountdown.ensureTimer();
}
}
}
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);