/* iAOP SPA 外壳导航(2026-08,对齐 Ant Design Pro 体验)。 * * 仅在 web/index.html(data-pro-shell)使用: * · 登录守卫(未登录跳登录页,?next 回跳本壳); * · 菜单点击 → IAOP_SHELL.navigate(url) → 换 iframe src, * 外壳(Sider/顶栏)常驻不刷新,消灭整页白闪; * · URL 同步:外壳 hash `#/admin/#models` ↔ iframe 地址, * 支持浏览器前进/后退与分享直达子页; * · iframe 地址经 250ms 轮询同步菜单高亮与顶栏页题(经 pro-header * 的 IAOP_PRO.setActive);iframe 内的子页切换同样回同步外壳 * URL(replaceState,不产生额外历史); * · 移动端模块不在 iframe 内打开(移动 UI 不适合桌面框),新标签打开。 */ "use strict"; (function () { var frame = document.getElementById("module-frame"); if (!frame) return; var currentUrl = null; // 当前 iframe 模块地址(相对,如 "admin/#models") /* 外壳 hash `#/admin/#models` → 模块地址 "admin/#models" */ function parseShellHash() { var h = location.hash || ""; if (h.indexOf("#/") !== 0) return null; return h.slice(2) || null; // "#/admin/#models".slice(2) = "admin/#models" } /* 模块地址 → 模块 key + 子 hash */ function resolveModule(url) { var parts = url.split("#"); var path = parts[0]; var mods = IAOP_SESSION.MODULES; for (var i = 0; i < mods.length; i++) { var mu = mods[i].url.replace(/\/$/, ""); if (path === mods[i].url || path === mu || path.indexOf(mu + "/") === 0) { return { key: mods[i].key, title: mods[i].title, child: parts[1] || "" }; } } return { key: "", title: "", child: parts[1] || "" }; } function syncChrome(url) { var m = resolveModule(url); if (window.IAOP_PRO && IAOP_PRO.setActive) { IAOP_PRO.setActive(m.key, m.child, m.title); } } function navigate(url) { // 移动端模块新标签打开(移动 UI 不嵌入桌面框) if (url.indexOf("mobile/") === 0) { window.open(url, "_blank"); return; } if (url === currentUrl) return; currentUrl = url; // 用 location.replace 而非直接赋 frame.src:避免 iframe 产生自己的 // 会话历史条目污染 joint history(否则浏览器后退会绕过外壳直接 // 恢复 iframe 旧地址,造成外壳 URL / 菜单高亮与内容脱节)。 // 注意相对地址必须先按外壳页面解析成绝对地址——iframe 处于 // about:blank 时 location.replace 的相对解析不可靠。 var abs = new URL(url, location.href).href; try { frame.contentWindow.location.replace(abs); } catch (e) { frame.src = abs; // 兜底 } var shellHash = "#/" + url; if (location.hash !== shellHash) location.hash = shellHash; // 产生历史记录 // 乐观高亮:目标模块已知,立即同步菜单/顶栏页题,不等 iframe 加载 // (后台标签下 setInterval 轮询会被浏览器节流,不能作为唯一途径)。 lastSeen = url; syncChrome(url); } /* iframe 地址同步:轮询而非 load/hashchange 事件—— * contentWindow.location.replace() 导航不触发 iframe 元素 load 事件, * 轮询同时覆盖「模块切换」与「iframe 内 Tab 切子页」两种场景。 */ var lastSeen = null; function pollFrame() { try { var w = frame.contentWindow; if (!w || w.location.href === "about:blank") return; var url = w.location.pathname.replace(/^\//, "") + w.location.hash; if (url === lastSeen) return; lastSeen = url; // 浏览器前进/后退可能直接恢复了 iframe 旧地址(joint history 里 // 的历史条目):此时以外壳 hash 为准,强制重新对齐。 var expected = parseShellHash(); if (expected && expected !== url) { currentUrl = null; // 绕过 navigate 的同值短路 navigate(expected); return; } currentUrl = url; // iframe 内 Tab 切子页:外壳 URL 跟随但不新增历史(replaceState) if (location.hash !== "#/" + url) { history.replaceState(null, "", "#/" + url); } syncChrome(url); } catch (e) { /* 跨域保护兜底 */ } } setInterval(pollFrame, 250); /* 外壳 hash(前进/后退/分享链接)→ 导航 */ window.addEventListener("hashchange", function () { var url = parseShellHash(); if (url && url !== currentUrl) navigate(url); }); /* 菜单渲染晚于首次导航的竞态:菜单就绪后重放当前高亮 */ window.addEventListener("pro:menu-rendered", function () { if (currentUrl) syncChrome(currentUrl); }); window.IAOP_SHELL = { navigate: navigate }; /* 启动:登录守卫 → 加载初始模块(hash 指定或默认驾驶舱) */ IAOP_SESSION.requireLoginElseRedirect("auth/login.html").then(function (user) { if (!user) return; // 正在跳转登录页 navigate(parseShellHash() || "cockpit/"); }); })();