react的滑动图片验证码组件的示例代码


Posted in Javascript onFebruary 27, 2019

业务需求,需要在系统登陆的时候,使用“滑动图片验证码”,来验证操作的不是机器人。

效果图

react的滑动图片验证码组件的示例代码

使用方式

在一般的页面组件引用即可。onReload这个函数一般是用来请求后台图片的。

class App extends Component {
  state = {
    url: ""
  }

  componentDidMount() {
    this.setState({ url: getImage() })
  }

  onReload = () => {
    this.setState({ url: getImage() })
  }
  render() {
    return (
      <div>
        <ImageCode
          imageUrl={this.state.url}
          onReload={this.onReload}
          onMatch={() => {
            console.log("code is match")
          }}
        />
      </div>
    )
  }
}

上代码

// index.js
/**
 * @name ImageCode
 * @desc 滑动拼图验证
 * @author darcrand
 * @version 2019-02-26
 *
 * @param {String} imageUrl 图片的路径
 * @param {Number} imageWidth 展示图片的宽带
 * @param {Number} imageHeight 展示图片的高带
 * @param {Number} fragmentSize 滑动图片的尺寸
 * @param {Function} onReload 当点击'重新验证'时执行的函数
 * @param {Function} onMath 匹配成功时执行的函数
 * @param {Function} onError 匹配失败时执行的函数
 */

import React from "react"

import "./styles.css"

const icoSuccess = require("./icons/success.png")
const icoError = require("./icons/error.png")
const icoReload = require("./icons/reload.png")
const icoSlider = require("./icons/slider.png")

const STATUS_LOADING = 0 // 还没有图片
const STATUS_READY = 1 // 图片渲染完成,可以开始滑动
const STATUS_MATCH = 2 // 图片位置匹配成功
const STATUS_ERROR = 3 // 图片位置匹配失败

const arrTips = [{ ico: icoSuccess, text: "匹配成功" }, { ico: icoError, text: "匹配失败" }]

// 生成裁剪路径
function createClipPath(ctx, size = 100, styleIndex = 0) {
  const styles = [
    [0, 0, 0, 0],
    [0, 0, 0, 1],
    [0, 0, 1, 0],
    [0, 0, 1, 1],
    [0, 1, 0, 0],
    [0, 1, 0, 1],
    [0, 1, 1, 0],
    [0, 1, 1, 1],
    [1, 0, 0, 0],
    [1, 0, 0, 1],
    [1, 0, 1, 0],
    [1, 0, 1, 1],
    [1, 1, 0, 0],
    [1, 1, 0, 1],
    [1, 1, 1, 0],
    [1, 1, 1, 1]
  ]
  const style = styles[styleIndex]

  const r = 0.1 * size
  ctx.save()
  ctx.beginPath()
  // left
  ctx.moveTo(r, r)
  ctx.lineTo(r, 0.5 * size - r)
  ctx.arc(r, 0.5 * size, r, 1.5 * Math.PI, 0.5 * Math.PI, style[0])
  ctx.lineTo(r, size - r)
  // bottom
  ctx.lineTo(0.5 * size - r, size - r)
  ctx.arc(0.5 * size, size - r, r, Math.PI, 0, style[1])
  ctx.lineTo(size - r, size - r)
  // right
  ctx.lineTo(size - r, 0.5 * size + r)
  ctx.arc(size - r, 0.5 * size, r, 0.5 * Math.PI, 1.5 * Math.PI, style[2])
  ctx.lineTo(size - r, r)
  // top
  ctx.lineTo(0.5 * size + r, r)
  ctx.arc(0.5 * size, r, r, 0, Math.PI, style[3])
  ctx.lineTo(r, r)

  ctx.clip()
  ctx.closePath()
}

class ImageCode extends React.Component {
  static defaultProps = {
    imageUrl: "",
    imageWidth: 500,
    imageHeight: 300,
    fragmentSize: 80,
    onReload: () => {},
    onMatch: () => {},
    onError: () => {}
  }

  state = {
    isMovable: false,
    offsetX: 0, //图片截取的x
    offsetY: 0, //图片截取的y
    startX: 0, // 开始滑动的 x
    oldX: 0,
    currX: 0, // 滑块当前 x,
    status: STATUS_LOADING,
    showTips: false,
    tipsIndex: 0
  }

