验证码

This commit is contained in:
2026-06-10 13:17:59 +08:00
parent e3875ebcfb
commit 6cc608377b
3 changed files with 84 additions and 157 deletions

View File

@ -26,7 +26,7 @@
"axios": "^1.13.5",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.20",
"element-plus": "^2.13.2",
"element-plus": "^2.14.0",
"face-api.js": "^0.22.2",
"file-saver": "^2.0.5",
"mqtt": "^5.15.1",

View File

@ -39,8 +39,8 @@ importers:
specifier: ^1.11.20
version: 1.11.20
element-plus:
specifier: ^2.13.2
version: 2.13.2(vue@3.5.29(typescript@5.9.3))
specifier: ^2.14.0
version: 2.14.1(vue@3.5.29(typescript@5.9.3))
face-api.js:
specifier: ^0.22.2
version: 0.22.2
@ -303,9 +303,9 @@ packages:
'@casl/ability': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.1.0 || ^6.0.0
vue: ^3.0.0
'@ctrl/tinycolor@3.6.1':
resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==}
engines: {node: '>=10'}
'@ctrl/tinycolor@4.2.0':
resolution: {integrity: sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==}
engines: {node: '>=14'}
'@element-plus/icons-vue@2.3.2':
resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==}
@ -498,14 +498,14 @@ packages:
resolution: {integrity: sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@floating-ui/core@1.7.4':
resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==}
'@floating-ui/core@1.7.5':
resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
'@floating-ui/dom@1.7.5':
resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==}
'@floating-ui/dom@1.7.6':
resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==}
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@floating-ui/utils@0.2.11':
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
@ -979,9 +979,6 @@ packages:
'@types/seedrandom@2.4.27':
resolution: {integrity: sha512-YvMLqFak/7rt//lPBtEHv3M4sRNA+HGxrhFZ+DQs9K2IkYJbNwVIb8avtJfhDiuaUBX/AW0jnjv48FV8h3u9bQ==}
'@types/web-bluetooth@0.0.20':
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
'@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
@ -1213,28 +1210,32 @@ packages:
vue:
optional: true
'@vueuse/core@10.11.1':
resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==}
'@vueuse/core@14.2.1':
resolution: {integrity: sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==}
peerDependencies:
vue: ^3.5.0
'@vueuse/metadata@10.11.1':
resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==}
'@vueuse/core@14.3.0':
resolution: {integrity: sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==}
peerDependencies:
vue: ^3.5.0
'@vueuse/metadata@14.2.1':
resolution: {integrity: sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==}
'@vueuse/shared@10.11.1':
resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
'@vueuse/metadata@14.3.0':
resolution: {integrity: sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==}
'@vueuse/shared@14.2.1':
resolution: {integrity: sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==}
peerDependencies:
vue: ^3.5.0
'@vueuse/shared@14.3.0':
resolution: {integrity: sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==}
peerDependencies:
vue: ^3.5.0
'@wangeditor/basic-modules@1.1.7':
resolution: {integrity: sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==}
peerDependencies:
@ -1566,10 +1567,10 @@ packages:
electron-to-chromium@1.5.302:
resolution: {integrity: sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==}
element-plus@2.13.2:
resolution: {integrity: sha512-Zjzm1NnFXGhV4LYZ6Ze9skPlYi2B4KAmN18FL63A3PZcjhDfroHwhtM6RE8BonlOPHXUnPQynH0BgaoEfvhrGw==}
element-plus@2.14.1:
resolution: {integrity: sha512-UFnm1+BckNi+azkKJ7L32q1uXs9ekr99Z9pWTQPeDR05jqEWUwQq51ro4kZMVrANbjknX3Z7ukCZwTi2T6Tr9A==}
peerDependencies:
vue: ^3.3.0
vue: ^3.3.7
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@ -1992,8 +1993,8 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
lodash-es@4.17.23:
resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==}
lodash-es@4.18.1:
resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
lodash-unified@1.0.3:
resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==}
@ -2024,8 +2025,8 @@ packages:
lodash.toarray@4.4.0:
resolution: {integrity: sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==}
lodash@4.17.23:
resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
lodash@4.18.1:
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
@ -2626,16 +2627,8 @@ packages:
vscode-uri@3.1.0:
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
engines: {node: '>=12'}
hasBin: true
peerDependencies:
'@vue/composition-api': ^1.0.0-rc.1
vue: ^3.0.0-0 || ^2.6.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
vue-component-type-helpers@3.3.4:
resolution: {integrity: sha512-joip1uZTaQR0nD23N400gIdJ7xY+WiiiMA/BCKz842gvGBknqDQAzklUvDEhqFvvrhQY8S2ZANBMu4X70VMFGw==}
vue-eslint-parser@10.4.0:
resolution: {integrity: sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==}
@ -2982,7 +2975,7 @@ snapshots:
'@casl/ability': 6.8.0
vue: 3.5.29(typescript@5.9.3)
'@ctrl/tinycolor@3.6.1': {}
'@ctrl/tinycolor@4.2.0': {}
'@element-plus/icons-vue@2.3.2(vue@3.5.29(typescript@5.9.3))':
dependencies:
@ -3096,16 +3089,16 @@ snapshots:
'@eslint/core': 1.1.0
levn: 0.4.1
'@floating-ui/core@1.7.4':
'@floating-ui/core@1.7.5':
dependencies:
'@floating-ui/utils': 0.2.10
'@floating-ui/utils': 0.2.11
'@floating-ui/dom@1.7.5':
'@floating-ui/dom@1.7.6':
dependencies:
'@floating-ui/core': 1.7.4
'@floating-ui/utils': 0.2.10
'@floating-ui/core': 1.7.5
'@floating-ui/utils': 0.2.11
'@floating-ui/utils@0.2.10': {}
'@floating-ui/utils@0.2.11': {}
'@humanfs/core@0.19.1': {}
@ -3419,8 +3412,6 @@ snapshots:
'@types/seedrandom@2.4.27': {}
'@types/web-bluetooth@0.0.20': {}
'@types/web-bluetooth@0.0.21': {}
'@types/webgl-ext@0.0.30': {}
@ -3764,16 +3755,6 @@ snapshots:
typescript: 5.9.3
vue: 3.5.29(typescript@5.9.3)
'@vueuse/core@10.11.1(vue@3.5.29(typescript@5.9.3))':
dependencies:
'@types/web-bluetooth': 0.0.20
'@vueuse/metadata': 10.11.1
'@vueuse/shared': 10.11.1(vue@3.5.29(typescript@5.9.3))
vue-demi: 0.14.10(vue@3.5.29(typescript@5.9.3))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@vueuse/core@14.2.1(vue@3.5.29(typescript@5.9.3))':
dependencies:
'@types/web-bluetooth': 0.0.21
@ -3781,21 +3762,25 @@ snapshots:
'@vueuse/shared': 14.2.1(vue@3.5.29(typescript@5.9.3))
vue: 3.5.29(typescript@5.9.3)
'@vueuse/metadata@10.11.1': {}
'@vueuse/core@14.3.0(vue@3.5.29(typescript@5.9.3))':
dependencies:
'@types/web-bluetooth': 0.0.21
'@vueuse/metadata': 14.3.0
'@vueuse/shared': 14.3.0(vue@3.5.29(typescript@5.9.3))
vue: 3.5.29(typescript@5.9.3)
'@vueuse/metadata@14.2.1': {}
'@vueuse/shared@10.11.1(vue@3.5.29(typescript@5.9.3))':
dependencies:
vue-demi: 0.14.10(vue@3.5.29(typescript@5.9.3))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@vueuse/metadata@14.3.0': {}
'@vueuse/shared@14.2.1(vue@3.5.29(typescript@5.9.3))':
dependencies:
vue: 3.5.29(typescript@5.9.3)
'@vueuse/shared@14.3.0(vue@3.5.29(typescript@5.9.3))':
dependencies:
vue: 3.5.29(typescript@5.9.3)
'@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3)':
dependencies:
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.3)
@ -4126,25 +4111,24 @@ snapshots:
electron-to-chromium@1.5.302: {}
element-plus@2.13.2(vue@3.5.29(typescript@5.9.3)):
element-plus@2.14.1(vue@3.5.29(typescript@5.9.3)):
dependencies:
'@ctrl/tinycolor': 3.6.1
'@ctrl/tinycolor': 4.2.0
'@element-plus/icons-vue': 2.3.2(vue@3.5.29(typescript@5.9.3))
'@floating-ui/dom': 1.7.5
'@floating-ui/dom': 1.7.6
'@popperjs/core': '@sxzz/popperjs-es@2.11.8'
'@types/lodash': 4.17.24
'@types/lodash-es': 4.17.12
'@vueuse/core': 10.11.1(vue@3.5.29(typescript@5.9.3))
'@vueuse/core': 14.3.0(vue@3.5.29(typescript@5.9.3))
async-validator: 4.2.5
dayjs: 1.11.20
lodash: 4.17.23
lodash-es: 4.17.23
lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.23)(lodash@4.17.23)
lodash: 4.18.1
lodash-es: 4.18.1
lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1)
memoize-one: 6.0.0
normalize-wheel-es: 1.2.0
vue: 3.5.29(typescript@5.9.3)
transitivePeerDependencies:
- '@vue/composition-api'
vue-component-type-helpers: 3.3.4
emoji-regex@8.0.0: {}
@ -4558,13 +4542,13 @@ snapshots:
dependencies:
p-locate: 5.0.0
lodash-es@4.17.23: {}
lodash-es@4.18.1: {}
lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.23)(lodash@4.17.23):
lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1):
dependencies:
'@types/lodash-es': 4.17.12
lodash: 4.17.23
lodash-es: 4.17.23
lodash: 4.18.1
lodash-es: 4.18.1
lodash.camelcase@4.3.0: {}
@ -4580,7 +4564,7 @@ snapshots:
lodash.toarray@4.4.0: {}
lodash@4.17.23: {}
lodash@4.18.1: {}
lru-cache@10.4.3: {}
@ -5202,9 +5186,7 @@ snapshots:
vscode-uri@3.1.0: {}
vue-demi@0.14.10(vue@3.5.29(typescript@5.9.3)):
dependencies:
vue: 3.5.29(typescript@5.9.3)
vue-component-type-helpers@3.3.4: {}
vue-eslint-parser@10.4.0(eslint@10.0.2(jiti@2.6.1)):
dependencies:

