<!-- 图形验证码组件2024-03-26
Props:
width: number 组件宽度
height: number 组件高度
Events:
@change: (code: string) => void 当验证码改变时触发该事件
Methods:
generateCode: () => void 生成并绘制验证码
--------------------------------------------------------------
Example:
<template>
<input v-model="inputValue" />
<ImgIdentifier ref="identifierRef" @change="onChange" :width="100" :height="34" />
</template>
<script setup>
/** ...省略... */
import ImgIdentifier, {IdentifierRef} from 'xxx/ImgIdentifier.vue'
const identifierRef = ref<IdentifierRef>()
const inputValue = ref('')
const code = ref('')
// 取得验证码
const onChange = (val: string) => code.value = val
// 判断输入值和验证码是否一致(根据实际情况看要不要忽略大小写?)
const isValid = () => inputValue.value?.toLowerCase() === code.value?.toLowerCase()
// 主动调用重绘方法(比如可以在输入错误后重绘一下验证码?)
const refreshCode = () => identifierRef.value?.generateCode()
</script>
-->
<template>
<div class="img-identifier" :style="`width:${width}px;height:${height}px`">
<canvas
ref="canvasRef"
:width="width"
:height="height"
@click="generateCode"
/>
</div>
</template>
<script setup lang="ts">
import { nextTick, ref, watch } from 'vue'
export interface IdentifierRef {
/** 生成并绘制验证码 */
generateCode: () => void
}
const emit = defineEmits<{
// 每当验证码生成时emit出该验证码
(event: 'change', code: string): void
}>()
const props = defineProps<{
/** @param width 组件宽度 */
width: number
/** @param height 组件宽度 */
height: number
}>()
const canvasRef = ref<HTMLCanvasElement>()
const pool = ref('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890')
const code = ref('')
const generateCode = () => {
code.value = draw()
emit('change', code.value)
}
// 暴露出验证码生成方法,可以主动调用来重绘验证码
defineExpose({ generateCode })
// 组件初始化时或者宽高改变时重绘验证码
watch(
[() => props.width, () => props.height],
async () => {
await nextTick()
generateCode()
},
{ immediate: true },
)
// 随机数
const randomNum = (min: number, max: number) => {
return parseInt((Math.random() * (max - min) + min + '') as string)
}
// 随机颜色
const randomColor = (min: number, max: number) => {
const r = randomNum(min, max)
const g = randomNum(min, max)
const b = randomNum(min, max)
return `rgb(${r},${g},${b})`
}
// 绘制验证码
const draw = () => {
const ctx = canvasRef.value?.getContext('2d') as CanvasRenderingContext2D
const { length } = pool.value
let result = ''
ctx.fillStyle = randomColor(180, 230) // 填充颜色
ctx.fillRect(0, 0, props.width, props.height) // 填充的位置
// 生成随机的4个字
for (let i = 0; i < 4; i++) {
const text = pool.value[randomNum(0, length)]
result += text
const fontSize = randomNum(18, 40) // 随机的字体大小
const deg = randomNum(-45, 45) // 随机的旋转角度
ctx.font = fontSize + 'px Simhei' // 定义字体
ctx.textBaseline = 'middle' // 定义对齐方式
ctx.fillStyle = randomColor(80, 150) // 填充不同的颜色
ctx.save() // 保存当前的状态
ctx.translate((props.width / 4) * i + 10, 15) // 平移
ctx.rotate((deg * Math.PI) / 180) // 旋转
ctx.fillText(text, -10, 0) // 在画布上绘制填色文本
ctx.restore() // 恢复canvas之前保存的状态,防止save后执行的操作对后续绘制有影响
}
// 随机5条干扰线
for (let i = 0; i < 5; i++) {
ctx.beginPath()
ctx.moveTo(randomNum(0, props.width), randomNum(0, props.height))
ctx.lineTo(randomNum(0, props.width), randomNum(0, props.height))
ctx.strokeStyle = randomColor(180, 230)
ctx.closePath()
ctx.stroke()
}
// 随机40个干扰点
for (let i = 0; i < 40; i++) {
ctx.beginPath()
ctx.arc(
randomNum(0, props.width),
randomNum(0, props.height),
1,
0,
2 * Math.PI,
)
ctx.closePath()
ctx.fillStyle = randomColor(150, 200)
ctx.fill()
}
return result
}
</script>
<style scoped>
.img-identifier {
display: inline-block;
}
.img-identifier > canvas {
cursor: pointer;
}
</style>