  componentDidUpdate(prevProps) {
    // 当父组件传入新的图片后,开始渲染
    if (!!this.props.imageUrl && prevProps.imageUrl !== this.props.imageUrl) {
      this.renderImage()
    }
  }

  renderImage = () => {
    // 初始化状态
    this.setState({ status: STATUS_LOADING })

    // 创建一个图片对象,主要用于canvas.context.drawImage()
    const objImage = new Image()

    objImage.addEventListener("load", () => {
      const { imageWidth, imageHeight, fragmentSize } = this.props

      // 先获取两个ctx
      const ctxShadow = this.refs.shadowCanvas.getContext("2d")
      const ctxFragment = this.refs.fragmentCanvas.getContext("2d")

      // 让两个ctx拥有同样的裁剪路径(可滑动小块的轮廓)
      const styleIndex = Math.floor(Math.random() * 16)
      createClipPath(ctxShadow, fragmentSize, styleIndex)
      createClipPath(ctxFragment, fragmentSize, styleIndex)

      // 随机生成裁剪图片的开始坐标
      const clipX = Math.floor(fragmentSize + (imageWidth - 2 * fragmentSize) * Math.random())
      const clipY = Math.floor((imageHeight - fragmentSize) * Math.random())

      // 让小块绘制出被裁剪的部分
      ctxFragment.drawImage(objImage, clipX, clipY, fragmentSize, fragmentSize, 0, 0, fragmentSize, fragmentSize)

      // 让阴影canvas带上阴影效果
      ctxShadow.fillStyle = "rgba(0, 0, 0, 0.5)"
      ctxShadow.fill()

      // 恢复画布状态
      ctxShadow.restore()
      ctxFragment.restore()

      // 设置裁剪小块的位置
      this.setState({ offsetX: clipX, offsetY: clipY })

      // 修改状态
      this.setState({ status: STATUS_READY })
    })

    objImage.src = this.props.imageUrl
  }

  onMoveStart = e => {
    if (this.state.status !== STATUS_READY) {
      return
    }

    // 记录滑动开始时的绝对坐标x
    this.setState({ isMovable: true, startX: e.clientX })
  }

  onMoving = e => {
    if (this.state.status !== STATUS_READY || !this.state.isMovable) {
      return
    }
    const distance = e.clientX - this.state.startX
    let currX = this.state.oldX + distance

    const minX = 0
    const maxX = this.props.imageWidth - this.props.fragmentSize
    currX = currX < minX ? 0 : currX > maxX ? maxX : currX

    this.setState({ currX })
  }

  onMoveEnd = () => {
    if (this.state.status !== STATUS_READY || !this.state.isMovable) {
      return
    }
    // 将旧的固定坐标x更新
    this.setState(pre => ({ isMovable: false, oldX: pre.currX }))

    const isMatch = Math.abs(this.state.currX - this.state.offsetX) < 5
    if (isMatch) {
      this.setState(pre => ({ status: STATUS_MATCH, currX: pre.offsetX }), this.onShowTips)
      this.props.onMatch()
    } else {
      this.setState({ status: STATUS_ERROR }, () => {
        this.onReset()
        this.onShowTips()
      })
      this.props.onError()
    }
  }

  onReset = () => {
    const timer = setTimeout(() => {
      this.setState({ oldX: 0, currX: 0, status: STATUS_READY })
      clearTimeout(timer)
    }, 1000)
  }

  onReload = () => {
    if (this.state.status !== STATUS_READY && this.state.status !== STATUS_MATCH) {
      return
    }
    const ctxShadow = this.refs.shadowCanvas.getContext("2d")
    const ctxFragment = this.refs.fragmentCanvas.getContext("2d")

    // 清空画布
    ctxShadow.clearRect(0, 0, this.props.fragmentSize, this.props.fragmentSize)
    ctxFragment.clearRect(0, 0, this.props.fragmentSize, this.props.fragmentSize)

    this.setState(
      {
        isMovable: false,
        offsetX: 0, //图片截取的x
        offsetY: 0, //图片截取的y
        startX: 0, // 开始滑动的 x
        oldX: 0,
        currX: 0, // 滑块当前 x,
        status: STATUS_LOADING
      },
      this.props.onReload
    )
  }

