Files
iAOP/web/shared/pro-header.js
T
bot_dev1 8b68d015c2 fix: 组件配色全面对齐 Ant Design 5 token + 修复 iframe 内页签初始激活(issue #131)
组件审查结论:全站组件为自绘 HTML+CSS 仿 AntD(纯静态无组件库),
pro.css 已统一设计 token,但各页 CSS/JS 残留一批旧配色硬编码,
pro.css 覆盖不到,本次全部清理:

- 旧 iOS 红 #ff3b30 → --ant-error #ff4d4f(告警 P0/严重度/DLP 条/
  工艺 alarm 态/diff 删除行)
- 旧橙 #f5a623 → --ant-warning #faad14(P1 告警/warning 态/暂存徽章)
- 旧 iOS 绿 #2ecc71 → --ant-success #52c41a(running 态/prod 徽章/
  索引完成/路由本地标签/权限矩阵)
- 杂色蓝统一 → --ant-primary #1677ff:发送键 #0ea5e9、P2 告警
  #3aa0ff、编排画布残留青 rgba(24,211,200)、旧气泡 #1d4ed8
- 主色按钮上的深色文字 #04202a → #fff(对比度修正)
- 灰 #64748b → --ant-text-tertiary rgba(0,0,0,0.45)
- 各页 CSS :root 深色默认变量值(#0b1220/#13203a/#18d3c8 等死值)
  对齐为 AntD 亮色默认值,修正"深色主题"过时注释
- 删除已无任何引用的旧深色登录页样式 auth/auth.css

顺带修复实测发现的 bug:SPA 外壳内直达 admin/studio 子页时页签
不切换——studio.js/admin.js 的 Tab 事件在异步 boot(user) 中绑定,
bindPageTabs 的 window load 初始激活可能早于绑定完成,改为带重试
的激活(直到 Tab 点亮,最多 10s)。实测 #/admin/#alerts 直达后
page-alerts 正确激活。
2026-08-06 08:20:17 +08:00

327 lines
13 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) {
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(user) {
menu.innerHTML = "";
// AntD Menu.ItemGroup 风格分组标题
var group = el("div", "pro-menu-group-title", "功能模块");
menu.appendChild(group);
// 菜单即模块清单,按角色过滤
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);
});
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;
}
};
}
/* ---- 用户区(后端优先、本地降级由 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();
// 子菜单高亮全局同步(独立模式:同页 hash 切换 / 分享直达 / 跨页返回)
if (!SHELL) window.addEventListener("hashchange", syncSubHighlight);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", build);
} else {
build();
}
})();