/* iAOP 全站统一布局(Ant Design Pro 左侧边栏风格,2026-08)。 * * 三种运行模式(自动判别): * 1. SPA 外壳模式(web/index.html,body[data-pro-shell]): * Sider/顶栏常驻;菜单点击不整页跳转,改调 IAOP_SHELL.navigate(url) * 换 iframe 内容(对齐 Ant Design Pro 的无刷新体验); * 导出 IAOP_PRO.setActive(moduleKey, childHash, title) 供 shell.js * 在 iframe 加载/子页切换后同步菜单高亮与顶栏页题。 * 2. 被嵌模式(模块页在 iframe 内,window.self !== window.top): * 不注入 Sider/顶栏、不铺 Sider 底色,保留页内 Tab ↔ hash 联动; * 隐藏页内顶栏的跨页链接与重复用户标识(外壳已提供全局导航)。 * 3. 独立模式(模块页直接打开):注入 Sider/顶栏(老行为), * 菜单按角色过滤,整页跳转。 * * 页内 Tab ↔ URL hash 联动(studio/admin)在三种模式下都生效: * #tabs .tab[data-page] 自动隐藏,改由 URL hash 驱动切换,URL 可分享直达。 * * 用法(各模块页相同,无需改动): *
* * * * * 本组件不强制登录(守卫由 IAOP_SESSION 负责)。 */ "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)绑好点击事件后再做初始激活。 // 注意:studio.js/admin.js 的 Tab 事件在异步 boot(user) 里绑定, // window load 时可能尚未就绪——带重试地激活,直到 Tab 真的点亮。 var activateTries = 0; function activateInitial() { var h = activeChildKey(); if (!h) return; var btn = tabsBar.querySelector('.tab[data-page="' + h + '"]'); if (!btn) return; btn.click(); if (!btn.classList.contains("active") && ++activateTries < 40) { setTimeout(activateInitial, 250); // 最多重试 10s } } window.addEventListener("load", activateInitial); // 页内 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) return; /* ---- 被嵌模式:模块页在 SPA 外壳的 iframe 内,只做 Tab↔hash 联动 ------ */ if (window.self !== window.top) { // 覆盖模块页 head 内联的「左深右灰」防闪底色(iframe 内容区无 Sider) document.documentElement.style.background = "#f0f2f5"; // 外壳已提供全局导航与登录用户展示:隐藏页内顶栏里的跨页跳转链接 // (驾驶舱/配置台等 ,避免 iframe 内自行跳走)与重复的用户标识 #who。 // 保留功能性控件(模板/角色下拉、状态点等)。 var whoEl = document.getElementById("who"); if (whoEl) whoEl.style.display = "none"; Array.prototype.forEach.call( document.querySelectorAll("#topbar-right a, #topbar a"), function (a) { a.style.display = "none"; } ); bindPageTabs(); return; } if (document.querySelector(".pro-sider")) return; var SHELL = body.hasAttribute("data-pro-shell"); var title = body.getAttribute("data-pro-title") || document.title || ""; var home = body.getAttribute("data-pro-home") || "../cockpit/"; var loginUrl = body.getAttribute("data-pro-login") || "../auth/login.html"; var prefix = rootPrefix(); var active = SHELL ? "" : 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"); var titleEl = el("span", "pro-header-page-title", title); if (title) bar.appendChild(titleEl); 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"); /* ---- 菜单项导航:外壳模式走 IAOP_SHELL(无刷新),独立模式整页跳转 ----- */ function go(url) { if (SHELL && window.IAOP_SHELL) { IAOP_SHELL.navigate(url); } else { location.href = url; } } /* ---- 菜单渲染(需要登录角色,异步) ----------------------------------- */ function addLeafItem(key, icon, text, href) { var a = el("a", "pro-menu-item" + (key === active ? " active" : "")); a.href = href; a.title = text; a.setAttribute("data-module", key); a.appendChild(el("span", "pro-menu-icon", icon)); a.appendChild(el("span", "pro-menu-text", text)); if (SHELL) { a.addEventListener("click", function (e) { e.preventDefault(); go(href); }); } menu.appendChild(a); } function addSubMenu(m) { var isActiveModule = (m.key === active); var wrap = el("div", "pro-submenu" + (isActiveModule ? " open" : "")); wrap.setAttribute("data-module", m.key); var head = el("a", "pro-menu-item pro-submenu-title" + (isActiveModule ? " active" : "")); head.href = "#"; 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); // AntD Pro inline 菜单交互:整行点击 = 就地展开/收起(不跳转) head.addEventListener("click", function (e) { e.preventDefault(); 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" : "")); var href = prefix + m.url.replace(/\/$/, "/") + c.hash; a.href = href; a.textContent = c.title; a.title = c.title; a.setAttribute("data-child", c.key); if (SHELL) { a.addEventListener("click", function (e) { e.preventDefault(); go(href); }); } childBox.appendChild(a); }); wrap.appendChild(childBox); menu.appendChild(wrap); } function renderMenu(mods) { menu.innerHTML = ""; // AntD Menu.ItemGroup 风格分组标题 var group = el("div", "pro-menu-group-title", "功能模块"); menu.appendChild(group); // 菜单即模块清单:Epic #159 Phase 2 起由 session.js menuTree() // 输出(FBA 服务端按角色授权过滤,本地角色过滤兜底) (mods || []).forEach(function (m) { if (m.children && m.children.length) addSubMenu(m); else addLeafItem(m.key, m.icon, m.title, prefix + m.url); }); if (!SHELL) syncSubHighlight(); // 菜单(重)渲染完成事件:shell.js 据此重放当前高亮, // 消除「导航先于菜单渲染」的竞态(直达链接/刷新场景)。 window.dispatchEvent(new CustomEvent("pro:menu-rendered")); } /* hash → 子菜单高亮同步(独立模式:同页切换与分享直达都经过这里) */ function syncSubHighlight() { var h = activeChildKey(); menu.querySelectorAll(".pro-subitem").forEach(function (x) { var href = x.getAttribute("href") || ""; var hash = href.indexOf("#") >= 0 ? href.slice(href.indexOf("#")) : ""; x.classList.toggle("active", !!h && hash === "#" + h); }); } /* ---- 外壳模式:供 shell.js 同步菜单高亮与顶栏页题 ---------------------- */ if (SHELL) { window.IAOP_PRO = { setActive: function (moduleKey, childHash, moduleTitle) { menu.querySelectorAll(".pro-menu-item").forEach(function (x) { x.classList.remove("active"); }); menu.querySelectorAll(".pro-subitem").forEach(function (x) { x.classList.remove("active"); }); if (!moduleKey) return; var wrap = menu.querySelector('.pro-submenu[data-module="' + moduleKey + '"]'); if (wrap) { wrap.classList.add("open"); wrap.querySelector(".pro-submenu-title").classList.add("active"); if (childHash) { var sub = wrap.querySelector('.pro-subitem[data-child="' + childHash + '"]'); if (sub) sub.classList.add("active"); } } else { var leaf = menu.querySelector('.pro-menu-item[data-module="' + moduleKey + '"]'); if (leaf) leaf.classList.add("active"); } if (moduleTitle) titleEl.textContent = moduleTitle; } }; } /* ---- 用户区 + 菜单(FBA 服务端授权优先,本地角色过滤兜底) ------------- */ IAOP_SESSION.currentUser().then(function (u) { userBox.innerHTML = ""; if (!u) { userBox.appendChild(el("span", "pro-tag pro-tag-default", "未登录")); renderMenu([]); 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)); // 菜单异步:FBA token 存在时取服务端授权菜单树(Phase 2) IAOP_SESSION.menuTree(u).then(renderMenu); }); bindPageTabs(); // 子菜单高亮全局同步(独立模式:同页 hash 切换 / 分享直达 / 跨页返回) if (!SHELL) window.addEventListener("hashchange", syncSubHighlight); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", build); } else { build(); } })();