  onShowTips = () => {
    if (this.state.showTips) {
      return
    }

    const tipsIndex = this.state.status === STATUS_MATCH ? 0 : 1
    this.setState({ showTips: true, tipsIndex })
    const timer = setTimeout(() => {
      this.setState({ showTips: false })
      clearTimeout(timer)
    }, 2000)
  }

  render() {
    const { imageUrl, imageWidth, imageHeight, fragmentSize } = this.props
    const { offsetX, offsetY, currX, showTips, tipsIndex } = this.state
    const tips = arrTips[tipsIndex]

    return (
      <div className="image-code" style={{ width: imageWidth }}>
        <div className="image-container" style={{ height: imageHeight, backgroundImage: `url("${imageUrl}")` }}>
          <canvas
            ref="shadowCanvas"
            className="canvas"
            width={fragmentSize}
            height={fragmentSize}
            style={{ left: offsetX + "px", top: offsetY + "px" }}
          />
          <canvas
            ref="fragmentCanvas"
            className="canvas"
            width={fragmentSize}
            height={fragmentSize}
            style={{ top: offsetY + "px", left: currX + "px" }}
          />

          <div className={showTips ? "tips-container--active" : "tips-container"}>
            <i className="tips-ico" style={{ backgroundImage: `url("${tips.ico}")` }} />
            <span className="tips-text">{tips.text}</span>
          </div>
        </div>

        <div className="reload-container">
          <div className="reload-wrapper" onClick={this.onReload}>
            <i className="reload-ico" style={{ backgroundImage: `url("${icoReload}")` }} />
            <span className="reload-tips">刷新验证</span>
          </div>
        </div>

        <div className="slider-wrpper" onMouseMove={this.onMoving} onMouseLeave={this.onMoveEnd}>
          <div className="slider-bar">按住滑块,拖动完成拼图</div>
          <div
            className="slider-button"
            onMouseDown={this.onMoveStart}
            onMouseUp={this.onMoveEnd}
            style={{ left: currX + "px", backgroundImage: `url("${icoSlider}")` }}
          />
        </div>
      </div>
    )
  }
}

export default ImageCode
// styles.css

.image-code {
  padding: 10px;
  user-select: none;
}

.image-container {
  position: relative;
  background-color: #ddd;
}

.canvas {
  position: absolute;
  top: 0;
  left: 0;
}

.reload-container {
  margin: 20px 0;
}

.reload-wrapper {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
}

.reload-ico {
  width: 20px;
  height: 20px;
  margin-right: 10px;
  background: center/cover no-repeat;
}

.reload-tips {
  font-size: 14px;
  color: #666;
}

.slider-wrpper {
  position: relative;
  margin: 10px 0;
}

.slider-bar {
  padding: 10px;
  font-size: 14px;
  text-align: center;
  color: #999;
  background-color: #ddd;
}

.slider-button {
  position: absolute;
  top: 50%;
  left: 0;
  width: 50px;
  height: 50px;
  border-radius: 25px;
  transform: translateY(-50%);
  cursor: pointer;
  background: #fff center/80% 80% no-repeat;
  box-shadow: 0 2px 10px 0 #333;
}

/* 提示信息 */
.tips-container,
.tips-container--active {
  position: absolute;
  top: 50%;
  left: 50%;
  display: flex;
  align-items: center;
  padding: 10px;
  transform: translate(-50%, -50%);
  transition: all 0.25s;
  background: #fff;
  border-radius: 5px;

  visibility: hidden;
  opacity: 0;
}

.tips-container--active {
  visibility: visible;
  opacity: 1;
}

.tips-ico {
  width: 20px;
  height: 20px;
  margin-right: 10px;
  background: center/cover no-repeat;
}

