您的当前位置:首页react的滑动图片验证码组件的示例代码

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

2020-11-27 来源:飒榕旅游知识分享网

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

效果图

使用方式

在一般的页面组件引用即可。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;
}
显示全文