视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
react的滑动图片验证码组件的示例代码
2020-11-27 22:00: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;
}

下载本文
显示全文
专题