434 lines
13 KiB
JavaScript
434 lines
13 KiB
JavaScript
(function (root, factory) {
|
|
// 认证页脚本同时给浏览器页面和 Node 测试使用。
|
|
if (typeof module === 'object' && module.exports) {
|
|
module.exports = factory(root);
|
|
return;
|
|
}
|
|
|
|
root.AuthPages = factory(root);
|
|
if (root.document) {
|
|
root.document.addEventListener('DOMContentLoaded', function () {
|
|
root.AuthPages.init();
|
|
});
|
|
}
|
|
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
|
'use strict';
|
|
|
|
var documentRef = root.document;
|
|
var FORM_CONFIG = {
|
|
login: {
|
|
selector: '#login-password-form',
|
|
captchaScene: 'WEB_H5_LOGIN',
|
|
successUrl: 'profile.html'
|
|
},
|
|
'sms-login': {
|
|
selector: '#login-sms-form',
|
|
captchaScene: 'WEB_H5_LOGIN',
|
|
successUrl: 'profile.html'
|
|
},
|
|
register: {
|
|
selector: '#register-form',
|
|
captchaScene: 'WEB_H5_REGISTER',
|
|
successUrl: 'login.html'
|
|
},
|
|
'password-reset': {
|
|
selector: '#password-reset-form',
|
|
captchaScene: 'WEB_H5_FORGOT_PASSWORD',
|
|
successUrl: 'login.html'
|
|
}
|
|
};
|
|
|
|
function hashPassword(value) {
|
|
// 接口文档要求密码传 32 位 MD5;没有加载 md5.js 时保留原值,便于测试定位。
|
|
if (root.hex_md5) return root.hex_md5(value);
|
|
if (root.md5) return root.md5(value);
|
|
return value;
|
|
}
|
|
|
|
function buildLoginBody(values, hashFn) {
|
|
// 这里只组装页面字段,grantType、tenantId、clientId 由 api-client 统一补齐。
|
|
var hash = hashFn || hashPassword;
|
|
var body = {
|
|
phone: values.phone,
|
|
password: hash(values.password)
|
|
};
|
|
|
|
if (values.validToken) body.validToken = values.validToken;
|
|
return body;
|
|
}
|
|
|
|
function buildSmsLoginBody(values) {
|
|
// 短信登录接口只需要页面字段,grantType、tenantId、clientId 由 api-client 统一补齐。
|
|
var body = {
|
|
phone: values.phone,
|
|
smsCode: values.smsCode
|
|
};
|
|
|
|
if (values.validToken) body.validToken = values.validToken;
|
|
return body;
|
|
}
|
|
|
|
function buildRegisterBody(values, hashFn) {
|
|
// 注册验证码当前对应 validToken,可选字段只在有值时提交。
|
|
var hash = hashFn || hashPassword;
|
|
var body = {
|
|
phone: values.phone,
|
|
password: hash(values.password),
|
|
nickName: values.nickName || values.phone
|
|
};
|
|
|
|
if (values.validToken) body.validToken = values.validToken;
|
|
return body;
|
|
}
|
|
|
|
function buildPasswordResetBody(values, hashFn) {
|
|
// 找回密码接口使用 newPassword 字段,页面确认密码只在提交前校验,不传给后端。
|
|
var hash = hashFn || hashPassword;
|
|
|
|
return {
|
|
phone: values.phone,
|
|
smsCode: values.smsCode,
|
|
newPassword: hash(values.newPassword),
|
|
validToken: values.validToken || ''
|
|
};
|
|
}
|
|
|
|
function getApi() {
|
|
return root.GenealogyApi && root.GenealogyApi.defaultClient;
|
|
}
|
|
|
|
function getCaptchaScene(formType) {
|
|
// PC/H5 认证场景来自后台验证码配置,客户端 Key 为 web_pc。
|
|
var config = FORM_CONFIG[formType] || {};
|
|
|
|
return config.captchaScene || '';
|
|
}
|
|
|
|
function getSuccessUrl(formType) {
|
|
var config = FORM_CONFIG[formType] || {};
|
|
|
|
return config.successUrl || '';
|
|
}
|
|
|
|
function getFormCaptchaScene(form, formType) {
|
|
// 场景码由当前脚本统一管理,避免散落在 HTML 属性中。
|
|
var sceneCode = getCaptchaScene(formType);
|
|
|
|
return sceneCode;
|
|
}
|
|
|
|
async function ensureCaptcha(form, sceneCode, subject) {
|
|
// 未配置 PC/H5 验证场景时不调用验证中心,也不伪造 validToken。
|
|
if (!sceneCode) return true;
|
|
|
|
// 验证适配层不存在时不阻塞测试环境;真实页面会加载 captcha-pages.js。
|
|
if (!root.CaptchaPages || !root.CaptchaPages.ensureToken) return true;
|
|
|
|
return Boolean(await root.CaptchaPages.ensureToken(form, {
|
|
sceneCode: sceneCode,
|
|
subject: subject
|
|
}));
|
|
}
|
|
|
|
function query(selector, rootNode) {
|
|
return (rootNode || documentRef).querySelector(selector);
|
|
}
|
|
|
|
function queryAll(selector, rootNode) {
|
|
return Array.prototype.slice.call((rootNode || documentRef).querySelectorAll(selector));
|
|
}
|
|
|
|
function readForm(form) {
|
|
// 页面通过 name 属性和接口字段对齐,避免按 placeholder 文案取值。
|
|
var values = {};
|
|
|
|
queryAll('input[name], textarea[name], select[name]', form).forEach(function (field) {
|
|
values[field.name] = String(field.value || '').trim();
|
|
});
|
|
|
|
return values;
|
|
}
|
|
|
|
function setBusy(button, busy) {
|
|
// 加载状态只切 class 和 aria,不在 JS 中写样式。
|
|
if (!button) return;
|
|
button.classList.toggle('is-loading', busy);
|
|
if ('disabled' in button) button.disabled = busy;
|
|
button.setAttribute('aria-busy', busy ? 'true' : 'false');
|
|
}
|
|
|
|
function showMessage(message) {
|
|
if (root.MessageUtil && root.MessageUtil.show) {
|
|
root.MessageUtil.show(message);
|
|
return;
|
|
}
|
|
|
|
if (root.console && root.console.warn) root.console.warn(message);
|
|
}
|
|
|
|
function isPhone(value) {
|
|
return /^1\d{10}$/.test(value || '');
|
|
}
|
|
|
|
function isSmsCode(value) {
|
|
return /^\d{4,6}$/.test(value || '');
|
|
}
|
|
|
|
function validatePassword(value, message) {
|
|
if (!value) return message;
|
|
if (value.length < 6) return '密码至少需要 6 位';
|
|
return '';
|
|
}
|
|
|
|
function validateAuthValues(formType, values) {
|
|
var data = values || {};
|
|
|
|
if (!data.phone) return '请填写手机号';
|
|
if (!isPhone(data.phone)) return '请输入正确的手机号';
|
|
|
|
if (formType === 'login') {
|
|
return validatePassword(data.password, '请填写密码');
|
|
}
|
|
|
|
if (formType === 'sms-login') {
|
|
if (!data.smsCode) return '请填写短信验证码';
|
|
if (!isSmsCode(data.smsCode)) return '请输入正确的短信验证码';
|
|
return '';
|
|
}
|
|
|
|
if (formType === 'register') {
|
|
return validatePassword(data.password, '请填写密码');
|
|
}
|
|
|
|
if (formType === 'password-reset') {
|
|
if (!data.smsCode) return '请填写验证码';
|
|
if (!isSmsCode(data.smsCode)) return '请输入正确的短信验证码';
|
|
|
|
var passwordMessage = validatePassword(data.newPassword, '请填写新密码');
|
|
if (passwordMessage) return passwordMessage;
|
|
if (!data.confirmPassword) return '请再次输入新密码';
|
|
if (data.newPassword !== data.confirmPassword) return '两次输入的新密码不一致';
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
function validateBeforeSubmit(formType, values) {
|
|
var message = validateAuthValues(formType, values);
|
|
|
|
if (!message) return true;
|
|
showMessage(message);
|
|
return false;
|
|
}
|
|
|
|
function validatePhoneOnly(values) {
|
|
var data = values || {};
|
|
|
|
if (!data.phone) return '请填写手机号';
|
|
if (!isPhone(data.phone)) return '请输入正确的手机号';
|
|
return '';
|
|
}
|
|
|
|
function validateBeforeSendCode(values) {
|
|
var message = validatePhoneOnly(values);
|
|
|
|
if (!message) return true;
|
|
showMessage(message);
|
|
return false;
|
|
}
|
|
|
|
function bindSubmit(selector, handler) {
|
|
// 所有认证表单统一拦截 submit,避免链接跳转绕过接口请求。
|
|
var form = query(selector);
|
|
|
|
if (!form) return;
|
|
form.addEventListener('submit', function (event) {
|
|
event.preventDefault();
|
|
handler(form, query('[type="submit"]', form));
|
|
});
|
|
}
|
|
|
|
function setLoginMode(activePanelId) {
|
|
// 登录方式切换只更新状态,具体样式由 login.css 控制。
|
|
queryAll('.login-mode-tab').forEach(function (button) {
|
|
var isActive = button.getAttribute('aria-controls') === activePanelId;
|
|
|
|
button.classList.toggle('is-active', isActive);
|
|
button.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
|
});
|
|
|
|
queryAll('.login-mode-panel').forEach(function (panel) {
|
|
var isActive = panel.id === activePanelId;
|
|
|
|
panel.hidden = !isActive;
|
|
});
|
|
}
|
|
|
|
async function submitLogin(form, button) {
|
|
// 登录成功后跳转地址由 FORM_CONFIG 统一控制,HTML 只保留表单结构。
|
|
var api = getApi();
|
|
var values = readForm(form);
|
|
|
|
if (!api) return;
|
|
if (!validateBeforeSubmit('login', values)) return;
|
|
if (!await ensureCaptcha(form, getFormCaptchaScene(form, 'login'), values.phone)) return;
|
|
|
|
values = readForm(form);
|
|
|
|
setBusy(button, true);
|
|
try {
|
|
await api.login(buildLoginBody(values));
|
|
root.location.href = getSuccessUrl('login');
|
|
} catch (error) {
|
|
showMessage(error.message || '登录失败');
|
|
} finally {
|
|
setBusy(button, false);
|
|
}
|
|
}
|
|
|
|
async function submitSmsLogin(form, button) {
|
|
// PC 短信登录与密码登录共用 WEB_H5_LOGIN 场景,短信码由同页按钮发送。
|
|
var api = getApi();
|
|
var values = readForm(form);
|
|
|
|
if (!api) return;
|
|
if (!validateBeforeSubmit('sms-login', values)) return;
|
|
if (!await ensureCaptcha(form, getFormCaptchaScene(form, 'sms-login'), values.phone)) return;
|
|
|
|
values = readForm(form);
|
|
|
|
setBusy(button, true);
|
|
try {
|
|
await api.loginBySms(buildSmsLoginBody(values));
|
|
root.location.href = getSuccessUrl('sms-login');
|
|
} catch (error) {
|
|
showMessage(error.message || '短信登录失败');
|
|
} finally {
|
|
setBusy(button, false);
|
|
}
|
|
}
|
|
|
|
async function submitRegister(form, button) {
|
|
// 注册完成后回到登录页,创建家谱必须在登录后进行。
|
|
var api = getApi();
|
|
var values = readForm(form);
|
|
|
|
if (!api) return;
|
|
if (!validateBeforeSubmit('register', values)) return;
|
|
if (!await ensureCaptcha(form, getFormCaptchaScene(form, 'register'), values.phone)) return;
|
|
|
|
values = readForm(form);
|
|
|
|
setBusy(button, true);
|
|
try {
|
|
await api.register(buildRegisterBody(values));
|
|
root.location.href = getSuccessUrl('register');
|
|
} catch (error) {
|
|
showMessage(error.message || '注册失败');
|
|
} finally {
|
|
setBusy(button, false);
|
|
}
|
|
}
|
|
|
|
async function sendCode(button) {
|
|
// 短信验证码场景由表单配置决定,不在按钮属性中重复声明。
|
|
var api = getApi();
|
|
var form = button.closest('form');
|
|
var values = readForm(form);
|
|
var formType = getFormType(form);
|
|
var sceneCode = getFormCaptchaScene(form, formType);
|
|
|
|
if (!api) return;
|
|
if (!validateBeforeSendCode(values)) return;
|
|
if (!await ensureCaptcha(form, sceneCode, values.phone)) return;
|
|
|
|
values = readForm(form);
|
|
|
|
setBusy(button, true);
|
|
try {
|
|
await api.sendSmsCode({
|
|
sceneCode: sceneCode,
|
|
phone: values.phone,
|
|
validToken: values.validToken || ''
|
|
});
|
|
showMessage('验证码已发送');
|
|
} catch (error) {
|
|
showMessage(error.message || '验证码发送失败');
|
|
} finally {
|
|
setBusy(button, false);
|
|
}
|
|
}
|
|
|
|
async function submitPasswordReset(form, button) {
|
|
// 前端先校验两次密码一致,再按接口字段提交重置请求。
|
|
var api = getApi();
|
|
var values = readForm(form);
|
|
|
|
if (!api) return;
|
|
if (!validateBeforeSubmit('password-reset', values)) return;
|
|
if (!await ensureCaptcha(form, getFormCaptchaScene(form, 'password-reset'), values.phone)) return;
|
|
|
|
values = readForm(form);
|
|
|
|
setBusy(button, true);
|
|
try {
|
|
await api.resetPassword(buildPasswordResetBody(values));
|
|
showMessage('密码已重设,请重新登录');
|
|
root.location.href = getSuccessUrl('password-reset');
|
|
} catch (error) {
|
|
showMessage(error.message || '重设密码失败');
|
|
} finally {
|
|
setBusy(button, false);
|
|
}
|
|
}
|
|
|
|
function bindCodeButtons() {
|
|
queryAll('[data-api-send-code]').forEach(function (button) {
|
|
button.addEventListener('click', function (event) {
|
|
event.preventDefault();
|
|
sendCode(button);
|
|
});
|
|
});
|
|
}
|
|
|
|
function bindLoginModeTabs() {
|
|
queryAll('.login-mode-tab').forEach(function (button) {
|
|
button.addEventListener('click', function () {
|
|
setLoginMode(button.getAttribute('aria-controls'));
|
|
});
|
|
});
|
|
}
|
|
|
|
function getFormType(form) {
|
|
if (!form) return '';
|
|
|
|
return Object.keys(FORM_CONFIG).find(function (formType) {
|
|
return FORM_CONFIG[formType].selector === '#' + form.id;
|
|
}) || '';
|
|
}
|
|
|
|
function init() {
|
|
// 每个页面只会命中自己的表单 id,不需要拆多个入口文件。
|
|
if (!documentRef) return;
|
|
|
|
bindSubmit(FORM_CONFIG.login.selector, submitLogin);
|
|
bindSubmit(FORM_CONFIG['sms-login'].selector, submitSmsLogin);
|
|
bindSubmit(FORM_CONFIG.register.selector, submitRegister);
|
|
bindSubmit(FORM_CONFIG['password-reset'].selector, submitPasswordReset);
|
|
bindLoginModeTabs();
|
|
bindCodeButtons();
|
|
}
|
|
|
|
return {
|
|
buildLoginBody: buildLoginBody,
|
|
buildSmsLoginBody: buildSmsLoginBody,
|
|
buildRegisterBody: buildRegisterBody,
|
|
buildPasswordResetBody: buildPasswordResetBody,
|
|
getCaptchaScene: getCaptchaScene,
|
|
getFormCaptchaScene: getFormCaptchaScene,
|
|
getSuccessUrl: getSuccessUrl,
|
|
validateAuthValues: validateAuthValues,
|
|
init: init
|
|
};
|
|
});
|