react框架实践——滑动验证组件

183 阅读3分钟

代码结构

  1. 接口
  2. 主画布
  3. 滑块验证视图区域
  4. 滑块拖动条范围区
  5. 滑块
  6. 滑动验证组件组装

接口schema.tsx

import {
    TStringDefaultType,
    IStringConfigType
  } from "@/components/FormComponents/types";
  export type TSliderCaptchaEditData = Array<
     IStringConfigType
  >;
  export interface ISliderCaptchaConfig {
    width: TStringDefaultType;
    height: TStringDefaultType;
  }
  
  export interface ISliderCaptchaSchema {
    editData: TSliderCaptchaEditData;
    config: ISliderCaptchaConfig;
  }
  
  const SliderCaptcha: ISliderCaptchaSchema = {
    editData: [
      {
        key: "width",
        name: "宽度",
        type: "String"
      },
      {
        key: "height",
        name: "高度",
        type: "String"
      }
    ],
    config: {
      width: '300px',
      height: '200px'
    }
  };
  
  export default SliderCaptcha;
  

主画布

const MainCanvas = React.forwardRef<HTMLCanvasElement, ISliderCaptchaConfig>((props, ref) => {
  return (
    <canvas width={props.width} height={props.height} ref={ref} />
  )
})

滑块验证视图区域

const SliderCanvas = React.forwardRef<HTMLCanvasElement, ISliderCaptchaConfig>((props, ref) => {
  return (
    <canvas style={{position: 'absolute', top: '0', left: '0'}} width={props.width} height={props.height} ref={ref} />
  )
})

滑块拖动条范围区

const SliderBar = React.forwardRef((props:ISliderCaptchaConfig, ref) => {
  return <div style={{width: props.width, height: '40px', background: '#f0f0f0', position: 'absolute', top: props.height}} />
})

滑块

const Slider = React.forwardRef<HTMLDivElement, Sliderprop>((props, ref) => {
  return (
    <div onMouseDown={props.onMouseDown} onMouseMove={props.onMouseMove} onMouseUp={props.onMouseUp} ref={ref} style={{width: '50px', height: '40px', background: '#fff', border: '1px solid #ccc', position: 'absolute', cursor: 'pointer', top: props.height, left: '0'}} />
  )
})

滑动验证组件组装

  1. 初始化状态
  2. 定义drawCanvasimg函数
  3. 定义mousedown,mousemove,mouseup事件
