/* iAOP 全站统一布局(Ant Design Pro 左侧边栏风格,2026-08)。 * * 在页面 末尾引入即可自动注入 Ant Design Pro 经典管理布局: * · 左侧深色 Sider(#001529,AntD Pro 默认):logo + 纵向菜单 * (菜单 = 门户 + IAOP_SESSION.MODULES 按当前登录角色过滤; * 带 children 的模块渲染为可展开 SubMenu——如「管理控制台」下挂 * 模型管理/知识库管理/告警确认/审计查询,当前页菜单项高亮 #1677ff, * 支持底部折叠按钮收成 48px 图标栏); * · 顶部 48px 白色细栏:当前页标题 + 用户徽章(角色 Tag)+ 退出登录; * · 页面原内容整体右移(body.has-pro-sider 的 padding-left,见 pro.css), * 窄屏(≤720px)自动隐藏 Sider 退回纯顶栏。 * * 子菜单 ↔ 页内 Tab 联动(studio/admin): * 页面里原有的 #tabs .tab[data-page] 页内 Tab 条会被隐藏,改由 Sider * 子菜单通过 URL hash 驱动(admin/#models、studio/#import …): * 首次加载按 hash 激活对应 Tab,hashchange 时同步切换,页内 Tab 点击 * 也会回写 hash,URL 可分享直达子页。 * * 用法(各模块页相同,无需改动): * * * * * * 本组件不强制登录(守卫由 IAOP_SESSION.requireLoginElseRedirect 负责); * 未登录时菜单仅显示「门户」,用户区显示「未登录」。 */ "use strict"; (function () { function el(tag, cls, text) { var n = document.createElement(tag); if (cls) n.className = cls; if (text !== undefined) n.textContent = text; return n; } /* 站点根相对前缀:/index.html 为 "",/cockpit/、/auth/x.html 等一级目录为 "../" */ function rootPrefix() { var path = location.pathname.replace(/\/index\.html$/, "/"); var depth = path.split("/").filter(Boolean).length; return depth > 0 ? "../" : ""; } /* 当前页面命中的模块 key(用于菜单高亮/展开) */ function activeModuleKey() { var path = location.pathname; var mods = IAOP_SESSION.MODULES; for (var i = 0; i < mods.length; i++) { var url = mods[i].url.replace(/\/$/, ""); if (path.indexOf("/" + url) === 0 || path.indexOf("/" + url + "/") === 0) { return mods[i].key; } } return ""; // 根路径已重定向驾驶舱,无独立「门户」页 } /* 当前 hash 命中的子菜单 key(# 前缀归一化) */ function activeChildKey() { return location.hash.replace(/^#/, ""); } /* ---- 页内 Tab ↔ URL hash 联动(studio/admin 的 #tabs .tab[data-page]) --- */ function bindPageTabs() { var tabsBar = document.getElementById("tabs"); if (!tabsBar) return; var tabs = tabsBar.querySelectorAll(".tab[data-page]"); if (!tabs.length) return; // 子菜单已上收到 Sider:隐藏页内 Tab 条 tabsBar.classList.add("pro-tabs-hidden"); function activate(page) { var btn = tabsBar.querySelector('.tab[data-page="' + page + '"]'); if (btn) btn.click(); } window.addEventListener("hashchange", function () { activate(activeChildKey()); }); // 等页面自身脚本(studio.js/admin.js)绑好点击事件后再做初始激活 window.addEventListener("load", function () { var h = activeChildKey(); if (h) activate(h); }); // 页内 Tab 点击回写 hash(保持 URL 可分享) tabsBar.addEventListener("click", function (e) { var t = e.target.closest(".tab[data-page]"); if (t && ("#" + t.getAttribute("data-page")) !== location.hash) { history.replaceState(null, "", "#" + t.getAttribute("data-page")); } }); } function build() { var body = document.body; if (!body || document.querySelector(".pro-sider")) return; var title = body.getAttribute("data-pro-title") || document.title || ""; // 无门户页(2026-08):logo 与默认首页均指向驾驶舱 var home = body.getAttribute("data-pro-home") || "../cockpit/"; var loginUrl = body.getAttribute("data-pro-login") || "../auth/login.html"; var prefix = rootPrefix(); var active = activeModuleKey(); /* ---- 左侧 Sider ----------------------------------------------------- */ var sider = el("aside", "pro-sider"); var logo = el("a", "pro-sider-logo"); logo.href = home; logo.appendChild(el("span", "pro-logo-badge", "iA")); logo.appendChild(el("span", "pro-sider-logo-text", "iAOP 云美工业AI优化平台")); sider.appendChild(logo); var menu = el("nav", "pro-sider-menu"); sider.appendChild(menu); // 折叠按钮(AntD Pro Sider 底部 trigger) var trigger = el("button", "pro-sider-trigger", "⟨ 折叠"); trigger.type = "button"; trigger.addEventListener("click", function () { var collapsed = body.classList.toggle("pro-sider-collapsed"); trigger.textContent = collapsed ? "⟩" : "⟨ 折叠"; }); sider.appendChild(trigger); body.insertBefore(sider, body.firstChild); /* ---- 顶部细栏 -------------------------------------------------------- */ var bar = el("div", "pro-header"); if (title) bar.appendChild(el("span", "pro-header-page-title", title)); var right = el("div", "pro-header-right"); var userBox = el("span", "pro-user"); right.appendChild(userBox); var logoutBtn = el("button", "pro-logout", "退出"); logoutBtn.type = "button"; logoutBtn.addEventListener("click", function () { IAOP_SESSION.logout(loginUrl); }); right.appendChild(logoutBtn); bar.appendChild(right); body.insertBefore(bar, sider.nextSibling); body.classList.add("has-pro-sider"); /* ---- 菜单渲染(需要登录角色,异步) ----------------------------------- */ function addLeafItem(key, icon, text, href) { var a = el("a", "pro-menu-item" + (key === active ? " active" : "")); a.href = href; a.title = text; a.appendChild(el("span", "pro-menu-icon", icon)); a.appendChild(el("span", "pro-menu-text", text)); menu.appendChild(a); } function addSubMenu(m) { var isActiveModule = (m.key === active); var wrap = el("div", "pro-submenu" + (isActiveModule ? " open" : "")); var head = el("a", "pro-menu-item pro-submenu-title" + (isActiveModule ? " active" : "")); head.href = prefix + m.url; head.title = m.title; head.appendChild(el("span", "pro-menu-icon", m.icon)); head.appendChild(el("span", "pro-menu-text", m.title)); var arrow = el("span", "pro-submenu-arrow", "▸"); head.appendChild(arrow); // 箭头区域点击只展开/收起,不跳转;点标题文字跳转 arrow.addEventListener("click", function (e) { e.preventDefault(); e.stopPropagation(); wrap.classList.toggle("open"); }); wrap.appendChild(head); var childBox = el("div", "pro-submenu-items"); var currentChild = activeChildKey(); m.children.forEach(function (c) { var a = el("a", "pro-subitem" + (isActiveModule && c.key === currentChild ? " active" : "")); a.href = prefix + m.url.replace(/\/$/, "/") + c.hash; a.textContent = c.title; a.title = c.title; // 同页子菜单:只改 hash,由 bindPageTabs 的 hashchange 驱动切换 if (isActiveModule) { a.addEventListener("click", function (e) { e.preventDefault(); childBox.querySelectorAll(".pro-subitem").forEach(function (x) { x.classList.remove("active"); }); a.classList.add("active"); location.hash = c.hash; // 触发 hashchange → 激活对应 Tab }); } childBox.appendChild(a); }); wrap.appendChild(childBox); menu.appendChild(wrap); } function renderMenu(user) { menu.innerHTML = ""; // 无门户页(2026-08):菜单即模块清单,按角色过滤 var mods = user ? IAOP_SESSION.modulesFor(user.role) : []; mods.forEach(function (m) { if (m.children && m.children.length) addSubMenu(m); else addLeafItem(m.key, m.icon, m.title, prefix + m.url); }); } /* ---- 用户区(后端优先、本地降级由 session.js 内部处理) --------------- */ IAOP_SESSION.currentUser().then(function (u) { renderMenu(u); userBox.innerHTML = ""; if (!u) { userBox.appendChild(el("span", "pro-tag pro-tag-default", "未登录")); return; } var avatar = el("span", "pro-avatar", (u.username || "?").slice(0, 1).toUpperCase()); userBox.appendChild(avatar); userBox.appendChild(el("span", null, u.username)); var color = IAOP_SESSION.ROLE_TAG_COLORS[u.role] || "default"; var label = IAOP_SESSION.ROLE_LABELS[u.role] || u.role; userBox.appendChild(el("span", "pro-tag pro-tag-" + color, label)); }); bindPageTabs(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", build); } else { build(); } })();