Files
iAOP/web/shared/session.js
T
bot_dev1 2f6b329745 feat: Sider 子菜单上收页内 Tab(管理控制台/模板配置台,issue #131)
- MODULES 注册表支持 children:管理控制台(模型管理/知识库管理/告警确认/
  审计查询)、模板配置台(点位导入/模型超参/驾驶舱编排/版本发布)
- Sider 渲染 AntD Menu.SubMenu 风格可展开子菜单,当前页自动展开、
  子项按 URL hash 高亮(如 admin/#models)
- 页内 #tabs 自动隐藏,改由子菜单经 hash 驱动切换;页内 Tab 点击回写
  hash,URL 可分享直达子页;各页面业务逻辑零改动
2026-08-05 14:40:15 +08:00

204 lines
8.9 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)。
*
* 背景:#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",
children: [
{ key: "import", title: "点位导入", hash: "#import" },
{ key: "hyper", title: "模型超参", hash: "#hyper" },
{ key: "layout", title: "驾驶舱编排", hash: "#layout" },
{ key: "version", title: "版本发布", hash: "#version" }
] },
{ key: "admin", title: "管理控制台", icon: "⚙️", url: "admin/",
desc: "模型管理(版本/阶段/回滚)、知识库管理、告警确认、审计查询",
roles: ["admin"], tag: "管理员", tagColor: "gold",
children: [
{ key: "models", title: "模型管理", hash: "#models" },
{ key: "kb", title: "知识库管理", hash: "#kb" },
{ key: "alerts", title: "告警确认", hash: "#alerts" },
{ key: "audit", title: "审计查询", hash: "#audit" }
] },
{ 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: ""
};