feat: 全站导航改为 Ant Design Pro 左侧边栏布局(issue #131)
- pro-header.js 重构:深色 Sider(#001529,AntD Pro 经典)+ 顶部 48px 细栏 - Sider 菜单 = 门户 + 模块按登录角色过滤,当前页高亮 #1677ff, 底部折叠按钮可收成 48px 图标栏;窄屏(≤720px)自动隐藏退回纯顶栏 - 菜单/高亮按 URL 自动计算站点根前缀,六个模块页零改动接入
This commit is contained in:
+86
-22
@@ -1,26 +1,32 @@
|
||||
/* iAOP 全站统一导航条(Ant Design Pro 风格重构,2026-08)。
|
||||
/* iAOP 全站统一布局(Ant Design Pro 左侧边栏风格,2026-08)。
|
||||
*
|
||||
* 在页面 <body> 末尾引入即可自动注入一条 48px 白色 Pro 导航条
|
||||
* (logo + 平台名 + 当前页标题 + 门户入口 + 当前用户/角色 Tag + 退出登录),
|
||||
* 各模块页视觉框架统一。样式在 pro.css 的 .pro-header 系列。
|
||||
* 在页面 <body> 末尾引入即可自动注入 Ant Design Pro 经典管理布局:
|
||||
* · 左侧深色 Sider(#001529,AntD Pro 默认):logo + 纵向菜单
|
||||
* (菜单 = 门户 + IAOP_SESSION.MODULES 按当前登录角色过滤,
|
||||
* 当前页菜单项高亮 #1677ff,支持底部折叠按钮收成 48px 图标栏);
|
||||
* · 顶部 48px 白色细栏:当前页标题 + 用户徽章(角色 Tag)+ 退出登录;
|
||||
* · 页面原内容整体右移(body.has-pro-sider 的 padding-left,见 pro.css),
|
||||
* 窄屏(≤720px)自动隐藏 Sider 退回纯顶栏。
|
||||
*
|
||||
* 用法:
|
||||
* 用法(各模块页相同,无需改动):
|
||||
* <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>
|
||||
*
|
||||
* data-pro-title:当前页标题(显示在平台名右侧,竖线分隔)。
|
||||
* data-pro-home :门户(首页)相对路径,缺省 ../index.html。
|
||||
* data-pro-title:当前页标题(顶部细栏左侧 + 菜单高亮匹配)。
|
||||
* data-pro-home :门户相对路径,缺省 ../index.html。
|
||||
* data-pro-login:登录页相对路径(退出后跳转),缺省 ../auth/login.html。
|
||||
*
|
||||
* 本组件不强制登录(守卫由页面或 IAOP_SESSION.requireLoginElseRedirect 负责);
|
||||
* 已登录时显示用户徽章,未登录时用户区显示「未登录」。
|
||||
* 本组件不强制登录(守卫由 IAOP_SESSION.requireLoginElseRedirect 负责);
|
||||
* 未登录时菜单仅显示「门户」,用户区显示「未登录」。
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
(function () {
|
||||
var SIDER_WIDTH = 208; // AntD Pro Sider 默认宽度
|
||||
var COLLAPSED_WIDTH = 48; // AntD 折叠后宽度
|
||||
|
||||
function el(tag, cls, text) {
|
||||
var n = document.createElement(tag);
|
||||
if (cls) n.className = cls;
|
||||
@@ -28,32 +34,67 @@
|
||||
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 activeKey(prefix) {
|
||||
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 path === "/" || /\/index\.html$/.test(path) ? "portal" : "";
|
||||
}
|
||||
|
||||
function build() {
|
||||
var body = document.body;
|
||||
if (!body || document.querySelector(".pro-header")) return;
|
||||
if (!body || document.querySelector(".pro-sider")) return;
|
||||
|
||||
var title = body.getAttribute("data-pro-title") || document.title || "";
|
||||
var home = body.getAttribute("data-pro-home") || "../index.html";
|
||||
var loginUrl = body.getAttribute("data-pro-login") || "../auth/login.html";
|
||||
var prefix = rootPrefix();
|
||||
var active = activeKey();
|
||||
|
||||
var bar = el("div", "pro-header");
|
||||
/* ---- 左侧 Sider ----------------------------------------------------- */
|
||||
var sider = el("aside", "pro-sider");
|
||||
|
||||
var logo = el("a", "pro-logo");
|
||||
var logo = el("a", "pro-sider-logo");
|
||||
logo.href = home;
|
||||
logo.appendChild(el("span", "pro-logo-badge", "iA"));
|
||||
logo.appendChild(el("span", "pro-logo-text", "iAOP 云美工业AI优化平台"));
|
||||
bar.appendChild(logo);
|
||||
logo.appendChild(el("span", "pro-sider-logo-text", "iAOP 云美工业AI优化平台"));
|
||||
sider.appendChild(logo);
|
||||
|
||||
if (title) bar.appendChild(el("span", "pro-page-title", title));
|
||||
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 homeLink = el("a", "pro-nav-link", "门户");
|
||||
homeLink.href = home;
|
||||
right.appendChild(homeLink);
|
||||
|
||||
var logoutBtn = el("button", "pro-logout", "退出");
|
||||
logoutBtn.type = "button";
|
||||
logoutBtn.addEventListener("click", function () {
|
||||
@@ -62,10 +103,33 @@
|
||||
right.appendChild(logoutBtn);
|
||||
|
||||
bar.appendChild(right);
|
||||
body.insertBefore(bar, body.firstChild);
|
||||
body.insertBefore(bar, sider.nextSibling);
|
||||
|
||||
// 异步填充当前用户(后端优先、本地降级由 session.js 内部处理)
|
||||
body.classList.add("has-pro-sider");
|
||||
|
||||
/* ---- 菜单渲染(需要登录角色,异步) ----------------------------------- */
|
||||
function renderMenu(user) {
|
||||
menu.innerHTML = "";
|
||||
|
||||
function addItem(key, icon, text, href, tag) {
|
||||
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);
|
||||
}
|
||||
|
||||
addItem("portal", "🏠", "门户", prefix + "index.html");
|
||||
var mods = user ? IAOP_SESSION.modulesFor(user.role) : [];
|
||||
mods.forEach(function (m) {
|
||||
addItem(m.key, m.icon, m.title, prefix + m.url, m.tag);
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- 用户区(后端优先、本地降级由 session.js 内部处理) --------------- */
|
||||
IAOP_SESSION.currentUser().then(function (u) {
|
||||
renderMenu(u);
|
||||
userBox.innerHTML = "";
|
||||
if (!u) {
|
||||
userBox.appendChild(el("span", "pro-tag pro-tag-default", "未登录"));
|
||||
|
||||
+96
-34
@@ -69,26 +69,34 @@ body {
|
||||
line-height: 1.5715;
|
||||
}
|
||||
|
||||
/* ---- 统一全站导航条(pro-header.js 注入) ------------------------------ */
|
||||
.pro-header {
|
||||
/* ---- Ant Design Pro 左侧边栏布局(pro-header.js 注入) ------------------- */
|
||||
/* 深色 Sider(#001529,AntD Pro 默认)+ 顶部白色细栏 + 内容区右移 */
|
||||
.pro-sider {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 208px;
|
||||
background: #001529;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
|
||||
transition: width 0.2s;
|
||||
}
|
||||
.pro-sider-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
gap: 10px;
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
background: var(--ant-bg-container);
|
||||
box-shadow: var(--ant-shadow-header);
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
.pro-header .pro-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pro-header .pro-logo-badge {
|
||||
.pro-sider-logo .pro-logo-badge {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: var(--ant-radius);
|
||||
@@ -100,19 +108,82 @@ body {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
letter-spacing: 0.5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.pro-header .pro-logo-text {
|
||||
.pro-sider-logo-text {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.pro-sider-menu {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.pro-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
height: 40px;
|
||||
margin: 4px 8px;
|
||||
padding: 0 12px;
|
||||
border-radius: var(--ant-radius);
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
.pro-menu-item:hover { color: #fff; }
|
||||
.pro-menu-item.active {
|
||||
background: var(--ant-primary);
|
||||
color: #fff;
|
||||
}
|
||||
.pro-menu-icon { font-size: 16px; flex-shrink: 0; width: 20px; text-align: center; }
|
||||
.pro-menu-text { overflow: hidden; text-overflow: ellipsis; }
|
||||
.pro-sider-trigger {
|
||||
flex-shrink: 0;
|
||||
height: 40px;
|
||||
border: none !important;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08) !important;
|
||||
border-radius: 0 !important;
|
||||
background: transparent !important;
|
||||
color: rgba(255, 255, 255, 0.65) !important;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
.pro-sider-trigger:hover { color: #fff !important; }
|
||||
|
||||
/* 内容区右移:Sider 固定定位,页面原内容整体让位 */
|
||||
body.has-pro-sider { padding-left: 208px; transition: padding-left 0.2s; }
|
||||
body.has-pro-sider.pro-sider-collapsed { padding-left: 48px; }
|
||||
body.pro-sider-collapsed .pro-sider { width: 48px; }
|
||||
body.pro-sider-collapsed .pro-sider-logo-text,
|
||||
body.pro-sider-collapsed .pro-menu-text { display: none; }
|
||||
body.pro-sider-collapsed .pro-menu-item { justify-content: center; padding: 0; }
|
||||
body.pro-sider-collapsed .pro-sider-logo { justify-content: center; padding: 0; }
|
||||
|
||||
/* ---- 顶部白色细栏(页标题 + 用户区) ------------------------------------- */
|
||||
.pro-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
background: var(--ant-bg-container);
|
||||
box-shadow: var(--ant-shadow-header);
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
.pro-header-page-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--ant-text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pro-header .pro-page-title {
|
||||
font-size: 14px;
|
||||
color: var(--ant-text-tertiary);
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid var(--ant-split);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -140,16 +211,6 @@ body {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.pro-header a.pro-nav-link {
|
||||
color: var(--ant-text-secondary);
|
||||
text-decoration: none;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--ant-radius);
|
||||
}
|
||||
.pro-header a.pro-nav-link:hover {
|
||||
color: var(--ant-primary);
|
||||
background: var(--ant-bg-fill);
|
||||
}
|
||||
.pro-header .pro-logout {
|
||||
border: none;
|
||||
background: none;
|
||||
@@ -288,8 +349,9 @@ table th, table td {
|
||||
.status-dot.ok, .dot.ok { color: var(--ant-success) !important; }
|
||||
.status-dot.down, .dot.down { color: var(--ant-error) !important; }
|
||||
|
||||
/* ---- 窄屏适配:导航条允许换行收缩 --------------------------------------- */
|
||||
/* ---- 窄屏适配:隐藏 Sider 退回纯顶栏 ------------------------------------- */
|
||||
@media (max-width: 720px) {
|
||||
.pro-header .pro-page-title { display: none; }
|
||||
.pro-header .pro-logo-text { font-size: 14px; }
|
||||
.pro-sider { display: none; }
|
||||
body.has-pro-sider, body.has-pro-sider.pro-sider-collapsed { padding-left: 0; }
|
||||
.pro-header-page-title { font-size: 14px; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user