perf: 验证码支持测试,登录验证码需要测试通过后才能开启

This commit is contained in:
xiaojunnuo
2025-09-26 01:21:01 +08:00
parent 03f317ffdb
commit 83e6476408
18 changed files with 485 additions and 60 deletions
@@ -0,0 +1,208 @@
<template>
<div ref="captchaRef" class="tencent_captcha_wrapper" :class="{ tencent_captcha_ok: modelValue }" @click="triggerCaptcha">
<div class="validation-box" :class="{ validated: modelValue != null }">
<div class="sweep-animation"></div>
<div class="box-content">
<div class="box-icon"></div>
<span class="status-text">点击进行校验</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, defineProps, defineEmits, ref, onUnmounted, Ref, watch } from "vue";
import { notification } from "ant-design-vue";
defineOptions({
name: "TencentCaptcha",
});
const emit = defineEmits(["update:modelValue", "change"]);
const props = defineProps<{
modelValue: any;
captchaGet: () => Promise<any>;
}>();
const captchaRef = ref(null);
const captchaInstanceRef: Ref = ref({});
// 定义回调函数
function callback(res: { ret: number; ticket: string; randstr: string; errorCode?: number; errorMessage?: string }) {
// 第一个参数传入回调结果,结果如下:
// ret Int 验证结果,0:验证成功。2:用户主动关闭验证码。
// ticket String 验证成功的票据,当且仅当 ret = 0 时 ticket 有值。
// CaptchaAppId String 验证码应用ID。
// bizState Any 自定义透传参数。
// randstr String 本次验证的随机串,后续票据校验时需传递该参数。
// verifyDuration Int 验证码校验接口耗时(ms)。
// actionDuration Int 操作校验成功耗时(用户动作+校验完成)(ms)。
// sid String 链路sid。
console.log("callback:", res);
// res(用户主动关闭验证码)= {ret: 2, ticket: null}
// res(验证成功) = {ret: 0, ticket: "String", randstr: "String"}
// res(请求验证码发生错误,验证码自动返回trerror_前缀的容灾票据) = {ret: 0, ticket: "String", randstr: "String", errorCode: Number, errorMessage: "String"}
// 此处代码仅为验证结果的展示示例,真实业务接入,建议基于ticket和errorCode情况做不同的业务处理
if (res.ret === 0) {
debugger;
emitChange({
ticket: res.ticket,
randstr: res.randstr,
});
} else if (res.ret === 2) {
console.log("用户主动关闭验证码");
}
}
// 定义验证码js加载错误处理函数
function loadErrorCallback(error: any) {
// var appid = "您的CaptchaAppId";
// // 生成容灾票据或自行做其它处理
// var ticket = "trerror_1001_" + appid + "_" + Math.floor(new Date().getTime() / 1000);
// callback({
// ret: 0,
// randstr: "@" + Math.random().toString(36).substr(2),
// ticket: ticket,
// errorCode: 1001,
// errorMessage: "jsload_error",
// });
notification.error({
message: `验证码加载失败:${error?.message || error}`,
});
}
async function triggerCaptcha() {
const { captchaAppId } = await props.captchaGet();
try {
// 生成一个验证码对象
// CaptchaAppId:登录验证码控制台,从【验证管理】页面进行查看。如果未创建过验证,请先新建验证。注意:不可使用客户端类型为小程序的CaptchaAppId,会导致数据统计错误。
//callback:定义的回调函数
// @ts-ignore
var captcha = new TencentCaptcha(captchaAppId + "", callback, {
userLanguage: "zh-cn",
// showFn: (ret: any) => {
// const {
// duration, // 验证码渲染完成的耗时(ms)
// sid, // 链路sid
// } = ret;
// },
});
// 调用方法,显示验证码
captcha.show();
} catch (error) {
// 加载异常,调用验证码js加载错误处理函数
loadErrorCallback(error);
}
}
function emitChange(value: any) {
emit("update:modelValue", value);
emit("change", value);
}
function reset() {
captchaInstanceRef.value.instance.reset();
}
watch(
() => {
return props.modelValue;
},
value => {
if (value == null) {
reset();
}
}
);
defineExpose({
reset,
});
</script>
<style lang="less">
.tencent_captcha_wrapper {
.validation-box {
width: 100%;
height: 40px;
margin: 0 auto 30px;
border: 1px solid #ddd;
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
background-color: #f9f9f9;
}
.validation-box:hover {
border-color: #aaa;
background-color: #f0f0f0;
}
.validation-box.validated {
border-color: #4caf50;
background-color: #f1f8e9;
}
.box-content {
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
position: relative;
}
.box-icon {
font-size: 18px;
color: #bbb;
margin-right: 15px;
transition: all 0.3s ease;
}
.validation-box.validated .box-icon {
color: #4caf50;
}
.status-text {
font-size: 14px;
font-weight: 500;
color: #888;
transition: all 0.3s ease;
}
.validation-box.validated .status-text {
color: #4caf50;
font-weight: 600;
}
/* 划过动画效果 */
.sweep-animation {
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(76, 175, 80, 0.2), transparent);
z-index: 1;
opacity: 0;
transition: opacity 0.3s;
}
.validation-box.validated .sweep-animation {
animation: sweep 0.8s ease forwards;
opacity: 1;
}
@keyframes sweep {
0% {
left: -100%;
}
50% {
left: 0;
}
100% {
left: 100%;
}
}
}
</style>
@@ -45,6 +45,16 @@ export async function DoVerify(id: number) {
});
}
export async function ResetStatus(id: number) {
return await request({
url: apiPrefix + "/resetStatus",
method: "post",
data: {
id,
},
});
}
export async function ParseDomain(fullDomain: string) {
return await request({
url: subDomainApiPrefix + "/parseDomain",
@@ -16,6 +16,9 @@
<a-tooltip v-if="cnameRecord.error" :title="cnameRecord.error">
<fs-icon class="ml-5 color-red" icon="ion:warning-outline"></fs-icon>
</a-tooltip>
<a-tooltip v-if="cnameRecord.status === 'valid'" title="重置校验状态,重新校验">
<fs-icon class="ml-5 color-red" icon="solar:undo-left-square-bold" @click="resetStatus"></fs-icon>
</a-tooltip>
</td>
<td class="center">
<template v-if="cnameRecord.status !== 'valid'">
@@ -71,12 +74,15 @@ function onRecordChange() {
});
}
async function loadRecord() {
cnameRecord.value = await GetByDomain(props.domain);
}
let refreshIntervalId: any = null;
async function doRefresh() {
if (!props.domain) {
return;
}
cnameRecord.value = await GetByDomain(props.domain);
await loadRecord();
onRecordChange();
if (cnameRecord.value.status === "validating") {
@@ -114,6 +120,11 @@ async function doVerify() {
}
await doRefresh();
}
async function resetStatus() {
await api.ResetStatus(cnameRecord.value.id);
await loadRecord();
}
</script>
<style lang="less">
@@ -723,7 +723,9 @@ export default {
captchaEnabled: "Enable Login Captcha",
captchaHelper: "Whether to enable captcha verification for login",
captchaType: "Captcha Setting",
captchaTest: "Captcha Test",
// 保存后再点击测试,请务必测试通过了,再开启登录验证码
captchaTestHelper: "Save and click test, please make sure the test is passed before enabling login captcha",
baseSetting: "Base Settings",
registerSetting: "Register Settings",
safeSetting: "Safe Settings",
@@ -725,7 +725,8 @@ export default {
captchaEnabled: "启用登录验证码",
captchaHelper: "登录时是否启用验证码",
captchaType: "验证码配置",
captchaTest: "测试验证码",
captchaTestHelper: "保存后再点击测试,请务必测试通过了,再开启登录验证码",
baseSetting: "基本设置",
registerSetting: "注册设置",
safeSetting: "安全设置",
@@ -79,6 +79,14 @@ export async function SysSettingsSave(data: SysSettings) {
});
}
export async function TestCaptcha(form: any) {
return await request({
url: apiPrefix + "/captchaTest",
method: "post",
data: form,
});
}
export async function TestProxy() {
return await request({
url: apiPrefix + "/testProxy",
@@ -58,7 +58,7 @@ function onChange(value: string) {
<style lang="less">
.page-sys-settings {
.sys-settings-form {
width: 600px;
width: 800px;
max-width: 100%;
padding: 20px;
}
@@ -54,6 +54,12 @@
<a-form-item :label="t('certd.sys.setting.captchaType')" :name="['public', 'captchaAddonId']">
<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" :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>
</a-form-item>
<a-form-item :name="['public', 'captchaType']" class="hidden">
<a-input v-model:model-value="formState.public.captchaType"></a-input>
@@ -76,12 +82,32 @@ import { notification } from "ant-design-vue";
import { util } from "/@/utils";
import { useI18n } from "/src/locales";
import AddonSelector from "../../../certd/addon/addon-selector/index.vue";
import CaptchaInput from "/@/components/captcha/captcha-input.vue";
const { t } = useI18n();
defineOptions({
name: "SettingBase",
});
const captchaTestForm = reactive({
captcha: null,
pass: false,
});
async function doCaptchaValidate() {
if (!captchaTestForm.captcha) {
notification.error({
message: "请进行验证码验证",
});
return;
}
await api.TestCaptcha(captchaTestForm.captcha);
notification.success({
message: "校验通过",
});
captchaTestForm.pass = true;
}
const formState = reactive<Partial<SysSettings>>({
public: {
icpNo: "",
@@ -106,6 +132,14 @@ const settingsStore = useSettingStore();
const onFinish = async (form: any) => {
try {
saveLoading.value = true;
if (form.public.captchaEnabled && !captchaTestForm.pass) {
notification.error({
message: "请先通过验证码测试之后再开启登录验证码",
});
return;
}
await api.SysSettingsSave(form);
await settingsStore.loadSysSettings();
notification.success({
@@ -113,6 +147,7 @@ const onFinish = async (form: any) => {
});
} finally {
saveLoading.value = false;
captchaTestForm.pass = false;
}
};