(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 }; });