From 3a1cccc0c699d5cc74266563a700271d64a6a141 Mon Sep 17 00:00:00 2001 From: bot_dev1 Date: Wed, 5 Aug 2026 14:31:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=85=A8=E7=AB=99=E5=AF=BC=E8=88=AA?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=20Ant=20Design=20Pro=20=E5=B7=A6=E4=BE=A7?= =?UTF-8?q?=E8=BE=B9=E6=A0=8F=E5=B8=83=E5=B1=80=EF=BC=88issue=20#131?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pro-header.js 重构:深色 Sider(#001529,AntD Pro 经典)+ 顶部 48px 细栏 - Sider 菜单 = 门户 + 模块按登录角色过滤,当前页高亮 #1677ff, 底部折叠按钮可收成 48px 图标栏;窄屏(≤720px)自动隐藏退回纯顶栏 - 菜单/高亮按 URL 自动计算站点根前缀,六个模块页零改动接入 --- web/shared/pro-header.js | 108 +++++++++++++++++++++++++------- web/shared/pro.css | 130 +++++++++++++++++++++++++++++---------- 2 files changed, 182 insertions(+), 56 deletions(-) diff --git a/web/shared/pro-header.js b/web/shared/pro-header.js index c1ae372..b4be8d3 100644 --- a/web/shared/pro-header.js +++ b/web/shared/pro-header.js @@ -1,26 +1,32 @@ -/* iAOP 全站统一导航条(Ant Design Pro 风格重构,2026-08)。 +/* iAOP 全站统一布局(Ant Design Pro 左侧边栏风格,2026-08)。 * - * 在页面 末尾引入即可自动注入一条 48px 白色 Pro 导航条 - * (logo + 平台名 + 当前页标题 + 门户入口 + 当前用户/角色 Tag + 退出登录), - * 各模块页视觉框架统一。样式在 pro.css 的 .pro-header 系列。 + * 在页面 末尾引入即可自动注入 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 退回纯顶栏。 * - * 用法: + * 用法(各模块页相同,无需改动): * - * ... * * * * - * 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", "未登录")); diff --git a/web/shared/pro.css b/web/shared/pro.css index f5bf38c..80ab5c9 100644 --- a/web/shared/pro.css +++ b/web/shared/pro.css @@ -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; } }