Files
LocalNav/templates/index.html
T
2026-05-30 11:52:21 +08:00

390 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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 %}