443 lines
15 KiB
HTML
443 lines
15 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<script>
|
||
/* Epic #163 · C1(issue #176):旧登录页收口到 FBA UI(/fba-admin 原生登录)。 */
|
||
(function () {
|
||
fetch('/fba-admin/', { method: 'HEAD' })
|
||
.then(function (r) { if (r.ok || r.redirected) location.replace('/fba-admin/'); })
|
||
.catch(function () { /* FBA 离线:保留本页兜底 */ });
|
||
})();
|
||
</script>
|
||
<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); }
|
||
|
||
/* ---- FBA 图形验证码(Epic #159 Phase 1) -------------------------------- */
|
||
.captcha-row {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: stretch;
|
||
}
|
||
.captcha-row .input-wrap { flex: 1; }
|
||
.captcha-img {
|
||
height: 40px;
|
||
min-width: 110px;
|
||
border: 1px solid var(--ant-border);
|
||
border-radius: var(--ant-radius);
|
||
cursor: pointer;
|
||
object-fit: cover;
|
||
background: #fafafa;
|
||
}
|
||
.captcha-img:hover { border-color: var(--ant-primary-hover); }
|
||
|
||
.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 风格重构 + Epic #159 Phase 1)。
|
||
认证三轨(shared/session.js):
|
||
· FBA 可用(/fba/api/v1 探测通过)→ FBA JWT 登录(默认强制图形验证码);
|
||
· 旧后端 core/auth 可达 → POST /auth/login(HMAC 会话,HttpOnly cookie);
|
||
· 纯静态部署 → UserStore 本地账号(首次访问自动播种演示账号)。
|
||
角色三级(PRD 8.2):readonly 只读 / engineer 可配置 / admin 可发布回滚,
|
||
FBA 侧角色按 session.js 的 FBA_ROLE_MAP 命名即可映射到三级角色,
|
||
登录后门户按角色过滤可见模块。
|
||
-->
|
||
<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>
|
||
|
||
<!-- FBA 图形验证码:仅当 FBA 认证可用且开启验证码时显示(session.js fbaStatus) -->
|
||
<div class="field" id="captchaField" hidden>
|
||
<label for="captcha">图形验证码</label>
|
||
<div class="captcha-row">
|
||
<div class="input-wrap">
|
||
<span class="input-icon">🛡</span>
|
||
<input id="captcha" name="captcha" type="text"
|
||
autocomplete="off" placeholder="请输入验证码" maxlength="8">
|
||
</div>
|
||
<img class="captcha-img" id="captchaImg" alt="点击刷新验证码"
|
||
title="看不清?点击刷新">
|
||
</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 () {});
|
||
|
||
/* Epic #159 Phase 1:FBA 可用且开启图形验证码时显示验证码字段 */
|
||
var captchaState = { enabled: false, uuid: null };
|
||
function refreshCaptcha() {
|
||
IAOP_SESSION.getCaptcha().then(function (c) {
|
||
if (c) {
|
||
captchaState.uuid = c.uuid;
|
||
$("captchaImg").src = c.imageSrc;
|
||
$("captcha").value = "";
|
||
}
|
||
});
|
||
}
|
||
IAOP_SESSION.fbaStatus().then(function (st) {
|
||
if (st.available && st.captchaEnabled) {
|
||
captchaState.enabled = true;
|
||
$("captchaField").hidden = false;
|
||
refreshCaptcha();
|
||
}
|
||
if (st.available) {
|
||
// FBA 模式下隐藏本地演示账号提示,避免误导
|
||
document.querySelector(".demo-accounts").hidden = true;
|
||
}
|
||
});
|
||
$("captchaImg").addEventListener("click", refreshCaptcha);
|
||
|
||
// 已登录直接跳转
|
||
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");
|
||
$("captcha").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;
|
||
}
|
||
var captchaOpt = null;
|
||
if (captchaState.enabled) {
|
||
var c = $("captcha").value.trim();
|
||
if (!c) {
|
||
$("captcha").setAttribute("aria-invalid", "true");
|
||
showAlert("请输入图形验证码", "error");
|
||
return;
|
||
}
|
||
captchaOpt = { uuid: captchaState.uuid, captcha: c };
|
||
}
|
||
$("loginBtn").disabled = true;
|
||
clearAlert();
|
||
IAOP_SESSION.login(u, p, captchaOpt).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");
|
||
// FBA 验证码一次性有效,失败后必须刷新
|
||
if (captchaState.enabled) refreshCaptcha();
|
||
}).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>
|