Files
iAOP/web/shared/pro-header.js
T
bot_dev1 c0af291502 fix: 导航交互对齐 Ant Design Pro 体验(issue #131)
- 父级菜单整行点击 = 就地展开/收起 inline 子菜单(不再跳转),
  只有子项才导航,与 Pro 的 Menu inline 模式一致
- 子菜单展开加 max-height 过渡动画
- 菜单加 AntD Menu.ItemGroup 风格分组标题,hover 态补半透明白底
- 页内旧 topbar 去背景去边线,融入内容区当页标题行,消除双顶栏感
2026-08-05 16:20:52 +08:00

243 lines
9.4 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)。
*
* 在页面 <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();
}
})();