3588AdminBackend/internal/web/ui/templates/layout.html

160 lines
5.9 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" />
<link rel="stylesheet" href="/ui/assets/graph_editor.css" />
</head>
<body class="theme-light">
<div class="app-shell">
<aside class="sidebar">
<div class="brand-block">
<div class="brand-mark">AI</div>
<div>
<div class="brand-title">视觉识别运维平台</div>
<div class="brand-subtitle">Fleet Operations Console</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/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>
<a href="/ui/diagnostics"><span class="nav-icon">{{icon "logs"}}</span><span>诊断</span></a>
</nav>
<div class="sidebar-footer">
<div class="sidebar-note">总览 fleet 状态,进入设备工作台,统一查看任务与诊断</div>
</div>
</aside>
<div class="workspace">
<header class="topbar">
<div>
<div class="crumb">多设备视觉识别运维平台</div>
<h1>{{.Title}}</h1>
</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 () {
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 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 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;
}
}
});
})();
</script>
</body>
</html>
{{end}}