抽出 sign.css,统一全页登录/注册与找回/绑定视觉;中文文案与用户协议契约对齐 Auth_Service;弹层字段与 logo 同步。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
485 lines
16 KiB
JavaScript
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, '&')
|
|
.replace(/"/g, '"')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
}
|
|
|
|
// ── 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();
|
|
}
|
|
})();
|