390 lines
12 KiB
HTML
390 lines
12 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>
|
||
<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-hub-login"
|
||
title="通过本地导航代登录云端中控(需配置 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" title="内嵌服务" hidden></iframe>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
(function () {
|
||
var hubAutoLogin = {{ 'true' if hub_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 btnHubLogin = document.getElementById("frame-hub-login");
|
||
var currentBaseUrl = "";
|
||
var currentOpenUrl = "";
|
||
var currentEmbedKind = "";
|
||
var currentServiceId = "";
|
||
var currentOrigin = "";
|
||
var currentNextPath = "/monitor";
|
||
|
||
function isHubEmbed(kind) {
|
||
return (kind || "").toLowerCase() === "hub";
|
||
}
|
||
|
||
function toggleHubLoginBtn(show) {
|
||
if (btnHubLogin) btnHubLogin.hidden = !show;
|
||
}
|
||
|
||
function applyIframeUrl(url) {
|
||
if (!url) return;
|
||
frame.src = url;
|
||
}
|
||
|
||
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));
|
||
});
|
||
}
|
||
|
||
window.addEventListener("message", function (ev) {
|
||
var data = ev.data;
|
||
if (!data || data.type !== "hub:login-ok") return;
|
||
if (data.embed_auth_url) {
|
||
applyIframeUrl(data.embed_auth_url);
|
||
}
|
||
});
|
||
|
||
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";
|
||
nameEl.textContent = name || "";
|
||
dashboard.hidden = true;
|
||
frameStack.hidden = false;
|
||
frame.hidden = false;
|
||
toggleHubLoginBtn(isHubEmbed(currentEmbedKind));
|
||
if (isHubEmbed(currentEmbedKind) && hubAutoLogin) {
|
||
hubLoginViaProxy(function (ok, err) {
|
||
if (!ok) applyIframeUrl(url);
|
||
});
|
||
var nav = preferredNav || findNavLink(url);
|
||
setActive(nav);
|
||
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() {
|
||
var u = currentOpenUrl || currentBaseUrl;
|
||
if (!u) return;
|
||
frame.src = buildCacheBustUrl(u, false);
|
||
}
|
||
|
||
function forceReloadUrl() {
|
||
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 = "";
|
||
frame.src = "about:blank";
|
||
frame.hidden = true;
|
||
frameStack.hidden = true;
|
||
dashboard.hidden = false;
|
||
toggleHubLoginBtn(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 (btnHubLogin) {
|
||
btnHubLogin.addEventListener("click", function () {
|
||
btnHubLogin.disabled = true;
|
||
hubLoginViaProxy(function (ok, err) {
|
||
btnHubLogin.disabled = false;
|
||
if (!ok && err) window.alert(err);
|
||
});
|
||
});
|
||
}
|
||
|
||
btnRefresh.addEventListener("click", function () {
|
||
reloadUrl();
|
||
});
|
||
|
||
btnForceRefresh.addEventListener("click", function () {
|
||
forceReloadUrl();
|
||
});
|
||
|
||
btnBack.addEventListener("click", function () {
|
||
showDashboard();
|
||
});
|
||
})();
|
||
</script>
|
||
{% endblock %}
|