View File

@ -1,15 +1,13 @@
<template>
<el-form-item class="captcha-container" label="请输入验证码" label-position="top" :prop="prop">
<el-input v-for="(item, index) in inputList" :key="index" v-model="inputList[index]" class="captcha-input-item"
:maxlength="1" @input="handleInput(index, $event)" @focus="handleFocus(index)" @keyup.delete="handleDelete(index)"
ref="inputRefs" type="text" />
<el-input-otp v-model="captchaValue" :length="length" integer @change="handleChange" @finish="handleFinish" size="large" />
<el-button type="primary" class="form-btn" v-if="showSendButton" @click="sendCode" :disabled="isCounting">{{
isCounting ? `${countdown}s后重新获取` : '发送验证码' }}</el-button>
</el-form-item>
</template>
<script setup lang="ts">
import { ref, watch, nextTick, toRefs, reactive, computed } from 'vue'
import { ref, watch, toRefs } from 'vue'
defineOptions({
name: 'Captcha'
@ -60,75 +58,35 @@ watch(
const { showSendButton } = toRefs(props)
const emit = defineEmits(['update:modelValue', 'change', 'finish', 'sendCode'])
const inputRefs = ref<Array<HTMLElement | null>>([]) // TS
const inputList = reactive<string[]>(new Array(props.length).fill(''))
// 使 el-input-otp
const captchaValue = ref('')
// modelValue
// modelValue
watch(
() => props.modelValue,
(newVal) => {
if (newVal && newVal.length === props.length && newVal !== inputList.join('')) {
for (let i = 0; i < props.length; i++) {
inputList[i] = newVal[i] || '';
}
if (newVal !== captchaValue.value) {
captchaValue.value = newVal || '';
}
},
{ immediate: true }
)
// modelValue
watch(
() => [...inputList], //
(newList) => {
const newVal = newList.join('');
emit('update:modelValue', newVal);
emit('change', newVal);
if (newVal.length === props.length) {
emit('finish', newVal);
}
},
{ deep: true }
)
//
const handleInput = (index: number, val: string) => {
//
const cleanVal = val.replace(/\D/g, '').slice(0, 1);
inputList[index] = cleanVal; //
//
if (cleanVal && index < props.length - 1) {
nextTick(() => {
inputRefs.value[index + 1]?.focus();
});
}
//
const handleChange = (val: string) => {
emit('update:modelValue', val);
emit('change', val);
}
const handleFocus = (index: number) => {
//
// inputList[index] = '';
}
//
const handleDelete = (index: number) => {
if (inputList[index] === '') {
if (index > 0) {
nextTick(() => {
inputRefs.value[index - 1]?.focus();
inputList[index - 1] = ''; //
});
}
} else {
inputList[index] = '';
}
//
const handleFinish = (val: string) => {
emit('update:modelValue', val);
emit('finish', val);
}
const clear = () => {
inputList.fill('');
captchaValue.value = '';
emit('update:modelValue', '');
nextTick(() => {
inputRefs.value[0]?.focus();
});
}
defineExpose({ clear })
@ -146,23 +104,10 @@ const sendCode = () => {
gap: 8px;
}
.captcha-input-item {
:deep(.el-input__wrapper) {
padding: 0;
justify-content: center;
border-radius: 4px;
flex-grow: 0;
}
:deep(.el-input__inner) {
text-align: center;
padding: 0;
font-size: 18px;
font-weight: 500;
width: 40px;
height: 40px;
line-height: 40px;
}
:deep(.el-input-otp) {
--el-input-otp-font-size: 18px;
--el-input-otp-width: 40px;
--el-input-otp-height: 40px;
}
}