Files
iAOP/web/auth/login.html
T

364 lines
12 KiB
HTML
Raw Normal View History

<!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>