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:
@@ -37,8 +37,9 @@ from .layout import ( # 复用 #50 已定义的布局模型与常量
|
||||
# 主题 token(PRD 5.5「配置点:主题」dark / light 两套调色)
|
||||
# ---------------------------------------------------------------------------
|
||||
# 工业驾驶舱约定配色:dark 偏深蓝底 + 青绿强调(车间大屏常用);
|
||||
# light 偏白底 + 蓝色强调(白天值班室)。前端按这些 CSS 变量渲染,
|
||||
# 切换主题 = 切一套变量,组件代码不动。
|
||||
# light 采用 Ant Design 5 官方色板(2026-08 全站 Ant Design Pro 风格重构:
|
||||
# 白底卡片 + #1677ff 主色 + 灰底 #f0f2f5,与门户/配置台/管理台统一)。
|
||||
# 前端按这些 CSS 变量渲染,切换主题 = 切一套变量,组件代码不动。
|
||||
THEME_TOKENS: Dict[str, Dict[str, str]] = {
|
||||
"dark": {
|
||||
"--cockpit-bg": "#0b1220",
|
||||
@@ -50,13 +51,13 @@ THEME_TOKENS: Dict[str, Dict[str, str]] = {
|
||||
"--cockpit-grid-line": "rgba(255,255,255,0.06)",
|
||||
},
|
||||
"light": {
|
||||
"--cockpit-bg": "#f5f7fb",
|
||||
"--cockpit-surface": "#ffffff",
|
||||
"--cockpit-fg": "#1f2d3d",
|
||||
"--cockpit-fg-muted": "#6b7c93",
|
||||
"--cockpit-accent": "#2f7af2", # 蓝
|
||||
"--cockpit-warn": "#e08600",
|
||||
"--cockpit-grid-line": "rgba(0,0,0,0.06)",
|
||||
"--cockpit-bg": "#f0f2f5", # AntD colorBgLayout
|
||||
"--cockpit-surface": "#ffffff", # AntD colorBgContainer
|
||||
"--cockpit-fg": "rgba(0,0,0,0.88)", # AntD colorText
|
||||
"--cockpit-fg-muted": "rgba(0,0,0,0.45)", # AntD colorTextTertiary
|
||||
"--cockpit-accent": "#1677ff", # AntD 5 主色蓝(KPI / 趋势主线)
|
||||
"--cockpit-warn": "#faad14", # AntD warning(告警强调)
|
||||
"--cockpit-grid-line": "rgba(5,5,5,0.06)", # AntD colorSplit
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
# alarm_panel(告警面板)/ nl_query(NL查询入口)。
|
||||
$schema: iAOP-cockpit-layout-v1
|
||||
title: 吸附树脂车间驾驶舱
|
||||
theme: dark
|
||||
# 2026-08 全站 Ant Design Pro 风格重构:dark → light(与 ti-cl4 一致)
|
||||
theme: light
|
||||
widgets:
|
||||
- type: process_view
|
||||
src: resin_four_state.svg
|
||||
|
||||
@@ -12,20 +12,21 @@
|
||||
$schema: iAOP-cockpit-alarm-panel-v1
|
||||
# severityColors:P0/P1/P2 三级展示配色(覆盖主题默认告警色)。
|
||||
# P0=红色(驾驶舱红色告警,PRD 场景A)、P1=黄色(加强监控)、P2=蓝色(提示)。
|
||||
# 2026-08 对齐 Ant Design 5 色板:error #ff4d4f / warning #faad14 / primary #1677ff。
|
||||
severityColors:
|
||||
- severity: P0
|
||||
fg: "#ff3b30" # 红色前景(告警文本 / 图标)
|
||||
bg: "rgba(255,59,48,0.12)"
|
||||
fg: "#ff4d4f" # AntD error 红(告警文本 / 图标)
|
||||
bg: "rgba(255,77,79,0.10)"
|
||||
icon: alert-octagon
|
||||
border: "#ff3b30"
|
||||
border: "#ff4d4f"
|
||||
- severity: P1
|
||||
fg: "#f5a623" # 黄色
|
||||
bg: "rgba(245,166,35,0.12)"
|
||||
fg: "#faad14" # AntD warning 黄
|
||||
bg: "rgba(250,173,20,0.10)"
|
||||
icon: alert-triangle
|
||||
border: "#f5a623"
|
||||
border: "#faad14"
|
||||
- severity: P2
|
||||
fg: "#3aa0ff" # 蓝色(提示)
|
||||
bg: "rgba(58,160,255,0.10)"
|
||||
fg: "#1677ff" # AntD primary 蓝(提示)
|
||||
bg: "rgba(22,119,255,0.08)"
|
||||
icon: info
|
||||
# rulesSource:告警面板订阅哪份规则资产(阈值/规则外置)。
|
||||
# 指向 issue #72 落地的炉层杂质预警规则模板(templates/ti-cl4/impurity-forecast/
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
# bind 的 point_id 对齐 templates/ti-cl4/point-dict/point_dict.default.csv。
|
||||
$schema: iAOP-cockpit-layout-v1
|
||||
title: 海绵钛车间驾驶舱
|
||||
theme: dark
|
||||
# 2026-08 全站 Ant Design Pro 风格重构:dark → light(Ant Design 5 色板,
|
||||
# token 见 core/cockpit/renderer.py THEME_TOKENS["light"])
|
||||
theme: light
|
||||
widgets:
|
||||
# ---- 四状态工艺流程主视图(首屏立即加载) ---------------------------
|
||||
- type: process_view
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>iAOP 管理控制台</title>
|
||||
<link rel="stylesheet" href="admin.css">
|
||||
<link rel="stylesheet" href="../shared/pro.css">
|
||||
</head>
|
||||
<body>
|
||||
<body data-pro-title="管理控制台">
|
||||
<!-- iAOP 管理功能界面(issue #135 / PRD 8.1/8.2)
|
||||
纯静态单页(无构建链):模型管理 / 知识库管理 / 告警确认 / 审计查询。
|
||||
语义对齐后端内核:template_registry(dev/staging/prod 逐级 promote + 回滚)、
|
||||
@@ -116,8 +117,9 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="../auth/auth.js"></script>
|
||||
<script src="../auth/user_store.js"></script>
|
||||
<script src="../shared/session.js"></script>
|
||||
<script src="../shared/pro-header.js"></script>
|
||||
<script src="admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+341
-52
@@ -1,73 +1,362 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>iAOP 登录</title>
|
||||
<link rel="stylesheet" href="auth.css">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>登录 · iAOP 云美工业AI优化平台</title>
|
||||
<style>
|
||||
/* iAOP 登录页(Ant Design Pro 风格重构,2026-08)。
|
||||
* 布局对齐 Ant Design Pro 登录页:顶部品牌区 + 居中白卡 + 底部版权。
|
||||
* 认证走 shared/session.js「后端优先 + 本地账号降级」双轨。 */
|
||||
:root {
|
||||
--ant-primary: #1677ff;
|
||||
--ant-primary-hover: #4096ff;
|
||||
--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-error: #ff4d4f;
|
||||
--ant-success: #52c41a;
|
||||
--ant-radius: 6px;
|
||||
--ant-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB",
|
||||
"Microsoft YaHei", "Noto Sans", sans-serif;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: var(--ant-font);
|
||||
background: var(--ant-bg-layout);
|
||||
color: var(--ant-text);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ---- 品牌区(AntD Pro 登录页:logo + 标题 + slogan 居中) ------------- */
|
||||
.brand {
|
||||
text-align: center;
|
||||
padding: 72px 24px 24px;
|
||||
}
|
||||
.brand .logo-row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.brand .logo-badge {
|
||||
width: 44px; height: 44px;
|
||||
border-radius: 10px;
|
||||
background: var(--ant-primary);
|
||||
color: #fff; font-size: 20px; font-weight: 600;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
box-shadow: 0 4px 12px rgba(22, 119, 255, 0.35);
|
||||
}
|
||||
.brand h1 { font-size: 28px; font-weight: 600; letter-spacing: 0.5px; }
|
||||
.brand .slogan {
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
color: var(--ant-text-tertiary);
|
||||
}
|
||||
|
||||
/* ---- 登录卡片 ---------------------------------------------------------- */
|
||||
main { flex: 1; display: flex; justify-content: center; padding: 16px 24px 48px; }
|
||||
.login-card {
|
||||
width: 368px;
|
||||
max-width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0,0,0,.03), 0 1px 6px -1px rgba(0,0,0,.02),
|
||||
0 2px 4px 0 rgba(0,0,0,.02);
|
||||
padding: 32px 32px 24px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
.login-card h2 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid var(--ant-split);
|
||||
color: var(--ant-text);
|
||||
}
|
||||
.login-card h2 .active-tab { color: var(--ant-primary); }
|
||||
|
||||
.alert {
|
||||
display: none;
|
||||
font-size: 13px;
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--ant-radius);
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.alert.show { display: block; }
|
||||
.alert-error { color: #cf1322; background: #fff2f0; border: 1px solid #ffccc7; }
|
||||
.alert-ok { color: #237804; background: #f6ffed; border: 1px solid #b7eb8f; }
|
||||
|
||||
.field { margin-bottom: 20px; }
|
||||
.field label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: var(--ant-text-secondary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.field .input-wrap { position: relative; }
|
||||
.field .input-icon {
|
||||
position: absolute;
|
||||
left: 11px; top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--ant-text-tertiary);
|
||||
font-size: 14px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.field input {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 4px 40px 4px 34px;
|
||||
font-size: 14px;
|
||||
border: 1px solid var(--ant-border);
|
||||
border-radius: var(--ant-radius);
|
||||
transition: border-color .2s, box-shadow .2s;
|
||||
}
|
||||
.field input:hover { border-color: var(--ant-primary-hover); }
|
||||
.field input:focus {
|
||||
border-color: var(--ant-primary);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, .1);
|
||||
}
|
||||
.field input[aria-invalid="true"] { border-color: var(--ant-error); }
|
||||
.field .pwd-toggle {
|
||||
position: absolute;
|
||||
right: 4px; top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border: none; background: none;
|
||||
cursor: pointer; font-size: 14px;
|
||||
padding: 6px 8px;
|
||||
color: var(--ant-text-tertiary);
|
||||
}
|
||||
.field .pwd-toggle:hover { color: var(--ant-primary); }
|
||||
|
||||
.btn-login {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: var(--ant-radius);
|
||||
background: var(--ant-primary);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: background .2s;
|
||||
font-family: inherit;
|
||||
}
|
||||
.btn-login:hover { background: var(--ant-primary-hover); }
|
||||
.btn-login:disabled { opacity: .6; cursor: not-allowed; }
|
||||
|
||||
.btn-sso {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
border: 1px solid var(--ant-border);
|
||||
border-radius: var(--ant-radius);
|
||||
background: #fff;
|
||||
color: var(--ant-text-secondary);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
.btn-sso:hover { color: var(--ant-primary); border-color: var(--ant-primary); }
|
||||
|
||||
/* ---- 演示账号提示(本地降级模式) --------------------------------------- */
|
||||
.demo-accounts {
|
||||
margin-top: 20px;
|
||||
padding: 12px;
|
||||
background: #fafafa;
|
||||
border: 1px solid var(--ant-split);
|
||||
border-radius: var(--ant-radius);
|
||||
font-size: 12px;
|
||||
color: var(--ant-text-tertiary);
|
||||
line-height: 1.8;
|
||||
}
|
||||
.demo-accounts summary { cursor: pointer; color: var(--ant-text-secondary); }
|
||||
.demo-accounts code {
|
||||
background: rgba(0,0,0,.04);
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--ant-text-secondary);
|
||||
}
|
||||
.demo-accounts .fill-link {
|
||||
color: var(--ant-primary);
|
||||
cursor: pointer;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
/* ---- 页脚 --------------------------------------------------------------- */
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
font-size: 12px;
|
||||
color: var(--ant-text-tertiary);
|
||||
line-height: 1.8;
|
||||
}
|
||||
footer .links a {
|
||||
color: var(--ant-text-tertiary);
|
||||
text-decoration: none;
|
||||
margin: 0 10px;
|
||||
}
|
||||
footer .links a:hover { color: var(--ant-primary); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!--
|
||||
iAOP 登录页(issue #150 / PRD 8.2)。
|
||||
后端:core/auth/auth_api.py(POST /auth/login → token + Set-Cookie)。
|
||||
会话:core/auth/session.py(HMAC token,HttpOnly cookie iaop_session)。
|
||||
写操作守卫:core/auth/auth_api.py require_auth / can_write(未登录/readonly 拒绝写)。
|
||||
iAOP 登录页(issue #150/#134 + 2026-08 Ant Design Pro 风格重构)。
|
||||
认证双轨(shared/session.js):
|
||||
· 后端 core/auth 可达 → POST /auth/login(HMAC 会话,HttpOnly cookie);
|
||||
· 纯静态部署 → UserStore 本地账号(首次访问自动播种演示账号)。
|
||||
角色三级(PRD 8.2):readonly 只读 / engineer 可配置 / admin 可发布回滚,
|
||||
登录后门户按角色过滤可见模块。
|
||||
-->
|
||||
<form class="auth-card" id="loginForm" autocomplete="on" novalidate>
|
||||
<div class="auth-brand">
|
||||
<div class="auth-logo">iA</div>
|
||||
<h1>云美工业AI优化平台</h1>
|
||||
<div class="brand">
|
||||
<div class="logo-row">
|
||||
<span class="logo-badge">iA</span>
|
||||
<h1>iAOP 云美工业AI优化平台</h1>
|
||||
</div>
|
||||
<div class="auth-sub">Template-Ti 一期 · 配置台登录</div>
|
||||
<div class="slogan">iAOP-Core 通用内核 · Template-Ti(海绵钛)一期 · 配置化驾驶舱 / 模板配置台 / LLM 助手</div>
|
||||
</div>
|
||||
|
||||
<div class="alert" id="alert" role="alert" aria-live="polite"></div>
|
||||
<main>
|
||||
<form class="login-card" id="loginForm" autocomplete="on" novalidate>
|
||||
<h2><span class="active-tab">账号密码登录</span></h2>
|
||||
|
||||
<div class="field">
|
||||
<label for="username">用户名</label>
|
||||
<input id="username" name="username" type="text" required
|
||||
autocomplete="username" placeholder="请输入用户名" maxlength="64">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="password">密码</label>
|
||||
<input id="password" name="password" type="password" required
|
||||
autocomplete="current-password" placeholder="请输入密码" minlength="8">
|
||||
</div>
|
||||
<div class="alert" id="alert" role="alert" aria-live="polite"></div>
|
||||
|
||||
<div class="auth-actions">
|
||||
<button type="submit" class="btn btn-primary" id="loginBtn">登录</button>
|
||||
<button type="button" class="btn btn-ghost" id="togglePwd" title="显示/隐藏密码" aria-label="显示或隐藏密码">👁</button>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="username">用户名</label>
|
||||
<div class="input-wrap">
|
||||
<span class="input-icon">👤</span>
|
||||
<input id="username" name="username" type="text" required
|
||||
autocomplete="username" placeholder="请输入用户名" maxlength="64">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="password">密码</label>
|
||||
<div class="input-wrap">
|
||||
<span class="input-icon">🔒</span>
|
||||
<input id="password" name="password" type="password" required
|
||||
autocomplete="current-password" placeholder="请输入密码" minlength="8">
|
||||
<button type="button" class="pwd-toggle" id="togglePwd"
|
||||
title="显示/隐藏密码" aria-label="显示或隐藏密码">👁</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- issue #152:OIDC SSO 双轨入口——配置了 issuer/client_id 时显示,
|
||||
未配置仅本地账号(PRD 8.2 双轨)。配置点在用户管理页维护。 -->
|
||||
<div class="auth-actions" id="sso-row" hidden>
|
||||
<button type="button" class="btn btn-ghost" id="ssoBtn" style="width:100%">企业 SSO 登录(OIDC)</button>
|
||||
</div>
|
||||
<button type="submit" class="btn-login" id="loginBtn">登 录</button>
|
||||
|
||||
<div class="role-hint">
|
||||
角色:readonly 只读 / engineer 可配置 / admin 可发布回滚。<br>
|
||||
未登录或只读角色不可执行写操作(PRD 8.2)。
|
||||
</div>
|
||||
<!-- issue #152:OIDC SSO 双轨入口——配置了 issuer/client_id 时显示 -->
|
||||
<button type="button" class="btn-sso" id="ssoBtn" hidden>企业 SSO 登录(OIDC)</button>
|
||||
|
||||
<div class="auth-foot">
|
||||
认证后端 core/auth · 会话 HttpOnly cookie · 密码 PBKDF2-HMAC-SHA256
|
||||
<details class="demo-accounts">
|
||||
<summary>演示账号(纯静态部署·本地账号模式)</summary>
|
||||
<div>
|
||||
管理员 <code>admin / Admin@12345</code><span class="fill-link" data-u="admin" data-p="Admin@12345">填入</span><br>
|
||||
工程师 <code>engineer / Engineer@12</code><span class="fill-link" data-u="engineer" data-p="Engineer@12">填入</span><br>
|
||||
只读 <code>viewer / Viewer@1234</code><span class="fill-link" data-u="viewer" data-p="Viewer@1234">填入</span><br>
|
||||
不同角色登录后可见模块不同;生产环境请立即改密。
|
||||
</div>
|
||||
</details>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="links">
|
||||
<a href="../index.html">平台门户</a>
|
||||
<a href="../cockpit/">驾驶舱</a>
|
||||
<a href="../chat/assistant.html">对话助手</a>
|
||||
</div>
|
||||
</form>
|
||||
iAOP-Core v1.0 · 认证后端 core/auth(PBKDF2-HMAC-SHA256)· 部署底座 K8s/Helm
|
||||
</footer>
|
||||
|
||||
<script src="user_store.js"></script>
|
||||
<script src="auth.js"></script>
|
||||
<script src="../shared/session.js"></script>
|
||||
<script>
|
||||
// issue #152:OIDC 双轨——已配置 issuer/client_id 则显示 SSO 入口,
|
||||
// 点击跳转标准授权码流程 authorize URL(state 已写入 sessionStorage);
|
||||
// 未配置则保持纯本地账号登录。
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
if (typeof UserStore === "undefined") return;
|
||||
var url = UserStore.oidcAuthorizeUrl("/auth/login.html");
|
||||
if (!url) return;
|
||||
var row = document.getElementById("sso-row");
|
||||
row.hidden = false;
|
||||
document.getElementById("ssoBtn").onclick = function () { location.href = url; };
|
||||
});
|
||||
(function () {
|
||||
"use strict";
|
||||
function $(id) { return document.getElementById(id); }
|
||||
|
||||
function showAlert(msg, kind) {
|
||||
var el = $("alert");
|
||||
el.textContent = msg || "";
|
||||
el.className = "alert show " + (kind === "ok" ? "alert-ok" : "alert-error");
|
||||
}
|
||||
function clearAlert() { $("alert").className = "alert"; }
|
||||
|
||||
function nextUrl() {
|
||||
return new URLSearchParams(location.search).get("next") || "../index.html";
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// 首次访问播种本地演示账号(幂等;后端模式不影响)
|
||||
UserStore.seedDefaults().catch(function () {});
|
||||
|
||||
// 已登录直接跳转
|
||||
IAOP_SESSION.currentUser().then(function (u) {
|
||||
if (u) location.href = nextUrl();
|
||||
});
|
||||
|
||||
$("loginForm").addEventListener("submit", function (e) {
|
||||
e.preventDefault();
|
||||
var u = $("username").value.trim();
|
||||
var p = $("password").value;
|
||||
$("username").removeAttribute("aria-invalid");
|
||||
$("password").removeAttribute("aria-invalid");
|
||||
if (!u) {
|
||||
$("username").setAttribute("aria-invalid", "true");
|
||||
showAlert("请输入用户名", "error");
|
||||
return;
|
||||
}
|
||||
if (!p || p.length < 8) {
|
||||
$("password").setAttribute("aria-invalid", "true");
|
||||
showAlert("密码不少于 8 位", "error");
|
||||
return;
|
||||
}
|
||||
$("loginBtn").disabled = true;
|
||||
clearAlert();
|
||||
IAOP_SESSION.login(u, p).then(function (user) {
|
||||
showAlert("登录成功,欢迎 " + user.username + "(" +
|
||||
(IAOP_SESSION.ROLE_LABELS[user.role] || user.role) + ")", "ok");
|
||||
setTimeout(function () { location.href = nextUrl(); }, 400);
|
||||
}).catch(function (err) {
|
||||
$("password").setAttribute("aria-invalid", "true");
|
||||
showAlert(err.message || "登录失败", "error");
|
||||
}).finally(function () {
|
||||
$("loginBtn").disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
$("togglePwd").addEventListener("click", function () {
|
||||
var pwd = $("password");
|
||||
pwd.type = pwd.type === "password" ? "text" : "password";
|
||||
});
|
||||
|
||||
// 演示账号一键填入
|
||||
Array.prototype.forEach.call(
|
||||
document.querySelectorAll(".fill-link"), function (link) {
|
||||
link.addEventListener("click", function () {
|
||||
$("username").value = link.getAttribute("data-u");
|
||||
$("password").value = link.getAttribute("data-p");
|
||||
$("password").focus();
|
||||
});
|
||||
});
|
||||
|
||||
// issue #152:OIDC 双轨——已配置 issuer/client_id 则显示 SSO 入口
|
||||
var url = UserStore.oidcAuthorizeUrl("/auth/login.html");
|
||||
if (url) {
|
||||
var btn = $("ssoBtn");
|
||||
btn.hidden = false;
|
||||
btn.addEventListener("click", function () { location.href = url; });
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -37,9 +37,19 @@ var UserStore = (function () {
|
||||
});
|
||||
}
|
||||
|
||||
/* 默认演示账号(Ant Design Pro 风格重构新增):纯静态部署(无 core/auth
|
||||
* 后端)时的开箱登录账号,首次进入登录页自动播种。生产环境登录后请立即
|
||||
* 在用户管理页改密或删除。密码均 ≥ 8 位(对齐 auth.js 前端校验)。 */
|
||||
var DEFAULT_ACCOUNTS = [
|
||||
{ username: "admin", password: "Admin@12345", role: "admin" },
|
||||
{ username: "engineer", password: "Engineer@12", role: "engineer" },
|
||||
{ username: "viewer", password: "Viewer@1234", role: "readonly" }
|
||||
];
|
||||
|
||||
return {
|
||||
ROLES: ROLES,
|
||||
ROLE_LABELS: ROLE_LABELS,
|
||||
DEFAULT_ACCOUNTS: DEFAULT_ACCOUNTS,
|
||||
|
||||
/* ---------- 用户 CRUD(对齐 core/auth/users.py 的 User 字段语义) ---------- */
|
||||
list: function () { return load(USERS_KEY, []); },
|
||||
@@ -91,6 +101,28 @@ var UserStore = (function () {
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
/* ---------- 登录校验 + 默认账号播种(纯静态部署降级,见 shared/session.js) ---------- */
|
||||
/* 校验用户名密码,成功返回 {username, role, active},失败返回 null。
|
||||
* 不区分「用户不存在/密码错误/已禁用」,防用户名枚举。 */
|
||||
verify: function (username, password) {
|
||||
var u = this.find(username);
|
||||
if (!u || !u.active) return Promise.resolve(null);
|
||||
return hashPassword(password, u.salt).then(function (h) {
|
||||
return h === u.hash ? { username: u.username, role: u.role,
|
||||
active: u.active } : null;
|
||||
});
|
||||
},
|
||||
/* 用户库为空时播种默认演示账号(幂等:已有任何用户则跳过)。 */
|
||||
seedDefaults: function () {
|
||||
var self = this;
|
||||
if (this.list().length > 0) return Promise.resolve(false);
|
||||
return DEFAULT_ACCOUNTS.reduce(function (p, acc) {
|
||||
return p.then(function () {
|
||||
return self.create(acc.username, acc.password, acc.role);
|
||||
});
|
||||
}, Promise.resolve()).then(function () { return true; });
|
||||
},
|
||||
|
||||
/* ---------- 写操作审计(template_registry._log 风格:时间/操作人/动作/资源/原因) ---------- */
|
||||
audit: function (action, resource, reason, actor) {
|
||||
var log = load(AUDIT_KEY, []);
|
||||
|
||||
+7
-5
@@ -29,16 +29,17 @@
|
||||
button.primary { background:var(--accent); color:#04202a; }
|
||||
button.danger { background:#7a1f1a; color:#ffb4ad; }
|
||||
.row { display:flex; gap:8px; margin:10px 0; flex-wrap:wrap; align-items:center; }
|
||||
.hint { font-size:11px; color:#64748b; }
|
||||
.role-readonly { color:#8aa0bd; } .role-engineer { color:#38bdf8; }
|
||||
.role-admin { color:var(--accent); }
|
||||
.hint { font-size:11px; color:rgba(0,0,0,.45); }
|
||||
.role-readonly { color:rgba(0,0,0,.45); } .role-engineer { color:#237804; }
|
||||
.role-admin { color:#ad6800; }
|
||||
.disabled-user { opacity:.45; }
|
||||
#audit-list { max-height:420px; overflow-y:auto; font-size:12px; font-family:Consolas,monospace; }
|
||||
#audit-list div { padding:3px 0; border-bottom:1px solid var(--line); color:var(--muted); }
|
||||
@media (max-width:1000px) { main { grid-template-columns:1fr; } }
|
||||
</style>
|
||||
<link rel="stylesheet" href="../shared/pro.css">
|
||||
</head>
|
||||
<body>
|
||||
<body data-pro-title="用户与角色管理" data-pro-home="../index.html" data-pro-login="login.html">
|
||||
<header id="topbar">
|
||||
<h1>用户/角色管理 <span class="sub">RBAC 三级:readonly / engineer / admin · PRD 8.2</span></h1>
|
||||
<div id="topbar-right">
|
||||
@@ -93,8 +94,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script src="auth.js"></script>
|
||||
<script src="user_store.js"></script>
|
||||
<script src="../shared/session.js"></script>
|
||||
<script src="../shared/pro-header.js"></script>
|
||||
<script src="users.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>iAOP 对话助手(增强版)</title>
|
||||
<link rel="stylesheet" href="assistant.css">
|
||||
<link rel="stylesheet" href="../shared/pro.css">
|
||||
</head>
|
||||
<body>
|
||||
<body data-pro-title="对话助手">
|
||||
<!-- iAOP 对话助手增强版(issue #133 / PRD 5.4「④ LLM 网关 + RAG」)
|
||||
在 chat_widget.html(#77 雏形)之上完善:消息流 + RAG 引用溯源 +
|
||||
路由分级提示(本地/云端)+ DLP 拦截反馈,直连已部署推理服务。
|
||||
@@ -58,6 +59,13 @@
|
||||
回答附 RAG 引用溯源(知识库命中时)。
|
||||
</div>
|
||||
</div>
|
||||
<script src="../auth/user_store.js"></script>
|
||||
<script src="../shared/session.js"></script>
|
||||
<script>
|
||||
// 登录守卫(PRD 8.2):未登录跳登录页,登录后回跳本页(后端优先/本地降级)
|
||||
IAOP_SESSION.requireLoginElseRedirect("../auth/login.html");
|
||||
</script>
|
||||
<script src="../shared/pro-header.js"></script>
|
||||
<script src="assistant.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
/* iAOP 配置化驾驶舱样式(issue #131 / PRD 5.5)。
|
||||
* 调色全部由 plan JSON 的 themeTokens 注入(--cockpit-* CSS 变量),
|
||||
* 这里只写默认值(= #51 THEME_TOKENS 的 dark 方案),切主题 = 换一套变量。 */
|
||||
* 这里只写默认值(= #51 THEME_TOKENS 的 light 方案,Ant Design 5 色板,
|
||||
* 2026-08 全站 Ant Design Pro 风格重构),切主题 = 换一套变量。 */
|
||||
:root {
|
||||
--cockpit-bg: #0b1220;
|
||||
--cockpit-surface: #13203a;
|
||||
--cockpit-fg: #e6edf6;
|
||||
--cockpit-fg-muted: #8aa0bd;
|
||||
--cockpit-accent: #18d3c8;
|
||||
--cockpit-warn: #f5a623;
|
||||
--cockpit-grid-line: rgba(255,255,255,0.06);
|
||||
--cockpit-bg: #f0f2f5;
|
||||
--cockpit-surface: #ffffff;
|
||||
--cockpit-fg: rgba(0, 0, 0, 0.88);
|
||||
--cockpit-fg-muted: rgba(0, 0, 0, 0.45);
|
||||
--cockpit-accent: #1677ff;
|
||||
--cockpit-warn: #faad14;
|
||||
--cockpit-grid-line: rgba(5, 5, 5, 0.06);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
@@ -20,11 +20,12 @@ var TEMPLATES = [
|
||||
{ id: "ti-cl4", name: "海绵钛(Ti)", plan: "plans/ti-cl4.json", alarmPlan: "plans/alarm_panel.ti.json" },
|
||||
{ id: "resin", name: "吸附树脂", plan: "plans/resin.json", alarmPlan: null }
|
||||
];
|
||||
// 树脂模板暂无告警面板配置资产时的兜底三级配色(语义同 alarm_panel.ti.yaml)
|
||||
// 树脂模板暂无告警面板配置资产时的兜底三级配色(语义同 alarm_panel.ti.yaml,
|
||||
// 2026-08 对齐 Ant Design 5 色板)
|
||||
var FALLBACK_SEVERITY_STYLES = {
|
||||
P0: { fg: "#ff3b30", bg: "rgba(255,59,48,0.12)", border: "#ff3b30" },
|
||||
P1: { fg: "#f5a623", bg: "rgba(245,166,35,0.12)", border: "#f5a623" },
|
||||
P2: { fg: "#3aa0ff", bg: "rgba(58,160,255,0.10)" }
|
||||
P0: { fg: "#ff4d4f", bg: "rgba(255,77,79,0.10)", border: "#ff4d4f" },
|
||||
P1: { fg: "#faad14", bg: "rgba(250,173,20,0.10)", border: "#faad14" },
|
||||
P2: { fg: "#1677ff", bg: "rgba(22,119,255,0.08)" }
|
||||
};
|
||||
|
||||
var state = {
|
||||
@@ -193,7 +194,7 @@ var ComponentRegistry = {
|
||||
var min = Math.min.apply(null, points), max = Math.max.apply(null, points);
|
||||
var span = (max - min) || 1;
|
||||
ctx.strokeStyle = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--cockpit-accent").trim() || "#18d3c8";
|
||||
.getPropertyValue("--cockpit-accent").trim() || "#1677ff";
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath();
|
||||
points.forEach(function (p, idx) {
|
||||
@@ -202,7 +203,8 @@ var ComponentRegistry = {
|
||||
if (idx === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
||||
});
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = "#8aa0bd";
|
||||
ctx.fillStyle = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--cockpit-fg-muted").trim() || "rgba(0,0,0,0.45)";
|
||||
ctx.font = "11px sans-serif";
|
||||
ctx.fillText(max.toFixed(1), 4, 12);
|
||||
ctx.fillText(min.toFixed(1), 4, h - 4);
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>iAOP 配置化驾驶舱</title>
|
||||
<link rel="stylesheet" href="cockpit.css">
|
||||
<link rel="stylesheet" href="../shared/pro.css">
|
||||
</head>
|
||||
<body>
|
||||
<body data-pro-title="配置化驾驶舱">
|
||||
<!-- iAOP 配置化驾驶舱(issue #131 / PRD 5.5「⑤ 配置化驾驶舱」)
|
||||
纯静态 HTML + 原生 JS(无构建链)。页面只消费 plans/*.json 渲染计划,
|
||||
切换行业模板 = 加载另一份 plan JSON,前端代码零改动。 -->
|
||||
@@ -24,6 +25,13 @@
|
||||
<span id="plan-meta"></span>
|
||||
<span>布局 Schema: iAOP-cockpit-layout-v1 · 首屏静态资源本地化,目标 ≤ 2s(PRD 9)</span>
|
||||
</footer>
|
||||
<script src="../auth/user_store.js"></script>
|
||||
<script src="../shared/session.js"></script>
|
||||
<script>
|
||||
// 登录守卫(PRD 8.2):未登录跳登录页,登录后回跳本页(后端优先/本地降级)
|
||||
IAOP_SESSION.requireLoginElseRedirect("../auth/login.html");
|
||||
</script>
|
||||
<script src="../shared/pro-header.js"></script>
|
||||
<script src="cockpit.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
"subscribe": "alarm_stream",
|
||||
"severityStyles": {
|
||||
"P0": {
|
||||
"fg": "#ff3b30",
|
||||
"bg": "rgba(255,59,48,0.12)",
|
||||
"fg": "#ff4d4f",
|
||||
"bg": "rgba(255,77,79,0.10)",
|
||||
"icon": "alert-octagon",
|
||||
"border": "#ff3b30"
|
||||
"border": "#ff4d4f"
|
||||
},
|
||||
"P1": {
|
||||
"fg": "#f5a623",
|
||||
"bg": "rgba(245,166,35,0.12)",
|
||||
"fg": "#faad14",
|
||||
"bg": "rgba(250,173,20,0.10)",
|
||||
"icon": "alert-triangle",
|
||||
"border": "#f5a623"
|
||||
"border": "#faad14"
|
||||
},
|
||||
"P2": {
|
||||
"fg": "#3aa0ff",
|
||||
"bg": "rgba(58,160,255,0.10)",
|
||||
"fg": "#1677ff",
|
||||
"bg": "rgba(22,119,255,0.08)",
|
||||
"icon": "info"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"$schema": "iAOP-cockpit-layout-v1",
|
||||
"title": "吸附树脂车间驾驶舱",
|
||||
"theme": "dark",
|
||||
"theme": "light",
|
||||
"themeTokens": {
|
||||
"--cockpit-bg": "#0b1220",
|
||||
"--cockpit-surface": "#13203a",
|
||||
"--cockpit-fg": "#e6edf6",
|
||||
"--cockpit-fg-muted": "#8aa0bd",
|
||||
"--cockpit-accent": "#18d3c8",
|
||||
"--cockpit-warn": "#f5a623",
|
||||
"--cockpit-grid-line": "rgba(255,255,255,0.06)"
|
||||
"--cockpit-bg": "#f0f2f5",
|
||||
"--cockpit-surface": "#ffffff",
|
||||
"--cockpit-fg": "rgba(0,0,0,0.88)",
|
||||
"--cockpit-fg-muted": "rgba(0,0,0,0.45)",
|
||||
"--cockpit-accent": "#1677ff",
|
||||
"--cockpit-warn": "#faad14",
|
||||
"--cockpit-grid-line": "rgba(5,5,5,0.06)"
|
||||
},
|
||||
"grid": {
|
||||
"columns": 12
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"$schema": "iAOP-cockpit-layout-v1",
|
||||
"title": "海绵钛车间驾驶舱",
|
||||
"theme": "dark",
|
||||
"theme": "light",
|
||||
"themeTokens": {
|
||||
"--cockpit-bg": "#0b1220",
|
||||
"--cockpit-surface": "#13203a",
|
||||
"--cockpit-fg": "#e6edf6",
|
||||
"--cockpit-fg-muted": "#8aa0bd",
|
||||
"--cockpit-accent": "#18d3c8",
|
||||
"--cockpit-warn": "#f5a623",
|
||||
"--cockpit-grid-line": "rgba(255,255,255,0.06)"
|
||||
"--cockpit-bg": "#f0f2f5",
|
||||
"--cockpit-surface": "#ffffff",
|
||||
"--cockpit-fg": "rgba(0,0,0,0.88)",
|
||||
"--cockpit-fg-muted": "rgba(0,0,0,0.45)",
|
||||
"--cockpit-accent": "#1677ff",
|
||||
"--cockpit-warn": "#faad14",
|
||||
"--cockpit-grid-line": "rgba(5,5,5,0.06)"
|
||||
},
|
||||
"grid": {
|
||||
"columns": 12
|
||||
|
||||
+171
-84
@@ -3,96 +3,183 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>iAOP 云美工业AI优化平台</title>
|
||||
<title>iAOP 云美工业AI优化平台 · 门户</title>
|
||||
<link rel="stylesheet" href="shared/pro.css">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
|
||||
background: linear-gradient(160deg, #0b1526 0%, #111c33 60%, #0d1a2e 100%);
|
||||
color: #e2e8f0; min-height: 100vh; display: flex; flex-direction: column;
|
||||
/* iAOP 平台门户(Ant Design Pro 风格重构,2026-08)。
|
||||
* 未登录跳登录页(?next 回跳);登录后按角色过滤可见模块:
|
||||
* readonly → 驾驶舱/对话助手/移动端
|
||||
* engineer → + 模板配置台
|
||||
* admin → + 管理控制台/用户与角色(全部 6 模块) */
|
||||
body { background: var(--ant-bg-layout); min-height: 100vh;
|
||||
display: flex; flex-direction: column; margin: 0; }
|
||||
|
||||
.page-container { flex: 1; max-width: 1200px; width: 100%;
|
||||
margin: 0 auto; padding: 24px; }
|
||||
|
||||
/* ---- PageHeader(AntD Pro 页头:标题 + 描述 + 右侧状态) --------------- */
|
||||
.page-header {
|
||||
background: var(--ant-bg-container);
|
||||
border-radius: var(--ant-radius-lg);
|
||||
box-shadow: var(--ant-shadow-card);
|
||||
padding: 20px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
header {
|
||||
padding: 48px 24px 24px; text-align: center;
|
||||
border-bottom: 1px solid rgba(148,163,184,.15);
|
||||
}
|
||||
header h1 { font-size: 28px; color: #34d399; letter-spacing: 1px; }
|
||||
header p { margin-top: 8px; color: #94a3b8; font-size: 14px; }
|
||||
main {
|
||||
flex: 1; max-width: 960px; margin: 0 auto; width: 100%;
|
||||
padding: 32px 24px;
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
.page-header .ph-title { font-size: 20px; font-weight: 600; }
|
||||
.page-header .ph-desc { margin-top: 4px; font-size: 13px;
|
||||
color: var(--ant-text-tertiary); }
|
||||
.page-header .ph-extra { display: flex; align-items: center; gap: 8px;
|
||||
font-size: 13px; color: var(--ant-text-secondary); }
|
||||
|
||||
/* ---- 模块卡片网格 ------------------------------------------------------ */
|
||||
.module-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
a.card {
|
||||
display: block; text-decoration: none; color: inherit;
|
||||
background: rgba(148,163,184,.06); border: 1px solid rgba(148,163,184,.18);
|
||||
border-radius: 12px; padding: 20px; transition: all .2s;
|
||||
a.module-card {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
background: var(--ant-bg-container);
|
||||
border: 1px solid var(--ant-split);
|
||||
border-radius: var(--ant-radius-lg);
|
||||
box-shadow: var(--ant-shadow-card);
|
||||
padding: 20px 24px;
|
||||
transition: box-shadow .2s, transform .2s;
|
||||
color: var(--ant-text);
|
||||
}
|
||||
a.card:hover { transform: translateY(-2px); border-color: #34d399;
|
||||
background: rgba(52,211,153,.08); }
|
||||
.card h2 { font-size: 17px; color: #7dd3fc; margin-bottom: 6px; }
|
||||
.card p { font-size: 13px; color: #94a3b8; line-height: 1.6; }
|
||||
.card .tag { display: inline-block; margin-top: 10px; font-size: 11px;
|
||||
color: #34d399; border: 1px solid rgba(52,211,153,.4); border-radius: 20px;
|
||||
padding: 2px 10px; }
|
||||
footer { text-align: center; padding: 24px; color: #64748b; font-size: 12px; }
|
||||
#health { display: inline-block; margin-left: 10px; font-size: 12px;
|
||||
color: #fbbf24; border: 1px solid rgba(251,191,36,.4); border-radius: 20px;
|
||||
padding: 2px 10px; vertical-align: middle; }
|
||||
a.module-card:hover {
|
||||
box-shadow: 0 3px 6px -4px rgba(0,0,0,.12), 0 6px 16px 0 rgba(0,0,0,.08),
|
||||
0 9px 28px 8px rgba(0,0,0,.05);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.module-card .mc-head { display: flex; align-items: center; gap: 10px; }
|
||||
.module-card .mc-icon {
|
||||
width: 40px; height: 40px;
|
||||
border-radius: var(--ant-radius-lg);
|
||||
background: #e6f4ff;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.module-card h2 { font-size: 16px; font-weight: 600; margin: 0; }
|
||||
.module-card .mc-desc { margin-top: 12px; font-size: 13px;
|
||||
color: var(--ant-text-tertiary); line-height: 1.7;
|
||||
min-height: 44px; }
|
||||
.module-card .mc-foot { margin-top: 12px; display: flex;
|
||||
justify-content: space-between; align-items: center; }
|
||||
.module-card .mc-enter { font-size: 13px; color: var(--ant-primary); }
|
||||
a.module-card:hover .mc-enter { text-decoration: underline; }
|
||||
|
||||
.portal-footer { text-align: center; padding: 24px; font-size: 12px;
|
||||
color: var(--ant-text-tertiary); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>iAOP 云美工业AI优化平台 <span id="health">推理服务检测中…</span></h1>
|
||||
<p>iAOP-Core 通用内核 · Template-Ti(海绵钛)一期 · 配置化驾驶舱 / 模板配置台 / LLM 助手</p>
|
||||
</header>
|
||||
<main>
|
||||
<a class="card" href="cockpit/">
|
||||
<h2>🚀 驾驶舱</h2>
|
||||
<p>四状态工艺流程视图、实时趋势、KPI 卡片、告警面板、NL 查询(PRD 5.5)</p>
|
||||
<span class="tag">业务用户 · 只读为主</span>
|
||||
</a>
|
||||
<a class="card" href="chat/assistant.html">
|
||||
<h2>💬 对话助手</h2>
|
||||
<p>NL 工艺查询 / 报警智能解释 / 交接班报告生成(PRD 5.4)</p>
|
||||
<span class="tag">值班长 · 工程师</span>
|
||||
</a>
|
||||
<a class="card" href="studio/">
|
||||
<h2>🧩 模板配置台</h2>
|
||||
<p>点位字典导入 / 模型超参 / RAG 知识库 / 驾驶舱布局编排 / 版本发布(PRD 5.7)</p>
|
||||
<span class="tag">实施 · 行业工程师</span>
|
||||
</a>
|
||||
<a class="card" href="admin/">
|
||||
<h2>⚙️ 管理控制台</h2>
|
||||
<p>模型管理(版本/阶段/回滚)、知识库管理、告警确认、审计查询</p>
|
||||
<span class="tag">管理员</span>
|
||||
</a>
|
||||
<a class="card" href="auth/users.html">
|
||||
<h2>👤 用户与角色</h2>
|
||||
<p>用户管理、角色分配(readonly / engineer / admin)、审计(PRD 8.2)</p>
|
||||
<span class="tag">管理员</span>
|
||||
</a>
|
||||
<a class="card" href="mobile/">
|
||||
<h2>📱 移动端</h2>
|
||||
<p>移动端只读驾驶舱与交接班摘要(iOS14+ / Android 10+)</p>
|
||||
<span class="tag">移动端 · 值班长</span>
|
||||
</a>
|
||||
</main>
|
||||
<footer>iAOP-Core v1.0 · 部署底座 K8s/Helm · 推理服务 :30800 · 前端 :8090</footer>
|
||||
<script>
|
||||
// 推理服务健康检测(直连 NodePort,CORS 已放行)
|
||||
fetch("http://39.101.182.167:30800/health", { method: "GET" })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) {
|
||||
var el = document.getElementById("health");
|
||||
el.textContent = d.status === "ok" || d.status === "dry-run"
|
||||
? "● 推理服务在线(" + d.backend + ")" : "推理服务异常";
|
||||
el.style.color = "#34d399";
|
||||
})
|
||||
.catch(function () {
|
||||
var el = document.getElementById("health");
|
||||
el.textContent = "推理服务不可达";
|
||||
});
|
||||
</script>
|
||||
<body data-pro-title="平台门户" data-pro-home="index.html" data-pro-login="auth/login.html">
|
||||
<!--
|
||||
iAOP 平台门户(2026-08 Ant Design Pro 风格重构)。
|
||||
守卫:IAOP_SESSION.requireLoginElseRedirect —— 未登录跳 auth/login.html。
|
||||
模块可见性:IAOP_SESSION.modulesFor(role)(PRD 8.2 三级 RBAC)。
|
||||
-->
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="ph-title" id="welcome">平台门户</div>
|
||||
<div class="ph-desc">iAOP-Core 通用内核 · Template-Ti(海绵钛)一期 · 配置化驾驶舱 / 模板配置台 / LLM 助手</div>
|
||||
</div>
|
||||
<div class="ph-extra">
|
||||
推理服务
|
||||
<span id="health" class="pro-tag pro-tag-default">检测中…</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="module-grid" id="moduleGrid" aria-busy="true"></div>
|
||||
</div>
|
||||
|
||||
<div class="portal-footer">iAOP-Core v1.0 · 部署底座 K8s/Helm · 推理服务 :30800 · 前端 :8090</div>
|
||||
|
||||
<script src="auth/user_store.js"></script>
|
||||
<script src="shared/session.js"></script>
|
||||
<script src="shared/pro-header.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
function renderModules(user) {
|
||||
var grid = document.getElementById("moduleGrid");
|
||||
grid.innerHTML = "";
|
||||
grid.removeAttribute("aria-busy");
|
||||
document.getElementById("welcome").textContent =
|
||||
"你好," + user.username + ",欢迎使用 iAOP 平台";
|
||||
|
||||
var modules = IAOP_SESSION.modulesFor(user.role);
|
||||
modules.forEach(function (m) {
|
||||
var a = document.createElement("a");
|
||||
a.className = "module-card";
|
||||
a.href = m.url;
|
||||
|
||||
var head = document.createElement("div");
|
||||
head.className = "mc-head";
|
||||
var icon = document.createElement("div");
|
||||
icon.className = "mc-icon";
|
||||
icon.textContent = m.icon;
|
||||
var h2 = document.createElement("h2");
|
||||
h2.textContent = m.title;
|
||||
head.appendChild(icon);
|
||||
head.appendChild(h2);
|
||||
|
||||
var desc = document.createElement("div");
|
||||
desc.className = "mc-desc";
|
||||
desc.textContent = m.desc;
|
||||
|
||||
var foot = document.createElement("div");
|
||||
foot.className = "mc-foot";
|
||||
var tag = document.createElement("span");
|
||||
tag.className = "pro-tag pro-tag-" + (m.tagColor || "default");
|
||||
tag.textContent = m.tag;
|
||||
var enter = document.createElement("span");
|
||||
enter.className = "mc-enter";
|
||||
enter.textContent = "进入 →";
|
||||
foot.appendChild(tag);
|
||||
foot.appendChild(enter);
|
||||
|
||||
a.appendChild(head);
|
||||
a.appendChild(desc);
|
||||
a.appendChild(foot);
|
||||
grid.appendChild(a);
|
||||
});
|
||||
}
|
||||
|
||||
// 登录守卫:未登录 → auth/login.html?next=<回跳地址>
|
||||
IAOP_SESSION.requireLoginElseRedirect("auth/login.html").then(function (user) {
|
||||
if (!user) return; // 正在跳转登录页
|
||||
renderModules(user);
|
||||
});
|
||||
|
||||
// 推理服务健康检测(直连 NodePort,CORS 已放行)
|
||||
fetch("http://39.101.182.167:30800/health", { method: "GET" })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) {
|
||||
var el = document.getElementById("health");
|
||||
if (d.status === "ok" || d.status === "dry-run") {
|
||||
el.textContent = "在线 · " + d.backend;
|
||||
el.className = "pro-tag pro-tag-green";
|
||||
} else {
|
||||
el.textContent = "异常";
|
||||
el.className = "pro-tag pro-tag-red";
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
var el = document.getElementById("health");
|
||||
el.textContent = "不可达";
|
||||
el.className = "pro-tag pro-tag-red";
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>iAOP 移动端驾驶舱</title>
|
||||
<link rel="stylesheet" href="mobile.css">
|
||||
<link rel="stylesheet" href="../shared/pro.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- iAOP 移动端只读驾驶舱 + 交接班摘要(issue #136 / PRD 2.2 场景C、7.5)
|
||||
@@ -59,6 +60,12 @@
|
||||
<button class="tab" data-view="handover">交接班</button>
|
||||
</nav>
|
||||
|
||||
<script src="../auth/user_store.js"></script>
|
||||
<script src="../shared/session.js"></script>
|
||||
<script>
|
||||
// 登录守卫(PRD 8.2):未登录跳登录页(移动端不加 Pro 桌面导航条,仅守卫)
|
||||
IAOP_SESSION.requireLoginElseRedirect("../auth/login.html");
|
||||
</script>
|
||||
<script src="mobile.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
})();
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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: ""
|
||||
};
|
||||
@@ -5,8 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>iAOP 模板配置台</title>
|
||||
<link rel="stylesheet" href="studio.css">
|
||||
<link rel="stylesheet" href="../shared/pro.css">
|
||||
</head>
|
||||
<body>
|
||||
<body data-pro-title="模板配置台">
|
||||
<!-- iAOP 模板配置台(issue #132 / PRD 5.7「⑤.7 模板配置台」)
|
||||
纯静态单页应用(无构建链):导入页 / 超参配置页 / 驾驶舱编排页 / 版本页。
|
||||
三级 RBAC 对齐 core/template-console/rbac.py 的 readonly/engineer/admin;
|
||||
@@ -145,8 +146,9 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="../auth/auth.js"></script>
|
||||
<script src="../auth/user_store.js"></script>
|
||||
<script src="../shared/session.js"></script>
|
||||
<script src="../shared/pro-header.js"></script>
|
||||
<script src="studio.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user