react使用ant design pro时的滑动图片组件
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:;
left:;
}
.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:;
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:;
}
.tipsContainerActive {
visibility: visible;
opacity:;
}
.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()
}}
/>
react使用ant design pro时的滑动图片组件的更多相关文章
- (二)React Ant Design Pro + .Net5 WebApi:前端环境搭建
首先,你需要先装一个Nodejs,这是基础哦.如果没有这方面知识的小伙伴可以在园子里搜索cnpm yarn等关键字,内容繁多,此不赘述,参考链接 一. 简介 1. Ant Design Pro v5 ...
- Ant Design Pro+Electron+electron-builder实现React应用脱离浏览器,桌面安装运行
ant-design-pro ----> version :2.3.1 由于网上Ant Design Pro+Electron的资料太少,我就贡献一点经验 最近需要讲AntD Pro项目(以 ...
- ant design pro如何实现分步表单时,返回上一步值依然被保存
首先,分步表单ant design pro支持,看官方Demo即可,那么如何实现如题,关键在于设置initialValue {getFieldDecorator('name', { initialVa ...
- ant design pro (十二)advanced UI 测试
一.概述 原文地址:https://pro.ant.design/docs/ui-test-cn UI 测试是项目研发流程中的重要一环,有效的测试用例可以梳理业务需求,保证研发的质量和进度,让工程师可 ...
- ant design pro (八)构建和发布
一.概述 原文地址:https://pro.ant.design/docs/deploy-cn 二.详细 2.1.构建 当项目开发完毕,只需要运行一行命令就可以打包你的应用: npm run buil ...
- ant design pro (六)样式
一.概述 参看地址:https://pro.ant.design/docs/style-cn 基础的 CSS 知识或查阅属性,可以参考 MDN文档. 二.详细介绍 2.1.less Ant Desig ...
- ant design pro(一)安装、目录结构、项目加载启动【原始、以及idea开发】
一.概述 1.1.脚手架概念 编程领域中的“脚手架(Scaffolding)”指的是能够快速搭建项目“骨架”的一类工具.例如大多数的React项目都有src,public,webpack配置文件等等, ...
- Ant Design Pro快速入门
在上一篇文章中,我们介绍了如何构建一个Ant Design Pro的环境. 同时讲解了如何启动服务并查看前端页面功能. 在本文中,我们将简单讲解如何在Ant Design Pro框架下实现自己的业务功 ...
- 初探ant design pro
1.增加路由子页面&配置菜单 因为ant design pro采取的是umi路由配置,所以只要在对应的文件夹下新建相关的文件夹以及文件,它会自动解析.按照如下的步骤做即可 PS.如果想要给菜单 ...
随机推荐
- [BAT脚本] 1、BAT脚本FOR循环操作文件和命令返回实例
Wednesday, 31. October 2018 08:18PM - beautifulzzzz 一.需求 需要在windows上实现一个bat脚本解析json,将json转换为自己想要的key ...
- Jenkins Pipeline 参数详解
Pipeline 是什么 Jenkins Pipeline 实际上是基于 Groovy 实现的 CI/CD 领域特定语言(DSL),主要分为两类,一类叫做 Declarative Pipeline,一 ...
- 奥展项目笔记02--一个bat文件运行多个java jar包
奥展项目中后端微服务有很多jar包,一个一个启动又费时间效率又低,怎么才能一下让所有的jar包一块运行呢?我们可以编写.bat文件来一键启动. 1.我们将.bat文件放到jar包的同一级目录文件夹中: ...
- 基于FlexBox的无约束自适应
全手打原创,转载请标明出处:https://www.cnblogs.com/dreamsqin/p/11972664.html,多谢,=.=~ 一.背景描述 对于通用型带过滤条件的列表查询项目中, ...
- SQL Server使用sp_executesql在存储过程中执行多个批处理
SQL Server中有些SQL语句只能在一个批处理里面完成,例如CREATE SCHEMA语句创建SCHEMA的时候,每个SCHEMA都需要在一个单独的批处理里面完成: CREATE SCHEMA ...
- asp.net core不通过构造方法从容器中获取对象及解决通过这种方法NLog获取对象失败的问题
一般想从容器中获取对象,我们都是通过构造方法获取对象,但有些条件不允许不能通过构造方法获取对象,我们必须单独从容器中单独创建获取找个对象,这样我们就不行把找个容器静态保存起来供全局diaoy 一. 简 ...
- 2019 医渡云java面试笔试题 (含面试题解析)
本人5年开发经验.18年年底开始跑路找工作,在互联网寒冬下成功拿到阿里巴巴.今日头条.医渡云等公司offer,岗位是Java后端开发,因为发展原因最终选择去了医渡云,入职一年时间了,也成为了面试官 ...
- Python文件属性模块Os.path
Python文件属性模块Os.path介绍 os.path模块主要用于文件属性获取和判断,在编程中会经常用到,需要熟练掌握.以下是该模块的几种常用方法. os.path官方文档:http://docs ...
- 【JZOJ】2126. 最大约数和
题目大意 选取和不超过S的若干个不同的正整数,使得所有数的约数(不含它本身)之和最大. 分析 把我们分解出来的因数进行合并,存在一个不知名的数组里,然后我们大可开始我们的迪屁!!(bag),我们可以 ...
- Mybatis逆向工程的使用。
指定配置文件与main运行生成 public class GeneratorSqlmap { public void generator() throws Exception { List<St ...