perf: 验证码支持 Cloudflare Turnstile ,谨慎启用,国内被墙了

This commit is contained in:
xiaojunnuo
2026-01-29 17:21:39 +08:00
parent b204182c13
commit ca43c77525
16 changed files with 214 additions and 28 deletions
@@ -0,0 +1,97 @@
<template>
<div class="cf-turnstile">
<div id="turnstile-container" class="cf-turnstile-container" :data-sitekey="siteKeyRef"></div>
</div>
</template>
<script setup lang="ts">
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import { loadScript } from "vue-plugin-load-script";
const loaded = ref(false);
async function loadCaptchaScript() {
await loadScript("https://challenges.cloudflare.com/turnstile/v0/api.js");
loaded.value = true;
}
defineOptions({
name: "CfTurnstileCaptcha",
});
const emit = defineEmits(["update:modelValue", "change"]);
const props = defineProps<{
modelValue: any;
captchaGet: () => Promise<any>;
}>();
const captchaRef = ref(null);
const siteKeyRef = ref("");
const widgetIdRef = ref(null);
onMounted(async () => {
await loadCaptchaScript();
await nextTick();
const { siteKey } = await props.captchaGet();
siteKeyRef.value = siteKey; //这里确定是string类型
//@ts-ignore
const widgetId = turnstile.render("#turnstile-container", {
sitekey: siteKey,
size: "flexible",
callback: function (token: string) {
console.log("turnstile success:", token);
emitChange({
token: token,
});
},
});
widgetIdRef.value = widgetId;
});
onUnmounted(() => {
//@ts-ignore
if (turnstile && widgetIdRef.value) {
//@ts-ignore
turnstile.remove(widgetIdRef.value);
}
});
function checkExpired() {
//@ts-ignore
if (turnstile && widgetIdRef.value) {
//@ts-ignore
return turnstile.isExpired(widgetIdRef.value);
}
}
function emitChange(value: any) {
emit("update:modelValue", value);
emit("change", value);
}
function reset() {
// 重置验证码
//@ts-ignore
if (turnstile && widgetIdRef.value) {
//@ts-ignore
turnstile.reset(widgetIdRef.value);
}
}
watch(
() => {
return props.modelValue;
},
value => {
if (value == null) {
reset();
}
}
);
defineExpose({
reset,
});
</script>
<style lang="less">
.cf-turnstile-container {
iframe {
width: 100% !important;
}
}
</style>
@@ -11,8 +11,7 @@
</div>
</template>
<script setup lang="ts">
import { defineEmits, defineExpose, defineProps, ref, watch } from "vue";
import { nanoid } from "nanoid";
import { ref, watch } from "vue";
const props = defineProps<{
modelValue: any;
@@ -11,8 +11,8 @@
</div>
</template>
<script setup lang="ts">
import { onMounted, defineProps, defineEmits, ref, onUnmounted, Ref, watch } from "vue";
import { notification } from "ant-design-vue";
import { ref, Ref, watch } from "vue";
import { loadScript } from "vue-plugin-load-script";
const loaded = ref(false);
@@ -455,4 +455,10 @@ h6 {
background: #ffecb3;
color: #f57c00;
}
}
.ant-tag{
.fs-icon{
margin-right: 5px;
}
}
@@ -9,9 +9,11 @@
<addon-selector v-model:model-value="formState.public.captchaAddonId" addon-type="captcha" from="sys" @selected-change="onAddonChanged" />
</a-form-item>
<a-form-item v-if="formState.public.captchaType === settingsStore.sysPublic.captchaType" :label="t('certd.sys.setting.captchaTest')">
<div class="flex">
<CaptchaInput v-model:model-value="captchaTestForm.captcha" class="w-50%"></CaptchaInput>
<a-button class="ml-2" type="primary" @click="doCaptchaValidate">后端验证</a-button>
<div class="flex items-center">
<CaptchaInput v-model:model-value="captchaTestForm.captcha" class="w-60%"></CaptchaInput>
<a-button class="ml-2 mr-2" type="primary" @click="doCaptchaValidate">后端验证</a-button>
<a-tag v-if="captchaTestForm.pass" color="green" class="flex items-center"> <fs-icon icon="material-symbols:check-circle-rounded"></fs-icon> 校验通过</a-tag>
<a-tag v-else class="flex items-center"> <fs-icon icon="material-symbols:info-rounded"></fs-icon> 请先点击验证</a-tag>
</div>
</a-form-item>