Files
wordpress-theme-aksrt/assets/js/modal-auth.js
T

611 lines
21 KiB
JavaScript

/**
* AKSRT modal auth interactions.
*
* @package AKSRT
*/
(function () {
'use strict';
/**
* Resolve REST root for aksrt/v1.
* Prefer aksrtData.restUrl (works with plain permalinks: index.php?rest_route=/aksrt/v1).
* Do not hardcode /wp-json when rewrite rules are off — that 404s on Apache.
*/
function resolveApiRoot() {
var d = window.aksrtData || {};
var raw = d.apiUrl || d.restUrl || d.rest_url || '';
if (!raw) {
raw = '/index.php?rest_route=/aksrt/v1';
}
raw = String(raw).trim();
// restUrl may be site root rest_url() without namespace in some localizes — ensure aksrt/v1.
if (/rest_route=\/?$/.test(raw) || /\/wp-json\/?$/.test(raw)) {
if (raw.indexOf('rest_route=') !== -1) {
raw = raw.replace(/rest_route=\/?$/, 'rest_route=/aksrt/v1');
} else {
raw = raw.replace(/\/?$/, '/') + 'aksrt/v1';
}
}
return raw.replace(/\/$/, '');
}
var API_ROOT = resolveApiRoot();
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 = 'fa fas fa-eye-slash';
} else {
input.type = 'password';
icon.className = 'fa fas 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 withCaptcha(scene, form, next) {
var store = window.AKSRTCaptchaState && window.AKSRTCaptchaState.scenes
? window.AKSRTCaptchaState.scenes
: null;
var instance = store && store[scene] ? store[scene] : null;
if (instance && typeof instance.verify === 'function') {
instance.verify(function () {
next(serializeForm(form));
});
return;
}
next(serializeForm(form));
}
function resetCaptcha(scene) {
try {
var store = window.AKSRTCaptchaState && window.AKSRTCaptchaState.scenes
? window.AKSRTCaptchaState.scenes
: null;
if (store && store[scene] && typeof store[scene].reset === 'function') {
store[scene].reset();
}
} catch (e) { /* ignore */ }
}
function handleLogin(form) {
if (!form.querySelector('[name="log"]') || !form.querySelector('[name="pwd"]')) {
showMessage('login', '请输入用户名和密码', 'error');
return;
}
setLoading(form, true);
clearMessages();
withCaptcha('login', form, function (payload) {
if (!payload.log || !payload.pwd) {
setLoading(form, false);
showMessage('login', '请输入用户名和密码', 'error');
return;
}
request('/auth/modal-login', payload)
.then(function (result) {
if (result.ok && result.data.success) {
onAuthSuccess(result.data);
return;
}
resetCaptcha('login');
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 () {
resetCaptcha('login');
showMessage('login', '网络错误,请稍后重试', 'error');
})
.finally(function () {
setLoading(form, false);
});
});
}
function handleRegister(form) {
setLoading(form, true);
clearMessages();
withCaptcha('register', form, function (payload) {
if (!payload.user_login || !payload.user_email || !payload.user_pass || !payload.user_pass_confirm) {
setLoading(form, false);
showMessage('register', '请完整填写注册信息', 'error');
return;
}
if (payload.user_pass !== payload.user_pass_confirm) {
setLoading(form, false);
showMessage('register', '两次输入的密码不一致', 'error');
return;
}
request('/auth/modal-register', payload)
.then(function (result) {
if (!result.ok || !result.data.success) {
resetCaptcha('register');
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.notice) verifyMessage += '<br>' + escapeAttribute(result.data.notice);
if (result.data.verify_url) {
verifyMessage += ' <a href="' + escapeAttribute(result.data.verify_url) + '">前往验证</a>';
}
showMessage('register', verifyMessage, 'success');
form.reset();
resetCaptcha('register');
return;
}
var loginMessage = result.data.message || '注册成功,请登录';
if (result.data.notice) loginMessage += ' ' + result.data.notice;
switchTab('login');
prefillLogin(result.data.user_login || result.data.user_email || '');
showMessage('login', loginMessage, 'success');
form.reset();
resetCaptcha('register');
})
.catch(function () {
resetCaptcha('register');
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;
}
var url = joinRestPath(API_ROOT, path);
return fetch(url, {
method: 'POST',
headers: headers,
credentials: 'same-origin',
body: JSON.stringify(payload),
}).then(function (response) {
return response.json().then(function (data) {
return { ok: response.ok, data: data };
}).catch(function () {
return { ok: false, data: { message: '响应解析失败' } };
});
});
}
/**
* Join REST root + path for both pretty (/wp-json/aksrt/v1/x)
* and plain (?rest_route=/aksrt/v1/x) permalinks.
*/
function joinRestPath(root, path) {
path = String(path || '');
if (path.charAt(0) !== '/') {
path = '/' + path;
}
root = String(root || '').replace(/\/$/, '');
if (root.indexOf('rest_route=') !== -1) {
// e.g. https://host/index.php?rest_route=/aksrt/v1 + /auth/modal-login
return root + path;
}
return root + path;
}
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);
}
notyf(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 bindWeixingzhCodeForm(container) {
if (!container) return;
var form = container.querySelector('[data-aksrt-weixingzh-code]');
if (!form || form._bound) return;
form._bound = true;
form.addEventListener('submit', function (e) {
e.preventDefault();
var input = form.querySelector('[name="code"]');
var code = input ? String(input.value || '').trim() : '';
if (!code) return;
var btn = form.querySelector('button[type="submit"]');
if (btn) btn.disabled = true;
request('/auth/weixingzh-code', { code: code })
.then(function (result) {
var data = (result && result.data) || result || {};
if ((result && result.ok) || (result && result.success) || data.logged_in) {
onAuthSuccess(data.data || data);
return;
}
throw new Error((result && (result.message || result.msg)) || '验证码错误');
})
.catch(function (err) {
alert(err.message || '登录失败');
})
.finally(function () {
if (btn) btn.disabled = false;
});
});
}
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="fa fas fa-spinner fa-spin"></i></div>';
request('/auth/qrcode-login', {})
.then(function (result) {
var payload = (result && result.data) || result || {};
var qrHtml = payload.qr_html || (payload.data && payload.data.qr_html) || '';
var checkUrl = payload.check_url || (payload.data && payload.data.check_url) || '/auth/check-qrcode-login';
var token = payload.token || (payload.data && payload.data.token) || '';
if (qrHtml) {
container.innerHTML = qrHtml;
bindWeixingzhCodeForm(container);
if (checkUrl && token) {
startQrPolling(checkUrl, 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) {
var data = (result && result.data) || result || {};
var body = data.data || data;
if ((result && result.ok) || data.success || body.logged_in) {
if (body.logged_in || (data.data && data.data.logged_in)) {
stopQrPolling();
onAuthSuccess(body.data || body);
}
}
})
.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');
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();
}
})();