(function (root, factory) { // 验证码适配层同时支持浏览器运行和 Node 单元测试。 if (typeof module === 'object' && module.exports) { module.exports = factory(root); return; } root.CaptchaPages = factory(root); })(typeof globalThis !== 'undefined' ? globalThis : window, function (root) { 'use strict'; var DEFAULT_SCENE_CODE = 'WEB_H5_LOGIN'; var CAPTCHA_LAYER_AREA = ['318px', '318px']; var captchaLayerSeed = 0; function getApi() { return root.GenealogyApi && root.GenealogyApi.defaultClient; } function query(selector, rootNode) { return (rootNode || root.document).querySelector(selector); } 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 buildChallengeBody(options, api) { // 请求体字段严格对应 PC YAML 的 VerificationChallengeBody。 var settings = options || {}; return { tenantId: settings.tenantId || api.tenantId, clientId: settings.clientId || api.clientId, sceneCode: settings.sceneCode || DEFAULT_SCENE_CODE, subject: settings.subject || '' }; } function normalizeChallengeData(data) { // TAC 需要 data.type、data.id 和图片字段;接口文档返回 captchaType、challengeId、payload。 var source = data || {}; var payload = source.payload || {}; return Object.assign({}, payload, { type: source.captchaType || payload.type || 'SLIDER', id: payload.id || source.challengeId, challengeId: source.challengeId, providerCode: source.providerCode || 'tianai', captchaType: source.captchaType || payload.type || 'SLIDER' }); } function adaptChallengeResponse(response) { var payload = response || {}; if (Number(payload.code) !== 200 || !payload.data) return payload; return Object.assign({}, payload, { data: normalizeChallengeData(payload.data) }); } function buildVerifyBody(requestData, context, api) { // requestData 是 TAC 拖动完成后的轨迹数据;context 保存本次挑战的接口字段。 var settings = context || {}; var source = requestData || {}; var track = normalizeVerifyTrack(source.data || {}); return { tenantId: settings.tenantId || api.tenantId, clientId: settings.clientId || api.clientId, sceneCode: settings.sceneCode || DEFAULT_SCENE_CODE, subject: settings.subject || '', challengeId: settings.challengeId || source.challengeId || source.id, providerCode: settings.providerCode || 'tianai', captchaType: settings.captchaType || 'SLIDER', payload: { track: track } }; } function normalizeVerifyTrack(data) { // 后端 /captcha/verify 要求轨迹统一放在 payload.track,并显式携带 left/top。 var track = Object.assign({}, data || {}); var trackList = Array.isArray(track.trackList) ? track.trackList : []; var first = trackList[0] || {}; var last = trackList[trackList.length - 1] || {}; if (typeof track.left === 'undefined') { track.left = toNumber(last.x) - toNumber(first.x); } if (typeof track.top === 'undefined') { track.top = toNumber(last.y) - toNumber(first.y); } return track; } function toNumber(value) { var numberValue = Number(value); return Number.isFinite(numberValue) ? numberValue : 0; } function normalizeVerifyResponse(response) { // TAC 只看 code 是否为 200;接口通过 passed 表示业务验证结果。 var payload = response || {}; if (Number(payload.code) === 200 && payload.data && payload.data.passed === false) { return Object.assign({}, payload, { code: 4001, msg: payload.data.message || payload.msg || '验证失败' }); } return payload; } function extractValidToken(response) { var payload = response || {}; var data = payload.data || {}; if (Number(payload.code) !== 200) return ''; if (data.passed === false) return ''; return data.validToken || payload.validToken || ''; } function getTokenField(form) { return query('input[name="validToken"]', form); } function createCaptchaLayer() { // Layui 只负责提供默认弹层容器,验证码外观完全由 TAC 自带文件决定。 if (!root.layui || !root.layui.layer || !root.layui.layer.open || !root.document) { throw new Error('缺少 layui.layer 验证码弹层依赖'); } var layerId = 'captcha-layer-' + (++captchaLayerSeed); var selector = '[data-captcha-layer-box="' + layerId + '"]'; var layerIndex = root.layui.layer.open({ type: 1, title: false, closeBtn: 0, // 尺寸来自 TAC 原生 #tianai-captcha-parent,提前交给 Layui 计算居中位置。 area: CAPTCHA_LAYER_AREA, offset: 'auto', content: '
' }); var box = query(selector); if (!box) { root.layui.layer.close(layerIndex); throw new Error('缺少 Layui 验证码实际挂载点'); } return { box: box, layerIndex: layerIndex }; } function closeCaptchaLayer(captchaLayer) { // 组件结束后清空第三方 DOM 并关闭本次创建的 Layui 层。 if (!captchaLayer) return; if (captchaLayer.box) captchaLayer.box.innerHTML = ''; if (root.layui && root.layui.layer && root.layui.layer.close) { root.layui.layer.close(captchaLayer.layerIndex); } } function writeValidToken(form, token) { var field = getTokenField(form); if (field) field.value = token || ''; } function shouldRequireCaptcha(result) { // /captcha/require 返回 required=false 时允许直接继续业务请求。 if (!result) return true; return result.required !== false; } function createTac(form, options, api, resolve, reject) { var settings = options || {}; var captchaLayer; var box; var context = buildChallengeBody(settings, api); var lastChallenge = {}; var captcha; var config; if (!root.TAC || !root.CaptchaConfig) { reject(new Error('滑动验证组件未加载')); return null; } try { captchaLayer = createCaptchaLayer(); box = captchaLayer.box; } catch (error) { reject(error); return null; } config = new root.CaptchaConfig({ bindEl: box, requestCaptchaDataUrl: api.buildApiUrl('/captcha/challenge'), validCaptchaUrl: api.buildApiUrl('/captcha/verify'), requestHeaders: { clientid: api.clientId }, validSuccess: function (response, captchaControl, captchaInstance) { var token = extractValidToken(response); if (!token) { if (captchaInstance && captchaInstance.destroyWindow) captchaInstance.destroyWindow(); closeCaptchaLayer(captchaLayer); reject(new Error('滑动验证未返回有效票据')); return; } writeValidToken(form, token); if (captchaInstance && captchaInstance.destroyWindow) captchaInstance.destroyWindow(); closeCaptchaLayer(captchaLayer); resolve(token); }, validFail: function (response, captchaControl, captchaInstance) { if (captchaInstance && captchaInstance.reloadCaptcha) captchaInstance.reloadCaptcha(); showMessage((response && (response.msg || response.message)) || '滑动验证失败,请重试'); }, btnCloseFun: function (event, captchaInstance) { if (captchaInstance) captchaInstance.destroyWindow(); closeCaptchaLayer(captchaLayer); reject(new Error('已取消滑动验证')); } }); config.addRequestChain({ preRequest: function (type, request) { if (type === 'requestCaptchaData') { request.data = buildChallengeBody(settings, api); } if (type === 'validCaptcha') { request.data = buildVerifyBody(request.data, Object.assign({}, context, lastChallenge), api); } return true; }, postRequest: function (type, request, response) { if (type === 'requestCaptchaData') { var adapted = adaptChallengeResponse(response); lastChallenge = { challengeId: adapted.data && adapted.data.challengeId, providerCode: adapted.data && adapted.data.providerCode, captchaType: adapted.data && adapted.data.captchaType }; Object.keys(response || {}).forEach(function (key) { delete response[key]; }); Object.assign(response, adapted); } if (type === 'validCaptcha') { Object.assign(response, normalizeVerifyResponse(response)); } return true; } }); try { captcha = new root.TAC(config); captcha.init(); } catch (error) { closeCaptchaLayer(captchaLayer); reject(error); return null; } return captcha; } async function ensureToken(form, options) { // 认证页提交前调用:已有 validToken 直接通过,否则按验证中心策略弹出 TAC。 var api = getApi(); var field = getTokenField(form); var settings = options || {}; var requirement; if (!api) return ''; if (field && field.value) return field.value; if (!settings.sceneCode) return ''; try { requirement = await api.captchaRequirement(buildChallengeBody(settings, api)); if (!shouldRequireCaptcha(requirement)) return 'CAPTCHA_NOT_REQUIRED'; } catch (error) { // 查询策略失败时仍尝试直接拉起验证,避免认证接口缺少 validToken。 requirement = null; } return new Promise(function (resolve, reject) { createTac(form, settings, api, resolve, reject); }).catch(function (error) { showMessage(error.message || '请先完成滑动验证'); return ''; }); } return { buildChallengeBody: buildChallengeBody, adaptChallengeResponse: adaptChallengeResponse, buildVerifyBody: buildVerifyBody, normalizeVerifyTrack: normalizeVerifyTrack, createCaptchaLayer: createCaptchaLayer, closeCaptchaLayer: closeCaptchaLayer, extractValidToken: extractValidToken, normalizeVerifyResponse: normalizeVerifyResponse, shouldRequireCaptcha: shouldRequireCaptcha, ensureToken: ensureToken }; });