1294 lines
50 KiB
Smarty
1294 lines
50 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>{{.LoginTitle}} - 统一认证中心</title>
|
||
<style>
|
||
:root {
|
||
--primary: #3973ff;
|
||
--primary-dark: #2560a9;
|
||
--primary-light: #52a8ff;
|
||
--text: #1f2b48;
|
||
--text-sub: #5b6780;
|
||
--muted: #8a94ad;
|
||
--border: #e3e9f5;
|
||
--radius: 10px;
|
||
}
|
||
|
||
* { 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: #f3f6fc;
|
||
min-height: 100vh;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
|
||
.login-page { display: flex; min-height: 100vh; width: 100%; }
|
||
|
||
/* ---------- 左侧品牌区 ---------- */
|
||
.brand-panel {
|
||
position: relative;
|
||
flex: 1 1 58%;
|
||
min-height: 100vh;
|
||
padding: 64px 72px 52px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
background: linear-gradient(150deg, #1b3b8f 0%, #2f6fe4 52%, #52a8ff 100%);
|
||
color: #fff;
|
||
}
|
||
.brand-panel::before {
|
||
content: '';
|
||
position: absolute;
|
||
width: 420px; height: 420px;
|
||
top: -150px; right: -160px;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.10);
|
||
}
|
||
.brand-panel::after {
|
||
content: '';
|
||
position: absolute;
|
||
width: 300px; height: 300px;
|
||
bottom: -120px; left: -100px;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
}
|
||
/* 配置了背景图:浅色底 + 深色文字,叠一层白色蒙版保证可读性 */
|
||
.brand-panel.has-bg { background: #eaf1fb; color: var(--text); }
|
||
.brand-panel.has-bg::before,
|
||
.brand-panel.has-bg::after { display: none; }
|
||
.brand-bg {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
.brand-veil {
|
||
display: block;
|
||
position: absolute;
|
||
inset: 0;
|
||
background: linear-gradient(100deg, rgba(255, 255, 255, 0.86) 0%, rgba(255, 255, 255, 0.46) 52%, rgba(255, 255, 255, 0.10) 100%);
|
||
}
|
||
|
||
.brand-inner {
|
||
position: relative;
|
||
z-index: 1;
|
||
width: 100%;
|
||
max-width: 580px;
|
||
}
|
||
.brand { display: flex; align-items: center; gap: 12px; margin-bottom: 54px; }
|
||
.brand-logo {
|
||
width: 44px; height: 44px;
|
||
border-radius: 12px;
|
||
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: 24px; height: 24px; }
|
||
.brand-name { font-size: 19px; font-weight: 600; letter-spacing: .5px; }
|
||
.brand-img { height: 44px; max-width: 240px; object-fit: contain; display: block; }
|
||
|
||
.brand-title {
|
||
font-size: clamp(34px, 3.6vw, 52px);
|
||
font-weight: 700;
|
||
line-height: 1.28;
|
||
letter-spacing: 1px;
|
||
white-space: pre-line;
|
||
}
|
||
.brand-sub {
|
||
margin-top: 20px;
|
||
font-size: 16px;
|
||
line-height: 1.9;
|
||
color: rgba(255, 255, 255, 0.78);
|
||
max-width: 520px;
|
||
white-space: pre-line;
|
||
}
|
||
.brand-panel.has-bg .brand-sub { color: var(--text-sub); }
|
||
.brand-panel.has-bg .brand-name { color: var(--text); }
|
||
.brand-panel.has-bg .brand-logo {
|
||
background: rgba(57, 115, 255, 0.12);
|
||
border-color: rgba(57, 115, 255, 0.18);
|
||
}
|
||
.brand-panel.has-bg .brand-logo svg { stroke: var(--primary); }
|
||
|
||
.brand-foot {
|
||
position: relative;
|
||
z-index: 1;
|
||
margin-top: 72px;
|
||
font-size: 12px;
|
||
color: rgba(255, 255, 255, 0.62);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
.brand-panel.has-bg .brand-foot { color: var(--muted); }
|
||
.brand-foot svg { width: 13px; height: 13px; flex: 0 0 auto; }
|
||
|
||
/* ---------- 右侧表单区 ---------- */
|
||
.form-panel {
|
||
position: relative;
|
||
flex: 0 0 42%;
|
||
min-width: 460px;
|
||
min-height: 100vh;
|
||
background: #fff;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 64px 56px 88px;
|
||
}
|
||
.form-inner { width: 100%; max-width: 380px; }
|
||
|
||
.panel-head { margin-bottom: 30px; }
|
||
.panel-title { font-size: 30px; font-weight: 700; letter-spacing: .5px; }
|
||
.panel-sub { margin-top: 10px; font-size: 13.5px; color: var(--text-sub); line-height: 1.7; }
|
||
|
||
/* ---------- 登录方式 tab ---------- */
|
||
.login-tabs {
|
||
display: flex;
|
||
gap: 4px;
|
||
margin: 0 0 24px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.login-tab {
|
||
position: relative;
|
||
flex: 1 1 0;
|
||
padding: 11px 4px 13px;
|
||
border: none;
|
||
background: transparent;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: var(--text-sub);
|
||
cursor: pointer;
|
||
transition: color .2s;
|
||
}
|
||
.login-tab:hover { color: var(--primary); }
|
||
.login-tab.active { color: var(--primary); font-weight: 600; }
|
||
.login-tab.active::after {
|
||
content: '';
|
||
position: absolute;
|
||
left: 12px; right: 12px; bottom: -1px;
|
||
height: 2px;
|
||
border-radius: 2px;
|
||
background: linear-gradient(90deg, var(--primary) 0%, var(--primary-light) 100%);
|
||
}
|
||
.login-panel { display: none; }
|
||
.login-panel.active { display: block; animation: panelIn .24s ease both; }
|
||
@keyframes panelIn {
|
||
from { opacity: 0; transform: translateY(6px); }
|
||
to { opacity: 1; transform: none; }
|
||
}
|
||
|
||
/* 小屏时左侧品牌区隐藏,在表单上方展示紧凑品牌头 */
|
||
.mobile-brand { display: none; align-items: center; gap: 12px; margin-bottom: 28px; }
|
||
.mobile-brand .brand-img { height: 36px; max-width: 180px; }
|
||
.mobile-brand .brand-logo {
|
||
width: 40px; height: 40px;
|
||
background: rgba(57, 115, 255, 0.10);
|
||
border-color: rgba(57, 115, 255, 0.18);
|
||
}
|
||
.mobile-brand .brand-logo svg { stroke: var(--primary); }
|
||
.mobile-brand-text { display: flex; flex-direction: column; gap: 2px; }
|
||
.mobile-brand-name { font-size: 15px; font-weight: 600; }
|
||
.mobile-brand-sub { font-size: 12px; color: var(--muted); }
|
||
|
||
.form-foot {
|
||
position: absolute;
|
||
left: 0; right: 0; bottom: 26px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
text-align: center;
|
||
font-size: 12px;
|
||
color: #a3acbf;
|
||
}
|
||
/* 底部「清除缓存」文字按钮 */
|
||
.cache-clear-btn {
|
||
border: 0;
|
||
background: none;
|
||
padding: 0;
|
||
font: inherit;
|
||
color: var(--primary);
|
||
cursor: pointer;
|
||
transition: color .2s;
|
||
}
|
||
.cache-clear-btn:hover { color: var(--primary-dark); text-decoration: underline; }
|
||
.cache-clear-btn:disabled { opacity: .6; cursor: default; text-decoration: none; }
|
||
|
||
.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.is-info { background: #f2f7ff; border-color: #d2e2ff; color: #2b5fc7; }
|
||
.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); }
|
||
}
|
||
|
||
.notice-box {
|
||
display: flex; align-items: flex-start; gap: 9px;
|
||
background: #f0fff4; border: 1px solid #b7ebc2; color: #1f7a37;
|
||
padding: 11px 14px; border-radius: 10px; font-size: 13px; line-height: 1.6;
|
||
margin-bottom: 18px;
|
||
}
|
||
.notice-box.warn {
|
||
background: #fffbe6; border-color: #ffe89e; color: #a96a00;
|
||
}
|
||
.notice-box svg { width: 16px; height: 16px; flex: 0 0 auto; margin-top: 2px; }
|
||
|
||
.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: 8px;
|
||
padding: 8px 16px;
|
||
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);
|
||
}
|
||
/* 第三方图标色块:微信绿 / QQ 蓝 / 其余统一主色 */
|
||
.third-icon {
|
||
width: 22px; height: 22px;
|
||
flex: 0 0 22px;
|
||
border-radius: 50%;
|
||
display: inline-flex; align-items: center; justify-content: center;
|
||
font-size: 9.5px; font-weight: 700; letter-spacing: -.2px;
|
||
}
|
||
.third-icon svg { width: 13px; height: 13px; }
|
||
.third-icon.is-wechat { background: rgba(7, 193, 96, .12); color: #07c160; }
|
||
.third-icon.is-qq { background: rgba(18, 183, 245, .14); color: #12b7f5; }
|
||
.third-icon.is-generic { background: rgba(57, 115, 255, .10); color: var(--primary); }
|
||
|
||
/* 短信验证码 */
|
||
.code-row { display: flex; align-items: center; gap: 10px; }
|
||
.code-row .input-wrap { flex: 1 1 auto; }
|
||
.code-btn {
|
||
flex: 0 0 auto;
|
||
height: 46px;
|
||
padding: 0 16px;
|
||
border: 1px solid rgba(57, 115, 255, .28);
|
||
border-radius: var(--radius);
|
||
background: rgba(57, 115, 255, .06);
|
||
color: var(--primary);
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
cursor: pointer;
|
||
transition: background .2s, border-color .2s, color .2s;
|
||
}
|
||
.code-btn:hover { background: rgba(57, 115, 255, .12); border-color: rgba(57, 115, 255, .45); }
|
||
.code-btn:disabled { background: #f5f7fb; border-color: var(--border); color: var(--muted); cursor: not-allowed; }
|
||
.form-hint { margin-top: 14px; font-size: 12px; color: var(--muted); line-height: 1.8; text-align: center; }
|
||
|
||
/* 企业选择 */
|
||
.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: 18px; font-size: 13px; color: var(--text-sub); line-height: 1.7; }
|
||
|
||
/* ---------- 扫码登录 ---------- */
|
||
.qr-panel { padding: 4px 0 2px; text-align: center; }
|
||
.qr-box {
|
||
position: relative;
|
||
width: 200px; height: 200px;
|
||
margin: 0 auto 20px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 16px;
|
||
background: #fff;
|
||
box-shadow: 0 14px 34px rgba(31, 43, 72, 0.08);
|
||
display: flex; align-items: center; justify-content: center;
|
||
overflow: hidden;
|
||
}
|
||
.qr-img { width: 100%; height: 100%; object-fit: contain; }
|
||
.qr-mask {
|
||
position: absolute; inset: 0;
|
||
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px;
|
||
padding: 12px;
|
||
background: rgba(255, 255, 255, .96);
|
||
font-size: 12.5px; color: var(--text-sub); line-height: 1.6; text-align: center;
|
||
white-space: pre-line;
|
||
}
|
||
.scan-btn {
|
||
border: none; background: var(--primary); color: #fff;
|
||
border-radius: 8px; padding: 7px 18px; font-size: 12px; cursor: pointer;
|
||
}
|
||
.qr-title { font-size: 14.5px; font-weight: 600; color: var(--text); letter-spacing: .3px; }
|
||
.qr-desc { margin-top: 8px; font-size: 12.5px; line-height: 1.9; color: var(--text-sub); }
|
||
.qr-desc strong { color: var(--primary); font-weight: 600; }
|
||
.qr-note { margin-top: 12px; font-size: 12px; color: var(--muted); }
|
||
|
||
.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: 1100px) {
|
||
.brand-panel { padding: 48px 44px; }
|
||
.brand-title { font-size: 34px; }
|
||
.form-panel { min-width: 400px; padding: 56px 40px 84px; }
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.login-page { flex-direction: column; }
|
||
.brand-panel { display: none; }
|
||
.form-panel {
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
min-height: 100vh;
|
||
align-items: flex-start;
|
||
padding: 40px 24px 72px;
|
||
}
|
||
.form-inner { max-width: 440px; margin: 0 auto; }
|
||
.mobile-brand { display: flex; }
|
||
.panel-title { font-size: 26px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="login-page">
|
||
<!-- 左侧品牌区:背景图 / Logo / 主副标题由 platform「平台设置 → 登录页设置」提供 -->
|
||
<aside class="brand-panel{{if .LoginBg}} has-bg{{end}}">
|
||
{{if .LoginBg}}
|
||
<img class="brand-bg" src="{{.LoginBg}}" alt="" />
|
||
<span class="brand-veil"></span>
|
||
{{end}}
|
||
|
||
<div class="brand-inner">
|
||
<div class="brand">
|
||
{{if .LoginLogo}}
|
||
<img class="brand-img" src="{{.LoginLogo}}" alt="{{.LoginTitle}}" />
|
||
{{else}}
|
||
<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>
|
||
{{end}}
|
||
</div>
|
||
|
||
<h1 class="brand-title">{{.LoginTitle}}</h1>
|
||
<p class="brand-sub">{{.LoginSubtitle}}</p>
|
||
</div>
|
||
|
||
<div class="brand-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="form-panel">
|
||
<div class="form-inner">
|
||
<div class="mobile-brand">
|
||
{{if .LoginLogo}}
|
||
<img class="brand-img" src="{{.LoginLogo}}" alt="{{.LoginTitle}}" />
|
||
{{else}}
|
||
<span class="brand-logo">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" 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>
|
||
{{end}}
|
||
<span class="mobile-brand-text">
|
||
<span class="mobile-brand-name">云泽统一认证中心</span>
|
||
<span class="mobile-brand-sub">{{.LoginTitle}}</span>
|
||
</span>
|
||
</div>
|
||
{{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>
|
||
|
||
<!-- 登录方式切换:默认账号密码 -->
|
||
<div class="login-tabs" role="tablist">
|
||
<button class="login-tab active" type="button" role="tab" data-tab="account" onclick="switchLoginTab('account')">账号密码</button>
|
||
<button class="login-tab" type="button" role="tab" data-tab="sms" onclick="switchLoginTab('sms')">短信登录</button>
|
||
<button class="login-tab" type="button" role="tab" data-tab="qr" onclick="switchLoginTab('qr')">扫码登录</button>
|
||
</div>
|
||
|
||
<!-- 账号密码登录 -->
|
||
<div class="login-panel active" id="panel-account">
|
||
{{if .Notice}}
|
||
<div class="notice-box{{if eq .Notice "registered_warn"}} warn{{end}}">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
{{if eq .Notice "registered_warn"}}<circle cx="12" cy="12" r="9"/><path d="M12 8v5M12 16h.01"/>{{else}}<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"/>{{end}}
|
||
</svg>
|
||
<span>
|
||
{{if eq .Notice "reset"}}密码已重置,请使用新密码登录{{else if eq .Notice "registered_warn"}}{{.WarnMsg}},可先登录{{else}}注册成功,请使用新账号登录{{end}}
|
||
</span>
|
||
</div>
|
||
{{end}}
|
||
|
||
<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>
|
||
<a class="link" href="/auth/forgot?client_id={{urlquery .ClientID}}&redirect={{urlquery .Redirect}}">忘记密码?</a>
|
||
</div>
|
||
|
||
<button class="btn" id="submitBtn" type="submit">
|
||
<span class="spinner"></span>
|
||
<span id="btnLabel">登 录</span>
|
||
</button>
|
||
</form>
|
||
|
||
<div class="back">还没有账号?<a class="link" href="/auth/register?client_id={{urlquery .ClientID}}&redirect={{urlquery .Redirect}}">立即注册</a></div>
|
||
</div>
|
||
|
||
<!-- 手机号验证码登录 -->
|
||
<div class="login-panel" id="panel-sms">
|
||
<form onsubmit="return submitSmsLogin(event)" autocomplete="on">
|
||
<div class="field">
|
||
<label class="field-label" for="smsMobile">手机号</label>
|
||
<div class="input-wrap">
|
||
<input id="smsMobile" type="tel" inputmode="numeric" maxlength="11" placeholder="请输入手机号" autocomplete="tel">
|
||
<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="6" y="2" width="12" height="20" rx="3"/><path d="M11 18h2"/>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label class="field-label" for="smsCode">验证码</label>
|
||
<div class="code-row">
|
||
<div class="input-wrap">
|
||
<input id="smsCode" type="text" inputmode="numeric" maxlength="6" placeholder="请输入短信验证码" autocomplete="one-time-code">
|
||
<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="5" width="16" height="14" rx="2"/><path d="M8 9h8M8 13h5"/>
|
||
</svg>
|
||
</div>
|
||
<button class="code-btn" type="button" id="smsCodeBtn" onclick="sendSmsCode()">获取验证码</button>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="btn" type="submit" id="smsSubmitBtn">
|
||
<span class="spinner"></span>
|
||
<span id="smsBtnLabel">登 录</span>
|
||
</button>
|
||
</form>
|
||
<p class="form-hint">未注册的手机号验证通过后将自动创建账号</p>
|
||
</div>
|
||
|
||
<!-- 扫码登录:微信扫码 → 小程序内确认 -->
|
||
<div class="login-panel" id="panel-qr">
|
||
<div class="qr-panel">
|
||
<div class="qr-box">
|
||
<img id="scanQrImg" class="qr-img" alt="微信扫码登录" style="display:none" />
|
||
<div class="qr-mask" id="scanQrMask">
|
||
<span id="scanQrMaskText">正在获取二维码…</span>
|
||
<button class="scan-btn" onclick="restartWechatScan()" style="display:none" id="scanQrRefreshBtn">刷新</button>
|
||
</div>
|
||
</div>
|
||
<p class="qr-title">微信扫码登录</p>
|
||
<p class="qr-desc">使用微信「扫一扫」,扫码后在<strong>云泽小程序</strong>中点击确认<br />即可完成登录</p>
|
||
<p class="qr-note">二维码 5 分钟内有效,过期可点击刷新</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 第三方登录(未配置时点击提示即将上线) -->
|
||
<div class="third-area" id="thirdArea">
|
||
<div class="divider"><span>其他登录方式</span></div>
|
||
<div class="third-list" id="thirdList">
|
||
<button class="third-btn" type="button" data-third-provider="wechat" onclick="thirdLogin('wechat', '微信')">
|
||
<span class="third-icon is-wechat">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M4 5h13v9H9l-4 4z"/><path d="M9 9h.01M13 9h.01"/>
|
||
</svg>
|
||
</span>
|
||
微信登录
|
||
</button>
|
||
<button class="third-btn" type="button" data-third-provider="qq" onclick="thirdLogin('qq', 'QQ')">
|
||
<span class="third-icon is-qq">QQ</span>
|
||
QQ 登录
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
|
||
</div>
|
||
|
||
<div class="form-foot">
|
||
<span>© 2026 云泽科技 版权所有</span>
|
||
<button id="cacheClearBtn" class="cache-clear-btn" type="button" onclick="clearSiteCache()" title="仅清除本站在当前浏览器中的缓存数据">清除缓存</button>
|
||
</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, tone) {
|
||
var box = document.getElementById('error');
|
||
var text = document.getElementById('errorText');
|
||
if (!box || !text) return;
|
||
box.classList.toggle('is-info', tone === 'info');
|
||
text.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');
|
||
}
|
||
|
||
// ==================== 登录方式切换(账号密码 / 短信 / 扫码) ====================
|
||
var LOGIN_PANELS = { account: 'panel-account', sms: 'panel-sms', qr: 'panel-qr' };
|
||
|
||
function switchLoginTab(name) {
|
||
if (!LOGIN_PANELS[name]) return;
|
||
var tabs = document.querySelectorAll('.login-tab');
|
||
Array.prototype.forEach.call(tabs, function (tab) {
|
||
tab.classList.toggle('active', tab.getAttribute('data-tab') === name);
|
||
});
|
||
Object.keys(LOGIN_PANELS).forEach(function (key) {
|
||
var panel = document.getElementById(LOGIN_PANELS[key]);
|
||
if (panel) panel.classList.toggle('active', key === name);
|
||
});
|
||
hideError();
|
||
// 扫码 tab 才拉二维码,离开则停止轮询
|
||
if (name === 'qr') restartWechatScan(); else stopWechatScan();
|
||
}
|
||
|
||
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();
|
||
|
||
// 已开通的第三方登录方式:记录状态,供静态 QQ / 微信入口判断是否可用
|
||
var thirdEnabled = {};
|
||
var THIRD_STATIC = { wechat: 1, qq: 1 };
|
||
|
||
function loadThirdProviders() {
|
||
var box = document.getElementById('thirdList');
|
||
if (!box) return;
|
||
fetch('/auth/third/list')
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
var list = (res && res.data) || [];
|
||
thirdEnabled = {};
|
||
// 清掉上一轮动态追加的按钮(静态 QQ / 微信入口保留)
|
||
Array.prototype.slice.call(box.querySelectorAll('[data-dynamic="1"]')).forEach(function (n) {
|
||
if (n.parentNode) n.parentNode.removeChild(n);
|
||
});
|
||
list.forEach(function (item) {
|
||
if (!item || !item.enabled) return;
|
||
thirdEnabled[item.provider] = true;
|
||
if (THIRD_STATIC[item.provider]) return;
|
||
var btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = 'third-btn';
|
||
btn.setAttribute('data-dynamic', '1');
|
||
var icon = document.createElement('span');
|
||
icon.className = 'third-icon is-generic';
|
||
icon.textContent = (item.name || '?').slice(0, 1);
|
||
btn.appendChild(icon);
|
||
btn.appendChild(document.createTextNode(item.name || item.provider));
|
||
btn.onclick = function () { startThird(item.provider); };
|
||
box.appendChild(btn);
|
||
});
|
||
// 未开通的入口:保留样式,悬停提示即将上线,点击给中性提示
|
||
Array.prototype.forEach.call(box.querySelectorAll('[data-third-provider]'), function (btn) {
|
||
var provider = btn.getAttribute('data-third-provider');
|
||
btn.title = thirdEnabled[provider] ? '' : '即将上线';
|
||
});
|
||
})
|
||
.catch(function () { /* 静默失败,不影响其他登录方式 */ });
|
||
}
|
||
|
||
function thirdLogin(provider, label) {
|
||
if (thirdEnabled[provider]) { startThird(provider); return; }
|
||
showError((label || provider) + '登录即将上线,敬请期待', 'info');
|
||
}
|
||
|
||
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 sendSmsCode() {
|
||
var input = document.getElementById('smsMobile');
|
||
var mobile = input ? input.value.trim() : '';
|
||
if (!/^1[3-9]\d{9}$/.test(mobile)) { showError('请输入正确的手机号'); return; }
|
||
showError('短信验证码服务即将上线,请先使用账号密码登录', 'info');
|
||
}
|
||
|
||
function submitSmsLogin(e) {
|
||
e.preventDefault();
|
||
hideError();
|
||
var mobileEl = document.getElementById('smsMobile');
|
||
var codeEl = document.getElementById('smsCode');
|
||
var mobile = mobileEl ? mobileEl.value.trim() : '';
|
||
var code = codeEl ? codeEl.value.trim() : '';
|
||
if (!/^1[3-9]\d{9}$/.test(mobile)) { showError('请输入正确的手机号'); return false; }
|
||
if (!code) { showError('请输入验证码'); return false; }
|
||
showError('手机号验证码登录即将上线,请先使用账号密码登录', 'info');
|
||
return false;
|
||
}
|
||
|
||
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; }
|
||
// 会话已建立但确实没有回跳地址:明确提示,绝不再跳回登录页造成死循环
|
||
showError('登录成功,但未获取到回跳地址,请从业务系统重新进入');
|
||
setBtnLoading(false);
|
||
})
|
||
.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; }
|
||
showError('已进入所选企业,但未获取到回跳地址,请从业务系统重新进入');
|
||
})
|
||
.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);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 清除本站在当前浏览器里的全部缓存数据。
|
||
*
|
||
* localStorage / sessionStorage / document.cookie / Cache Storage /
|
||
* IndexedDB / Service Worker 都受同源策略约束,只会作用于认证中心自己的
|
||
* origin,不会影响 back.yunzer.cn 等其它站点。
|
||
* HttpOnly 的 yz_sid 前端读不到,交给 /auth/clear-site-data 的
|
||
* Clear-Site-Data 响应头与 Set-Cookie 一起清除。
|
||
*/
|
||
function clearSiteCache() {
|
||
var btn = document.getElementById('cacheClearBtn');
|
||
if (btn) { btn.disabled = true; btn.textContent = '清除中...'; }
|
||
|
||
// 1. Web Storage(记住账号、各类前端状态)
|
||
try {
|
||
localStorage.clear();
|
||
sessionStorage.clear();
|
||
} catch (e) { /* 隐私模式下存储可能不可访问 */ }
|
||
|
||
// 2. 当前站点可写的 Cookie。只针对本站 host:不尝试 .yunzer.cn 等父域,
|
||
// 否则会连带清掉 back.yunzer.cn / platform.yunzer.cn 等兄弟站点的数据。
|
||
// 两次删除分别覆盖 host-only cookie 与显式 Domain=当前 host 的 cookie。
|
||
try {
|
||
var cookieNames = (document.cookie || '').split(';').map(function (item) {
|
||
return item.split('=')[0].trim();
|
||
}).filter(Boolean);
|
||
var host = window.location.hostname;
|
||
var expired = '=; Max-Age=0; Path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
|
||
cookieNames.forEach(function (name) {
|
||
document.cookie = name + expired;
|
||
document.cookie = name + expired + '; Domain=' + host;
|
||
});
|
||
} catch (e) { /* ignore */ }
|
||
|
||
var tasks = [];
|
||
|
||
// 3. Cache Storage(离线资源 / 静态资源缓存)
|
||
if (window.caches && caches.keys) {
|
||
tasks.push(caches.keys().then(function (keys) {
|
||
return Promise.all((keys || []).map(function (key) { return caches.delete(key); }));
|
||
}).catch(function () {}));
|
||
}
|
||
|
||
// 4. Service Worker 注册
|
||
if (navigator.serviceWorker && navigator.serviceWorker.getRegistrations) {
|
||
tasks.push(navigator.serviceWorker.getRegistrations().then(function (regs) {
|
||
return Promise.all((regs || []).map(function (reg) { return reg.unregister(); }));
|
||
}).catch(function () {}));
|
||
}
|
||
|
||
// 5. IndexedDB(databases() 在部分浏览器不可用,跳过即可)
|
||
if (window.indexedDB && indexedDB.databases) {
|
||
tasks.push(indexedDB.databases().then(function (dbs) {
|
||
return Promise.all((dbs || []).map(function (db) {
|
||
return new Promise(function (resolve) {
|
||
if (!db || !db.name) { resolve(); return; }
|
||
var done = false;
|
||
var finish = function () { if (!done) { done = true; resolve(); } };
|
||
var req = indexedDB.deleteDatabase(db.name);
|
||
req.onsuccess = req.onerror = req.onblocked = finish;
|
||
// 其它标签页占用数据库时 onblocked 可能不触发,兜底避免卡住
|
||
setTimeout(finish, 1500);
|
||
});
|
||
}));
|
||
}).catch(function () {}));
|
||
}
|
||
|
||
// 6. 后端清 HttpOnly 会话 Cookie,并让浏览器按 origin 清 HTTP 缓存与存储;
|
||
// 服务尚未部署新端点(404)或接口异常时退回已有登出接口,
|
||
// 至少保证会话 Cookie 被清除;整体超时也不阻断前面的前端清理结果
|
||
var legacyClear = function () {
|
||
return fetch('/auth/logout', { method: 'POST', credentials: 'same-origin', cache: 'no-store' })
|
||
.catch(function () {});
|
||
};
|
||
var clearRequest = fetch('/auth/clear-site-data', {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
cache: 'no-store'
|
||
}).then(function (res) {
|
||
if (res.ok) return;
|
||
return legacyClear();
|
||
}).catch(legacyClear);
|
||
tasks.push(Promise.race([
|
||
clearRequest,
|
||
new Promise(function (resolve) { setTimeout(resolve, 5000); })
|
||
]));
|
||
|
||
Promise.all(tasks).then(function () {
|
||
// 保留 client_id / redirect,清完直接从登录页重新开始
|
||
window.location.replace('/auth/login?client_id=' + encodeURIComponent(CLIENT_ID) +
|
||
(REDIRECT ? '&redirect=' + encodeURIComponent(REDIRECT) : ''));
|
||
});
|
||
}
|
||
|
||
// ==================== 微信扫码登录(扫码 → 小程序内确认;已接 scan/start、scan/status) ====================
|
||
var scanScene = '';
|
||
var scanExpireAt = 0;
|
||
var scanPollTimer = null;
|
||
|
||
// 显隐由 switchLoginTab 控制,切换 tab 时自动启停轮询
|
||
|
||
function stopWechatScan() {
|
||
if (scanPollTimer) { clearInterval(scanPollTimer); scanPollTimer = null; }
|
||
}
|
||
|
||
function setScanQrMask(text, showRefresh) {
|
||
var img = document.getElementById('scanQrImg');
|
||
var mask = document.getElementById('scanQrMask');
|
||
img.style.display = 'none';
|
||
mask.style.display = 'flex';
|
||
document.getElementById('scanQrMaskText').textContent = text || '';
|
||
document.getElementById('scanQrRefreshBtn').style.display = showRefresh ? 'block' : 'none';
|
||
}
|
||
|
||
function restartWechatScan() {
|
||
stopWechatScan();
|
||
setScanQrMask('获取中…', false);
|
||
fetch('/auth/wechat/scan/start', { method: 'POST' })
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.code !== 200) {
|
||
setScanQrMask('获取失败', true);
|
||
return;
|
||
}
|
||
var d = res.data || {};
|
||
scanScene = d.scene || '';
|
||
var qr = d.qrcode_url || d.follow_qrcode || '';
|
||
if (qr) {
|
||
var img = document.getElementById('scanQrImg');
|
||
img.src = qr;
|
||
img.style.display = 'block';
|
||
document.getElementById('scanQrMask').style.display = 'none';
|
||
} else {
|
||
setScanQrMask('暂无二维码', true);
|
||
}
|
||
// 回退原因只写控制台,不在页面展示
|
||
if (d.notice && window.console) console.warn('[wechat-scan]', d.notice);
|
||
scanPollTimer = setInterval(pollWechatScan, 3000);
|
||
})
|
||
.catch(function () {
|
||
setScanQrMask('网络异常', true);
|
||
});
|
||
}
|
||
|
||
function pollWechatScan() {
|
||
if (!scanScene) return;
|
||
fetch('/auth/wechat/scan/status?scene=' + encodeURIComponent(scanScene) +
|
||
'&client_id=' + encodeURIComponent(CLIENT_ID) +
|
||
'&redirect=' + encodeURIComponent(REDIRECT))
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
var d = (res && res.data) || {};
|
||
if (Number(d.status) === 1) {
|
||
stopWechatScan();
|
||
window.location.href = d.redirect || ('/auth/login?client_id=' + encodeURIComponent(CLIENT_ID));
|
||
} else if (Number(d.status) === 2) {
|
||
stopWechatScan();
|
||
setScanQrMask('已过期', true);
|
||
}
|
||
})
|
||
.catch(function () { /* 下个周期重试 */ });
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|