- rehydrate.js 新增 boot()/aksrt:ready 事件,支持 boot 后晚注册立即以 init 触发 - aksrt-runtime.js 初始加载改走 AksrtFrontend.boot,tooltip/倒计时注册为组件 - 约 30 个前端脚本(首页、评论、支付、路由、页面叶子、widgets) 从直接 DOMContentLoaded 迁移为 AksrtFrontend.register,并加幂等标记 - section-navs 依赖改为 aksrt-runtime,保证注册时序 - 清理死代码:删除孤儿 loader.js/main.js 及 runtime/page-navs 遗留副本 - loader.js 移除已迁移模块的 tbquire 路径映射,main.js 相应瘦身 - build-public-assets.mjs 增加 runtime/main.min.js 自动压缩步骤 - 新增 4 个契约测试(homepage-dom/rehydrate-runtime/global-dom-init/ public-bundle-parity),3 个既有 harness 适配注册表初始化方式
724 lines
28 KiB
JavaScript
724 lines
28 KiB
JavaScript
/** Shared authentication state machine for modal and /sign/. */
|
|
(function () {
|
|
'use strict';
|
|
|
|
var config = window.aksrtData || {};
|
|
var apiRoot = String(config.apiUrl || config.restUrl || '/wp-json/aksrt/v1').replace(/\/$/, '');
|
|
var nonce = config.restNonce || '';
|
|
var modal = null;
|
|
var lastModalTrigger = null;
|
|
var qrStates = [];
|
|
var globalEventsBound = false;
|
|
var oauthResultChecked = false;
|
|
var oauthErrorMessages = {
|
|
access_denied: '已取消第三方授权',
|
|
cancelled: '已取消第三方授权',
|
|
canceled: '已取消第三方授权',
|
|
invalid_state: '授权状态已失效,请重新尝试',
|
|
state_invalid: '授权状态已失效,请重新尝试',
|
|
state_mismatch: '授权状态已失效,请重新尝试',
|
|
state_user_mismatch: '当前账号与授权发起账号不一致,请重新尝试',
|
|
provider_disabled: '该登录方式暂未启用',
|
|
provider_unavailable: '该登录方式暂时不可用',
|
|
provider_cancelled: '已取消第三方授权',
|
|
provider_error: '第三方授权失败,请稍后重试',
|
|
callback_failed: '第三方授权失败,请稍后重试',
|
|
code_missing: '授权回调缺少必要信息,请重新尝试',
|
|
proxy_config: '代理登录配置不可用,请联系管理员',
|
|
account_match_failed: '第三方账号匹配失败,请重试',
|
|
login_failed: '第三方登录失败,请重试',
|
|
bind_failed: '第三方账号绑定失败,请重试',
|
|
bind_verification_required: '请先完成人机验证后再绑定',
|
|
binding_conflict: '该第三方账号已绑定其他账号',
|
|
account_conflict: '该第三方账号无法绑定到当前账号',
|
|
already_bound: '该第三方账号已绑定其他账号',
|
|
timeout: '第三方授权已超时,请重试',
|
|
};
|
|
var oauthProviderLabels = {
|
|
github: 'GitHub',
|
|
gitee: 'Gitee',
|
|
qq: 'QQ',
|
|
google: 'Google',
|
|
weibo: '微博',
|
|
weixin: '微信',
|
|
weixingzh: '微信公众号',
|
|
};
|
|
|
|
function request(path, data, options) {
|
|
options = options || {};
|
|
return fetch(apiRoot + '/auth/' + path, {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': nonce },
|
|
body: JSON.stringify(data),
|
|
signal: options.signal,
|
|
}).then(function (response) {
|
|
return response.json().catch(function () { return {}; }).then(function (body) {
|
|
if (!response.ok || body.success === false) {
|
|
throw new Error(body.message || (body.data && body.data.message) || '请求失败,请稍后再试');
|
|
}
|
|
return body.data || body;
|
|
});
|
|
});
|
|
}
|
|
|
|
function formData(form) {
|
|
var data = {};
|
|
new FormData(form).forEach(function (value, key) {
|
|
data[key] = value;
|
|
});
|
|
return data;
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value || '').replace(/[&<>"']/g, function (char) {
|
|
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char];
|
|
});
|
|
}
|
|
|
|
function notify(text, type) {
|
|
if (!text || typeof window.notyf !== 'function') return;
|
|
window.notyf(escapeHtml(text), type || 'info');
|
|
}
|
|
|
|
function cleanOAuthResult(url) {
|
|
['oauth_action', 'oauth_provider', 'oauth_error', 'oauth_bound'].forEach(function (key) {
|
|
url.searchParams.delete(key);
|
|
});
|
|
if (window.history && window.history.replaceState) {
|
|
window.history.replaceState({}, '', url.pathname + url.search + url.hash);
|
|
}
|
|
}
|
|
|
|
function showOAuthResultToast() {
|
|
if (!window.URL || document.documentElement.dataset.aksrtOauthResultHandled === 'true') return;
|
|
var url = new URL(window.location.href);
|
|
var action = url.searchParams.get('oauth_action');
|
|
if (['login', 'bind', 'error'].indexOf(action) === -1) return;
|
|
var userCenterOwnsBind = action === 'bind' && /\/user(?:\/settings)?\/?$/.test(url.pathname) && document.querySelector('[data-identity-root], .settings-section');
|
|
if (userCenterOwnsBind) return;
|
|
document.documentElement.dataset.aksrtOauthResultHandled = 'true';
|
|
|
|
var provider = url.searchParams.get('oauth_provider') || '';
|
|
var providerLabel = oauthProviderLabels[provider] || '';
|
|
var message = '';
|
|
var type = 'success';
|
|
if (action === 'login') message = providerLabel ? providerLabel + '登录成功' : '登录成功';
|
|
if (action === 'bind') message = providerLabel ? providerLabel + '绑定成功' : '第三方账号绑定成功';
|
|
if (action === 'error') {
|
|
type = 'danger';
|
|
message = oauthErrorMessages[url.searchParams.get('oauth_error') || ''] || '第三方授权失败,请重试';
|
|
}
|
|
|
|
notify(message, type);
|
|
cleanOAuthResult(url);
|
|
}
|
|
|
|
function qrState(root) {
|
|
if (root._aksrtQrState) return root._aksrtQrState;
|
|
root._aksrtQrState = {
|
|
root: root,
|
|
timer: null,
|
|
timeout: null,
|
|
controller: null,
|
|
inFlight: false,
|
|
sequence: 0,
|
|
token: '',
|
|
startedAt: 0,
|
|
backoff: 0,
|
|
terminal: false,
|
|
};
|
|
qrStates.push(root._aksrtQrState);
|
|
return root._aksrtQrState;
|
|
}
|
|
|
|
function qrNodes(root) {
|
|
var box = root.querySelector('[data-auth-qrcode]');
|
|
return {
|
|
box: box,
|
|
content: box && box.querySelector('[data-auth-qr-content]'),
|
|
status: box && box.querySelector('[data-auth-qr-status]'),
|
|
};
|
|
}
|
|
|
|
function setQrStatus(root, message) {
|
|
var nodes = qrNodes(root);
|
|
if (nodes.status) nodes.status.textContent = message || '';
|
|
}
|
|
|
|
function setQrRefresh(root, message) {
|
|
var nodes = qrNodes(root);
|
|
if (!nodes.box) return;
|
|
nodes.box.dataset.loading = '0';
|
|
nodes.box.dataset.loaded = '0';
|
|
if (nodes.content) nodes.content.innerHTML = '<button type="button" data-auth-refresh-qr>' + escapeHtml(message) + '</button>';
|
|
setQrStatus(root, message);
|
|
}
|
|
|
|
function isRootActive(root) {
|
|
if (!root || root.dataset.activeTab !== 'oauth' || document.visibilityState !== 'visible') return false;
|
|
if (root.dataset.surface !== 'modal') return document.documentElement.contains(root);
|
|
return !!modal && modal.classList.contains('is-open') && modal.contains(root);
|
|
}
|
|
|
|
function stopQr(root) {
|
|
var state = qrState(root);
|
|
state.sequence += 1;
|
|
if (state.timer) window.clearTimeout(state.timer);
|
|
if (state.timeout) window.clearTimeout(state.timeout);
|
|
state.timer = null;
|
|
state.timeout = null;
|
|
if (state.controller) {
|
|
try { state.controller.abort(); } catch (_) {}
|
|
}
|
|
state.controller = null;
|
|
state.inFlight = false;
|
|
}
|
|
|
|
function finishQr(root, status, message) {
|
|
var state = qrState(root);
|
|
stopQr(root);
|
|
state.terminal = true;
|
|
state.token = '';
|
|
setQrRefresh(root, message || (status === 'expired' ? '二维码已过期,点击刷新' : '二维码登录已结束,点击刷新'));
|
|
}
|
|
|
|
function qrRequest(root, path, payload) {
|
|
var state = qrState(root);
|
|
if (state.inFlight) return Promise.resolve(null);
|
|
state.inFlight = true;
|
|
var controller = window.AbortController ? new window.AbortController() : null;
|
|
var timedOut = false;
|
|
state.controller = controller;
|
|
state.timeout = window.setTimeout(function () {
|
|
timedOut = true;
|
|
if (controller) controller.abort();
|
|
}, 15000);
|
|
|
|
return request(path, payload, { signal: controller ? controller.signal : undefined }).catch(function (error) {
|
|
if (timedOut) {
|
|
var timeoutError = new Error('二维码请求超时');
|
|
timeoutError.code = 'QR_TIMEOUT';
|
|
throw timeoutError;
|
|
}
|
|
throw error;
|
|
}).finally(function () {
|
|
if (state.controller !== controller) return;
|
|
if (state.timeout) window.clearTimeout(state.timeout);
|
|
state.timeout = null;
|
|
state.controller = null;
|
|
state.inFlight = false;
|
|
});
|
|
}
|
|
|
|
function scheduleQrPoll(root, delay) {
|
|
var state = qrState(root);
|
|
if (!state.token || state.terminal || !isRootActive(root)) return;
|
|
if (state.startedAt && Date.now() - state.startedAt >= 300000) {
|
|
finishQr(root, 'timeout', '二维码登录已超时,点击刷新');
|
|
return;
|
|
}
|
|
if (state.timer) window.clearTimeout(state.timer);
|
|
var wait = delay === 0
|
|
? 0
|
|
: Math.min(30000, Math.max(3000, Number(delay) || 3000) * Math.pow(2, Math.min(3, state.backoff)));
|
|
state.timer = window.setTimeout(function () {
|
|
state.timer = null;
|
|
pollQr(root);
|
|
}, wait);
|
|
}
|
|
|
|
function pollQr(root) {
|
|
var state = qrState(root);
|
|
if (!state.token || state.terminal || state.inFlight || !isRootActive(root)) return;
|
|
qrRequest(root, 'check-qrcode-login', {
|
|
token: state.token,
|
|
action: root.dataset.oauthIntent || 'login',
|
|
redirect_to: root.dataset.redirectTo || window.location.href,
|
|
}).then(function (result) {
|
|
if (!result || !isRootActive(root)) return;
|
|
if (result.needs_binding && result.redirect) {
|
|
stopQr(root);
|
|
setQrStatus(root, '需要绑定本地账号,正在跳转');
|
|
notify('请绑定已有账号或注册新账号', 'info');
|
|
window.location.assign(result.redirect);
|
|
return;
|
|
}
|
|
if (result.bound && result.redirect) {
|
|
stopQr(root);
|
|
setQrStatus(root, '绑定成功,正在跳转');
|
|
notify('微信公众号绑定成功', 'success');
|
|
window.location.assign(result.redirect);
|
|
return;
|
|
}
|
|
if (result.logged_in || result.status === 'logged_in') {
|
|
stopQr(root);
|
|
setQrStatus(root, '登录成功,正在跳转');
|
|
notify('登录成功', 'success');
|
|
window.location.assign(result.redirect || window.location.href);
|
|
return;
|
|
}
|
|
if (result.status === 'expired') {
|
|
finishQr(root, 'expired', '二维码已过期,点击刷新');
|
|
notify('二维码已过期,请刷新后重试', 'warning');
|
|
return;
|
|
}
|
|
if (['denied', 'cancelled', 'canceled', 'failed', 'error'].indexOf(result.status) !== -1) {
|
|
finishQr(root, result.status, '二维码登录未完成,点击刷新');
|
|
notify('二维码登录未完成,请刷新后重试', 'danger');
|
|
return;
|
|
}
|
|
state.backoff = 0;
|
|
setQrStatus(root, result.message || (result.status === 'scanned' ? '已扫码,请在微信中确认' : '等待扫码确认'));
|
|
scheduleQrPoll(root, result.poll_interval ? Number(result.poll_interval) * 1000 : 3000);
|
|
}).catch(function (error) {
|
|
if (!isRootActive(root) || (error && error.name === 'AbortError')) return;
|
|
state.backoff = Math.min(3, state.backoff + 1);
|
|
setQrStatus(root, error && error.code === 'QR_TIMEOUT' ? '状态检查超时,正在重试' : '状态检查失败,正在重试');
|
|
scheduleQrPoll(root, 3000);
|
|
});
|
|
}
|
|
|
|
function loadQr(root, force) {
|
|
var nodes = qrNodes(root);
|
|
if (!nodes.box) return;
|
|
var state = qrState(root);
|
|
if (force) {
|
|
stopQr(root);
|
|
state.token = '';
|
|
state.startedAt = 0;
|
|
state.backoff = 0;
|
|
state.terminal = false;
|
|
nodes.box.dataset.loaded = '0';
|
|
}
|
|
if (state.inFlight || nodes.box.dataset.loading === '1') return;
|
|
if (state.token && !state.terminal) {
|
|
scheduleQrPoll(root, 0);
|
|
return;
|
|
}
|
|
if (nodes.box.dataset.loaded === '1' && state.terminal) return;
|
|
|
|
nodes.box.dataset.loading = '1';
|
|
setQrStatus(root, '正在获取二维码');
|
|
var qrPayload = {
|
|
action: root.dataset.oauthIntent || 'login',
|
|
redirect_to: root.dataset.redirectTo || window.location.href,
|
|
};
|
|
if (root.dataset.bindPass) qrPayload.bind_pass = root.dataset.bindPass;
|
|
qrRequest(root, 'qrcode-login', qrPayload).then(function (result) {
|
|
if (!result || !isRootActive(root)) return;
|
|
nodes.box.dataset.loading = '0';
|
|
nodes.box.dataset.loaded = '1';
|
|
if (nodes.content) nodes.content.innerHTML = result.qr_html || '';
|
|
state.token = result.token || '';
|
|
state.startedAt = Date.now();
|
|
state.backoff = 0;
|
|
state.terminal = result.mode !== 'scan' || !state.token;
|
|
setQrStatus(root, state.terminal ? '请按页面提示完成登录' : '请使用微信扫描二维码');
|
|
if (!state.terminal) scheduleQrPoll(root, result.poll_interval ? Number(result.poll_interval) * 1000 : 3000);
|
|
}).catch(function (error) {
|
|
if (!isRootActive(root) || (error && error.name === 'AbortError')) return;
|
|
state.terminal = true;
|
|
setQrRefresh(root, '二维码加载失败,点击重试');
|
|
notify(error.message, 'danger');
|
|
});
|
|
}
|
|
|
|
function activate(root, tab, updateUrl) {
|
|
var panel = root.querySelector('[data-auth-panel="' + tab + '"]');
|
|
if (!panel) return;
|
|
if (root.dataset.activeTab === 'oauth' && tab !== 'oauth') stopQr(root);
|
|
root.querySelectorAll('[data-auth-panel]').forEach(function (item) {
|
|
var active = item === panel;
|
|
item.hidden = !active;
|
|
item.setAttribute('aria-hidden', active ? 'false' : 'true');
|
|
});
|
|
root.querySelectorAll('[data-auth-tab]').forEach(function (item) {
|
|
item.setAttribute('aria-selected', item.dataset.authTab === tab ? 'true' : 'false');
|
|
});
|
|
root.dataset.activeTab = tab;
|
|
if (updateUrl && root.dataset.surface === 'page' && window.history && window.URL) {
|
|
var url = new URL(window.location.href);
|
|
url.searchParams.set('tab', tab);
|
|
window.history.replaceState({}, '', url.toString());
|
|
}
|
|
var focus = panel.querySelector('input:not([type="hidden"]), button, a[href], [tabindex]:not([tabindex="-1"])');
|
|
if (focus) window.setTimeout(function () { focus.focus(); }, 30);
|
|
if (tab === 'oauth') loadQr(root, false);
|
|
}
|
|
|
|
function submitQrCode(form) {
|
|
var data = formData(form);
|
|
var root = form.closest('[data-aksrt-auth]');
|
|
data.action = root ? (root.dataset.oauthIntent || 'login') : 'login';
|
|
data.redirect_to = root ? (root.dataset.redirectTo || window.location.href) : window.location.href;
|
|
if (root && root.dataset.bindPass) data.bind_pass = root.dataset.bindPass;
|
|
var button = form.querySelector('button[type="submit"]');
|
|
if (button) button.disabled = true;
|
|
request('weixingzh-code', data).then(function (result) {
|
|
if (result.needs_binding) {
|
|
notify('请绑定已有账号或注册新账号', 'info');
|
|
} else if (result.bound) {
|
|
notify('微信公众号绑定成功', 'success');
|
|
} else {
|
|
notify('登录成功', 'success');
|
|
}
|
|
window.location.assign(result.redirect || window.location.href);
|
|
}).catch(function (error) {
|
|
if (button) button.disabled = false;
|
|
notify(error.message, 'danger');
|
|
});
|
|
}
|
|
|
|
function setBusy(form, busy) {
|
|
var button = form.querySelector('.sign-submit');
|
|
if (!button) return;
|
|
button.disabled = busy;
|
|
var spinner = button.querySelector('.fa-spinner');
|
|
if (spinner) spinner.hidden = !busy;
|
|
}
|
|
|
|
function startCountdown(button, seconds) {
|
|
var original = button.dataset.label || button.textContent;
|
|
button.dataset.label = original;
|
|
button.disabled = true;
|
|
var left = Math.max(1, Number(seconds) || 60);
|
|
button.textContent = left + 's';
|
|
var timer = window.setInterval(function () {
|
|
left -= 1;
|
|
if (left <= 0) {
|
|
window.clearInterval(timer);
|
|
button.disabled = false;
|
|
button.textContent = original;
|
|
} else {
|
|
button.textContent = left + 's';
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
function captchaScene(scene) {
|
|
return scene === 'resetpassword' || scene === 'reset-password' ? 'lostpassword' : scene;
|
|
}
|
|
|
|
function captchaFormScene(form, fallback) {
|
|
var mount = form && form.querySelector ? form.querySelector('[data-aksrt-captcha-config][data-scene]') : null;
|
|
return mount ? (mount.getAttribute('data-scene') || captchaScene(fallback)) : captchaScene(fallback);
|
|
}
|
|
|
|
function captchaScope(form) {
|
|
return form || document;
|
|
}
|
|
|
|
function resetCaptcha(scene, scope) {
|
|
if (window.AKSRTCaptcha) window.AKSRTCaptcha.reset(captchaScene(scene), captchaScope(scope));
|
|
}
|
|
|
|
function sendCode(button) {
|
|
var form = button.closest('form');
|
|
var target = form && form.querySelector('[name="target"]');
|
|
if (!target || !target.value.trim()) {
|
|
notify('请先填写邮箱或手机号', 'warning');
|
|
return;
|
|
}
|
|
button.disabled = true;
|
|
var payload = formData(form);
|
|
payload.scene = button.dataset.sendCode;
|
|
payload.target = target.value.trim();
|
|
payload.channel = (form.querySelector('[name="channel"]') || {}).value || '';
|
|
var scene = captchaFormScene(form, payload.scene);
|
|
var captcha = window.AKSRTCaptcha;
|
|
var ticket = captcha ? captcha.run(captchaScene(scene), captchaScope(form)) : Promise.resolve({});
|
|
ticket.then(function (captchaPayload) {
|
|
if (captchaPayload === null) {
|
|
button.disabled = false;
|
|
notify('请先完成人机验证', 'warning');
|
|
return null;
|
|
}
|
|
payload = formData(form);
|
|
payload.scene = button.dataset.sendCode;
|
|
payload.target = target.value.trim();
|
|
payload.channel = (form.querySelector('[name="channel"]') || {}).value || '';
|
|
Object.assign(payload, captchaPayload || {});
|
|
return request('code', payload).then(function (result) {
|
|
notify(result.message || '验证码已发送', 'success');
|
|
startCountdown(button, result.countdown || 60);
|
|
}).catch(function (error) {
|
|
button.disabled = false;
|
|
notify(error.message, 'danger');
|
|
}).finally(function () {
|
|
resetCaptcha(scene, form);
|
|
});
|
|
}).catch(function (error) {
|
|
button.disabled = false;
|
|
resetCaptcha(scene, form);
|
|
notify(error.message || '人机验证失败,请重试', 'danger');
|
|
});
|
|
}
|
|
|
|
function submit(form) {
|
|
var action = form.dataset.authForm;
|
|
var scene = captchaFormScene(form, action);
|
|
var captcha = window.AKSRTCaptcha;
|
|
setBusy(form, true);
|
|
(captcha ? captcha.run(scene, captchaScope(form)) : Promise.resolve({})).then(function (captchaPayload) {
|
|
if (captchaPayload === null) {
|
|
setBusy(form, false);
|
|
notify('请先完成人机验证', 'warning');
|
|
return null;
|
|
}
|
|
var payload = formData(form);
|
|
if (action === 'register' && payload.channel !== 'phone') payload.user_email = payload.target;
|
|
Object.assign(payload, captchaPayload || {});
|
|
return request(action, payload).then(function (result) {
|
|
notify(result.message || '操作成功', 'success');
|
|
if (result.redirect) window.setTimeout(function () { window.location.assign(result.redirect); }, 350);
|
|
}).catch(function (error) {
|
|
notify(error.message, 'danger');
|
|
setBusy(form, false);
|
|
}).finally(function () {
|
|
resetCaptcha(scene, form);
|
|
});
|
|
}).catch(function (error) {
|
|
setBusy(form, false);
|
|
resetCaptcha(scene, form);
|
|
notify(error.message || '人机验证失败,请重试', 'danger');
|
|
});
|
|
}
|
|
|
|
function toggleLoginMode(root, button) {
|
|
var password = root.querySelector('[data-auth-password-fields]');
|
|
var code = root.querySelector('[data-auth-code-fields]');
|
|
if (!password || !code) return;
|
|
var showCode = code.hidden;
|
|
code.hidden = !showCode;
|
|
password.hidden = showCode;
|
|
button.textContent = showCode ? '使用账号密码登录' : '使用验证码登录';
|
|
}
|
|
|
|
function setOAuthRedirect(root, redirect) {
|
|
if (!redirect || !window.URL) return;
|
|
root.querySelectorAll('a[data-provider][href]').forEach(function (link) {
|
|
var url;
|
|
try { url = new URL(link.href, window.location.href); } catch (_) { return; }
|
|
if (url.origin !== window.location.origin) return;
|
|
if (!link.dataset.oauthStartHref) link.dataset.oauthStartHref = url.toString();
|
|
url = new URL(link.dataset.oauthStartHref, window.location.href);
|
|
url.searchParams.set('redirect_to', redirect);
|
|
link.href = url.toString();
|
|
});
|
|
}
|
|
|
|
function initRoot(root) {
|
|
root = root || document;
|
|
if (root.nodeType === 1 && root.matches('[data-aksrt-auth]')) {
|
|
initAuthRoot(root);
|
|
return;
|
|
}
|
|
if (root.querySelectorAll) root.querySelectorAll('[data-aksrt-auth]').forEach(initAuthRoot);
|
|
}
|
|
|
|
function initAuthRoot(root) {
|
|
if (root.dataset.aksrtAuthInitialized === '1') return;
|
|
root.dataset.aksrtAuthInitialized = '1';
|
|
root.querySelectorAll('input').forEach(function (input) {
|
|
if (!input.hasAttribute('placeholder')) input.setAttribute('placeholder', ' ');
|
|
});
|
|
root.querySelectorAll('form').forEach(function (form) { form.noValidate = true; });
|
|
root.querySelectorAll('[data-auth-tab]').forEach(function (tab) {
|
|
tab.setAttribute('role', 'tab');
|
|
var panel = root.querySelector('[data-auth-panel="' + tab.dataset.authTab + '"]');
|
|
if (panel && panel.id) tab.setAttribute('aria-controls', panel.id);
|
|
});
|
|
root.querySelectorAll('[data-password-show]').forEach(function (button) {
|
|
button.setAttribute('aria-pressed', 'false');
|
|
if (!button.getAttribute('aria-label')) button.setAttribute('aria-label', '显示密码');
|
|
});
|
|
setOAuthRedirect(root, root.dataset.redirectTo || '');
|
|
var initial = root.dataset.initialTab || 'signin';
|
|
if (initial === 'oauth' && window.matchMedia && window.matchMedia('(max-width: 767px)').matches) initial = 'signin';
|
|
var mayActivateQr = initial !== 'oauth' || document.visibilityState === 'visible';
|
|
if (root.dataset.surface === 'modal' && (!modal || !modal.classList.contains('is-open'))) mayActivateQr = false;
|
|
if (!mayActivateQr) initial = 'signin';
|
|
activate(root, initial, false);
|
|
root.addEventListener('click', function (event) {
|
|
var tab = event.target.closest('[data-auth-tab]');
|
|
if (tab) {
|
|
event.preventDefault();
|
|
activate(root, tab.dataset.authTab, true);
|
|
return;
|
|
}
|
|
var password = event.target.closest('[data-password-show]');
|
|
if (password) {
|
|
var input = password.closest('.floating-input').querySelector('input');
|
|
var visible = input.type === 'password';
|
|
input.type = visible ? 'text' : 'password';
|
|
password.setAttribute('aria-label', visible ? '隐藏密码' : '显示密码');
|
|
password.setAttribute('aria-pressed', visible ? 'true' : 'false');
|
|
password.querySelector('i').className = visible ? 'fa fas fa-eye-slash' : 'fa fas fa-eye';
|
|
return;
|
|
}
|
|
var send = event.target.closest('[data-send-code]');
|
|
if (send) {
|
|
event.preventDefault();
|
|
sendCode(send);
|
|
return;
|
|
}
|
|
var mode = event.target.closest('[data-login-mode]');
|
|
if (mode) {
|
|
event.preventDefault();
|
|
toggleLoginMode(root, mode);
|
|
return;
|
|
}
|
|
var refreshQr = event.target.closest('[data-auth-refresh-qr]');
|
|
if (refreshQr) {
|
|
event.preventDefault();
|
|
loadQr(root, true);
|
|
}
|
|
});
|
|
root.addEventListener('submit', function (event) {
|
|
var qrForm = event.target.closest('[data-aksrt-weixingzh-code]');
|
|
if (qrForm) {
|
|
event.preventDefault();
|
|
if (validateForm(qrForm)) submitQrCode(qrForm);
|
|
return;
|
|
}
|
|
var form = event.target.closest('[data-auth-form]');
|
|
if (!form) return;
|
|
event.preventDefault();
|
|
if (!validateForm(form)) return;
|
|
submit(form);
|
|
});
|
|
}
|
|
|
|
function validateForm(form) {
|
|
if (!form.checkValidity || form.checkValidity()) return true;
|
|
var field = form.querySelector(':invalid');
|
|
notify((field && field.validationMessage) || '请完整填写表单', 'warning');
|
|
if (field && typeof field.focus === 'function') field.focus();
|
|
return false;
|
|
}
|
|
|
|
function authPageUrl(tab, redirect) {
|
|
var routes = config.routes || {};
|
|
var base = tab === 'signup' ? routes.register : routes.login;
|
|
var url = new URL(base || '/sign/', window.location.href);
|
|
url.searchParams.set('tab', tab || 'signin');
|
|
if (redirect) url.searchParams.set('redirect_to', redirect);
|
|
return url.toString();
|
|
}
|
|
|
|
function focusableModalItems() {
|
|
if (!modal) return [];
|
|
return Array.prototype.filter.call(modal.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'), function (item) {
|
|
return item.offsetParent !== null && !item.closest('[hidden]');
|
|
});
|
|
}
|
|
|
|
function openModal(tab, redirect, trigger) {
|
|
if (!modal) {
|
|
window.location.assign(authPageUrl(tab, redirect));
|
|
return;
|
|
}
|
|
lastModalTrigger = trigger || document.activeElement;
|
|
modal.classList.add('is-open');
|
|
modal.setAttribute('aria-hidden', 'false');
|
|
document.body.classList.add('auth-modal-open');
|
|
var root = modal.querySelector('[data-aksrt-auth]');
|
|
if (root) {
|
|
var targetRedirect = redirect || window.location.href;
|
|
root.dataset.redirectTo = targetRedirect;
|
|
root.dataset.oauthIntent = tab === 'oauth' && trigger && trigger.dataset.oauthIntent === 'bind' ? 'bind' : 'login';
|
|
root.querySelectorAll('[name="redirect_to"]').forEach(function (input) { input.value = targetRedirect; });
|
|
setOAuthRedirect(root, targetRedirect);
|
|
activate(root, tab || 'signin', false);
|
|
}
|
|
}
|
|
|
|
function closeModal(storeDismissal) {
|
|
if (!modal || !modal.classList.contains('is-open')) return;
|
|
var root = modal.querySelector('[data-aksrt-auth]');
|
|
if (root) stopQr(root);
|
|
modal.classList.remove('is-open');
|
|
modal.setAttribute('aria-hidden', 'true');
|
|
document.body.classList.remove('auth-modal-open');
|
|
if (storeDismissal && Number(modal.dataset.autoExpires) > 0) {
|
|
var until = Date.now() + Number(modal.dataset.autoExpires) * 3600000;
|
|
window.localStorage.setItem('aksrtAuthAutoUntil', String(until));
|
|
}
|
|
var restore = lastModalTrigger;
|
|
lastModalTrigger = null;
|
|
if (restore && document.documentElement.contains(restore) && typeof restore.focus === 'function') restore.focus();
|
|
}
|
|
|
|
function handleModalKeydown(event) {
|
|
if (!modal || !modal.classList.contains('is-open')) return;
|
|
if (event.key === 'Escape') {
|
|
event.preventDefault();
|
|
closeModal(true);
|
|
return;
|
|
}
|
|
if (event.key !== 'Tab') return;
|
|
var items = focusableModalItems();
|
|
if (!items.length) {
|
|
event.preventDefault();
|
|
modal.focus();
|
|
return;
|
|
}
|
|
var first = items[0];
|
|
var last = items[items.length - 1];
|
|
if (event.shiftKey && (document.activeElement === first || !modal.contains(document.activeElement))) {
|
|
event.preventDefault();
|
|
last.focus();
|
|
} else if (!event.shiftKey && document.activeElement === last) {
|
|
event.preventDefault();
|
|
first.focus();
|
|
}
|
|
}
|
|
|
|
function initGlobal() {
|
|
modal = document.querySelector('[data-auth-modal]');
|
|
if (!oauthResultChecked) {
|
|
oauthResultChecked = true;
|
|
showOAuthResultToast();
|
|
}
|
|
if (globalEventsBound) return;
|
|
globalEventsBound = true;
|
|
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();
|
|
var tab = trigger.classList.contains('signup-loader') ? 'signup' : (trigger.classList.contains('qrcode-signin') ? 'oauth' : 'signin');
|
|
openModal(tab, trigger.dataset.authRedirect || window.location.href, trigger);
|
|
});
|
|
document.addEventListener('keydown', handleModalKeydown);
|
|
document.addEventListener('visibilitychange', function () {
|
|
qrStates.forEach(function (state) {
|
|
if (document.visibilityState !== 'visible') stopQr(state.root);
|
|
else if (isRootActive(state.root)) loadQr(state.root, false);
|
|
});
|
|
});
|
|
}
|
|
|
|
function initModal() {
|
|
if (!modal || modal.dataset.aksrtAuthModalInitialized === '1') return;
|
|
modal.dataset.aksrtAuthModalInitialized = '1';
|
|
modal.addEventListener('click', function (event) {
|
|
if (event.target.closest('[data-auth-close]')) closeModal(true);
|
|
});
|
|
if (modal.dataset.auto === '1' && Number(window.localStorage.getItem('aksrtAuthAutoUntil') || 0) < Date.now()) {
|
|
window.setTimeout(function () {
|
|
var root = modal.querySelector('[data-aksrt-auth]');
|
|
if (root) openModal(root.dataset.initialTab || 'signin', window.location.href, null);
|
|
}, Math.max(1, Number(modal.dataset.autoWait) || 5) * 1000);
|
|
}
|
|
}
|
|
|
|
if (window.AksrtFrontend && typeof window.AksrtFrontend.register === 'function') {
|
|
window.AksrtFrontend.register('aksrt-auth', function (root) {
|
|
initGlobal();
|
|
initRoot(root);
|
|
initModal();
|
|
});
|
|
}
|
|
}());
|