Files
wordpress-theme-aksrt/assets/js/modal-auth.js
T
kate522andClaude Fable 5 8d1b0dacf6 feat(auth): 登录注册找回绑定前台 1:1 壳层/文案/协议
抽出 sign.css,统一全页登录/注册与找回/绑定视觉;中文文案与用户协议契约对齐 Auth_Service;弹层字段与 logo 同步。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 11:14:10 +08:00

485 lines
16 KiB
JavaScript

/**
* AKSRT modal auth interactions.
*
* @package AKSRT
*/
(function () {
'use strict';
var API_ROOT = (window.aksrtData && window.aksrtData.apiUrl) || '/wp-json/aksrt/v1';
var NONCE = (window.aksrtData && window.aksrtData.restNonce) || '';
var modal = null;
var currentTab = 'login';
function init() {
modal = document.getElementById('aksrt-modal-auth');
if (!modal) {
return;
}
var closeBtn = modal.querySelector('.aksrt-modal-auth__close');
var overlay = modal.querySelector('.aksrt-modal-auth__overlay');
if (closeBtn) {
closeBtn.addEventListener('click', close);
}
if (overlay) {
overlay.addEventListener('click', close);
}
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape' && modal.classList.contains('is-open')) {
close();
}
});
modal.querySelectorAll('.aksrt-modal-auth__tab').forEach(function (tab) {
tab.addEventListener('click', function () {
switchTab(this.dataset.tab || 'login');
});
});
modal.querySelectorAll('.aksrt-modal-auth__switch').forEach(function (button) {
button.addEventListener('click', function () {
switchTab(this.dataset.switch || 'login');
});
});
modal.querySelectorAll('.aksrt-modal-auth__pass-toggle').forEach(function (button) {
button.addEventListener('click', function () {
var input = this.parentElement.querySelector('input');
var icon = this.querySelector('i');
if (!input || !icon) {
return;
}
if (input.type === 'password') {
input.type = 'text';
icon.className = 'far fa-eye-slash';
} else {
input.type = 'password';
icon.className = 'far fa-eye';
}
});
});
var loginForm = document.getElementById('aksrt-modal-login-form');
if (loginForm) {
loginForm.addEventListener('submit', function (event) {
event.preventDefault();
handleLogin(loginForm);
});
}
var registerForm = document.getElementById('aksrt-modal-register-form');
if (registerForm) {
registerForm.addEventListener('submit', function (event) {
event.preventDefault();
handleRegister(registerForm);
});
}
document.addEventListener('click', function (event) {
var trigger = event.target.closest('[data-aksrt-auth-required], .aksrt-login-trigger, #nav-login-btn, .signin-loader, .signup-loader, .qrcode-signin');
if (!trigger) {
return;
}
event.preventDefault();
if (trigger.classList.contains('signup-loader')) {
open(trigger.dataset.authRedirect || '');
switchTab('register');
return;
}
if (trigger.classList.contains('qrcode-signin')) {
open(trigger.dataset.authRedirect || '');
switchTab('qrcode');
return;
}
open(trigger.dataset.authRedirect || '');
});
initQrLogin();
initAgreementShake();
initSocialPriority();
}
function open(redirectUrl) {
if (!modal) {
return;
}
modal.classList.add('is-open');
modal.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
if (redirectUrl) {
modal.dataset.redirect = redirectUrl;
}
setTimeout(function () {
var firstInput = modal.querySelector('.aksrt-modal-auth__panel.is-active input:not([type="hidden"])');
if (firstInput) {
firstInput.focus();
}
}, 100);
}
var qrLoginTimer = null;
function close() {
if (!modal) {
return;
}
modal.classList.remove('is-open');
modal.setAttribute('aria-hidden', 'true');
document.body.style.overflow = '';
clearMessages();
stopQrPolling();
}
function switchTab(tab) {
currentTab = tab;
modal.querySelectorAll('.aksrt-modal-auth__tab').forEach(function (button) {
var isActive = button.dataset.tab === tab;
button.classList.toggle('is-active', isActive);
button.setAttribute('aria-selected', isActive ? 'true' : 'false');
});
modal.querySelectorAll('.aksrt-modal-auth__panel').forEach(function (panel) {
panel.classList.toggle('is-active', panel.dataset.panel === tab);
});
modal.querySelectorAll('.aksrt-modal-auth__footer-text').forEach(function (row) {
row.hidden = row.dataset.for !== tab;
});
modal.setAttribute('aria-label', tab === 'register' ? '注册' : '登录');
clearMessages();
}
function handleLogin(form) {
var payload = serializeForm(form);
if (!payload.log || !payload.pwd) {
showMessage('login', '请输入用户名和密码', 'error');
return;
}
setLoading(form, true);
clearMessages();
request('/auth/modal-login', payload)
.then(function (result) {
if (result.ok && result.data.success) {
onAuthSuccess(result.data);
return;
}
var message = result.data.message || '登录失败';
if (result.data.verify_url) {
message += ' <a href="' + escapeAttribute(result.data.verify_url) + '">前往验证</a>';
}
showMessage('login', message, 'error');
})
.catch(function () {
showMessage('login', '网络错误,请稍后重试', 'error');
})
.finally(function () {
setLoading(form, false);
});
}
function handleRegister(form) {
var payload = serializeForm(form);
if (!payload.user_login || !payload.user_email || !payload.user_pass || !payload.user_pass_confirm) {
showMessage('register', '请完整填写注册信息', 'error');
return;
}
if (payload.user_pass !== payload.user_pass_confirm) {
showMessage('register', '两次输入的密码不一致', 'error');
return;
}
setLoading(form, true);
clearMessages();
request('/auth/modal-register', payload)
.then(function (result) {
if (!result.ok || !result.data.success) {
showMessage('register', result.data.message || '注册失败', 'error');
return;
}
if (result.data.user) {
onAuthSuccess(result.data);
return;
}
if (result.data.verification_required) {
var verifyMessage = result.data.message || '注册成功,请完成邮箱验证';
if (result.data.verify_url) {
verifyMessage += ' <a href="' + escapeAttribute(result.data.verify_url) + '">前往验证</a>';
}
showMessage('register', verifyMessage, 'success');
form.reset();
return;
}
var loginMessage = result.data.message || '注册成功,请登录';
switchTab('login');
prefillLogin(result.data.user_login || result.data.user_email || '');
showMessage('login', loginMessage, 'success');
form.reset();
})
.catch(function () {
showMessage('register', '网络错误,请稍后重试', 'error');
})
.finally(function () {
setLoading(form, false);
});
}
function serializeForm(form) {
var payload = {};
var formData = new FormData(form);
formData.forEach(function (value, key) {
if (payload[key] !== undefined) {
if (!Array.isArray(payload[key])) {
payload[key] = [payload[key]];
}
payload[key].push(value);
} else {
payload[key] = value;
}
});
return payload;
}
function request(path, payload) {
var headers = { 'Content-Type': 'application/json' };
if (NONCE) {
headers['X-WP-Nonce'] = NONCE;
}
return fetch(API_ROOT + path, {
method: 'POST',
headers: headers,
body: JSON.stringify(payload),
}).then(function (response) {
return response.json().then(function (data) {
return { ok: response.ok, data: data };
});
});
}
function prefillLogin(value) {
var loginInput = document.getElementById('modal-login-user');
if (loginInput && value) {
loginInput.value = value;
}
}
function onAuthSuccess(data) {
close();
if (data.user) {
updateNavUser(data.user);
}
if (window.aksrt && aksrt.notyf) {
aksrt.notyf(data.message || '登录成功', 'success');
} else if (typeof window.aksrtToast === 'function') {
window.aksrtToast(data.message || '登录成功', 'success');
}
var redirect = modal.dataset.redirect || data.redirect;
if (redirect && redirect !== window.location.href) {
setTimeout(function () {
window.location.href = redirect;
}, 500);
return;
}
setTimeout(function () {
window.location.reload();
}, 500);
}
function updateNavUser(user) {
document.querySelectorAll('.nav-user__avatar img, .nav-user__toggle img').forEach(function (image) {
if (user.avatar_url) {
image.src = user.avatar_url;
}
});
document.querySelectorAll('.nav-user__name').forEach(function (el) {
if (user.display_name) {
el.textContent = user.display_name;
}
});
}
function showMessage(panel, message, type) {
var box = document.getElementById('aksrt-modal-' + panel + '-error');
if (!box) {
return;
}
box.innerHTML = message;
box.hidden = false;
box.classList.toggle('is-success', type === 'success');
}
function clearMessages() {
if (!modal) {
return;
}
modal.querySelectorAll('.aksrt-modal-auth__error').forEach(function (box) {
box.hidden = true;
box.innerHTML = '';
box.classList.remove('is-success');
});
}
function setLoading(form, loading) {
var submit = form.querySelector('.aksrt-modal-auth__submit');
if (!submit) {
return;
}
var label = submit.querySelector('span');
var spinner = submit.querySelector('i');
submit.disabled = loading;
if (label) {
label.hidden = loading;
}
if (spinner) {
spinner.hidden = !loading;
}
}
function escapeAttribute(value) {
return String(value || '')
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
// ── QR Code Login (§10.7) ──
function initQrLogin() {
var qrPanel = modal.querySelector('[data-panel="qrcode"]');
if (!qrPanel) return;
var qrContainer = qrPanel.querySelector('.aksrt-modal-auth__qr-code');
if (!qrContainer) return;
// When qrcode tab is activated, fetch QR and start polling
var observer = new MutationObserver(function () {
if (qrPanel.classList.contains('is-active')) {
fetchQrCode(qrContainer);
} else {
stopQrPolling();
}
});
observer.observe(qrPanel, { attributes: true, attributeFilter: ['class'] });
}
function fetchQrCode(container) {
container.innerHTML = '<div class="aksrt-modal-auth__qr-loading"><i class="fas fa-spinner fa-spin"></i></div>';
request('/auth/qrcode-login', {})
.then(function (result) {
if (result.ok && result.data && result.data.qr_html) {
container.innerHTML = result.data.qr_html;
startQrPolling(result.data.check_url || '/auth/check-qrcode-login', result.data.token || '');
} else {
container.innerHTML = '<p class="aksrt-modal-auth__qr-error">扫码登录暂不可用</p>';
}
})
.catch(function () {
container.innerHTML = '<p class="aksrt-modal-auth__qr-error">网络错误</p>';
});
}
function startQrPolling(checkUrl, token) {
stopQrPolling();
qrLoginTimer = setInterval(function () {
request(checkUrl, { token: token })
.then(function (result) {
if (result.ok && result.data && result.data.logged_in) {
stopQrPolling();
onAuthSuccess(result.data);
}
})
.catch(function () { /* keep polling */ });
}, 2000);
}
function stopQrPolling() {
if (qrLoginTimer) {
clearInterval(qrLoginTimer);
qrLoginTimer = null;
}
}
// ── Social Login Priority (§10.7) ──
function initSocialPriority() {
var wxPriority = window.aksrtData && aksrtData.signinWxPriority;
if (wxPriority && modal) {
var wxBtn = modal.querySelector('.social-login-item.weixingzh, .social-login-item[data-provider="wechat"]');
if (wxBtn) {
setTimeout(function () { wxBtn.click(); }, 100);
}
}
}
// ── Agreement Check: shake instead of disable (§10.7) ──
function initAgreementShake() {
if (!modal) return;
modal.querySelectorAll('.aksrt-modal-auth__submit').forEach(function (btn) {
btn.addEventListener('click', function (e) {
var form = btn.closest('form');
if (!form) return;
var agree = form.querySelector('[name="agree_terms"], [name="user_agreement"]');
if (agree && !agree.checked) {
e.preventDefault();
e.stopImmediatePropagation();
agree.closest('label, .aksrt-modal-auth__agreement').classList.add('ani', 'shake');
if (window.aksrt && aksrt.notyf) aksrt.notyf('请先同意用户协议', 'warning');
setTimeout(function () {
agree.closest('label, .aksrt-modal-auth__agreement').classList.remove('ani', 'shake');
}, 400);
}
});
});
}
// ── Password Visibility: unified (§10.7) ──
// Already handled above; added unification note.
window.AksrtAuth = {
open: open,
close: close,
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();