效果图:
index.js
import React from 'react'
import styles from './index.less'
import { Input } from 'antd'
import PropTypes from 'prop-types'
import { remove, throttle } from 'lodash'
// 回撤 方向键左 方向键右 对应keycode 分别是 8 ,37 ,39
const BACK_SPACE = 8
const LEFT = 37
const RIGHT = 39
class Captcha extends React.Component {
constructor(props) {
super(props)
this.state = {
values: [], // 验证码的值
}
// throttle 目的是阻止chrome85版本+微软中文输入法联合造成的input number重复输入两次
this.throttleHandleInputValue = throttle(this.handleInputValue, 100).bind(this)
}
/**
* 清洗arr数据
* @param arr
* @returns {{size: number, values: *}}
*/
cleanArray(arr) {
let values = arr
let empty = remove(values, function (n) {
return !n
})
empty.forEach(item => {
values.push('')
})
return { values: values, size: values.length - empty.length }
}
//验证码光标后移
handleInputValue = (e, index) => {
const { size, getValue } = this.props
const { value = '' } = e.target
if (!/^\d{1}$/.test(value)) {
return
}
let values = [].concat(this.state.values)
values[index] = value.slice(0, 1)
let cleanArray = this.cleanArray(values)
this.setState({ values: cleanArray.values })
if (value && index < size - 1) {
this[`focus${cleanArray.size}`].input.focus()
}
getValue(values.join(''))
}
//删除验证码
handleDel = (e, index) => {
const { getValue } = this.props
const isBackSpaceKey = e.keyCode === BACK_SPACE
const isLeftKey = e.keyCode === LEFT
const isRightKey = e.keyCode === RIGHT
if (isBackSpaceKey && e.target.value.length === 0) {
this.reFocus(e, 'prev')
let values = [].concat(this.state.values)
values.splice(index, 1)
values.push('')
this.setState({ values })
getValue(values.join(''))
} else if (isBackSpaceKey && e.target.value.length === 1) {
// todo:优化,让光标无论如何都在数值的右边
if (e.target.selectionStart === 0) {
// 调整光标位置
e.target.selectionStart = 1
e.target.selectionEnd = 1
} else {
// 回删
this.reFocus(e, 'prev')
let values = [].concat(this.state.values)
values.splice(index, 1)
values.push('')
this.setState({ values })
getValue(values.join(''))
}
} else if (isLeftKey) {
this.reFocus(e, 'prev')
} else if (isRightKey) {
this.reFocus(e, 'next')
}
}
/**
* focus 到上一个或下一个input
* @param e
* @param type <string>(prev|next)
*/
reFocus = (e, type) => {
let ele = e.target.parentElement
if (type === 'prev') {
ele = ele.previousElementSibling && ele.previousElementSibling.querySelector('input')
} else { //'next'
ele = ele.nextElementSibling && ele.nextElementSibling.querySelector('input')
}
if (ele && ele.tagName.toLowerCase() === 'input') {
ele.focus()
}
}
componentWillReceiveProps(nextProps, nextContext) {
if (nextProps.emptyValue && !this.props.emptyValue) {
let values = new Array(this.props.size).fill('')
this.setState({ values })
this.props.getValue(values.join(''))
}
}
render() {
const { values } = this.state
const { maxLength, size, wrapperStyle = {}, itemStyle = {} } = this.props
let inputArray = new Array(size).fill('')
return (
<div className={styles.wrapper} style={wrapperStyle}>
{
inputArray.map((item, index) => {
return <div className={styles.checkInputItem} style={itemStyle}>
<Input
ref={ref => {
this[`focus${index}`] = ref
}}
type={'tel'} // type 由number 改为tel,以允许selectionStart的使用
value={values[index]}
maxLength={maxLength}
onKeyDown={maxLength ? e => this.handleDel(e, index) : null}
onChange={e => {
e.persist()
this.throttleHandleInputValue(e, index)
}}
/>
</div>
})
}
</div>
)
}
}
Captcha.propTypes = {
size: PropTypes.number,// 验证码位数
maxLength: PropTypes.number, //单格验证码长度
wrapperStyle: PropTypes.object, // 整体样式
itemStyle: PropTypes.object, // 单项样式
getValue: PropTypes.func, //外部获取组件内的验证码
emptyValue: PropTypes.bool, //是否清空当前验证码内容
}
Captcha.defaultProps = {
size: 6,
maxLength: 1,
wrapperStyle: {},
itemStyle: {},
getValue: () => {},
emptyValue: false,
}
export default Captcha
index.less
.wrapper {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
padding-right: 8px;
padding-top: 4px;
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
}
input[type="number"] {
-moz-appearance: textfield;
}
.checkInputItem {
//flex: 1
}
:global {
.ant-input {
padding: 0;
text-align: center;
border: unset;
border-bottom: 1px #d9d9d9 solid;
border-radius: unset;
font-size: 16px;
font-family: PingFangSC-Semibold, PingFang SC, Sans-serif;
font-weight: 600;
color: #1A1A1A;
}
.ant-input:focus {
border-bottom: 1px #1890FF solid;
box-shadow: unset;
}
.ant-input:hover {
border-bottom: 1px #1890FF solid;
box-shadow: unset;
}
}
}
使用
<FormItem>
<div className={styles.formItemMessage}>
<FormattedMessage id={'login.captcha.placeholder'}
defauleMessage={'Captcha'}/>
</div>
<Row gutter={8}
style={{ display: 'flex', alignItems: 'center' }}>
<Col span={17}>
{getFieldDecorator('captcha', {
initialValue: '',
rules: [{
required: true,
message: <FormattedMessage id={'login.captcha'}
defaultMessage={'Please input Code!'}/>,
}, {
validator: (rule, value, callback) => {
if (value && value.length !== 6) {
callback(<FormattedMessage id={'login.captcha.error'}
defaultMessage={'Captcha type error!'}/>)
}
callback()
},
}],
})(
<Captcha getValue={(value) => {
this.props.form.setFieldsValue({ captcha: value })
}} itemStyle={{ width: 30 }}/>,
)}
</Col>
<Col span={8}>
<Button
disabled={count && count !== -1}
className={`${styles.getCaptcha} ${count && count !== -1 && styles.getCaptchaDisabled}`}
onClick={this.onGetCaptcha}
>
{count && count !== -1 ? `${count}s ${isCN ? '后可重发' : 'Resend'}` :
(count === -1 ? <FormattedMessage id={'login.getCode'} defaultMessage={'Get Code'}/> :
<FormattedMessage id={'login.resend'} defaultMessage={'Resend'}/>)}
</Button>
</Col>
</Row>
</FormItem>
通过getValue 将验证码组件的值传出去