738 lines
24 KiB
HTML
738 lines
24 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}导航 · 本地导航{% endblock %}
|
||
{% block body %}
|
||
<div class="app-shell">
|
||
<header class="topbar">
|
||
<h1>本地导航</h1>
|
||
<nav>
|
||
<span class="user">{{ current_user.username }}</span>
|
||
<a href="{{ url_for('admin_groups') }}">分组管理</a>
|
||
<a href="{{ url_for('admin_services') }}">服务管理</a>
|
||
<a href="{{ url_for('logout') }}">退出</a>
|
||
</nav>
|
||
</header>
|
||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||
{% if messages %}
|
||
<div class="flash-wrap" style="padding-top: 0.5rem">
|
||
{% for cat, msg in messages %}
|
||
<div class="flash {{ cat }}">{{ msg }}</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
{% endwith %}
|
||
<div class="layout-main" id="layout-main">
|
||
<aside class="sidebar" id="sidebar" aria-label="服务分组导航">
|
||
<div class="sidebar-toolbar">
|
||
<button
|
||
type="button"
|
||
class="btn-sidebar-toggle"
|
||
id="sidebar-collapse"
|
||
title="收起侧栏"
|
||
aria-label="收起侧栏"
|
||
>
|
||
⟨
|
||
</button>
|
||
</div>
|
||
{% for group, services in grouped %}
|
||
<div class="sidebar-section">
|
||
<h2>{{ group.name }}</h2>
|
||
{% for svc in services %}
|
||
<a
|
||
href="#"
|
||
class="nav-link"
|
||
role="button"
|
||
data-url="{{ svc.build_open_url()|e }}"
|
||
data-base-url="{{ svc.build_url()|e }}"
|
||
data-origin="{{ svc.build_origin()|e }}"
|
||
data-next-path="{{ (svc.path or '/monitor')|e }}"
|
||
data-embed-kind="{{ (svc.embed_kind or '')|e }}"
|
||
data-service-id="{{ svc.id }}"
|
||
data-name="{{ svc.name | e }}"
|
||
>{{ svc.name }}</a
|
||
>
|
||
{% else %}
|
||
<div class="hint" style="padding: 0 1rem">该分组下暂无服务</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}
|
||
<div class="hint" style="padding: 1rem">
|
||
暂无分组与服务,请到「分组管理」「服务管理」添加。
|
||
</div>
|
||
{% endfor %}
|
||
</aside>
|
||
<button
|
||
type="button"
|
||
class="btn-sidebar-expand"
|
||
id="sidebar-expand"
|
||
title="展开侧栏"
|
||
aria-label="展开侧栏"
|
||
hidden
|
||
>
|
||
⟩
|
||
</button>
|
||
<div class="content-column">
|
||
<div class="service-dashboard" id="service-dashboard">
|
||
{% for group, services in grouped %}
|
||
<section class="dash-section">
|
||
<h3 class="dash-section-title">{{ group.name }}</h3>
|
||
<div class="service-card-grid">
|
||
{% for svc in services %}
|
||
<button
|
||
type="button"
|
||
class="service-card"
|
||
data-url="{{ svc.build_open_url()|e }}"
|
||
data-base-url="{{ svc.build_url()|e }}"
|
||
data-origin="{{ svc.build_origin()|e }}"
|
||
data-next-path="{{ (svc.path or '/monitor')|e }}"
|
||
data-embed-kind="{{ (svc.embed_kind or '')|e }}"
|
||
data-service-id="{{ svc.id }}"
|
||
data-name="{{ svc.name | e }}"
|
||
>
|
||
<span class="service-card-title">{{ svc.name }}</span>
|
||
<span class="service-card-group">{{ group.name }}</span>
|
||
</button>
|
||
{% endfor %}
|
||
</div>
|
||
</section>
|
||
{% else %}
|
||
<div class="dash-empty hint">
|
||
暂无分组与服务,请到「分组管理」「服务管理」添加。
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
<div class="frame-stack" id="frame-stack" hidden>
|
||
<div class="frame-toolbar">
|
||
<div class="frame-toolbar-left">
|
||
<button type="button" class="btn-frame-back" id="frame-back-overview" title="返回服务总览">
|
||
总览
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="btn-frame-back"
|
||
id="frame-back-hub"
|
||
title="返回复盘中控监控区"
|
||
hidden
|
||
>
|
||
← 返回中控
|
||
</button>
|
||
<span class="frame-title" id="current-service-name"></span>
|
||
</div>
|
||
<div class="frame-toolbar-actions">
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary btn-toolbar-refresh"
|
||
id="frame-gate-login"
|
||
title="通过本地导航代登录 Gate 扫单(需配置 NAV_GATE_SCOUT_USERNAME / NAV_GATE_SCOUT_PASSWORD)"
|
||
hidden
|
||
>
|
||
Gate 登录
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary btn-toolbar-refresh"
|
||
id="frame-hub-login"
|
||
title="通过本地导航代登录云端中控(需配置 NAV_HUB_USERNAME / NAV_HUB_PASSWORD)"
|
||
hidden
|
||
>
|
||
中控登录
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary btn-toolbar-refresh"
|
||
id="frame-instance-sso"
|
||
title="通过本地导航代签实例 SSO(需 NAV_HUB_USERNAME / NAV_HUB_PASSWORD)"
|
||
hidden
|
||
>
|
||
实例免密
|
||
</button>
|
||
<button type="button" class="btn btn-secondary btn-toolbar-refresh" id="frame-refresh">
|
||
刷新
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary btn-toolbar-refresh"
|
||
id="frame-force-refresh"
|
||
title="强制刷新(等同 Ctrl+F5,跳过缓存)"
|
||
>
|
||
强制刷新
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="frame-wrap">
|
||
<iframe id="svc-frame" name="svc-frame" title="内嵌服务" hidden></iframe>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
(function () {
|
||
var hubAutoLogin = {{ 'true' if hub_auto_login else 'false' }};
|
||
var gateScoutAutoLogin = {{ 'true' if gate_scout_auto_login else 'false' }};
|
||
var layoutMain = document.getElementById("layout-main");
|
||
var btnSidebarCollapse = document.getElementById("sidebar-collapse");
|
||
var btnSidebarExpand = document.getElementById("sidebar-expand");
|
||
var sidebarCollapsedKey = "localnav_sidebar_collapsed";
|
||
|
||
function setSidebarCollapsed(collapsed) {
|
||
if (!layoutMain) return;
|
||
layoutMain.classList.toggle("sidebar-collapsed", collapsed);
|
||
if (btnSidebarExpand) btnSidebarExpand.hidden = !collapsed;
|
||
}
|
||
|
||
if (layoutMain && btnSidebarCollapse && btnSidebarExpand) {
|
||
try {
|
||
setSidebarCollapsed(localStorage.getItem(sidebarCollapsedKey) === "1");
|
||
} catch (e) {
|
||
setSidebarCollapsed(false);
|
||
}
|
||
btnSidebarCollapse.addEventListener("click", function () {
|
||
setSidebarCollapsed(true);
|
||
try {
|
||
localStorage.setItem(sidebarCollapsedKey, "1");
|
||
} catch (e) {}
|
||
});
|
||
btnSidebarExpand.addEventListener("click", function () {
|
||
setSidebarCollapsed(false);
|
||
try {
|
||
localStorage.setItem(sidebarCollapsedKey, "0");
|
||
} catch (e) {}
|
||
});
|
||
}
|
||
|
||
var frame = document.getElementById("svc-frame");
|
||
var dashboard = document.getElementById("service-dashboard");
|
||
var frameStack = document.getElementById("frame-stack");
|
||
var nameEl = document.getElementById("current-service-name");
|
||
var links = document.querySelectorAll(".nav-link[data-url]");
|
||
var cards = document.querySelectorAll(".service-card[data-url]");
|
||
var btnRefresh = document.getElementById("frame-refresh");
|
||
var btnForceRefresh = document.getElementById("frame-force-refresh");
|
||
var btnBack = document.getElementById("frame-back-overview");
|
||
var btnBackHub = document.getElementById("frame-back-hub");
|
||
var btnHubLogin = document.getElementById("frame-hub-login");
|
||
var btnGateLogin = document.getElementById("frame-gate-login");
|
||
var btnInstanceSso = document.getElementById("frame-instance-sso");
|
||
var currentBaseUrl = "";
|
||
var currentOpenUrl = "";
|
||
var currentEmbedKind = "";
|
||
var currentServiceId = "";
|
||
var currentOrigin = "";
|
||
var currentNextPath = "/monitor";
|
||
var currentViewMode = "service";
|
||
var hubReturnState = null;
|
||
var instanceNavCtx = null;
|
||
|
||
function normalizeOrigin(raw) {
|
||
if (!raw) return "";
|
||
try {
|
||
var u = new URL(raw.indexOf("://") >= 0 ? raw : "http://" + raw);
|
||
var port = u.port;
|
||
if (u.protocol === "https:" && (!port || port === "443")) {
|
||
return u.protocol + "//" + u.hostname;
|
||
}
|
||
if (u.protocol === "http:" && (!port || port === "80")) {
|
||
return u.protocol + "//" + u.hostname;
|
||
}
|
||
return u.origin;
|
||
} catch (e) {
|
||
return String(raw).replace(/\/+$/, "");
|
||
}
|
||
}
|
||
|
||
function originsCompatible(expected, actual) {
|
||
var e = normalizeOrigin(expected);
|
||
var a = normalizeOrigin(actual);
|
||
if (!e || !a) return true;
|
||
return e === a;
|
||
}
|
||
|
||
function isHubEmbed(kind) {
|
||
return (kind || "").toLowerCase() === "hub";
|
||
}
|
||
|
||
function isGateScoutEmbed(kind) {
|
||
var k = (kind || "").toLowerCase();
|
||
return k === "gate_scout" || k === "gate_exec" || k === "scout" || k === "exec";
|
||
}
|
||
|
||
function toggleInstanceBackBtn(show) {
|
||
if (btnBackHub) btnBackHub.hidden = !show;
|
||
}
|
||
|
||
function toggleInstanceSsoBtn(show) {
|
||
if (btnInstanceSso) btnInstanceSso.hidden = !show;
|
||
}
|
||
|
||
function toggleHubLoginBtn(show) {
|
||
if (btnHubLogin) btnHubLogin.hidden = !show;
|
||
}
|
||
|
||
function toggleGateLoginBtn(show) {
|
||
if (btnGateLogin) btnGateLogin.hidden = !show;
|
||
}
|
||
|
||
function applyIframeUrl(url) {
|
||
if (!url) return;
|
||
frame.src = url;
|
||
}
|
||
|
||
function iframeLooksLikeHub(href) {
|
||
if (!href) return false;
|
||
var hubOrigin = normalizeOrigin(currentOrigin);
|
||
var h = String(href);
|
||
if (hubOrigin && h.indexOf(hubOrigin) !== 0) return false;
|
||
return (
|
||
/\/monitor(\?|#|$)/.test(h) ||
|
||
h.indexOf("/embed-auth") >= 0 ||
|
||
(h.indexOf("/login") >= 0 && h.indexOf("embed=1") >= 0) ||
|
||
h.indexOf("/settings") >= 0
|
||
);
|
||
}
|
||
|
||
function syncHubInstanceBackBtn() {
|
||
if (!isHubEmbed(currentEmbedKind) || frameStack.hidden || !currentBaseUrl) {
|
||
toggleInstanceBackBtn(false);
|
||
toggleInstanceSsoBtn(false);
|
||
return;
|
||
}
|
||
var onHub = false;
|
||
try {
|
||
onHub = iframeLooksLikeHub(frame.contentWindow.location.href);
|
||
} catch (e) {
|
||
onHub = false;
|
||
}
|
||
if (onHub) {
|
||
currentViewMode = "hub";
|
||
toggleInstanceBackBtn(false);
|
||
toggleInstanceSsoBtn(false);
|
||
toggleHubLoginBtn(true);
|
||
return;
|
||
}
|
||
currentViewMode = "hub-instance";
|
||
toggleInstanceBackBtn(true);
|
||
toggleHubLoginBtn(false);
|
||
toggleInstanceSsoBtn(!!(instanceNavCtx && instanceNavCtx.exchangeId));
|
||
}
|
||
|
||
if (frame) {
|
||
frame.addEventListener("load", syncHubInstanceBackBtn);
|
||
}
|
||
|
||
function hubLoginViaProxy(done) {
|
||
if (!currentServiceId && !currentOrigin) {
|
||
if (done) done(false, "未选择中控服务");
|
||
return;
|
||
}
|
||
var body = { service_id: parseInt(currentServiceId, 10) || undefined, next: currentNextPath };
|
||
fetch("/api/embed/hub-login", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
||
body: JSON.stringify(body),
|
||
})
|
||
.then(function (r) {
|
||
return r.json().then(function (j) {
|
||
return { ok: r.ok, j: j };
|
||
});
|
||
})
|
||
.then(function (res) {
|
||
if (res.ok && res.j.ok && res.j.embed_auth_url) {
|
||
currentBaseUrl = res.j.embed_auth_url.split("?")[0].replace(/\/embed-auth$/, "") + (currentNextPath || "/monitor");
|
||
applyIframeUrl(res.j.embed_auth_url);
|
||
if (done) done(true);
|
||
return;
|
||
}
|
||
if (done) done(false, (res.j && res.j.detail) || "中控登录失败");
|
||
})
|
||
.catch(function (e) {
|
||
if (done) done(false, String(e));
|
||
});
|
||
}
|
||
|
||
function gateScoutLoginViaProxy(done) {
|
||
if (!currentServiceId && !currentOrigin) {
|
||
if (done) done(false, "未选择 Gate 服务");
|
||
return;
|
||
}
|
||
var body = { service_id: parseInt(currentServiceId, 10) || undefined, next: currentNextPath };
|
||
fetch("/api/embed/gate-scout-login", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
||
body: JSON.stringify(body),
|
||
})
|
||
.then(function (r) {
|
||
return r.json().then(function (j) {
|
||
return { ok: r.ok, j: j };
|
||
});
|
||
})
|
||
.then(function (res) {
|
||
if (res.ok && res.j.ok && res.j.embed_auth_url) {
|
||
applyIframeUrl(res.j.embed_auth_url);
|
||
toggleGateLoginBtn(false);
|
||
if (done) done(true);
|
||
return;
|
||
}
|
||
if (done) done(false, (res.j && res.j.detail) || "Gate 登录失败");
|
||
})
|
||
.catch(function (e) {
|
||
if (done) done(false, String(e));
|
||
});
|
||
}
|
||
|
||
window.addEventListener("message", function (ev) {
|
||
var data = ev.data;
|
||
if (!data || !data.type) return;
|
||
if (data.type === "hub:login-ok") {
|
||
if (data.embed_auth_url) {
|
||
applyIframeUrl(data.embed_auth_url);
|
||
}
|
||
return;
|
||
}
|
||
if (data.type === "hub:open-instance-nav") {
|
||
instanceNavCtx = {
|
||
exchangeId: String(data.exchangeId || ""),
|
||
nextPath: data.nextPath || "/",
|
||
title: data.title || "交易所实例",
|
||
serviceId: currentServiceId,
|
||
};
|
||
currentViewMode = "hub-instance";
|
||
if (data.title) nameEl.textContent = data.title;
|
||
toggleHubLoginBtn(false);
|
||
toggleInstanceBackBtn(true);
|
||
toggleInstanceSsoBtn(!!instanceNavCtx.exchangeId);
|
||
return;
|
||
}
|
||
if (data.type !== "hub:open-instance") return;
|
||
if (frameStack.hidden || !currentServiceId) return;
|
||
if (!originsCompatible(currentOrigin, ev.origin)) {
|
||
console.warn(
|
||
"[LocalNav] hub:open-instance origin 不匹配,已忽略",
|
||
normalizeOrigin(currentOrigin),
|
||
normalizeOrigin(ev.origin)
|
||
);
|
||
return;
|
||
}
|
||
if (!data.url) return;
|
||
|
||
try {
|
||
if (ev.source) {
|
||
ev.source.postMessage({ type: "hub:open-instance-ack", ok: true }, ev.origin || "*");
|
||
}
|
||
} catch (e) {}
|
||
|
||
hubReturnState = {
|
||
openUrl: currentOpenUrl,
|
||
baseUrl: currentBaseUrl,
|
||
name: nameEl.textContent,
|
||
embedKind: currentEmbedKind,
|
||
serviceId: currentServiceId,
|
||
origin: currentOrigin,
|
||
nextPath: currentNextPath,
|
||
};
|
||
instanceNavCtx = {
|
||
exchangeId: String(data.exchangeId || ""),
|
||
nextPath: data.nextPath || "/",
|
||
title: data.title || "交易所实例",
|
||
serviceId: currentServiceId,
|
||
};
|
||
currentViewMode = "hub-instance";
|
||
nameEl.textContent = instanceNavCtx.title;
|
||
toggleHubLoginBtn(false);
|
||
toggleInstanceBackBtn(true);
|
||
applyIframeUrl(data.url);
|
||
});
|
||
|
||
function refreshInstanceViaProxy(done) {
|
||
if (!instanceNavCtx || !instanceNavCtx.exchangeId) {
|
||
if (done) done(false, null, "缺少实例上下文");
|
||
return;
|
||
}
|
||
fetch("/api/embed/hub-instance-url", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
||
body: JSON.stringify({
|
||
service_id: parseInt(instanceNavCtx.serviceId, 10) || undefined,
|
||
exchange_id: instanceNavCtx.exchangeId,
|
||
next: instanceNavCtx.nextPath || "/",
|
||
embed: "1",
|
||
}),
|
||
})
|
||
.then(function (r) {
|
||
return r.json().then(function (j) {
|
||
return { ok: r.ok, j: j };
|
||
});
|
||
})
|
||
.then(function (res) {
|
||
if (res.ok && res.j.ok && res.j.url) {
|
||
if (done) done(true, res.j.url, null);
|
||
return;
|
||
}
|
||
if (done) done(false, null, (res.j && res.j.detail) || "无法重新打开实例");
|
||
})
|
||
.catch(function (e) {
|
||
if (done) done(false, null, String(e));
|
||
});
|
||
}
|
||
|
||
function returnToHubMonitor() {
|
||
var st = hubReturnState;
|
||
currentViewMode = "hub";
|
||
instanceNavCtx = null;
|
||
hubReturnState = null;
|
||
toggleInstanceBackBtn(false);
|
||
toggleInstanceSsoBtn(false);
|
||
if (st) {
|
||
currentOpenUrl = st.openUrl || currentOpenUrl;
|
||
currentBaseUrl = st.baseUrl || st.openUrl || currentBaseUrl;
|
||
currentEmbedKind = st.embedKind || currentEmbedKind;
|
||
currentServiceId = st.serviceId || currentServiceId;
|
||
currentOrigin = st.origin || currentOrigin;
|
||
currentNextPath = st.nextPath || currentNextPath;
|
||
nameEl.textContent = st.name || nameEl.textContent;
|
||
}
|
||
toggleHubLoginBtn(isHubEmbed(currentEmbedKind));
|
||
if (isHubEmbed(currentEmbedKind) && hubAutoLogin) {
|
||
hubLoginViaProxy(function (ok) {
|
||
if (!ok) applyIframeUrl(currentOpenUrl || currentBaseUrl);
|
||
syncHubInstanceBackBtn();
|
||
});
|
||
return;
|
||
}
|
||
applyIframeUrl(currentOpenUrl || currentBaseUrl);
|
||
syncHubInstanceBackBtn();
|
||
}
|
||
|
||
function setActive(el) {
|
||
links.forEach(function (a) {
|
||
a.classList.remove("active");
|
||
});
|
||
if (el) el.classList.add("active");
|
||
}
|
||
|
||
function findNavLink(url) {
|
||
var found = null;
|
||
links.forEach(function (a) {
|
||
if (a.getAttribute("data-url") === url) found = a;
|
||
});
|
||
return found;
|
||
}
|
||
|
||
function openService(url, name, preferredNav, meta) {
|
||
if (!url) return;
|
||
meta = meta || {};
|
||
currentOpenUrl = url;
|
||
currentBaseUrl = meta.baseUrl || url;
|
||
currentEmbedKind = meta.embedKind || "";
|
||
currentServiceId = meta.serviceId || "";
|
||
currentOrigin = meta.origin || "";
|
||
currentNextPath = meta.nextPath || "/monitor";
|
||
currentViewMode = isHubEmbed(currentEmbedKind) ? "hub" : "service";
|
||
instanceNavCtx = null;
|
||
hubReturnState = null;
|
||
toggleInstanceBackBtn(false);
|
||
toggleGateLoginBtn(false);
|
||
nameEl.textContent = name || "";
|
||
dashboard.hidden = true;
|
||
frameStack.hidden = false;
|
||
frame.hidden = false;
|
||
toggleHubLoginBtn(isHubEmbed(currentEmbedKind) && !hubAutoLogin);
|
||
toggleGateLoginBtn(isGateScoutEmbed(currentEmbedKind) && !gateScoutAutoLogin);
|
||
if (isHubEmbed(currentEmbedKind) && hubAutoLogin) {
|
||
hubLoginViaProxy(function (ok, err) {
|
||
if (!ok) {
|
||
applyIframeUrl(url);
|
||
toggleHubLoginBtn(true);
|
||
}
|
||
});
|
||
var nav = preferredNav || findNavLink(url);
|
||
setActive(nav);
|
||
return;
|
||
}
|
||
if (isGateScoutEmbed(currentEmbedKind) && gateScoutAutoLogin) {
|
||
gateScoutLoginViaProxy(function (ok, err) {
|
||
if (!ok) {
|
||
applyIframeUrl(url);
|
||
toggleGateLoginBtn(true);
|
||
if (err) console.warn("[LocalNav] Gate 代登录失败:", err);
|
||
}
|
||
});
|
||
var navGate = preferredNav || findNavLink(url);
|
||
setActive(navGate);
|
||
return;
|
||
}
|
||
applyIframeUrl(url);
|
||
var nav = preferredNav || findNavLink(url);
|
||
setActive(nav);
|
||
}
|
||
|
||
function buildCacheBustUrl(u, hard) {
|
||
var sep = u.indexOf("?") >= 0 ? "&" : "?";
|
||
var hash = "";
|
||
var base = u;
|
||
var hashPos = u.indexOf("#");
|
||
if (hashPos >= 0) {
|
||
base = u.slice(0, hashPos);
|
||
hash = u.slice(hashPos);
|
||
}
|
||
var ts = Date.now();
|
||
if (hard) {
|
||
return (
|
||
base +
|
||
sep +
|
||
"_navts=" +
|
||
ts +
|
||
"&_navnocache=" +
|
||
Math.random().toString(36).slice(2) +
|
||
hash
|
||
);
|
||
}
|
||
return base + sep + "_navts=" + ts + hash;
|
||
}
|
||
|
||
function reloadUrl() {
|
||
if (currentViewMode === "hub-instance") {
|
||
refreshInstanceViaProxy(function (ok, url, err) {
|
||
if (ok && url) applyIframeUrl(url);
|
||
else if (err) window.alert(err);
|
||
});
|
||
return;
|
||
}
|
||
var u = currentOpenUrl || currentBaseUrl;
|
||
if (!u) return;
|
||
frame.src = buildCacheBustUrl(u, false);
|
||
}
|
||
|
||
function forceReloadUrl() {
|
||
if (currentViewMode === "hub-instance") {
|
||
refreshInstanceViaProxy(function (ok, url, err) {
|
||
if (!ok || !url) {
|
||
if (err) window.alert(err);
|
||
return;
|
||
}
|
||
frame.src = "about:blank";
|
||
frame.onload = function () {
|
||
frame.onload = null;
|
||
frame.src = buildCacheBustUrl(url, true);
|
||
};
|
||
});
|
||
return;
|
||
}
|
||
var u = currentOpenUrl || currentBaseUrl;
|
||
if (!u) return;
|
||
frame.src = "about:blank";
|
||
frame.onload = function () {
|
||
frame.onload = null;
|
||
frame.src = buildCacheBustUrl(u, true);
|
||
};
|
||
}
|
||
|
||
function showDashboard() {
|
||
currentBaseUrl = "";
|
||
currentOpenUrl = "";
|
||
currentEmbedKind = "";
|
||
currentServiceId = "";
|
||
currentOrigin = "";
|
||
currentViewMode = "service";
|
||
instanceNavCtx = null;
|
||
hubReturnState = null;
|
||
frame.src = "about:blank";
|
||
frame.hidden = true;
|
||
frameStack.hidden = true;
|
||
dashboard.hidden = false;
|
||
toggleHubLoginBtn(false);
|
||
toggleGateLoginBtn(false);
|
||
toggleInstanceSsoBtn(false);
|
||
setActive(null);
|
||
}
|
||
|
||
function readServiceMeta(el) {
|
||
return {
|
||
baseUrl: el.getAttribute("data-base-url") || el.getAttribute("data-url") || "",
|
||
embedKind: el.getAttribute("data-embed-kind") || "",
|
||
serviceId: el.getAttribute("data-service-id") || "",
|
||
origin: el.getAttribute("data-origin") || "",
|
||
nextPath: el.getAttribute("data-next-path") || "/monitor",
|
||
};
|
||
}
|
||
|
||
links.forEach(function (a) {
|
||
a.addEventListener("click", function (e) {
|
||
e.preventDefault();
|
||
var url = a.getAttribute("data-url");
|
||
var name = a.getAttribute("data-name") || "";
|
||
openService(url, name, a, readServiceMeta(a));
|
||
});
|
||
});
|
||
|
||
cards.forEach(function (btn) {
|
||
btn.addEventListener("click", function () {
|
||
var url = btn.getAttribute("data-url");
|
||
var name = btn.getAttribute("data-name") || "";
|
||
openService(url, name, null, readServiceMeta(btn));
|
||
});
|
||
});
|
||
|
||
if (btnGateLogin) {
|
||
btnGateLogin.addEventListener("click", function () {
|
||
btnGateLogin.disabled = true;
|
||
gateScoutLoginViaProxy(function (ok, err) {
|
||
btnGateLogin.disabled = false;
|
||
if (!ok && err) {
|
||
window.alert(
|
||
"Gate 登录失败:\n" +
|
||
err +
|
||
"\n\n请检查 LocalNav .env 的 NAV_GATE_SCOUT_USERNAME / NAV_GATE_SCOUT_PASSWORD,以及云端 PM2 的 NAV_EMBED_SESSION / NAV_EMBED_ORIGINS。"
|
||
);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
if (btnHubLogin) {
|
||
btnHubLogin.addEventListener("click", function () {
|
||
btnHubLogin.disabled = true;
|
||
hubLoginViaProxy(function (ok, err) {
|
||
btnHubLogin.disabled = false;
|
||
if (!ok && err) {
|
||
window.alert("中控登录失败:\n" + err + "\n\n请检查 LocalNav .env 的 NAV_HUB_USERNAME / NAV_HUB_PASSWORD 是否与云端 hub .env 一致。");
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
if (btnInstanceSso) {
|
||
btnInstanceSso.addEventListener("click", function () {
|
||
btnInstanceSso.disabled = true;
|
||
refreshInstanceViaProxy(function (ok, url, err) {
|
||
btnInstanceSso.disabled = false;
|
||
if (ok && url) {
|
||
applyIframeUrl(url);
|
||
syncHubInstanceBackBtn();
|
||
return;
|
||
}
|
||
if (err) window.alert("实例免密失败:\n" + err);
|
||
});
|
||
});
|
||
}
|
||
|
||
btnRefresh.addEventListener("click", function () {
|
||
reloadUrl();
|
||
});
|
||
|
||
btnForceRefresh.addEventListener("click", function () {
|
||
forceReloadUrl();
|
||
});
|
||
|
||
btnBack.addEventListener("click", function () {
|
||
showDashboard();
|
||
});
|
||
|
||
if (btnBackHub) {
|
||
btnBackHub.addEventListener("click", function () {
|
||
returnToHubMonitor();
|
||
});
|
||
}
|
||
})();
|
||
</script>
|
||
{% endblock %}
|