Align instance dashboard with snapshot+SSE and options net PnL.
Only dashboard path changes: background snapshot, SSE refresh, smaller header type, options net PnL. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,7 +5,10 @@
|
||||
(function (global) {
|
||||
const SECTION_ORDER = ["orders", "keys", "strategy", "options", "hedge_plan"];
|
||||
let loading = false;
|
||||
let timer = null;
|
||||
let localDashVersion = 0;
|
||||
let dashEventSource = null;
|
||||
let dashReconnectTimer = null;
|
||||
let booted = false;
|
||||
|
||||
function root() {
|
||||
const active = document.querySelector('.embed-tab-pane.is-active-pane [data-inst-dashboard="1"]');
|
||||
@@ -121,40 +124,6 @@
|
||||
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) {
|
||||
@@ -294,7 +263,7 @@
|
||||
);
|
||||
})
|
||||
.join("");
|
||||
return tableWrap(["合约", "来源", "类型", "张数", "到期时间", "目标监控", "盈亏"], rows);
|
||||
return tableWrap(["合约", "来源", "类型", "张数", "到期时间", "目标监控", "净盈亏"], rows);
|
||||
}
|
||||
|
||||
function renderHedgeTable(items) {
|
||||
@@ -391,31 +360,32 @@
|
||||
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;
|
||||
const options = opts || {};
|
||||
if (loading && !options.force) return;
|
||||
loading = true;
|
||||
if (status && !(opts && opts.silent)) status.textContent = "加载中…";
|
||||
if (status && !options.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 dashRes = await fetch("/api/instance/dashboard", { credentials: "same-origin" });
|
||||
if (dashRes.status === 401) {
|
||||
location.href = "/login?next=" + encodeURIComponent(location.pathname);
|
||||
return;
|
||||
}
|
||||
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.aggregating ||
|
||||
(data.msg && String(data.msg).indexOf("尚未就绪") >= 0)
|
||||
) {
|
||||
if (status) status.textContent = "后台聚合中…";
|
||||
return;
|
||||
}
|
||||
throw new Error(data.msg || data.error || dashRes.statusText || "加载失败");
|
||||
}
|
||||
const ver = Number(data.dashboard_version) || 0;
|
||||
if (ver) localDashVersion = ver;
|
||||
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 || "—");
|
||||
@@ -435,7 +405,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
if (status) status.textContent = "";
|
||||
const sec = Number(data.poll_interval_sec) || 5;
|
||||
if (status) {
|
||||
status.textContent = options.silent
|
||||
? "SSE 已连接 · 后台每 " + sec + "s 聚合"
|
||||
: "已更新 · 后台每 " + sec + "s 聚合";
|
||||
}
|
||||
} catch (e) {
|
||||
if (status) status.textContent = e.message || "加载失败";
|
||||
} finally {
|
||||
@@ -443,42 +418,81 @@
|
||||
}
|
||||
}
|
||||
|
||||
function stopAuto() {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
function closeDashboardStream() {
|
||||
if (dashEventSource) {
|
||||
dashEventSource.close();
|
||||
dashEventSource = null;
|
||||
}
|
||||
if (dashReconnectTimer) {
|
||||
clearTimeout(dashReconnectTimer);
|
||||
dashReconnectTimer = 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 connectDashboardStream() {
|
||||
const el = root();
|
||||
if (!el) return;
|
||||
closeDashboardStream();
|
||||
dashEventSource = new EventSource("/api/instance/dashboard/stream");
|
||||
dashEventSource.addEventListener("dashboard", function (ev) {
|
||||
try {
|
||||
const st = JSON.parse(ev.data || "{}");
|
||||
const ver = Number(st.dashboard_version) || 0;
|
||||
if (ver && ver !== localDashVersion) {
|
||||
load({ silent: true });
|
||||
} else if (st.aggregating) {
|
||||
const status = el.querySelector("#inst-dash-status") || document.getElementById("inst-dash-status");
|
||||
if (status) status.textContent = "后台聚合中…";
|
||||
}
|
||||
} catch (_) {}
|
||||
});
|
||||
dashEventSource.onerror = function () {
|
||||
closeDashboardStream();
|
||||
const status = el.querySelector("#inst-dash-status") || document.getElementById("inst-dash-status");
|
||||
if (status) status.textContent = "SSE 断开,8s 后重连…";
|
||||
dashReconnectTimer = setTimeout(function () {
|
||||
if (booted) {
|
||||
connectDashboardStream();
|
||||
load({ silent: true });
|
||||
}
|
||||
}, 8000);
|
||||
};
|
||||
}
|
||||
|
||||
async function requestDashboardRefresh() {
|
||||
try {
|
||||
await fetch("/api/instance/dashboard/refresh", {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
});
|
||||
} catch (_) {}
|
||||
load({ force: true });
|
||||
}
|
||||
|
||||
function stopAuto() {
|
||||
closeDashboardStream();
|
||||
}
|
||||
|
||||
function init(force) {
|
||||
const el = root();
|
||||
if (!el) return;
|
||||
if (!force && el.getAttribute("data-dash-booted") === "1") {
|
||||
booted = true;
|
||||
load({ silent: true });
|
||||
startAuto();
|
||||
connectDashboardStream();
|
||||
return;
|
||||
}
|
||||
el.setAttribute("data-dash-booted", "1");
|
||||
booted = true;
|
||||
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({});
|
||||
requestDashboardRefresh();
|
||||
});
|
||||
}
|
||||
load({});
|
||||
startAuto();
|
||||
connectDashboardStream();
|
||||
}
|
||||
|
||||
function refreshSoft(opts) {
|
||||
|
||||
@@ -30,9 +30,7 @@
|
||||
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);
|
||||
}
|
||||
// 数据看板自有 SSE + 快照,不跟 embed live tick 重拉.
|
||||
}
|
||||
|
||||
function scheduleRefresh(opts) {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
.order-preview-rr.order-preview-rr-ok strong{color:#8fc8ff}
|
||||
.form-row > button,.form-row > label{flex:0 0 auto}
|
||||
.form-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
|
||||
/* 复盘表单:长下拉文案需可收缩,否则会撑破四列网格 */
|
||||
/* å¤�盘表å�•:长下拉文案需å�¯æ”¶ç¼?å�¦åˆ™ä¼šæ’‘ç ´å››åˆ—ç½‘æ ?*/
|
||||
.journal-card .form-grid{gap:10px}
|
||||
.journal-card .form-grid > input,
|
||||
.journal-card .form-grid > select{
|
||||
@@ -270,10 +270,12 @@
|
||||
#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-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:6px}
|
||||
.inst-dash-card > .inst-dash-head h2{font-size:.88rem;margin:0 0 2px;font-weight:600}
|
||||
.inst-dash-desc{margin:0;font-size:.72rem;line-height:1.35}
|
||||
.inst-dash-head-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
||||
.inst-dash-updated{font-size:.72rem}
|
||||
.inst-dash-status{min-height:1.1em;margin:0 0 8px;font-size:.72rem}
|
||||
.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}
|
||||
|
||||
Reference in New Issue
Block a user