3588AdminBackend/internal/web/ui/templates/layout.html
2026-05-07 12:30:23 +08:00

338 lines
14 KiB
HTML

{{define "layout"}}
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{.Title}}</title>
<link rel="stylesheet" href="/ui/assets/vendor/tabler.min.css" />
<link rel="stylesheet" href="/ui/assets/style.css?v=20260429-ia02" />
<link rel="stylesheet" href="/ui/assets/graph_editor.css?v=20260429-ia01" />
</head>
<body data-theme="blue-dark">
<div class="app-shell">
<aside class="sidebar">
<div class="brand-block">
<div class="brand-mark">AI</div>
<div>
<div class="brand-title">视觉识别运维平台</div>
</div>
</div>
<nav class="side-nav" aria-label="主导航">
<div class="nav-section">主模块</div>
<a href="/ui/dashboard"><span class="nav-icon">{{icon "overview"}}</span><span>总览</span></a>
<a href="/ui/devices"><span class="nav-icon">{{icon "devices"}}</span><span>设备</span></a>
<a href="/ui/scene-templates"><span class="nav-icon">{{icon "profile"}}</span><span>场景</span></a>
<a href="/ui/recognition-units"><span class="nav-icon">{{icon "device"}}</span><span>视频通道</span></a>
<a href="/ui/device-assignments"><span class="nav-icon">{{icon "apply"}}</span><span>通道部署</span></a>
<a href="/ui/assets"><span class="nav-icon">{{icon "assets"}}</span><span>配置中心</span></a>
<a href="/ui/tasks"><span class="nav-icon">{{icon "task"}}</span><span>任务中心</span></a>
<details class="nav-group" id="system-nav-group">
<summary>
<span class="nav-icon">{{icon "system"}}</span>
<span>系统管理</span>
</summary>
<div class="nav-group-items">
<a class="nav-subitem" href="/ui/models"><span class="nav-icon nav-subicon">{{icon "assets"}}</span><span>模型管理</span></a>
<a class="nav-subitem" href="/ui/resources"><span class="nav-icon nav-subicon">{{icon "template"}}</span><span>资源管理</span></a>
<a class="nav-subitem" href="/ui/alarms"><span class="nav-icon nav-subicon">{{icon "bell"}}</span><span>告警中心</span></a>
<a class="nav-subitem" href="/ui/diagnostics"><span class="nav-icon nav-subicon">{{icon "logs"}}</span><span>日志审计</span></a>
<a class="nav-subitem" href="/ui/system"><span class="nav-icon nav-subicon">{{icon "heartbeat"}}</span><span>系统状态</span></a>
</div>
</details>
</nav>
</aside>
<div class="workspace">
<header class="topbar">
<div class="topbar-title">
<h1>{{.Title}}</h1>
</div>
<div class="topbar-actions">
<div class="theme-menu">
<button type="button" class="topbar-icon-btn js-theme-menu-toggle" aria-label="主题" title="主题">
{{icon "theme"}}
</button>
<div class="theme-menu-panel" hidden>
<button type="button" data-theme-option="blue-dark">黑白暗色</button>
<button type="button" data-theme-option="blue-light">蓝灰浅色</button>
<button type="button" data-theme-option="graphite-gold">石墨金色</button>
</div>
</div>
<a class="topbar-icon-btn" href="/ui/diagnostics" aria-label="日志审计" title="日志审计">
{{icon "bell"}}
<span class="topbar-dot" aria-hidden="true"></span>
</a>
<a class="topbar-icon-btn" href="/ui/system" aria-label="系统" title="系统">
{{icon "system"}}
</a>
</div>
</header>
<main>
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
{{if .Message}}<div class="msg">{{.Message}}</div>{{end}}
{{.ContentHTML}}
</main>
</div>
</div>
<script src="/ui/assets/vendor/tabler.min.js"></script>
<script>
(function () {
const themeKey = "3588-admin-theme";
const allowedThemes = new Set(["blue-dark", "blue-light", "graphite-gold"]);
const themeLabels = {
"blue-dark": "黑白暗色",
"blue-light": "蓝灰浅色",
"graphite-gold": "石墨金色"
};
function applyTheme(theme, persist) {
const nextTheme = allowedThemes.has(theme) ? theme : "blue-dark";
document.body.setAttribute("data-theme", nextTheme);
const themeToggle = document.querySelector(".js-theme-menu-toggle");
if (themeToggle) {
const label = "当前主题:" + themeLabels[nextTheme];
themeToggle.setAttribute("aria-label", label);
themeToggle.setAttribute("title", label);
}
if (persist) {
localStorage.setItem(themeKey, nextTheme);
}
document.querySelectorAll("[data-theme-option]").forEach(function (button) {
const active = button.getAttribute("data-theme-option") === nextTheme;
button.setAttribute("aria-pressed", active ? "true" : "false");
});
}
applyTheme(localStorage.getItem("3588-admin-theme") || document.body.getAttribute("data-theme"), false);
document.querySelectorAll("[data-theme-option]").forEach(function (themeOption) {
themeOption.addEventListener("click", function (event) {
event.preventDefault();
event.stopPropagation();
applyTheme(themeOption.getAttribute("data-theme-option"), true);
const panel = themeOption.closest(".theme-menu-panel");
if (panel) {
panel.hidden = true;
}
});
});
function openThemePanel(themeToggle, panel) {
if (panel.parentElement !== document.body) {
document.body.appendChild(panel);
}
const rect = themeToggle.getBoundingClientRect();
panel.hidden = false;
const panelWidth = panel.offsetWidth || 132;
const viewportPadding = 12;
const maxLeft = Math.max(viewportPadding, window.innerWidth - panelWidth - viewportPadding);
const preferredLeft = rect.right - panelWidth;
const left = Math.min(Math.max(viewportPadding, preferredLeft), maxLeft);
panel.style.top = Math.round(rect.bottom + 6) + "px";
panel.style.left = Math.round(left) + "px";
panel.style.right = "auto";
}
function suggestedFilenameFromHeader(headerValue, fallback) {
if (!headerValue) return fallback;
const match = /filename="?([^"]+)"?/i.exec(headerValue);
if (!match || !match[1]) return fallback;
return match[1];
}
async function saveBlobFromURL(url, defaultFilename, types, errorMessage) {
if (window.showSaveFilePicker) {
const response = await fetch(url, { credentials: "same-origin" });
if (!response.ok) {
throw new Error(errorMessage);
}
const blob = await response.blob();
const filename = suggestedFilenameFromHeader(response.headers.get("Content-Disposition"), defaultFilename);
const handle = await window.showSaveFilePicker({
suggestedName: filename,
types: types
});
const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
return;
}
throw new Error("当前浏览器不支持选择保存目录和文件名,请使用支持文件保存对话框的浏览器。");
}
async function saveJSONFromURL(url, defaultFilename) {
return saveBlobFromURL(
url,
defaultFilename || "config.json",
[{
description: "JSON 文件",
accept: { "application/json": [".json"] }
}],
"导出失败"
);
}
async function saveDBFromURL(url, defaultFilename) {
return saveBlobFromURL(
url,
defaultFilename || "app.db",
[{
description: "SQLite 数据库",
accept: { "application/octet-stream": [".db"], "application/x-sqlite3": [".db"] }
}],
"备份失败"
);
}
document.addEventListener("click", function (event) {
const themeToggle = event.target.closest(".js-theme-menu-toggle");
if (themeToggle) {
event.preventDefault();
const panel = document.querySelector(".theme-menu-panel");
if (panel) {
if (panel.hidden) {
openThemePanel(themeToggle, panel);
} else {
panel.hidden = true;
}
}
return;
}
const btn = event.target.closest(".js-export-json");
if (btn) {
event.preventDefault();
const url = btn.getAttribute("data-export-url");
const filename = btn.getAttribute("data-default-filename");
saveJSONFromURL(url, filename).catch(function (err) {
window.alert(err && err.message ? err.message : "导出失败");
});
return;
}
const dbBtn = event.target.closest(".js-export-db");
if (dbBtn) {
event.preventDefault();
const url = dbBtn.getAttribute("data-export-url");
const filename = dbBtn.getAttribute("data-default-filename");
saveDBFromURL(url, filename).catch(function (err) {
window.alert(err && err.message ? err.message : "备份失败");
});
return;
}
const editToggle = event.target.closest(".js-inline-edit-toggle");
if (editToggle) {
event.preventDefault();
const targetId = editToggle.getAttribute("data-target");
const form = targetId ? document.getElementById(targetId) : null;
const display = editToggle.closest(".editable-line");
if (form) {
form.hidden = false;
if (display) display.hidden = true;
const input = form.querySelector("input:not([type=hidden])");
if (input) {
input.focus();
input.select();
}
}
return;
}
const instanceRow = event.target.closest("[data-instance-row]");
if (instanceRow && !event.target.closest("button, a, input, select, textarea")) {
const index = instanceRow.getAttribute("data-instance-row");
const activeInput = document.getElementById("active-instance-input");
if (activeInput && index !== null) {
activeInput.value = index;
}
document.querySelectorAll("[data-instance-editor]").forEach(function (panel) {
panel.hidden = panel.getAttribute("data-instance-editor") !== index;
});
document.querySelectorAll("[data-instance-row]").forEach(function (row) {
row.classList.toggle("selected", row.getAttribute("data-instance-row") === index);
});
return;
}
const profileRow = event.target.closest("[data-profile-row]");
if (profileRow && !event.target.closest("button, a, input, select, textarea")) {
const href = profileRow.getAttribute("data-profile-href");
if (href) {
window.location.href = href;
}
return;
}
const navRow = event.target.closest("[data-nav-row]");
if (navRow && !event.target.closest("button, a, input, select, textarea")) {
const href = navRow.getAttribute("data-nav-href");
if (href) {
window.location.href = href;
}
return;
}
const instanceEditorToggle = event.target.closest(".js-open-instance-editor");
if (instanceEditorToggle) {
event.preventDefault();
const index = instanceEditorToggle.getAttribute("data-instance-index");
const activeInput = document.getElementById("active-instance-input");
if (activeInput && index !== null) {
activeInput.value = index;
}
document.querySelectorAll("[data-instance-editor]").forEach(function (panel) {
panel.hidden = panel.getAttribute("data-instance-editor") !== index;
});
document.querySelectorAll("[data-instance-row]").forEach(function (row) {
row.classList.toggle("selected", row.getAttribute("data-instance-row") === index);
});
const targetId = instanceEditorToggle.getAttribute("data-target");
const panel = targetId ? document.getElementById(targetId) : null;
if (panel) {
panel.scrollIntoView({ block: "start", behavior: "smooth" });
const input = panel.querySelector("input:not([type=hidden]):not([readonly]), textarea, select");
if (input) {
input.focus();
if (input.tagName === "INPUT" || input.tagName === "TEXTAREA") {
input.select();
}
}
}
return;
}
const editCancel = event.target.closest(".js-inline-edit-cancel");
if (editCancel) {
event.preventDefault();
const targetId = editCancel.getAttribute("data-target");
const form = targetId ? document.getElementById(targetId) : null;
if (form) {
form.hidden = true;
const container = form.parentElement;
const display = container ? container.querySelector(".editable-line") : null;
if (display) display.hidden = false;
}
}
});
const systemNavGroup = document.getElementById("system-nav-group");
if (systemNavGroup) {
const path = window.location.pathname || "";
if (
path === "/ui/models" ||
path === "/ui/resources" ||
path === "/ui/diagnostics" ||
path === "/ui/system" ||
path === "/ui/audit" ||
path === "/ui/api"
) {
systemNavGroup.open = true;
}
}
})();
</script>
</body>
</html>
{{end}}