react的滑动图片验证,是基于https://segmentfault.com/a/1190000018309458?utm_source=tag-newest做的修改,改动的主要有以下几点:
1.将css的改为less,适配ant design
2.将图片进行初次加载就执行裁剪的方法
3.适配手机的滑动事件
// index.js /** * @name Index * @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 stylecss from "./index.less" const icoSuccess = require("./icons/success.png") const icoError = require("./icons/error.png") const icoReload = require("./icons/refresh.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 ImgCode extends React.Component { static defaultProps = { imageUrl: "", imageWidth: 400, imageHeight: 200, 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 } componentDidMount() { this.renderImage() } componentDidUpdate(prevProps) { // 当父组件传入新的图片后,开始渲染 if (!!this.props.imageUrl && prevProps.imageUrl !== this.props.imageUrl) { this.renderImage() } } componentWillUnmount() { this.setState = (state, callback) => { return; }; } renderImage = () => { // 初始化状态 this.setState({ status: STATUS_LOADING,startX: 0, oldX: 0, currX: 0}) // 创建一个图片对象,主要用于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") ctxShadow.clearRect(0, 0, this.props.fragmentSize, this.props.fragmentSize) ctxFragment.clearRect(0, 0, this.props.fragmentSize, this.props.fragmentSize) // 让两个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() } } onPhoneMoveStart = e => { if (this.state.status !== STATUS_READY) { return } // 记录滑动开始时的绝对坐标x this.setState({ isMovable: true, startX: e.touches[0].pageX }) } onPhoneMoving = e => { if (this.state.status !== STATUS_READY || !this.state.isMovable) { return } const distance = e.touches[0].pageX - 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 }) } onPhoneMoveEnd = () => { 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] const icoSlider = require("./icons/slider.png") return ( <div className={stylecss.imageCode} style={{ width: imageWidth }}> <div className={stylecss.imageContainer} style={{ height: imageHeight, backgroundImage: `url("${imageUrl}")` }}> <canvas ref="shadowCanvas" className={stylecss.canvas} width={fragmentSize} height={fragmentSize} style={{ left: offsetX + "px", top: offsetY + "px" }} /> <canvas ref="fragmentCanvas" className={stylecss.canvas} width={fragmentSize} height={fragmentSize} style={{ top: offsetY + "px", left: currX + "px" }} /> <div className={showTips ? stylecss.tipsContainerActive : stylecss.tipsContainer}> <i className={stylecss.tipsIco} style={{ backgroundImage: `url("${tips.ico}")` }} /> <span className={stylecss.tipsText}>{tips.text}</span> </div> </div> <div className={stylecss.reloadContainer}> <div className={stylecss.reloadWrapper} onClick={this.onReload}> <i className={stylecss.reloadIco} style={{ backgroundImage: `url("${icoReload}")` }} /> <span className={stylecss.reloadTips}>刷新验证</span> </div> </div> <div className={stylecss.sliderWrpper} onMouseMove={this.onMoving} onTouchMove={this.onPhoneMoving} onMouseLeave={this.onMoveEnd}> <div className={stylecss.sliderBar}>按住滑块,拖动完成拼图</div> <div className={stylecss.sliderButton} onTouchStart={this.onPhoneMoveStart} onTouchEnd={this.onPhoneMoveEnd} onMouseDown={this.onMoveStart} onMouseUp={this.onMoveEnd} style={{ left: currX + "px", backgroundImage: `url("${icoSlider}")` }} /> </div> </div> ) } } export default ImgCode
样式
.imageCode { //padding: 10px; user-select: none; } .imageContainer { position: relative; background-color: #ddd; } .canvas { position: absolute; top: 0; left: 0; } .reloadContainer { margin: 5px 0; } .reloadWrapper { display: inline-flex; align-items: center; cursor: pointer; } .reloadIco { width: 25px; height: 20px; margin-right: 10px; background: center/cover no-repeat; } .reloadTips { font-size: 14px; color: #666; } .sliderWrpper { position: relative; margin: 10px 0; } .sliderBar { //padding: 10px; font-size: 14px; text-align: center; color: #999; background-color: #ddd; } .sliderButton { 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; } /* 提示信息 */ .tipsContainer, .tipsContainerActive { 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; } .tipsContainerActive { visibility: visible; opacity: 1; } .tipsIco { width: 20px; height: 20px; margin-right: 10px; background: center/cover no-repeat; } .tipsText { color: #666; }
使用页面
state = { imageCodeKey: undefined, //后台返回的redis的key值 url:'', //图片路径 fileName:1 //图片名称 }; componentDidMount() { this.fetchImageCode(); } onReload = () => { const {fileName} = this.state; this.getImage(fileName); } getImage=(fileName)=>{ let url = `/image/` if(fileName>=5){ fileName =1; url = url+'1.jpg' }else { fileName++ url = url+fileName+'.jpg' } this.setState({fileName:fileName,url:url,imageCodeKey:undefined}) return url } // 滑动成功 sildeImageCode = () => { const { dispatch } = this.props; dispatch({ type: 'login/slideImageCode', callback: res => { const { code, data } = res; if (code === API_RESPONSE_CODE.SUCCESS) { this.setState({ imageCodeKey: data, }); } }, }); }; // 加载验证码 fetchImageCode = () => { const {fileName} = this.state this.setState({ imageCodeKey: undefined, url: this.getImage(fileName) }); };
<ImgCode imageUrl={url} onReload={this.onReload} onMatch={() => { this.sildeImageCode() }} />