项目初始化
This commit is contained in:
47
web/src/components/clickCaptcha/index.ts
Normal file
47
web/src/components/clickCaptcha/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { createVNode, render } from 'vue'
|
||||
import ClickCaptchaConstructor from './index.vue'
|
||||
import { shortUuid } from '/@/utils/random'
|
||||
|
||||
interface ClickCaptchaOptions {
|
||||
// 验证码弹窗的自定义class
|
||||
class?: string
|
||||
// 前端验证成功时立即清理验证码数据,不可再进行二次验证,不开启则 600s 后自动清理数据
|
||||
unset?: boolean
|
||||
// 验证失败的提示信息
|
||||
error?: string
|
||||
// 验证成功的提示信息
|
||||
success?: string
|
||||
// 验证码 API 的基础 URL,默认为当前服务端 URL(VITE_AXIOS_BASE_URL)
|
||||
apiBaseURL?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹出点击验证码
|
||||
* @param uuid 开发者自定义的唯一标识
|
||||
* @param callback 验证成功的回调,业务接口可通过 captchaInfo 进行二次验证
|
||||
* @param options
|
||||
*/
|
||||
const clickCaptcha = (uuid: string, callback?: (captchaInfo: string) => void, options: ClickCaptchaOptions = {}) => {
|
||||
const container = document.createElement('div')
|
||||
const vnode = createVNode(ClickCaptchaConstructor, {
|
||||
uuid,
|
||||
callback,
|
||||
...options,
|
||||
key: shortUuid(),
|
||||
onDestroy: () => {
|
||||
render(null, container)
|
||||
},
|
||||
})
|
||||
render(vnode, container)
|
||||
document.body.appendChild(container.firstElementChild!)
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件的 props 类型定义
|
||||
*/
|
||||
export interface Props extends ClickCaptchaOptions {
|
||||
uuid: string
|
||||
callback?: (captchaInfo: string) => void
|
||||
}
|
||||
|
||||
export default clickCaptcha
|
||||
221
web/src/components/clickCaptcha/index.vue
Normal file
221
web/src/components/clickCaptcha/index.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<div :id="uuid">
|
||||
<div class="ba-click-captcha" :class="props.class">
|
||||
<div v-if="state.loading" class="loading">{{ i18n.global.t('utils.Loading') }}</div>
|
||||
<div v-else class="captcha-img-box">
|
||||
<img
|
||||
class="captcha-img"
|
||||
@click.prevent="onRecord($event)"
|
||||
:src="state.captcha.base64"
|
||||
:alt="i18n.global.t('validate.Captcha loading failed, please click refresh button')"
|
||||
/>
|
||||
<span
|
||||
v-for="(item, index) in state.xy"
|
||||
:key="index"
|
||||
class="step"
|
||||
@click="onCancelRecord(index)"
|
||||
:style="`left:${parseFloat(item.split(',')[0]) - 13}px;top:${parseFloat(item.split(',')[1]) - 13}px`"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="captcha-prompt" v-if="state.tip">
|
||||
{{ state.tip }}
|
||||
</div>
|
||||
<div v-else class="captcha-prompt">
|
||||
{{ i18n.global.t('validate.Please click') }}
|
||||
<span v-for="(text, index) in state.captcha.text" :key="index" :class="state.xy.length > index ? 'clicaptcha-clicked' : ''">
|
||||
{{ text }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="captcha-refresh-box">
|
||||
<div class="captcha-refresh-line captcha-refresh-line-l"></div>
|
||||
<i class="fa fa-refresh captcha-refresh-btn" :title="i18n.global.t('Refresh')" @click="load"></i>
|
||||
<div class="captcha-refresh-line captcha-refresh-line-r"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ba-layout-shade" @click="onClose"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from 'vue'
|
||||
import { Props } from './index'
|
||||
import { checkClickCaptcha, getCaptchaData } from '/@/api/common'
|
||||
import { i18n } from '/@/lang'
|
||||
import { SYSTEM_ZINDEX } from '/@/stores/constant/common'
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
uuid: '',
|
||||
callback: () => {},
|
||||
class: '',
|
||||
unset: false,
|
||||
error: i18n.global.t('validate.The correct area is not clicked, please try again!'),
|
||||
success: i18n.global.t('validate.Verification is successful!'),
|
||||
apiBaseURL: '',
|
||||
})
|
||||
|
||||
const state: {
|
||||
loading: boolean
|
||||
xy: string[]
|
||||
tip: string
|
||||
captcha: {
|
||||
id: string
|
||||
text: string
|
||||
base64: string
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
} = reactive({
|
||||
loading: true,
|
||||
xy: [],
|
||||
tip: '',
|
||||
captcha: {
|
||||
id: '',
|
||||
text: '',
|
||||
base64: '',
|
||||
width: 350,
|
||||
height: 200,
|
||||
},
|
||||
})
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'destroy'): void
|
||||
}>()
|
||||
|
||||
const load = () => {
|
||||
state.loading = true
|
||||
getCaptchaData(props.uuid, props.apiBaseURL).then((res) => {
|
||||
state.xy = []
|
||||
state.tip = ''
|
||||
state.loading = false
|
||||
state.captcha = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const onRecord = (event: MouseEvent) => {
|
||||
if (state.xy.length < state.captcha.text.length) {
|
||||
state.xy.push(event.offsetX + ',' + event.offsetY)
|
||||
if (state.xy.length == state.captcha.text.length) {
|
||||
const captchaInfo = [state.xy.join('-'), (event.target as HTMLImageElement).width, (event.target as HTMLImageElement).height].join(';')
|
||||
checkClickCaptcha(props.uuid, captchaInfo, props.unset, props.apiBaseURL)
|
||||
.then(() => {
|
||||
state.tip = props.success
|
||||
setTimeout(() => {
|
||||
props.callback?.(captchaInfo)
|
||||
onClose()
|
||||
}, 1500)
|
||||
})
|
||||
.catch(() => {
|
||||
state.tip = props.error
|
||||
setTimeout(() => {
|
||||
load()
|
||||
}, 1500)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onCancelRecord = (index: number) => {
|
||||
state.xy.splice(index, 1)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
emits('destroy')
|
||||
}
|
||||
|
||||
const captchaBoxTop = computed(() => (state.captcha.height + 200) / 2 + 'px')
|
||||
const captchaBoxLeft = computed(() => (state.captcha.width + 24) / 2 + 'px')
|
||||
|
||||
load()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ba-click-captcha {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--el-border-color-extra-light);
|
||||
background-color: var(--el-color-white);
|
||||
position: fixed;
|
||||
z-index: v-bind('SYSTEM_ZINDEX');
|
||||
left: calc(50% - v-bind('captchaBoxLeft'));
|
||||
top: calc(50% - v-bind('captchaBoxTop'));
|
||||
border-radius: 10px;
|
||||
box-shadow:
|
||||
0 0 0 1px hsla(0, 0%, 100%, 0.3) inset,
|
||||
0 0.5em 1em rgba(0, 0, 0, 0.6);
|
||||
.loading {
|
||||
color: var(--el-color-info);
|
||||
width: 350px;
|
||||
text-align: center;
|
||||
line-height: 200px;
|
||||
}
|
||||
.captcha-img-box {
|
||||
position: relative;
|
||||
.captcha-img {
|
||||
width: v-bind('state.captcha.width') px;
|
||||
height: v-bind('state.captcha.height') px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.step {
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
font-size: var(--el-font-size-small);
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
color: var(--el-color-white);
|
||||
border: 1px solid var(--el-border-color-extra-light);
|
||||
background-color: var(--el-color-primary);
|
||||
border-radius: 30px;
|
||||
box-shadow: 0 0 10px var(--el-color-white);
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.captcha-prompt {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
font-size: var(--el-font-size-base);
|
||||
text-align: center;
|
||||
color: var(--el-color-info);
|
||||
span {
|
||||
margin-left: 10px;
|
||||
font-size: var(--el-font-size-medium);
|
||||
font-weight: bold;
|
||||
color: var(--el-color-error);
|
||||
&.clicaptcha-clicked {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
.captcha-refresh-box {
|
||||
position: relative;
|
||||
margin-top: 10px;
|
||||
.captcha-refresh-line {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
width: 140px;
|
||||
height: 1px;
|
||||
background-color: #ccc;
|
||||
}
|
||||
.captcha-refresh-line-l {
|
||||
left: 5px;
|
||||
}
|
||||
.captcha-refresh-line-r {
|
||||
right: 5px;
|
||||
}
|
||||
.captcha-refresh-btn {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 32px;
|
||||
color: var(--el-color-info);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user