- 父级菜单整行点击 = 就地展开/收起 inline 子菜单(不再跳转), 只有子项才导航,与 Pro 的 Menu inline 模式一致 - 子菜单展开加 max-height 过渡动画 - 菜单加 AntD Menu.ItemGroup 风格分组标题,hover 态补半透明白底 - 页内旧 topbar 去背景去边线,融入内容区当页标题行,消除双顶栏感
243 lines
9.4 KiB
JavaScript
243 lines
9.4 KiB
JavaScript
/* iAOP 全站统一布局(Ant Design Pro 左侧边栏风格,2026-08)。
|
||
*
|
||
* 在页面 <body> 末尾引入即可自动注入 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 可分享直达子页。
|
||
*
|
||
* 用法(各模块页相同,无需改动):
|
||
* <body data-pro-title="配置化驾驶舱" data-pro-home="../index.html">
|
||
* <script src="../auth/user_store.js"></script>
|
||
* <script src="../shared/session.js"></script>
|
||
* <script src="../shared/pro-header.js"></script>
|
||
*
|
||
* 本组件不强制登录(守卫由 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 = "#";
|
||
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 菜单交互:整行点击 = 就地展开/收起(不跳转),
|
||
// 只有子项才导航(跨页跳转或同页 hash 切换)
|
||
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" : ""));
|
||
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 = "";
|
||
// AntD Menu.ItemGroup 风格分组标题
|
||
var group = el("div", "pro-menu-group-title", "功能模块");
|
||
menu.appendChild(group);
|
||
// 无门户页(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();
|
||
}
|
||
})();
|