.tips-text {
  color: #666;
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
js获取控件位置以及不同浏览器中的差别介绍
Aug 08 Javascript
使用Meteor配合Node.js编写实时聊天应用的范例
Jun 23 Javascript
js验证框架之RealyEasy验证详解
Jun 08 Javascript
BootStrap初学者对弹出框和进度条的使用感觉
Jun 27 Javascript
JavaScript交换两个变量值的七种解决方案
Dec 01 Javascript
layer实现弹窗提交信息
Dec 12 Javascript
js实现适配不同的屏幕大小
Apr 10 Javascript
vue2.0 better-scroll 实现移动端滑动的示例代码
Jan 25 Javascript
vue利用axios来完成数据的交互
Mar 23 Javascript
Node.js 使用AngularJS的方法示例
May 11 Javascript
详解基于Vue2.0实现的移动端弹窗(Alert, Confirm, Toast)组件
Aug 02 Javascript
Vuex的实战使用详解
Oct 31 Javascript
原生JS forEach()和map()遍历的区别、兼容写法及jQuery $.each、$.map遍历操作
Feb 27 #jQuery
js/jquery遍历对象和数组的方法分析【forEach,map与each方法】
Feb 27 #jQuery
spring+angular实现导出excel的实现代码
Feb 27 #Javascript
react native 原生模块桥接的简单说明小结
Feb 26 #Javascript
JavaScript实现获取两个排序数组的中位数算法示例
Feb 26 #Javascript
小程序hover-class点击态效果实现
Feb 26 #Javascript
JS实现计算小于非负数n的素数的数量算法示例
Feb 26 #Javascript
You might like
php中变量及部分适用方法
2008/03/27 PHP
ECSHOP完美解决Deprecated: preg_replace()报错的问题
2016/05/17 PHP
php多线程并发实现方法
2016/09/30 PHP
php通过执行CutyCapt命令实现网页截图的方法
2016/09/30 PHP
php pdo连接数据库操作示例
2019/11/18 PHP
jQuery 源码分析笔记(2) 变量列表
2011/05/28 Javascript
javascript字符串替换函数如何一次性全部替换掉
2015/10/30 Javascript
Base64(二进制)图片编码解析及在各种浏览器的兼容性处理
2017/02/09 Javascript
微信小程序获取微信运动步数的实例代码
2017/07/20 Javascript
vue使用drag与drop实现拖拽的示例代码
2017/09/07 Javascript
react-router4 配合webpack require.ensure 实现异步加载的示例
2018/01/18 Javascript
vue2.0实现移动端的输入框实时检索更新列表功能
2018/05/08 Javascript
vuejs 切换导航条高亮(路由菜单高亮)的方法示例
2018/05/29 Javascript
vue 纯js监听滚动条到底部的实例讲解
2018/09/03 Javascript
angular4笔记系列之内置指令小结
2018/11/09 Javascript
基于JS实现前端压缩上传图片的实例代码
2019/05/14 Javascript
vue实现两个区域滚动条同步滚动
2020/12/13 Vue.js
[49:18]2018DOTA2亚洲邀请赛 3.31 小组赛 A组 OG vs TNC
2018/04/01 DOTA
python爬取51job中hr的邮箱
2016/05/14 Python
python3 爬取图片的实例代码
2018/11/06 Python
详解Anconda环境下载python包的教程(图形界面+命令行+pycharm安装)
2019/11/11 Python
Django自定义用户表+自定义admin后台中的字段实例
2019/11/18 Python
python 视频逐帧保存为图片的完整实例
2019/12/10 Python
python批量修改文件名的示例
2020/09/27 Python
Web时代变迁及html5与html4的区别
2016/01/06 HTML / CSS
Lands’ End英国官方网站:高质量男女服装
2017/10/07 全球购物
List, Set, Map是否继承自Collection接口?
2016/05/16 面试题
凌阳科技股份有限公司C++程序员面试题笔试题
2014/11/20 面试题
酒吧副总经理岗位职责
2013/12/10 职场文书
快餐店的创业计划书范文
2014/01/29 职场文书
司机岗位职责说明书
2014/07/29 职场文书
公证委托书
2014/08/01 职场文书
银行贷款委托书范本
2014/10/11 职场文书
2014年基层党建工作总结
2014/11/11 职场文书
寒假生活随笔
2015/08/15 职场文书
数据库的高级查询六:表连接查询:外连接(左外连接,右外连接,UNION关键字,连接中ON与WHERE的不同)
2021/04/05 MySQL