图形验证码

可实现字母、数字、中文等多种验证码类型

该组件不负责生成验证码 只用于显示验证码

import React, { useEffect, useState } from 'react'

interface CaptchaProps {
  // 父组件传过来的值 可通过接口或随机生成
  content: string
  // 刷新验证码的方法 必传
  onClick: () => void
  // 是否需要干扰点
  point?: boolean
}

const Captcha = (props: CaptchaProps) => {
  const { content, onClick, point } = props
  const [options, setOptions] = useState({
    id: 'verifyCode', // 容器Id
    canvasId: 'verifyCanvas', // canvas的ID
    width: 150, // 默认canvas宽度
    height: 47, // 默认canvas高度
    code: '',
  })

  useEffect(() => {
    _init()
  }, [])
  
  useEffect(() => {
    refresh()
  }, [content])

  const _init = () => {
    const con = document.getElementById(options.id)!
    const canvas: HTMLCanvasElement = document.createElement('canvas')
    options.width = con.offsetWidth > 0 ? con.offsetWidth : 150
    options.height = con.offsetHeight > 0 ? con.offsetHeight : 47
    canvas.id = options.canvasId
    canvas.width = options.width
    canvas.height = options.height
    canvas.style.cursor = 'pointer'
    canvas.innerHTML = '您的浏览器版本不支持canvas'
    con.appendChild(canvas)
    // 可调用刷新验证码的方法
    con.onclick = onClick
  }
  
  const refresh = () => {
    let txtArr
    options.code = ''
    const canvas: any = document.getElementById(options.canvasId)
    let ctx = null
    if (canvas.getContext) {
      ctx = canvas.getContext('2d')
    } else {
      return
    }
    ctx.clearRect(0, 0, options.width, options.height)
    ctx.textBaseline = 'middle'

    ctx.fillStyle = randomColor(180, 240)
    ctx.fillStyle = 'rgba(206, 244, 196)' // 背景色
    ctx.fillRect(0, 0, options.width, options.height)
    
    txtArr = content

    for (let i = 0; i < content.length; i++) {
      const txt = txtArr[i]
      options.code += txt
      ctx.font = randomNum(options.height / 2, options.height) + 'px SimHei' // 随机生成字体大小
      ctx.fillStyle = randomColor(50, 160) // 随机生成字体颜色
      ctx.shadowOffsetX = randomNum(-3, 3)
      ctx.shadowOffsetY = randomNum(-3, 3)
      ctx.shadowBlur = randomNum(-3, 3)
      ctx.shadowColor = 'rgba(0, 0, 0, 0.3)'
      const x = getX(i)
      const y = options.height / 2
      const deg = randomNum(-30, 30)
      /** 设置旋转角度和坐标原点**/
      ctx.translate(x, y)
      ctx.rotate((deg * Math.PI) / 180)
      ctx.fillText(txt, 0, 0)
      /** 恢复旋转角度和坐标原点**/
      ctx.rotate((-deg * Math.PI) / 180)
      ctx.translate(-x, -y)
    }
    /** 绘制干扰线**/
    for (let i = 0; i < 4; i++) {
      ctx.strokeStyle = randomColor(40, 180)
      ctx.beginPath()
      ctx.moveTo(randomNum(0, options.width), randomNum(0, options.height))
      ctx.lineTo(randomNum(0, options.width), randomNum(0, options.height))
      ctx.stroke()
    }
    // 干扰点
    if (point) {
      for (let i = 0; i < 100; i++) {
        ctx.fillStyle = randomColor(0, 255)
        ctx.beginPath()
        ctx.arc(randomNum(0, options.width), randomNum(0, options.height), 1, 0, 2 * Math.PI)
        ctx.fill()
      }
    }
  }

  /** 生成一个随机数**/
  const randomNum = (min: number, max: number) => {
    return Math.floor(Math.random() * (max - min) + min)
  }
  
  /** 获取每个字符的x位置 使验证码在图片中均匀分布 */
  const getX = (i: number) => {
    // 计算平均每个字符的宽度
    const avgWidth = (options.width - 10) / content.length
    // 计算当前字符的x位置
    return avgWidth * i + avgWidth / 2
    
  }

  /** 生成一个随机色**/
  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 + ')'
  }

  return (
    <div
      id="verifyCode"
      className="w-full h-10 inline-block top-0 right-0"
    />
  )
}

export default Captcha

验证码的div采用tailwindcss, 如果项目中没有使用tailwindcss, 可以自行修改样式

w-full h-10 inline-block top-0 right-0转为style:

return (
  <div
    id="verifyCode"
    style={{ width: '100%', height: '40px', display: 'inline-block', top: 0, right: 0 }}
  />
)

效果如下

captcha

Last Updated:
Contributors: huangdingxin