Files
iAOP/web/shared/pro-header.js
T

349 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 可分享直达。
*
* 用法(各模块页相同,无需改动):
* <body data-pro-title="配置化驾驶舱">
* <script src="../auth/user_store.js"></script>
* <script src="../shared/session.js"></script>
* <script src="../shared/pro-header.js"></script>
*
* 本组件不强制登录(守卫由 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";
// 外壳已提供全局导航与登录用户展示:隐藏页内顶栏里的跨页跳转链接
// (驾驶舱/配置台等 <a>,避免 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, external) {
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 (external) {
// 外部承载模块(FBA UI 等):新标签打开,不进 iframe 外壳
a.target = "_blank";
a.rel = "noopener";
} else 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,
m.external ? m.url : prefix + m.url, !!m.external);
});
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));
// Epic #159 Phase 3:FBA 在线且当前为 admin → 顶栏提供「系统管理」
// 直达 FBA UI(用户/角色/菜单/日志管理),新标签打开
if (u.role === "admin" && IAOP_SESSION.fbaStatus) {
IAOP_SESSION.fbaStatus().then(function (st) {
if (!st.available) return;
var link = el("a", "pro-logout", "系统管理");
link.href = "/fba-admin/"; // nginx 绝对路径(deploy/fba/nginx-fba.conf)
link.target = "_blank";
link.rel = "noopener";
link.style.textDecoration = "none";
link.style.display = "inline-block";
right.insertBefore(link, logoutBtn);
});
}
// 菜单异步: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();
}
})();