/** * 登录/注册等场景的人机验证状态:先读站点开关,再识别图片或极验提供商。 * /captcha API 走统一后端 origin,但图片刷新 /captcha/tp_captcha/... 始终走前端同源反代。 */ import { onMounted } from "vue"; import { watch } from "vue"; import { onScopeDispose } from "vue"; import { ref } from "vue"; import { ApiError } from "~/lib/auth.js"; import { useApi } from "~/lib/auth.js"; import { t as useSiteConfig_t } from "~/lib/useSiteConfig.js"; var s = (e) => e === !0 || e === 1 || e === `1`; // 登录可由全局开关或后端“失败三次后验证”状态开启,其他场景读取各自开关。 function c(e, t) { return s(e?.[`captcha_client_${t}`]) ? !0 : t === `login` && s(e?.captcha_client_login_error) && s(e?.captcha_client_login_error_3_times); } /** 只解析 HTML 中的提供商标识,不执行其中脚本;未知插件或异常极验 id 明确报错。 */ function l(e) { if (typeof e != `string` || e.length > 262144) throw Error(`验证码响应无效`); let t = new DOMParser().parseFromString(e, `text/html`), n = t.querySelector(`[data-captcha-provider]`); if (n) { let e = n.getAttribute(`data-captcha-id`) ?? ``; if ( n.getAttribute(`data-captcha-provider`) !== `geetest` || !/^[a-f0-9]{32}$/i.test(e) ) throw Error(`验证码服务尚未配置完成,请联系管理员`); return { provider: `geetest`, captchaId: e, }; } if ( e.includes(`/captcha/tp_captcha/index/refresh`) && t.querySelector(`#captcha-img`) ) return { provider: `image`, captchaId: ``, }; throw Error(`当前验证码插件暂不受支持,请联系管理员`); } // 极验四个结果字段均需非空且有长度上限;真实挑战仍由服务端校验。 function u(e) { if (!e || typeof e != `object`) return !1; let t = e; return [`lot_number`, `captcha_output`, `pass_token`, `gen_time`].every( (e) => typeof t[e] == `string` && t[e].trim().length > 0 && t[e].length <= 8192, ); } /** d 为场景名;无场景时由调用方手动控制加载,组件销毁会取消请求并清空凭证。 */ function d(d) { let f = useSiteConfig_t(), p = useApi(), m = ref(!1), h = ref(`image`), g = ref(``), _ = ref(``), v = ref(``), y = ref(``), b = ref(!1), x = ref(``), S = ref(!(!d || f.config.value)), C = !1, w = 0, T = null, E = !1; // 每次加载或失效都清掉旧答案和 token,避免把不同验证码的结果混在一起提交。 function D() { ((y.value = ``), (v.value = ``)); } // 序号 w 和销毁标记 C 防止旧响应回填;定时 AbortController 限制同源图片请求。 async function O() { if (C || b.value) return; let e = ++w; ((b.value = !0), (x.value = ``), D(), (_.value = ``), (g.value = ``), (T = new AbortController())); let t = T, n = setTimeout(() => t.abort(), 1e4); try { let n = await p.get(`/captcha`, void 0, !1, { timeoutMs: 1e4, }); if (C || e !== w) return; let r = l(n.html); if ( ((h.value = r.provider), (g.value = r.captchaId), r.provider === `geetest`) ) return; let i = await fetch(`/captcha/tp_captcha/index/refresh`, { headers: { Accept: `application/json`, }, signal: t.signal, cache: `no-store`, }), a = await i.json(); if (C || e !== w) return; if ( !i.ok || typeof a?.base64 != `string` || !/^data:image\/(?:png|jpeg|gif|webp);base64,/.test(a.base64) || typeof a?.token != `string` || !a.token ) throw Error(`验证码加载失败`); ((_.value = a.base64), (v.value = a.token)); } catch (t) { if (C || e !== w) return; x.value = t instanceof Error && /验证码.*(?:配置|支持)/.test(t.message) ? t.message : `验证码加载失败,请点击重新加载`; } finally { (clearTimeout(n), !C && e === w && (b.value = !1)); } } // 仅在当前确实需要极验且没有加载时接受完成结果,按后端约定序列化四个字段。 function k(e) { (D(), !C && m.value && h.value === `geetest` && !b.value && u(e) && ((y.value = JSON.stringify({ lot_number: e.lot_number, captcha_output: e.captcha_output, pass_token: e.pass_token, gen_time: e.gen_time, })), (v.value = e.lot_number))); } // 提交前将“配置未加载”“正在加载”“未完成”等情况区分,空串才表示可继续。 function A() { return S.value ? `验证码设置正在加载,请稍候` : d && !f.config.value ? `验证码设置加载失败,请刷新页面重试` : m.value ? b.value ? `验证码正在加载,请稍候` : x.value ? x.value : h.value === `geetest` ? v.value && y.value ? `` : `请先完成人机验证` : v.value ? y.value.trim() ? `` : `请输入图形验证码` : `请先重新加载图形验证码` : ``; } // 后端报错可临时强制开启验证码,刷新后再由页面让用户重试原动作,不自动重复登录。 async function j(e) { let t = e instanceof ApiError && s(e.data?.captcha); return (t && ((E = !0), (m.value = !0)), m.value && (await O()), t); } return ( d && (watch( () => c(f.config.value, d), (e) => { ((m.value = E || e), m.value ? O() : ((w += 1), T?.abort(), (b.value = !1), (x.value = ``), D())); }, { immediate: !0, }, ), onMounted(async () => { (await f.load(), !C && ((S.value = !1), f.config.value || (x.value = `验证码设置加载失败,请刷新页面重试`))); })), // 组件离开后即使不可取消的 API 请求返回,也不能继续写入当前验证码状态。 onScopeDispose(() => { ((C = !0), (w += 1), T?.abort(), D()); }), { needed: m, provider: h, captchaId: g, img: _, token: v, value: y, loading: b, error: x, configLoading: S, load: O, complete: k, invalidate: D, validationError: A, handleError: j, } ); } export { d as t };