(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', operationCode: 'password-login', successUrl: 'profile.html' }, 'sms-login': { selector: '#login-sms-form', operationCode: 'sms-login', successUrl: 'profile.html' }, register: { selector: '#register-form', operationCode: 'register', successUrl: 'login.html' }, 'password-reset': { selector: '#password-reset-form', operationCode: 'forgot-password', successUrl: 'login.html' } }; var SMS_CODE_COOLDOWN_SECONDS = 60; 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) { // 这里只组装登录 DTO 字段,grantType、tenantId 由 api-client 统一补齐;clientid 只走请求头。 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 由 api-client 统一补齐;clientid 只走请求头。 return { phone: values.phone, smsCode: values.smsCode }; } function buildRegisterBody(values, hashFn) { // 确认密码只用于页面校验;注册接口需要手机号短信验证码。 var hash = hashFn || hashPassword; return { phone: values.phone, password: hash(values.password), nickName: values.nickName || values.phone, smsCode: values.smsCode }; } function buildPasswordResetBody(values, hashFn) { // 找回密码接口使用 newPassword 字段,页面确认密码只在提交前校验,不传给后端。 var hash = hashFn || hashPassword; return { phone: values.phone, smsCode: values.smsCode, newPassword: hash(values.newPassword) }; } function getApi() { return root.GenealogyApi && root.GenealogyApi.defaultClient; } function getCaptchaOperation(formType) { // PC 验证中心按业务动作解析服务端已激活的验证场景。 var config = FORM_CONFIG[formType] || {}; return config.operationCode || ''; } function getSuccessUrl(formType) { var config = FORM_CONFIG[formType] || {}; return config.successUrl || ''; } function getFormCaptchaOperation(form, formType) { // 业务动作由当前脚本统一管理,避免散落在 HTML 属性中。 var operationCode = getCaptchaOperation(formType); return operationCode; } async function ensureCaptcha(form, operationCode, subject) { // 未配置 PC 认证动作时不调用验证中心,也不伪造 validToken。 if (!operationCode) return true; if (!root.CaptchaPages || !root.CaptchaPages.ensureToken) { showMessage('验证码组件未加载,请刷新后重试'); return false; } return Boolean(await root.CaptchaPages.ensureToken(form, { operationCode: operationCode, 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 clearCaptchaToken(form) { var field = query('input[name="validToken"]', form); if (field) field.value = ''; } function takeCaptchaToken(form) { var field = query('input[name="validToken"]', form); var token = field && field.value || ''; if (field) field.value = ''; return token; } function startSmsCooldown(button) { var originalText; var remaining = SMS_CODE_COOLDOWN_SECONDS; var interval; if (!button || typeof root.setInterval !== 'function') return; if (button.__smsCooldownTimer && typeof root.clearInterval === 'function') { root.clearInterval(button.__smsCooldownTimer); } originalText = button.getAttribute('data-sms-code-label') || button.textContent.trim(); button.setAttribute('data-sms-code-label', originalText); button.disabled = true; button.textContent = remaining + ' 秒后重试'; interval = root.setInterval(function () { remaining -= 1; if (remaining > 0) { button.textContent = remaining + ' 秒后重试'; return; } if (typeof root.clearInterval === 'function') root.clearInterval(interval); button.__smsCooldownTimer = null; button.disabled = false; button.textContent = originalText; }, 1000); button.__smsCooldownTimer = interval; } 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}$/.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') { if (!data.smsCode) return '请填写短信验证码'; if (!isSmsCode(data.smsCode)) return '请输入正确的短信验证码'; var registerPasswordMessage = validatePassword(data.password, '请填写密码'); if (registerPasswordMessage) return registerPasswordMessage; if (!data.confirmPassword) return '请再次输入密码'; if (data.password !== data.confirmPassword) return '两次输入的密码不一致'; return ''; } 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; if (!isActive) clearCaptchaToken(panel); }); } async function submitLogin(form, button) { // 登录成功后跳转地址由 FORM_CONFIG 统一控制,HTML 只保留表单结构。 var api = getApi(); var values = readForm(form); if (!api) { showMessage('接口初始化失败,请刷新后重试'); return; } if (!validateBeforeSubmit('login', values)) return; if (!await ensureCaptcha(form, getFormCaptchaOperation(form, 'login'), values.phone)) return; values = readForm(form); values.validToken = takeCaptchaToken(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) { showMessage('接口初始化失败,请刷新后重试'); return; } if (!validateBeforeSubmit('sms-login', values)) return; 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) { showMessage('接口初始化失败,请刷新后重试'); return; } if (!validateBeforeSubmit('register', values)) return; 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 operationCode = getFormCaptchaOperation(form, formType); if (!api) { showMessage('接口初始化失败,请刷新后重试'); return; } if (!validateBeforeSendCode(values)) return; if (!await ensureCaptcha(form, operationCode, values.phone)) return; values = readForm(form); values.validToken = takeCaptchaToken(form); setBusy(button, true); try { await api.sendSmsCode(operationCode, { phone: values.phone, validToken: values.validToken || '' }); showMessage('验证码已发送'); setBusy(button, false); startSmsCooldown(button); } catch (error) { showMessage(error.message || '验证码发送失败'); setBusy(button, false); } } async function submitPasswordReset(form, button) { // 前端先校验两次密码一致,再按接口字段提交重置请求。 var api = getApi(); var values = readForm(form); if (!api) { showMessage('接口初始化失败,请刷新后重试'); return; } if (!validateBeforeSubmit('password-reset', values)) return; 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 bindCaptchaTokenInvalidation() { queryAll('form input[name="phone"]').forEach(function (field) { field.addEventListener('input', function () { clearCaptchaToken(field.closest('form')); }); }); } 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(); bindCaptchaTokenInvalidation(); } return { buildLoginBody: buildLoginBody, buildSmsLoginBody: buildSmsLoginBody, buildRegisterBody: buildRegisterBody, buildPasswordResetBody: buildPasswordResetBody, clearCaptchaToken: clearCaptchaToken, takeCaptchaToken: takeCaptchaToken, startSmsCooldown: startSmsCooldown, getCaptchaOperation: getCaptchaOperation, getFormCaptchaOperation: getFormCaptchaOperation, getSuccessUrl: getSuccessUrl, validateAuthValues: validateAuthValues, init: init }; });