const SliderCaptcha = ( props:ISliderCaptchaConfig) => {
    const { width,height } = props;
  // dom
  const canvasRef = useRef<HTMLCanvasElement>(null)
  const sliderCanvasRef = useRef<HTMLCanvasElement>(null)
  const sliderRef = useRef<HTMLDivElement>(null)
  // 拖动状态
  const draggingRef = useRef(false)
  // 验证状态
  const VerifiedRef = useRef(false)
  // 滑块偏移量
  const offsetRef = useRef(0)
  // 目标偏移量
  const targetOffsetRef = useRef(0)
  // 开始位置
  let startX = 0
  // 滑块位置
  let sliderLeft = 0
  const classNames = 'captcha-container'
  //初始化画布渲染
  useEffect(() => {
    drawCanvasimg()
  }, [])
  //此处画布背景使用image函数,并且onload异步加载
  const drawCanvasimg = useCallback(() => {
    let img = new window.Image()
    img.src = sliderImg
    img.onload = () => {
      drawCanvas(img)
    }
  }, [])
  //ctx主画布绘制背景,绘制空缺区域,
  //sliderCtx绘制滑块并置于初始位置,
  const drawCanvas = (img: HTMLImageElement) => {
    if (!canvasRef.current || !sliderCanvasRef.current || !sliderRef.current) return;
    
    const ctx = canvasRef.current.getContext('2d')
    const sliderCtx = sliderCanvasRef.current.getContext('2d')
    if (!ctx || !sliderCtx) return;
    
    // 清除画布
    sliderCtx.clearRect(0, 0, sliderCanvasRef.current.width, sliderCanvasRef.current.height)
    // 绘制背景
    ctx.drawImage(img, 0, 0, canvasRef.current.width, canvasRef.current.height)
    // 绘制目标区域
    if (!targetOffsetRef.current) {
      targetOffsetRef.current = Math.floor(Math.random() * (canvasRef.current.width - sliderRef.current.clientWidth))
    //   targetOffsetRef.current = 123
    }
    ctx.fillStyle = '#e8e8e8' // semi-transparent white
    ctx.fillRect(targetOffsetRef.current, (canvasRef.current.height - sliderRef.current.clientHeight) / 2,
      sliderRef.current.clientWidth, sliderRef.current.clientHeight)
    // 绘制滑块
    sliderCtx.drawImage(img, targetOffsetRef.current, (canvasRef.current.height - sliderRef.current.clientHeight) / 2,
      sliderRef.current.clientWidth, sliderRef.current.clientHeight, offsetRef.current, (canvasRef.current.height - sliderRef.current.clientHeight) / 2,
      sliderRef.current.clientWidth, sliderRef.current.clientHeight)
  }
//滑块按下事件
//记录滑块初识位置startX,滑块左侧偏移量sliderLeft
  const slidermouseDown = (e: React.MouseEvent | React.TouchEvent) => {
    if (VerifiedRef.current) return
    console.log('mouseDown')
    draggingRef.current = true
    startX = 'clientX' in e ? e.clientX : e.touches[0].clientX
    sliderLeft = sliderRef.current?.offsetLeft || 0
  }
  //滑块移动事件
  //记录触发事件e移动的距离,并计算滑块最新左侧偏移量,重绘画布
  const onMouseMove = (e: React.MouseEvent | React.TouchEvent) => {
    if (!draggingRef.current || !sliderRef.current) return;

    const currentX = 'clientX' in e ? e.clientX : e.touches[0].clientX
    let moveX = currentX - startX
    let newLeft = sliderLeft + moveX

    // 限制滑动范围
    if (newLeft < 0) newLeft = 0
    if (newLeft > parseInt(width) - sliderRef.current.clientWidth) {
      newLeft = parseInt(width) - sliderRef.current.clientWidth
    } 

    offsetRef.current = newLeft
    sliderRef.current.style.left = `${newLeft}px`
    drawCanvasimg()
  }
  //滑块松开事件
  //判断滑块是否和预留空缺重合
  const onMouseUp = () => {
    if (!draggingRef.current || !sliderRef.current) return;
    draggingRef.current = false

    // 验证是否滑到正确位置
    if (Math.abs(offsetRef.current - targetOffsetRef.current) < 5) {
      VerifiedRef.current = true
      sliderRef.current.style.background = '#91d5ff'
      alert('验证成功!')
    //   if (typeof onSuccess === 'function') {
    //     onSuccess()
    //   }
    } else {
      offsetRef.current = 0
      sliderRef.current.style.left = '0'
      drawCanvasimg()
    }
  }
  return (
    <div className={classNames} >
      <MainCanvas ref={canvasRef} width={width} height={height} />
      <SliderCanvas ref={sliderCanvasRef} width={width} height={height} />
      <SliderBar width={width} height={height} />
      <Slider onMouseDown={slidermouseDown} onMouseMove={onMouseMove} onMouseUp={onMouseUp} ref={sliderRef} width={width} height={height} />
    </div>
  )
}

drawImage函数用法

drawImage 是 HTML5 Canvas API 中的一个方法,用于在画布上绘制图像。它可以绘制完整的图像,也可以绘制图像的某一部分,并且支持缩放和裁剪。以下是 drawImage 的详细用法:

  1. 绘制完整图像
context.drawImage(image, dx, dy);
  • image:要绘制的图像(HTMLImageElementHTMLCanvasElement 或 HTMLVideoElement)。
  • dxdy:图像在画布上的目标坐标(左上角)。
  1. 绘制缩放图像
context.drawImage(image, dx, dy, dWidth, dHeight);
  • dWidthdHeight:图像在画布上的目标宽度和高度(缩放)。
  1. 绘制图像的某一部分并缩放
context.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
  • sxsy:源图像中裁剪区域的左上角坐标。
  • sWidthsHeight:源图像中裁剪区域的宽度和高度。
  • dxdy:裁剪区域在画布上的目标坐标。
  • dWidthdHeight:裁剪区域在画布上的目标宽度和高度(缩放)。

index.tsx

import React, { useCallback, useEffect, useRef, useMemo } from 'react'
import { ISliderCaptchaConfig } from "./schema";
import sliderImg from "@/assets/test.png";

import './index.less'

const MainCanvas = React.forwardRef<HTMLCanvasElement, ISliderCaptchaConfig>((props, ref) => {
  return (
    <canvas width={props.width} height={props.height} ref={ref} />
  )
})



const SliderCanvas = React.forwardRef<HTMLCanvasElement, ISliderCaptchaConfig>((props, ref) => {
 .......
})


const Slider = React.forwardRef<HTMLDivElement, Sliderprop>((props, ref) => {
  ......
})

interface Sliderprop {
  width: string,
  height: string,
  onMouseDown: React.MouseEventHandler<HTMLDivElement>,
  onMouseMove: React.MouseEventHandler<HTMLDivElement>,
  onMouseUp: React.MouseEventHandler<HTMLDivElement>
}

const SliderBar = React.forwardRef((props:ISliderCaptchaConfig, ref) => {
  ......
})



const SliderCaptcha = ( props:ISliderCaptchaConfig) => {
   ......
}


export default SliderCaptcha

index.less

.captcha-container {
    position: relative;
    text-align: center;
}

最终效果

Snipaste_2025-02-03_13-32-17.png