Files
mce/templates/MetaLab-2026/static/js/register.js
Victor_Jay 9a496d3055 feat: 密码强度多级校验 — low/medium/high/very_high
- site_settings 新增 password.strength 配置项
- 四级:low(len>=8) / medium(两类) / high(三类+无连续) / very_high(四类+无连续)
- 连续字符检查:重复/升序/降序/键盘水平/键盘垂直序列
- 注册和改密接口统一读取配置
- 前端 register.js 动态校验 + 管理后台安全设置区
2026-06-22 00:54:21 +08:00

352 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================
MetaLab Register JS — 注册页面专属脚本
============================================================ */
(function () {
'use strict';
// ---- DOM refs ----
var openGuidelines = document.getElementById('openGuidelines');
var modalOverlay = document.getElementById('modalOverlay');
var modalBody = document.getElementById('modalBody');
var modalClose = document.getElementById('modalClose');
var modalEnforce = document.getElementById('modalEnforce');
var confirmBtn = document.getElementById('confirmBtn');
var previewCloseBtn = document.getElementById('previewCloseBtn');
var timerCount = document.getElementById('timerCount');
var timerLabel = document.getElementById('timerLabel');
var timerUnit = document.getElementById('timerUnit');
var timerDone = document.getElementById('timerDone');
var timer = document.getElementById('timer');
var scrollHint = document.getElementById('scrollHint');
var submitBtn = document.getElementById('submitBtn');
var registerForm = document.getElementById('registerForm');
var toast = document.getElementById('toast');
var emailInput = document.getElementById('email');
var passwordInput = document.getElementById('password');
// 密码强度提示从 data 属性读取
var pwHint = passwordInput.getAttribute('placeholder') || '密码至少 8 位';
var pwMinLen = 8;
var pwLevel = passwordInput.getAttribute('data-pw-level') || 'medium';
var confirmPasswordInput = document.getElementById('confirmPassword');
var rememberMeCheckbox = document.getElementById('rememberMe');
var guidelinesAgreedCheckbox = document.getElementById('guidelinesAgreed');
var emailError = document.getElementById('emailError');
var passwordError = document.getElementById('passwordError');
var confirmPasswordError = document.getElementById('confirmPasswordError');
// Guard: exit if not on auth page
if (!registerForm) return;
// 准则配置(从模板 data 属性读取)
var forceGuidelines = modalOverlay ? modalOverlay.getAttribute('data-force-guidelines') === 'true' : true;
var guidelinesTimer = modalOverlay ? parseInt(modalOverlay.getAttribute('data-guidelines-timer')) || 60 : 60;
var showModal = !!modalOverlay;
// 读取 redirect 参数(注册成功后跳转目标)
function getRedirect() {
var m = window.location.search.match(/[?&]redirect=([^&]+)/);
if (m) return decodeURIComponent(m[1]);
return '/';
}
var countdown = guidelinesTimer;
var timerInterval = null;
var reachedBottom = false;
var agreed = false;
var isRegistrationMode = false;
var countdownPaused = false;
// ---- Modal Open ----
function openModal(onAgreed) {
isRegistrationMode = typeof onAgreed === 'function';
clearInterval(timerInterval);
countdown = guidelinesTimer;
reachedBottom = false;
agreed = false;
modalBody.scrollTop = 0;
modalOverlay.classList.add('active');
modalOverlay._onAgreed = isRegistrationMode ? onAgreed : null;
if (isRegistrationMode) {
modalEnforce.style.display = '';
confirmBtn.style.display = '';
previewCloseBtn.style.display = 'none';
if (forceGuidelines) {
timerCount.textContent = guidelinesTimer;
timerLabel.style.display = '';
timerCount.style.display = '';
timerUnit.style.display = '';
timerDone.style.display = 'none';
if (timer) timer.classList.remove('done');
countdownPaused = false;
confirmBtn.disabled = true;
scrollHint.style.display = 'flex';
scrollHint.textContent = '▼ 请滚动至底部完成阅读';
scrollHint.style.color = 'var(--color-danger)';
startCountdown();
} else {
// 非强制:隐藏倒计时和滚动提示,按钮直接可用
modalEnforce.style.display = 'none';
confirmBtn.disabled = false;
}
} else {
modalEnforce.style.display = 'none';
confirmBtn.style.display = 'none';
previewCloseBtn.style.display = '';
}
}
// ---- Countdown ----
function startCountdown() {
timerInterval = setInterval(function () {
if (countdownPaused) return;
countdown--;
timerCount.textContent = countdown;
if (countdown <= 0) {
clearInterval(timerInterval);
timerLabel.style.display = 'none';
timerCount.style.display = 'none';
timerUnit.style.display = 'none';
timerDone.style.display = '';
timer.classList.add('done');
scrollHint.textContent = '▼ 请滚动至底部完成阅读';
updateConfirmState();
}
}, 1000);
}
// ---- Scroll Detection ----
modalBody.addEventListener('scroll', function () {
if (reachedBottom) return;
if (modalBody.scrollTop + modalBody.clientHeight >= modalBody.scrollHeight - 5) {
reachedBottom = true;
scrollHint.textContent = '✓ 已阅读至底部';
scrollHint.style.color = '#27ae60';
updateConfirmState();
}
});
// ---- Pause countdown on tab/window inactive ----
document.addEventListener('visibilitychange', function () {
if (!isRegistrationMode) return;
countdownPaused = document.hidden;
});
window.addEventListener('blur', function () {
if (isRegistrationMode) countdownPaused = true;
});
window.addEventListener('focus', function () {
if (isRegistrationMode) countdownPaused = false;
});
// ---- Update Confirm Button ----
function updateConfirmState() {
if (countdown <= 0 && reachedBottom) {
confirmBtn.disabled = false;
scrollHint.style.display = 'none';
}
}
// ---- Confirm (agree to guidelines) ----
confirmBtn.addEventListener('click', function () {
agreed = true;
modalOverlay.classList.remove('active');
if (modalOverlay._onAgreed) {
modalOverlay._onAgreed();
modalOverlay._onAgreed = null;
}
});
// ---- Close Modal ----
modalClose.addEventListener('click', function () {
modalOverlay.classList.remove('active');
});
previewCloseBtn.addEventListener('click', function () {
modalOverlay.classList.remove('active');
});
modalOverlay.addEventListener('click', function (e) {
if (e.target === modalOverlay) {
modalOverlay.classList.remove('active');
}
});
// ---- Open Guidelines Link (preview only) ----
openGuidelines.addEventListener('click', function (e) {
e.preventDefault();
openModal(null);
});
// ---- Form Validation ----
function checkFormValidity() {
var email = emailInput.value.trim();
var password = passwordInput.value;
var confirm = confirmPasswordInput.value;
var emailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
var passwordValid = checkPWStrength(password);
var confirmValid = passwordValid && confirm === password;
submitBtn.disabled = !(emailValid && passwordValid && confirmValid);
}
emailInput.addEventListener('input', function () {
var v = emailInput.value.trim();
if (v && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) {
emailInput.classList.add('error');
emailError.style.display = 'block';
} else {
emailInput.classList.remove('error');
emailError.style.display = 'none';
}
checkFormValidity();
});
function checkPWStrength(pw) {
if (pw.length < pwMinLen) return false;
var cats = 0;
if (/[a-z]/.test(pw)) cats++;
if (/[A-Z]/.test(pw)) cats++;
if (/\d/.test(pw)) cats++;
if (/[^a-zA-Z0-9]/.test(pw)) cats++;
if (pwLevel === 'low') return true;
if (pwLevel === 'medium') return cats >= 2;
if (pwLevel === 'high' || pwLevel === 'very_high') return cats >= (pwLevel === 'very_high' ? 4 : 3);
return cats >= 2;
}
passwordInput.addEventListener('input', function () {
var v = passwordInput.value;
if (v && !checkPWStrength(v)) {
passwordInput.classList.add('error');
passwordError.style.display = 'block';
} else {
passwordInput.classList.remove('error');
passwordError.style.display = 'none';
}
if (confirmPasswordInput.value) {
validateConfirmPassword();
}
checkFormValidity();
});
confirmPasswordInput.addEventListener('input', function () {
validateConfirmPassword();
checkFormValidity();
});
function validateConfirmPassword() {
var confirm = confirmPasswordInput.value;
var password = passwordInput.value;
if (confirm && confirm !== password) {
confirmPasswordInput.classList.add('error');
confirmPasswordError.style.display = 'block';
} else {
confirmPasswordInput.classList.remove('error');
confirmPasswordError.style.display = 'none';
}
}
// ---- Form Submit: validate → check email → open guidelines → on agree → submit ----
registerForm.addEventListener('submit', function (e) {
e.preventDefault();
var email = emailInput.value.trim();
var password = passwordInput.value;
var confirm = confirmPasswordInput.value;
// Client-side validation
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
emailInput.classList.add('error');
emailError.style.display = 'block';
return;
}
if (password.length < 8 || !/[a-z]/.test(password) || !/[A-Z]/.test(password) || !/\d/.test(password)) {
passwordInput.classList.add('error');
passwordError.style.display = 'block';
return;
}
if (confirm !== password) {
confirmPasswordInput.classList.add('error');
confirmPasswordError.style.display = 'block';
return;
}
// 复选框模式:检查是否勾选准则
if (guidelinesAgreedCheckbox && !guidelinesAgreedCheckbox.checked) {
window.MetaLab.toast(toast, '请先阅读并同意社区准则', true);
return;
}
// Disable submit to prevent double-click
submitBtn.disabled = true;
submitBtn.textContent = '检查中...';
// Step 1: Check if email already registered
var checkXhr = new XMLHttpRequest();
checkXhr.open('POST', '/api/auth/check-email', true);
checkXhr.setRequestHeader('Content-Type', 'application/json');
checkXhr.onreadystatechange = function () {
if (checkXhr.readyState !== 4) return;
submitBtn.disabled = true;
var resp;
try { resp = JSON.parse(checkXhr.responseText); } catch (err) { resp = null; }
if (checkXhr.status === 200 && resp && resp.success) {
if (resp.data && resp.data.exists) {
window.MetaLab.toast(toast, '该邮箱已注册,请直接登录', true);
submitBtn.textContent = '注 册';
submitBtn.disabled = false;
} else {
submitBtn.textContent = '注 册';
if (showModal) {
// 弹窗模式:弹出准则阅读
openModal(function () {
doRegister(email, password, confirm);
});
} else {
// 复选框模式:直接注册
doRegister(email, password, confirm);
}
}
} else {
var msg = (resp && resp.message) ? resp.message : '网络异常,请稍后重试';
window.MetaLab.toast(toast, msg, true);
submitBtn.textContent = '注 册';
submitBtn.disabled = false;
}
};
checkXhr.send(JSON.stringify({ email: email }));
});
// ---- AJAX Register ----
function doRegister(email, password, confirmPassword) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/auth/register', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
var resp;
try { resp = JSON.parse(xhr.responseText); } catch (err) { resp = null; }
if (xhr.status === 200 && resp && resp.success) {
window.MetaLab.toast(toast, resp.message || '🎉 注册成功!', false);
// Token 已通过 HttpOnly Cookie 自动设置JS 无需处理
// 注册即登录,跳转首页
setTimeout(function () {
window.location.href = getRedirect();
}, 1500);
} else {
var msg = (resp && resp.message) ? resp.message : '注册失败,请稍后重试';
window.MetaLab.toast(toast, msg, true);
}
}
};
xhr.send(JSON.stringify({
email: email,
password: password,
confirm_password: confirmPassword,
remember_me: rememberMeCheckbox.checked
}));
}
})();