799 lines
29 KiB
Smarty
799 lines
29 KiB
Smarty
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta name="robots" content="noindex, nofollow">
|
||
<title>统一认证中心 - 登录</title>
|
||
<style>
|
||
:root {
|
||
--primary: #3973ff;
|
||
--primary-dark: #2560a9;
|
||
--primary-light: #52a8ff;
|
||
--text: #1f2b48;
|
||
--text-sub: #5b6780;
|
||
--muted: #8a94ad;
|
||
--border: #e3e9f5;
|
||
--radius: 12px;
|
||
}
|
||
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
html, body { height: 100%; }
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||
color: var(--text);
|
||
background: linear-gradient(120deg, #eef4ff 0%, #f7fbff 55%, #eaf3ff 100%);
|
||
min-height: 100vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 32px 20px;
|
||
position: relative;
|
||
overflow-x: hidden;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
|
||
.glow {
|
||
position: fixed;
|
||
border-radius: 50%;
|
||
filter: blur(60px);
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
}
|
||
.glow-1 { width: 380px; height: 380px; top: -120px; left: -100px; background: rgba(82, 168, 255, 0.28); }
|
||
.glow-2 { width: 320px; height: 320px; bottom: -120px; right: -80px; background: rgba(57, 115, 255, 0.20); }
|
||
|
||
.auth-card {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: flex;
|
||
width: 100%;
|
||
max-width: 940px;
|
||
min-height: 560px;
|
||
background: #fff;
|
||
border-radius: 22px;
|
||
box-shadow: 0 18px 50px 0 rgba(37, 96, 169, 0.14), 0 2px 6px 0 rgba(31, 43, 72, 0.04);
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* ---------- 左侧品牌区 ---------- */
|
||
.auth-side {
|
||
width: 340px;
|
||
flex: 0 0 340px;
|
||
padding: 44px 34px 28px;
|
||
background: linear-gradient(160deg, #1b3b8f 0%, #2f6fe4 52%, #52a8ff 100%);
|
||
color: #fff;
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
.auth-side::before {
|
||
content: '';
|
||
position: absolute;
|
||
width: 260px; height: 260px;
|
||
top: -90px; right: -110px;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.10);
|
||
}
|
||
.auth-side::after {
|
||
content: '';
|
||
position: absolute;
|
||
width: 190px; height: 190px;
|
||
bottom: -70px; left: -60px;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.brand { display: flex; align-items: center; gap: 12px; position: relative; z-index: 1; }
|
||
.brand-logo {
|
||
width: 40px; height: 40px;
|
||
border-radius: 11px;
|
||
background: rgba(255, 255, 255, 0.18);
|
||
border: 1px solid rgba(255, 255, 255, 0.30);
|
||
display: flex; align-items: center; justify-content: center;
|
||
flex: 0 0 auto;
|
||
}
|
||
.brand-logo svg { width: 22px; height: 22px; }
|
||
.brand-name { font-size: 16px; font-weight: 600; letter-spacing: .5px; }
|
||
|
||
.side-title {
|
||
margin-top: 44px;
|
||
font-size: 26px;
|
||
font-weight: 600;
|
||
line-height: 1.45;
|
||
position: relative; z-index: 1;
|
||
}
|
||
.side-desc {
|
||
margin-top: 12px;
|
||
font-size: 13.5px;
|
||
line-height: 1.9;
|
||
color: rgba(255, 255, 255, 0.76);
|
||
position: relative; z-index: 1;
|
||
}
|
||
|
||
.side-illus { margin: auto 0; padding: 22px 0; position: relative; z-index: 1; }
|
||
.side-illus svg { width: 100%; height: auto; display: block; }
|
||
|
||
.side-foot {
|
||
position: relative; z-index: 1;
|
||
padding-top: 16px;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.16);
|
||
font-size: 12px;
|
||
color: rgba(255, 255, 255, 0.62);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
.side-foot svg { width: 13px; height: 13px; }
|
||
|
||
/* ---------- 右侧表单区 ---------- */
|
||
.auth-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
padding: 52px 56px 40px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
}
|
||
|
||
.panel-head { margin-bottom: 28px; }
|
||
.panel-title { font-size: 27px; font-weight: 600; letter-spacing: .5px; }
|
||
.panel-sub { margin-top: 9px; font-size: 13.5px; color: var(--text-sub); }
|
||
|
||
.error {
|
||
display: none;
|
||
align-items: flex-start;
|
||
gap: 9px;
|
||
background: #fff2f0;
|
||
border: 1px solid #ffd4cf;
|
||
color: #cf1322;
|
||
padding: 11px 14px;
|
||
border-radius: 10px;
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
margin-bottom: 18px;
|
||
animation: shake .32s ease;
|
||
}
|
||
.error.show { display: flex; }
|
||
.error svg { width: 16px; height: 16px; flex: 0 0 auto; margin-top: 2px; }
|
||
@keyframes shake {
|
||
0%, 100% { transform: translateX(0); }
|
||
25% { transform: translateX(-4px); }
|
||
75% { transform: translateX(4px); }
|
||
}
|
||
|
||
.field { margin-bottom: 18px; }
|
||
.field-label {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
font-size: 13px; color: var(--text-sub); margin-bottom: 8px; font-weight: 500;
|
||
}
|
||
.input-wrap { position: relative; }
|
||
.input-icon {
|
||
position: absolute;
|
||
left: 14px; top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 18px; height: 18px;
|
||
color: #a7b1c7;
|
||
pointer-events: none;
|
||
transition: color .2s;
|
||
}
|
||
.input-wrap input {
|
||
width: 100%;
|
||
height: 46px;
|
||
padding: 0 44px 0 42px;
|
||
font-size: 14px;
|
||
color: var(--text);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
background: #fbfcff;
|
||
outline: none;
|
||
transition: border-color .2s, box-shadow .2s, background .2s;
|
||
}
|
||
.input-wrap input::placeholder { color: #adb6c9; }
|
||
.input-wrap input:hover { border-color: var(--border); background: #fff; }
|
||
.input-wrap input:focus {
|
||
border-color: var(--primary);
|
||
background: #fff;
|
||
box-shadow: 0 0 0 4px rgba(57, 115, 255, 0.12);
|
||
}
|
||
.input-wrap input:focus + .input-icon,
|
||
.input-wrap:focus-within .input-icon { color: var(--primary); }
|
||
|
||
.eye-btn {
|
||
position: absolute;
|
||
right: 8px; top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 32px; height: 32px;
|
||
border: none; background: transparent;
|
||
border-radius: 8px;
|
||
color: #a7b1c7;
|
||
cursor: pointer;
|
||
display: flex; align-items: center; justify-content: center;
|
||
transition: color .2s, background .2s;
|
||
}
|
||
.eye-btn:hover { color: var(--primary); background: #f0f5ff; }
|
||
.eye-btn svg { width: 18px; height: 18px; }
|
||
|
||
.row-extra {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
margin: -4px 0 22px;
|
||
font-size: 13px;
|
||
}
|
||
.remember { display: inline-flex; align-items: center; gap: 7px; cursor: pointer; color: var(--text-sub); user-select: none; }
|
||
.remember input { width: 15px; height: 15px; accent-color: var(--primary); cursor: pointer; }
|
||
.link { color: var(--primary); text-decoration: none; cursor: pointer; transition: color .2s; }
|
||
.link:hover { color: var(--primary-dark); text-decoration: underline; }
|
||
|
||
.btn {
|
||
width: 100%;
|
||
height: 46px;
|
||
border: none;
|
||
border-radius: var(--radius);
|
||
cursor: pointer;
|
||
background: linear-gradient(135deg, #3973ff 0%, #52a8ff 100%);
|
||
color: #fff;
|
||
font-size: 15.5px;
|
||
font-weight: 500;
|
||
letter-spacing: 2px;
|
||
display: flex; align-items: center; justify-content: center; gap: 8px;
|
||
box-shadow: 0 8px 20px rgba(57, 115, 255, 0.28);
|
||
transition: transform .15s, box-shadow .2s, opacity .2s;
|
||
}
|
||
.btn:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 12px 26px rgba(57, 115, 255, 0.34); }
|
||
.btn:active:not(:disabled) { transform: translateY(0); box-shadow: 0 6px 14px rgba(57, 115, 255, 0.26); }
|
||
.btn:disabled { opacity: .72; cursor: not-allowed; box-shadow: none; }
|
||
.spinner {
|
||
display: none;
|
||
width: 16px; height: 16px;
|
||
border: 2px solid rgba(255, 255, 255, 0.45);
|
||
border-top-color: #fff;
|
||
border-radius: 50%;
|
||
animation: spin .7s linear infinite;
|
||
}
|
||
.btn.is-loading .spinner { display: block; }
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
|
||
/* 第三方登录 */
|
||
.third-area { margin-top: 4px; }
|
||
.divider {
|
||
position: relative; text-align: center;
|
||
margin: 26px 0 16px;
|
||
font-size: 12px; color: #a3acbf;
|
||
}
|
||
.divider::before {
|
||
content: ''; position: absolute; top: 50%; left: 0; right: 0;
|
||
height: 1px; background: var(--border);
|
||
}
|
||
.divider span { position: relative; background: #fff; padding: 0 12px; }
|
||
.third-list { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
|
||
.third-btn {
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
padding: 9px 18px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 22px;
|
||
background: #fff;
|
||
font-size: 13px;
|
||
color: var(--text-sub);
|
||
cursor: pointer;
|
||
transition: all .2s;
|
||
}
|
||
.third-btn:hover {
|
||
border-color: var(--primary);
|
||
color: var(--primary);
|
||
background: #f5f9ff;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
/* 企业选择 */
|
||
.tenant-list { list-style: none; display: flex; flex-direction: column; gap: 10px; }
|
||
.tenant-item {
|
||
display: flex; align-items: center; gap: 14px;
|
||
padding: 14px 16px;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
cursor: pointer;
|
||
background: #fbfcff;
|
||
transition: all .2s;
|
||
}
|
||
.tenant-item:hover {
|
||
border-color: var(--primary);
|
||
background: #f5f9ff;
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 8px 18px rgba(57, 115, 255, 0.10);
|
||
}
|
||
.tenant-avatar {
|
||
width: 40px; height: 40px;
|
||
flex: 0 0 40px;
|
||
border-radius: 10px;
|
||
background: linear-gradient(135deg, rgba(57, 115, 255, 0.12) 0%, rgba(82, 168, 255, 0.16) 100%);
|
||
color: var(--primary);
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.tenant-avatar svg { width: 20px; height: 20px; }
|
||
.tenant-info { flex: 1; min-width: 0; }
|
||
.tenant-name {
|
||
font-size: 15px; font-weight: 500; color: var(--text);
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||
}
|
||
.tenant-meta { font-size: 12px; color: var(--muted); margin-top: 4px; }
|
||
.tenant-arrow { color: #c8cfdf; transition: transform .2s, color .2s; }
|
||
.tenant-arrow svg { width: 18px; height: 18px; display: block; }
|
||
.tenant-item:hover .tenant-arrow { color: var(--primary); transform: translateX(3px); }
|
||
|
||
.back { text-align: center; margin-top: 22px; font-size: 13px; }
|
||
|
||
.foot-tip {
|
||
margin-top: 26px;
|
||
padding-top: 18px;
|
||
border-top: 1px dashed var(--border);
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
display: flex; align-items: center; gap: 6px;
|
||
}
|
||
.foot-tip svg { width: 13px; height: 13px; flex: 0 0 auto; }
|
||
|
||
@media (max-width: 900px) {
|
||
body { padding: 20px 14px; align-items: flex-start; }
|
||
.auth-card { max-width: 520px; min-height: 0; flex-direction: column; }
|
||
.auth-side { width: 100%; flex: none; padding: 30px 26px 24px; }
|
||
.side-title { margin-top: 26px; font-size: 21px; }
|
||
.side-illus { display: none; }
|
||
.auth-main { padding: 34px 26px 30px; }
|
||
.panel-title { font-size: 23px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="glow glow-1"></div>
|
||
<div class="glow glow-2"></div>
|
||
|
||
<div class="auth-card">
|
||
<!-- 左侧品牌区 -->
|
||
<aside class="auth-side">
|
||
<div class="brand">
|
||
<span class="brand-logo">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M12 3l7 3v6c0 4.2-2.9 7.6-7 9-4.1-1.4-7-4.8-7-9V6l7-3z"/>
|
||
<path d="M9.2 12.2l2 2 3.6-4"/>
|
||
</svg>
|
||
</span>
|
||
<span class="brand-name">云泽统一认证中心</span>
|
||
</div>
|
||
|
||
<div class="side-title">一次登录<br>全平台通行</div>
|
||
<div class="side-desc">
|
||
基于 OIDC / OAuth 2.0 的统一身份底座,<br>
|
||
为所有接入应用提供安全、一致的登录体验。
|
||
</div>
|
||
|
||
<div class="side-illus">
|
||
<svg viewBox="0 0 260 170" fill="none">
|
||
<ellipse cx="130" cy="150" rx="98" ry="11" fill="rgba(255,255,255,.13)"/>
|
||
<rect x="26" y="60" width="56" height="38" rx="9" fill="rgba(255,255,255,.14)"/>
|
||
<rect x="36" y="70" width="26" height="4" rx="2" fill="rgba(255,255,255,.45)"/>
|
||
<rect x="36" y="79" width="36" height="4" rx="2" fill="rgba(255,255,255,.28)"/>
|
||
<rect x="184" y="52" width="50" height="34" rx="9" fill="rgba(255,255,255,.10)"/>
|
||
<rect x="193" y="62" width="22" height="4" rx="2" fill="rgba(255,255,255,.36)"/>
|
||
<path d="M130 26l40 15v30c0 22-17 39-40 47-23-8-40-25-40-47V41l40-15z"
|
||
fill="rgba(255,255,255,.17)" stroke="rgba(255,255,255,.55)" stroke-width="1.6"/>
|
||
<path d="M116 82l11 11 21-23" stroke="#fff" stroke-width="5.5"
|
||
stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
</div>
|
||
|
||
<div class="side-foot">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||
<rect x="4" y="11" width="16" height="10" rx="2"/>
|
||
<path d="M8 11V8a4 4 0 018 0v3"/>
|
||
</svg>
|
||
<span>© 2026 Yunzer · 连接已加密</span>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- 右侧表单区 -->
|
||
<main class="auth-main">
|
||
{{if eq .Step "tenant"}}
|
||
<div class="panel-head">
|
||
<h1 class="panel-title">选择企业</h1>
|
||
<p class="panel-sub">该账号已加入多家企业,请选择要进入的空间</p>
|
||
</div>
|
||
|
||
<div class="error" id="error">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||
<circle cx="12" cy="12" r="9"/><path d="M12 8v5M12 16h.01"/>
|
||
</svg>
|
||
<span id="errorText"></span>
|
||
</div>
|
||
|
||
<ul class="tenant-list">
|
||
{{range .Tenants}}
|
||
<li class="tenant-item" onclick="chooseTenant({{.Tid}})">
|
||
<span class="tenant-avatar">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M3 21h18M5 21V7l7-4 7 4v14"/><path d="M10 21v-5h4v5"/>
|
||
</svg>
|
||
</span>
|
||
<span class="tenant-info">
|
||
<span class="tenant-name">{{.TenantName}}</span>
|
||
<span class="tenant-meta">{{if .Name}}{{.Name}} · {{end}}企业ID {{.Tid}}</span>
|
||
</span>
|
||
<span class="tenant-arrow">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M9 6l6 6-6 6"/>
|
||
</svg>
|
||
</span>
|
||
</li>
|
||
{{end}}
|
||
</ul>
|
||
|
||
<div class="back"><a class="link" onclick="logout()">不是我的账号?切换账号</a></div>
|
||
{{else}}
|
||
<div class="panel-head">
|
||
<h1 class="panel-title">欢迎登录</h1>
|
||
<p class="panel-sub">请输入您的账号与密码以继续</p>
|
||
</div>
|
||
|
||
<div class="error" id="error">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||
<circle cx="12" cy="12" r="9"/><path d="M12 8v5M12 16h.01"/>
|
||
</svg>
|
||
<span id="errorText"></span>
|
||
</div>
|
||
|
||
<form id="loginForm" onsubmit="return submitLogin(event)" autocomplete="on">
|
||
<div class="field">
|
||
<label class="field-label" for="account">账号</label>
|
||
<div class="input-wrap">
|
||
<input id="account" type="text" placeholder="手机号 / 邮箱 / 企业账号" autocomplete="username" autofocus>
|
||
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="12" cy="8" r="4"/><path d="M4 21c0-4 3.6-6 8-6s8 2 8 6"/>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label class="field-label" for="password">密码</label>
|
||
<div class="input-wrap">
|
||
<input id="password" type="password" placeholder="请输入密码" autocomplete="current-password">
|
||
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||
<rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V8a4 4 0 018 0v3"/>
|
||
</svg>
|
||
<button class="eye-btn" type="button" id="eyeBtn" onclick="togglePassword()" title="显示密码" aria-label="显示密码">
|
||
<svg id="eyeOpen" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M2 12s3.6-6 10-6 10 6 10 6-3.6 6-10 6-10-6-10-6z"/><circle cx="12" cy="12" r="3"/>
|
||
</svg>
|
||
<svg id="eyeOff" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" style="display:none">
|
||
<path d="M3 3l18 18"/><path d="M10.6 6.2A9.9 9.9 0 0112 6c6.4 0 10 6 10 6a17 17 0 01-3.3 3.9M6.3 8.1A16.7 16.7 0 002 12s3.6 6 10 6c1.3 0 2.5-.3 3.6-.7"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row-extra">
|
||
<label class="remember"><input type="checkbox" id="remember"> 记住账号</label>
|
||
</div>
|
||
|
||
<button class="btn" id="submitBtn" type="submit">
|
||
<span class="spinner"></span>
|
||
<span id="btnLabel">登 录</span>
|
||
</button>
|
||
</form>
|
||
|
||
<div class="third-area" id="thirdArea" style="display:none">
|
||
<div class="divider"><span>其他登录方式</span></div>
|
||
<div class="third-list" id="thirdList"></div>
|
||
</div>
|
||
{{end}}
|
||
|
||
<div class="foot-tip">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M12 3l7 3v6c0 4.2-2.9 7.6-7 9-4.1-1.4-7-4.8-7-9V6l7-3z"/>
|
||
</svg>
|
||
<span>统一身份认证 · 一次登录,全平台通行</span>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<script>
|
||
var CLIENT_ID = "{{.ClientID}}";
|
||
var REDIRECT = "{{.Redirect}}";
|
||
|
||
// 人机验证配置:页面加载时拉取,决定是否弹出极验
|
||
var verifyCfg = { openVerify: 0, verifyType: '', needGeetest: false, geetestVersion: '4', captchaId: '' };
|
||
// 极验 4.0 SDK:优先官方 CDN,失败回退到本地副本(static/js/gt4.js)
|
||
var GEETEST_SDK_CDN = 'https://static.geetest.com/v4/gt4.js';
|
||
var GEETEST_SDK_LOCAL = '/static/js/gt4.js';
|
||
var geetestSDKPromise = null;
|
||
|
||
function showError(msg) {
|
||
var box = document.getElementById('error');
|
||
document.getElementById('errorText').textContent = msg;
|
||
box.classList.remove('show');
|
||
void box.offsetWidth; // 重启动画
|
||
box.classList.add('show');
|
||
}
|
||
|
||
function hideError() {
|
||
var box = document.getElementById('error');
|
||
if (box) box.classList.remove('show');
|
||
}
|
||
|
||
function setBtnLoading(on, text) {
|
||
var btn = document.getElementById('submitBtn');
|
||
if (!btn) return;
|
||
btn.disabled = !!on;
|
||
if (on) {
|
||
btn.classList.add('is-loading');
|
||
document.getElementById('btnLabel').textContent = text || '登录中...';
|
||
} else {
|
||
btn.classList.remove('is-loading');
|
||
document.getElementById('btnLabel').textContent = '登 录';
|
||
}
|
||
}
|
||
|
||
function togglePassword() {
|
||
var input = document.getElementById('password');
|
||
var open = document.getElementById('eyeOpen');
|
||
var off = document.getElementById('eyeOff');
|
||
var btn = document.getElementById('eyeBtn');
|
||
var show = input.type === 'password';
|
||
input.type = show ? 'text' : 'password';
|
||
open.style.display = show ? 'none' : 'block';
|
||
off.style.display = show ? 'block' : 'none';
|
||
btn.title = show ? '隐藏密码' : '显示密码';
|
||
}
|
||
|
||
// 记住账号:只存账号,绝不明文保存密码
|
||
(function initRemember() {
|
||
var box = document.getElementById('remember');
|
||
if (!box) return;
|
||
var saved = '';
|
||
try { saved = localStorage.getItem('authCenterAccount') || ''; } catch (e) { saved = ''; }
|
||
if (saved) {
|
||
document.getElementById('account').value = saved;
|
||
box.checked = true;
|
||
}
|
||
box.addEventListener('change', function () {
|
||
try {
|
||
if (box.checked) localStorage.setItem('authCenterAccount', document.getElementById('account').value.trim());
|
||
else localStorage.removeItem('authCenterAccount');
|
||
} catch (e) { /* 忽略隐私模式下的存储失败 */ }
|
||
});
|
||
})();
|
||
|
||
function loadScript(url) {
|
||
return new Promise(function (resolve, reject) {
|
||
var s = document.createElement('script');
|
||
s.src = url;
|
||
s.async = true;
|
||
s.onload = function () { resolve(); };
|
||
s.onerror = function () { reject(new Error('加载脚本失败')); };
|
||
document.head.appendChild(s);
|
||
});
|
||
}
|
||
|
||
// 加载极验 4.0 SDK(多次调用只加载一次)
|
||
function loadGeetestSDK() {
|
||
if (window.initGeetest4) return Promise.resolve();
|
||
if (geetestSDKPromise) return geetestSDKPromise;
|
||
geetestSDKPromise = loadScript(GEETEST_SDK_CDN)
|
||
.catch(function () { return loadScript(GEETEST_SDK_LOCAL); })
|
||
.then(function () {
|
||
if (!window.initGeetest4) throw new Error('极验 SDK 加载失败');
|
||
})
|
||
.catch(function (err) {
|
||
geetestSDKPromise = null;
|
||
throw err;
|
||
});
|
||
return geetestSDKPromise;
|
||
}
|
||
|
||
// 拉取登录验证配置(含极验 captcha_id)
|
||
function loadVerifyConfig() {
|
||
return fetch('/auth/verify-config')
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
var d = (res && res.data) || {};
|
||
verifyCfg.openVerify = Number(d.openVerify) || 0;
|
||
verifyCfg.verifyType = d.verifyType || '';
|
||
verifyCfg.needGeetest = !!d.needGeetest;
|
||
verifyCfg.geetestVersion = d.geetestVersion || '4';
|
||
verifyCfg.captchaId = d.captchaId || '';
|
||
// 预加载 SDK,点击登录时能立刻弹出
|
||
if (verifyCfg.needGeetest && verifyCfg.captchaId) {
|
||
loadGeetestSDK().catch(function () { /* 点击时再提示 */ });
|
||
}
|
||
})
|
||
.catch(function () { verifyCfg.needGeetest = false; });
|
||
}
|
||
|
||
loadVerifyConfig();
|
||
|
||
// 加载已开通的第三方登录方式
|
||
function loadThirdProviders() {
|
||
fetch('/auth/third/list')
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
var list = (res && res.data) || [];
|
||
var enabled = list.filter(function (i) { return i.enabled; });
|
||
if (!enabled.length) return;
|
||
var box = document.getElementById('thirdList');
|
||
enabled.forEach(function (item) {
|
||
var btn = document.createElement('button');
|
||
btn.className = 'third-btn';
|
||
btn.type = 'button';
|
||
btn.textContent = item.name;
|
||
btn.onclick = function () { startThird(item.provider); };
|
||
box.appendChild(btn);
|
||
});
|
||
document.getElementById('thirdArea').style.display = 'block';
|
||
})
|
||
.catch(function () { /* 静默失败,不影响账号密码登录 */ });
|
||
}
|
||
|
||
function startThird(provider) {
|
||
window.location.href = '/auth/third/' + encodeURIComponent(provider) +
|
||
'/authorize?redirect=' + encodeURIComponent(REDIRECT);
|
||
}
|
||
|
||
loadThirdProviders();
|
||
|
||
// 弹出极验 4.0,验证通过才 resolve 出校验参数
|
||
function showGeetest(captchaId) {
|
||
return loadGeetestSDK().then(function () {
|
||
return new Promise(function (resolve, reject) {
|
||
var settled = false;
|
||
var done = function (fn) { if (settled) return; settled = true; fn(); };
|
||
var instance = null;
|
||
var teardown = function () {
|
||
try { if (instance && instance.destroy) instance.destroy(); } catch (err) { /* ignore */ }
|
||
};
|
||
|
||
window.initGeetest4({
|
||
captchaId: captchaId,
|
||
product: 'bind',
|
||
language: 'zh-CN',
|
||
https: true
|
||
}, function (captcha) {
|
||
instance = captcha;
|
||
|
||
captcha.onSuccess(function () {
|
||
var r = captcha.getValidate() || {};
|
||
teardown();
|
||
done(function () {
|
||
resolve({
|
||
captcha_id: r.captcha_id || captchaId,
|
||
lot_number: r.lot_number || '',
|
||
pass_token: r.pass_token || '',
|
||
gen_time: r.gen_time || '',
|
||
captcha_output: r.captcha_output || ''
|
||
});
|
||
});
|
||
});
|
||
|
||
captcha.onFail(function () {
|
||
teardown();
|
||
done(function () { reject(new Error('人机验证未通过,请重新验证')); });
|
||
});
|
||
|
||
captcha.onError(function () {
|
||
teardown();
|
||
done(function () { reject(new Error('人机验证加载失败,请重试')); });
|
||
});
|
||
|
||
if (captcha.onClose) {
|
||
captcha.onClose(function () {
|
||
setTimeout(function () {
|
||
if (settled) return;
|
||
teardown();
|
||
done(function () { reject(new Error('已取消人机验证')); });
|
||
}, 400);
|
||
});
|
||
}
|
||
|
||
captcha.showCaptcha();
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
function submitLogin(e) {
|
||
e.preventDefault();
|
||
hideError();
|
||
var account = document.getElementById('account').value.trim();
|
||
var password = document.getElementById('password').value;
|
||
if (!account) { showError('请输入账号'); return false; }
|
||
if (!password) { showError('请输入密码'); return false; }
|
||
|
||
// 记住账号
|
||
try {
|
||
if (document.getElementById('remember').checked) localStorage.setItem('authCenterAccount', account);
|
||
else localStorage.removeItem('authCenterAccount');
|
||
} catch (err) { /* ignore */ }
|
||
|
||
// 开启极验时:先弹验证,通过后才继续登录
|
||
if (verifyCfg.needGeetest) {
|
||
if (!verifyCfg.captchaId) {
|
||
showError('人机验证未配置,请联系管理员');
|
||
return false;
|
||
}
|
||
setBtnLoading(true, '安全验证中...');
|
||
showGeetest(verifyCfg.captchaId)
|
||
.then(function (result) {
|
||
setBtnLoading(true, '登录中...');
|
||
doLogin(account, password, result);
|
||
})
|
||
.catch(function (err) {
|
||
showError(err.message || '人机验证失败,请重试');
|
||
setBtnLoading(false);
|
||
});
|
||
return false;
|
||
}
|
||
|
||
setBtnLoading(true, '登录中...');
|
||
doLogin(account, password, null);
|
||
return false;
|
||
}
|
||
|
||
function doLogin(account, password, captcha) {
|
||
var payload = { account: account, password: password, client_id: CLIENT_ID, redirect: REDIRECT };
|
||
if (captcha) {
|
||
payload.captcha_id = captcha.captcha_id;
|
||
payload.lot_number = captcha.lot_number;
|
||
payload.pass_token = captcha.pass_token;
|
||
payload.gen_time = captcha.gen_time;
|
||
payload.captcha_output = captcha.captcha_output;
|
||
}
|
||
|
||
fetch('/auth/login', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.code !== 200) {
|
||
showError(res.msg || '登录失败');
|
||
setBtnLoading(false);
|
||
return;
|
||
}
|
||
if (res.need_choose_tenant) {
|
||
window.location.href = '/auth/login?step=tenant&client_id=' + encodeURIComponent(CLIENT_ID) + '&redirect=' + encodeURIComponent(REDIRECT);
|
||
return;
|
||
}
|
||
if (res.redirect) { window.location.href = res.redirect; return; }
|
||
window.location.href = '/auth/login';
|
||
})
|
||
.catch(function () {
|
||
showError('网络异常,请稍后重试');
|
||
setBtnLoading(false);
|
||
});
|
||
}
|
||
|
||
function chooseTenant(tid) {
|
||
hideError();
|
||
fetch('/auth/switch-tenant', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ tid: tid, client_id: CLIENT_ID, redirect: REDIRECT })
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.code !== 200) { showError(res.msg || '切换失败'); return; }
|
||
if (res.redirect) { window.location.href = res.redirect; return; }
|
||
window.location.href = '/auth/login';
|
||
})
|
||
.catch(function () { showError('网络异常,请稍后重试'); });
|
||
}
|
||
|
||
function logout() {
|
||
fetch('/auth/logout', { method: 'POST' }).then(function () {
|
||
window.location.href = '/auth/login?client_id=' + encodeURIComponent(CLIENT_ID) + '&redirect=' + encodeURIComponent(REDIRECT);
|
||
});
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|