feat: 全站 Ant Design Pro 风格重构 + 登录守卫与角色门户(issue #131/#134)

- 新增 web/shared:pro.css(AntD 5 设计系统,旧 CSS 变量统一映射 AntD 色板)、
  session.js(后端优先+本地账号降级双轨会话,IAOP_AUTH 兼容层)、
  pro-header.js(全站统一 Pro 导航条:logo/页题/用户角色 Tag/退出)
- 门户 web/index.html:未登录跳登录页(next 回跳),登录后按角色过滤
  可见模块(readonly→驾驶舱/助手/移动端;engineer→+配置台;admin→全部)
- 登录页 auth/login.html 重写为 Ant Design Pro 风格,本地降级模式自动
  播种演示账号(admin/engineer/viewer),保留 OIDC SSO 双轨入口
- user_store.js 新增 verify() + seedDefaults() 默认账号播种
- 六模块页统一接入 pro.css + 统一导航条 + 登录守卫(PRD 8.2)
- 驾驶舱主题 dark→light(Ant Design 5 色板):renderer.py THEME_TOKENS、
  ti-cl4/resin 模板 theme、告警三级配色对齐 AntD(P0 #ff4d4f/P1 #faad14/
  P2 #1677ff),渲染计划已重编(build_plans.py)
- 验证:core/cockpit 86 单测通过;JS 语法检查通过;静态冒烟全 200
This commit is contained in:
2026-08-05 14:17:37 +08:00
parent b53e71c85d
commit 7a2e45e7c4
21 changed files with 1224 additions and 204 deletions
+89
View File
@@ -0,0 +1,89 @@
/* iAOP 全站统一导航条(Ant Design Pro 风格重构,2026-08)。
*
* 在页面 <body> 末尾引入即可自动注入一条 48px 白色 Pro 导航条
* (logo + 平台名 + 当前页标题 + 门户入口 + 当前用户/角色 Tag + 退出登录),
* 各模块页视觉框架统一。样式在 pro.css 的 .pro-header 系列。
*
* 用法:
* <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-login:登录页相对路径(退出后跳转),缺省 ../auth/login.html。
*
* 本组件不强制登录(守卫由页面或 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;
}
function build() {
var body = document.body;
if (!body || document.querySelector(".pro-header")) 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 bar = el("div", "pro-header");
var logo = el("a", "pro-logo");
logo.href = home;
logo.appendChild(el("span", "pro-logo-badge", "iA"));
logo.appendChild(el("span", "pro-logo-text", "iAOP 云美工业AI优化平台"));
bar.appendChild(logo);
if (title) bar.appendChild(el("span", "pro-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 () {
IAOP_SESSION.logout(loginUrl);
});
right.appendChild(logoutBtn);
bar.appendChild(right);
body.insertBefore(bar, body.firstChild);
// 异步填充当前用户(后端优先、本地降级由 session.js 内部处理)
IAOP_SESSION.currentUser().then(function (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));
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", build);
} else {
build();
}
})();
+295
View File
@@ -0,0 +1,295 @@
/* iAOP 全站统一设计系统(Ant Design Pro 风格重构,2026-08)。
*
* 目标:整个 Web 前端(门户 / 登录 / 驾驶舱 / 配置台 / 管理台 / 助手 / 移动端)
* 统一为 Ant Design Pro 视觉语言——浅灰页面底 + 白色卡片 + AntD 5 蓝色主色,
* 字体 / 圆角 / 阴影 / 间距全部对齐 Ant Design 5 设计 token。
*
* 实现方式(纯静态、无构建链):
* 各模块页原本就用 CSS 变量调色(--cockpit-* 与 --bg/--surface/--fg 两套命名),
* 本文件在页面原有样式表【之后】加载,把这两套变量统一映射到 AntD 色板,
* 并统一字体、顶栏、按钮、输入框、表格、标签等基础元素外观,
* 各模块页业务逻辑与布局结构零改动。
*
* 色板来源:Ant Design 5 官方设计 token
* (primary #1677ff / success #52c41a / warning #faad14 / error #ff4d4f)。
*/
:root {
/* ---- Ant Design 5 基础 token --------------------------------------- */
--ant-primary: #1677ff;
--ant-primary-hover: #4096ff;
--ant-primary-active: #0958d9;
--ant-success: #52c41a;
--ant-warning: #faad14;
--ant-error: #ff4d4f;
--ant-text: rgba(0, 0, 0, 0.88);
--ant-text-secondary: rgba(0, 0, 0, 0.65);
--ant-text-tertiary: rgba(0, 0, 0, 0.45);
--ant-border: #d9d9d9;
--ant-split: rgba(5, 5, 5, 0.06);
--ant-bg-layout: #f0f2f5;
--ant-bg-container: #ffffff;
--ant-bg-fill: rgba(0, 0, 0, 0.04);
--ant-radius: 6px;
--ant-radius-lg: 8px;
--ant-shadow-card: 0 1px 2px 0 rgba(0, 0, 0, 0.03),
0 1px 6px -1px rgba(0, 0, 0, 0.02),
0 2px 4px 0 rgba(0, 0, 0, 0.02);
--ant-shadow-header: 0 1px 4px rgba(0, 21, 41, 0.08);
--ant-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB",
"Microsoft YaHei", "Noto Sans", sans-serif;
/* ---- iAOP 旧变量 → AntD 映射(cockpit/studio/mobile 用 --cockpit-*) -- */
--cockpit-bg: var(--ant-bg-layout);
--cockpit-surface: var(--ant-bg-container);
--cockpit-fg: var(--ant-text);
--cockpit-fg-muted: var(--ant-text-tertiary);
--cockpit-accent: var(--ant-primary);
--cockpit-warn: var(--ant-warning);
--cockpit-grid-line: var(--ant-split);
/* ---- iAOP 旧变量 → AntD 映射(admin/assistant/users 用短名) -------- */
--bg: var(--ant-bg-layout);
--surface: var(--ant-bg-container);
--surface-2: var(--ant-bg-fill);
--fg: var(--ant-text);
--fg-muted: var(--ant-text-tertiary);
--muted: var(--ant-text-tertiary);
--accent: var(--ant-primary);
--warn: var(--ant-warning);
--line: var(--ant-split);
}
/* ---- 全局排版 ---------------------------------------------------------- */
body {
font-family: var(--ant-font) !important;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.5715;
}
/* ---- 统一全站导航条(pro-header.js 注入) ------------------------------ */
.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 .pro-logo {
display: flex;
align-items: center;
gap: 8px;
text-decoration: none;
flex-shrink: 0;
}
.pro-header .pro-logo-badge {
width: 28px;
height: 28px;
border-radius: var(--ant-radius);
background: var(--ant-primary);
color: #fff;
font-size: 13px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
letter-spacing: 0.5px;
}
.pro-header .pro-logo-text {
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;
}
.pro-header .pro-header-right {
margin-left: auto;
display: flex;
align-items: center;
gap: 12px;
font-size: 13px;
color: var(--ant-text-secondary);
}
.pro-header .pro-user {
display: flex;
align-items: center;
gap: 6px;
}
.pro-header .pro-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--ant-primary);
color: #fff;
font-size: 12px;
display: inline-flex;
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;
color: var(--ant-text-tertiary);
cursor: pointer;
font-size: 13px;
padding: 4px 8px;
border-radius: var(--ant-radius);
font-family: inherit;
}
.pro-header .pro-logout:hover {
color: var(--ant-error);
background: var(--ant-bg-fill);
}
/* ---- AntD Tag(角色 / 状态标记) --------------------------------------- */
.pro-tag {
display: inline-block;
font-size: 12px;
line-height: 20px;
padding: 0 7px;
border-radius: 4px;
border: 1px solid transparent;
white-space: nowrap;
}
.pro-tag-blue { color: #1d39c4; background: #e6f4ff; border-color: #91caff; }
.pro-tag-green { color: #237804; background: #f6ffed; border-color: #b7eb8f; }
.pro-tag-gold { color: #ad6800; background: #fffbe6; border-color: #ffe58f; }
.pro-tag-red { color: #cf1322; background: #fff1f0; border-color: #ffa39e; }
.pro-tag-default{ color: var(--ant-text-secondary); background: #fafafa;
border-color: var(--ant-border); }
/* ---- 各模块页页内顶栏 → AntD PageHeader 风格 --------------------------- */
/* 覆盖 cockpit/studio/admin/users 的 #topbar(结构不变,外观统一) */
#topbar {
background: var(--ant-bg-container) !important;
border-bottom: 1px solid var(--ant-split) !important;
}
#topbar h1, #topbar h1 .sub { color: var(--ant-text) !important; }
#topbar h1 .sub { color: var(--ant-text-tertiary) !important; }
/* ---- 页内 tab(studio/admin)→ AntD Tabs 风格 -------------------------- */
.tab {
border: none !important;
border-radius: 0 !important;
background: transparent !important;
color: var(--ant-text-secondary) !important;
border-bottom: 2px solid transparent !important;
padding: 8px 4px !important;
margin: 0 12px;
}
.tab.active {
color: var(--ant-primary) !important;
background: transparent !important;
border-bottom-color: var(--ant-primary) !important;
}
/* ---- 表单控件 → AntD Input/Select 风格 --------------------------------- */
input:not([type="checkbox"]):not([type="radio"]),
select,
textarea {
font-family: inherit;
border: 1px solid var(--ant-border) !important;
border-radius: var(--ant-radius) !important;
background: var(--ant-bg-container) !important;
color: var(--ant-text) !important;
transition: border-color 0.2s, box-shadow 0.2s;
}
input:not([type="checkbox"]):not([type="radio"]):hover,
select:hover,
textarea:hover {
border-color: var(--ant-primary-hover) !important;
}
input:not([type="checkbox"]):not([type="radio"]):focus,
select:focus,
textarea:focus {
border-color: var(--ant-primary) !important;
outline: none;
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
}
/* ---- 按钮 → AntD Button 风格 ------------------------------------------- */
button {
font-family: inherit;
border-radius: var(--ant-radius) !important;
border: 1px solid var(--ant-border);
background: var(--ant-bg-container);
color: var(--ant-text);
cursor: pointer;
transition: all 0.2s;
}
button:hover {
color: var(--ant-primary);
border-color: var(--ant-primary) !important;
}
button.primary, button.btn-primary, .btn-primary {
background: var(--ant-primary) !important;
border-color: var(--ant-primary) !important;
color: #fff !important;
}
button.primary:hover, button.btn-primary:hover, .btn-primary:hover {
background: var(--ant-primary-hover) !important;
border-color: var(--ant-primary-hover) !important;
}
button.danger, .btn-danger {
background: var(--ant-error) !important;
border-color: var(--ant-error) !important;
color: #fff !important;
}
button:disabled {
opacity: 0.55;
cursor: not-allowed;
}
/* ---- 表格 → AntD Table 风格 --------------------------------------------- */
table th {
background: #fafafa;
color: var(--ant-text-secondary) !important;
font-weight: 600 !important;
}
table th, table td {
border-bottom: 1px solid var(--ant-split) !important;
}
/* ---- 通用卡片(各页 .panel/.card 已用 --surface,此处补阴影圆角) ------- */
.panel, .card {
border: 1px solid var(--ant-split) !important;
border-radius: var(--ant-radius-lg) !important;
box-shadow: var(--ant-shadow-card);
}
/* ---- 对话助手气泡修正(亮底下的可读性) --------------------------------- */
.user .text { background: var(--ant-primary) !important; color: #fff !important; }
/* ---- 状态点(推理服务在线/离线) ---------------------------------------- */
.status-dot.ok, .dot.ok { color: var(--ant-success) !important; }
.status-dot.down, .dot.down { color: var(--ant-error) !important; }
/* ---- 窄屏适配:导航条允许换行收缩 --------------------------------------- */
@media (max-width: 720px) {
.pro-header .pro-page-title { display: none; }
.pro-header .pro-logo-text { font-size: 14px; }
}
+191
View File
@@ -0,0 +1,191 @@
/* iAOP 全站统一会话层(Ant Design Pro 风格重构,2026-08)。
*
* 背景:#150 的 auth.js 只对接 core/auth 后端(/auth/login|logout|me),
* 后端未部署时登录态完全不可用,门户只能裸奔展示全部模块。
* 本文件提供「后端优先 + 本地降级」双轨会话:
* 1. 后端可达(同源 /auth/*)→ 走后端 HMAC 会话(HttpOnly cookie);
* 2. 后端不可达(纯静态部署,如 nginx :8090)→ 走 UserStore 本地账号
* (PBKDF2 降级为 salt+SHA-256 的 Demo 级校验,见 user_store.js),
* 会话落 localStorage(iaop.session.v1)。
* 两种模式下 currentUser() 返回结构一致:{username, role, active}。
*
* 角色→模块映射(PRD 8.2 三级 RBAC,门户按此过滤可见模块):
* readonly(Viewer) : 驾驶舱 / 对话助手 / 移动端
* engineer(Editor) : + 模板配置台
* admin(Publisher) : + 管理控制台 / 用户与角色(全部 6 模块)
*
* 同时导出 window.IAOP_AUTH 兼容层(与 #150 auth.js 同签名),
* studio/admin/users 等既有页面无需修改业务代码即可获得本地降级能力。
*/
"use strict";
var IAOP_SESSION = (function () {
var SESSION_KEY = "iaop.session.v1";
var BACKEND_TIMEOUT_MS = 1500; // /auth/* 探测超时,超时即降级本地
/* ---- 模块注册表(门户卡片 + 访问控制单一数据源) ---------------------- */
var MODULES = [
{ key: "cockpit", title: "配置化驾驶舱", icon: "🚀", url: "cockpit/",
desc: "四状态工艺流程视图、实时趋势、KPI 卡片、告警面板、NL 查询(PRD 5.5)",
roles: ["readonly", "engineer", "admin"], tag: "只读", tagColor: "default" },
{ key: "chat", title: "对话助手", icon: "💬", url: "chat/assistant.html",
desc: "NL 工艺查询 / 报警智能解释 / 交接班报告生成(PRD 5.4)",
roles: ["readonly", "engineer", "admin"], tag: "AI 助手", tagColor: "blue" },
{ key: "studio", title: "模板配置台", icon: "🧩", url: "studio/",
desc: "点位字典导入 / 模型超参 / RAG 知识库 / 布局编排 / 版本发布(PRD 5.7)",
roles: ["engineer", "admin"], tag: "可配置", tagColor: "green" },
{ key: "admin", title: "管理控制台", icon: "⚙️", url: "admin/",
desc: "模型管理(版本/阶段/回滚)、知识库管理、告警确认、审计查询",
roles: ["admin"], tag: "管理员", tagColor: "gold" },
{ key: "users", title: "用户与角色", icon: "👤", url: "auth/users.html",
desc: "用户管理、角色分配(readonly / engineer / admin)、审计(PRD 8.2)",
roles: ["admin"], tag: "管理员", tagColor: "gold" },
{ key: "mobile", title: "移动端驾驶舱", icon: "📱", url: "mobile/",
desc: "移动端只读驾驶舱与交接班摘要(iOS14+ / Android 10+)",
roles: ["readonly", "engineer", "admin"], tag: "移动端", tagColor: "default" }
];
var ROLE_LABELS = { readonly: "Viewer · 只读", engineer: "Editor · 配置",
admin: "Publisher · 管理" };
var ROLE_TAG_COLORS = { readonly: "default", engineer: "green", admin: "gold" };
/* ---- 本地会话存取 ------------------------------------------------------ */
function saveSession(user) {
try {
localStorage.setItem(SESSION_KEY, JSON.stringify({
username: user.username, role: user.role,
loginAt: new Date().toISOString()
}));
} catch (e) { /* 隐私模式等场景忽略 */ }
}
function readSession() {
try {
var v = localStorage.getItem(SESSION_KEY);
return v ? JSON.parse(v) : null;
} catch (e) { return null; }
}
function clearSession() {
try { localStorage.removeItem(SESSION_KEY); } catch (e) {}
}
/* ---- 后端探测(带超时) ------------------------------------------------ */
function fetchWithTimeout(url, opts) {
return new Promise(function (resolve, reject) {
var timer = setTimeout(function () { reject(new Error("timeout")); },
BACKEND_TIMEOUT_MS);
fetch(url, opts).then(function (r) {
clearTimeout(timer); resolve(r);
}, function (e) {
clearTimeout(timer); reject(e);
});
});
}
/* ---- 当前登录用户:后端优先,本地会话降级 ------------------------------ */
function currentUser() {
return fetchWithTimeout("/auth/me", { credentials: "include" })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
if (d && d.user) return d.user;
throw new Error("no backend session");
})
.catch(function () {
// 后端不可达或无后端会话 → 本地会话降级,并用 UserStore 复核用户仍有效
var s = readSession();
if (!s || !s.username) return null;
if (typeof UserStore === "undefined") return s; // 无存储层时信任会话
var u = UserStore.find(s.username);
if (!u || !u.active) { clearSession(); return null; }
return { username: u.username, role: u.role, active: u.active };
});
}
/* ---- 登录:后端优先,本地账号降级 -------------------------------------- */
function login(username, password) {
return fetchWithTimeout("/auth/login", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: username, password: password })
}).then(function (r) {
if (r.ok) return r.json().then(function (d) {
saveSession(d.user);
return d.user;
});
// 后端在线但拒绝(401 等):是真实鉴权失败,不降级
return r.json().then(function (d) {
throw new Error((d && d.error) || "用户名或密码错误");
});
}).catch(function (e) {
if (e && e.message && e.message !== "timeout" &&
e.message !== "no backend session" &&
e.message !== "Failed to fetch" &&
e.name !== "TypeError" && !/fetch/i.test(e.message || "")) {
throw e; // 后端鉴权失败,直接抛
}
// 后端不可达 → 本地账号校验
if (typeof UserStore === "undefined") {
throw new Error("认证服务不可用,且本地账号存储未加载");
}
return UserStore.seedDefaults().then(function () {
return UserStore.verify(username, password);
}).then(function (u) {
if (!u) throw new Error("用户名或密码错误");
saveSession(u);
return u;
});
});
}
/* ---- 登出:双轨都清 ----------------------------------------------------- */
function logout(loginUrl) {
clearSession();
try {
fetch("/auth/logout", { method: "POST", credentials: "include" })
.catch(function () {});
} catch (e) {}
var next = encodeURIComponent(location.pathname + location.search);
location.href = (loginUrl || "../auth/login.html") + "?next=" + next;
}
/* ---- 路由守卫(与 #150 IAOP_AUTH 同语义) ------------------------------- */
function requireLoginElseRedirect(loginUrl) {
return currentUser().then(function (u) {
if (!u) {
var next = encodeURIComponent(location.pathname + location.search);
location.href = (loginUrl || "../auth/login.html") + "?next=" + next;
return false;
}
return u;
});
}
/* ---- 角色→模块 ---------------------------------------------------------- */
function modulesFor(role) {
return MODULES.filter(function (m) { return m.roles.indexOf(role) >= 0; });
}
function canAccess(moduleKey, role) {
var m = MODULES.find(function (x) { return x.key === moduleKey; });
return !!m && m.roles.indexOf(role) >= 0;
}
return {
MODULES: MODULES,
ROLE_LABELS: ROLE_LABELS,
ROLE_TAG_COLORS: ROLE_TAG_COLORS,
currentUser: currentUser,
login: login,
logout: logout,
requireLoginElseRedirect: requireLoginElseRedirect,
modulesFor: modulesFor,
canAccess: canAccess
};
})();
/* 兼容层:与 #150 auth.js 导出同签名(studio/admin/users 直接复用)。
* 若页面同时加载了 auth.js,本兼容层在后加载时覆盖之,获得本地降级能力。 */
window.IAOP_AUTH = {
currentUser: IAOP_SESSION.currentUser,
requireLoginElseRedirect: IAOP_SESSION.requireLoginElseRedirect,
AUTH_BASE: ""
};