Files
iAOP/web/auth/login.html
T
bot_dev1 cfa976cac3 feat: SPA 外壳——菜单切换零刷新,对齐 Ant Design Pro 体验(issue #131)
- web/index.html 重写为 SPA 外壳:Sider/顶栏常驻,模块页在
  #module-frame iframe 内容区加载,点菜单只换 iframe 地址,
  彻底消灭整页刷新与白闪(此前只是铺底色缓解)
- 新增 shared/shell.js:IAOP_SHELL.navigate() 导航、外壳 hash
  (#/admin/#models) 与 iframe 地址双向同步、登录守卫、移动端
  模块新标签打开;iframe 地址用 location.replace 避免污染
  joint history;250ms 轮询回同步(replace 不触发 load 事件);
  导航时乐观高亮(后台标签轮询被节流不能作为唯一途径);
  前进/后退若恢复了 iframe 旧地址则以外壳 hash 为准强制对齐
- pro-header.js 三模式:外壳(菜单点击走 IAOP_SHELL、导出
  IAOP_PRO.setActive)/被嵌(iframe 内,隐藏页内跨页链接与重复
  用户标识、保留 Tab-hash 联动)/独立(原行为);菜单渲染后派发
  pro:menu-rendered 事件消除导航先于渲染的高亮竞态
- session.js:logout/守卫重定向作用于顶层窗口(iframe 内不再
  只跳 iframe 自己)
- 修复 iframe 替换元素宽高 bug:fixed 时左右定位不拉伸(回退
  固有 300x150),改显式 calc 宽高
- 登录页 next 默认回外壳 ../index.html

实测(本地 http.server + WebBridge 真实浏览器):菜单切换/
子菜单切换/前进/后退/直达链接(#/admin/#models) URL、iframe、
菜单高亮、子菜单高亮全链路一致,Sider 节点全程不重建;
登录守卫 logout→顶层跳登录、登录后回外壳默认驾驶舱。
2026-08-05 22:43:47 +08:00

364 lines
12 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<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/#134 + 2026-08 Ant Design Pro 风格重构)。
认证双轨(shared/session.js):
· 后端 core/auth 可达 → POST /auth/login(HMAC 会话,HttpOnly cookie);
· 纯静态部署 → UserStore 本地账号(首次访问自动播种演示账号)。
角色三级(PRD 8.2):readonly 只读 / engineer 可配置 / admin 可发布回滚,
登录后门户按角色过滤可见模块。
-->
<div class="brand">
<div class="logo-row">
<span class="logo-badge">iA</span>
<h1>iAOP 云美工业AI优化平台</h1>
</div>
<div class="slogan">iAOP-Core 通用内核 · Template-Ti(海绵钛)一期 · 配置化驾驶舱 / 模板配置台 / LLM 助手</div>
</div>
<main>
<form class="login-card" id="loginForm" autocomplete="on" novalidate>
<h2><span class="active-tab">账号密码登录</span></h2>
<div class="alert" id="alert" role="alert" aria-live="polite"></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>
<button type="submit" class="btn-login" id="loginBtn">登 录</button>
<!-- issue #152:OIDC SSO 双轨入口——配置了 issuer/client_id 时显示 -->
<button type="button" class="btn-sso" id="ssoBtn" hidden>企业 SSO 登录(OIDC)</button>
<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="../cockpit/">驾驶舱</a>
<a href="../chat/assistant.html">对话助手</a>
<a href="../mobile/">移动端</a>
</div>
iAOP-Core v1.0 · 认证后端 core/auth(PBKDF2-HMAC-SHA256)· 部署底座 K8s/Helm
</footer>
<script src="user_store.js"></script>
<script src="../shared/session.js"></script>
<script>
(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() {
// 登录后默认进 SPA 外壳(无门户页;外壳默认加载驾驶舱)
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>