react 仿 antd 风格的季度选择组件

react

产品也真是够了,周选择、月份选择、年份选择都是 antd 直接支持的,然而他现在要求要季度选择和半年份的选择。

那就来实现一个仿 antd 风格的季度选择组件吧,本文部分参照博客园-真的想不出来-模仿 Antd 写一个季度的时间选择器 V1.0

我实现了复制可用的版本。

效果

功能:

  • 一个纯组件,并且是 ts 版本。
  • 可以切换年份
  • 点选某个季度,执行 props 传入的 onChange 函数,value 参数形如 "2019-Q2"
  • 点击外部收起下拉框
  • 支持 value 传入默认选择项并定位到此。

调用

<QuarterPicker value={selectStartMonth} onChange={this.startDataChange} style={{marginRight: 24}}></QuarterPicker>

startDataChange(data: any) {

dispatch.dataQueryDistributorDot.SET({

selectStartMonth: data

});

}

代码

-- 第二次更新 --

补充了 ts 的一些类型说明,并且将 componentWillReceiveProps(nextProps, prevState) 替换为 static getDerivedStateFromProps(nextProps, prevState),因为前者即将被 React 废弃。

import React, { Component } from 'react';

import moment from 'moment';

import './index.less';

type IProps = {

className?: string;

style?: React.CSSProperties;

value?: string;

defaultValue?: string;

startValue?: string;

endValue?: string;

open?: boolean;

disabled?: boolean;

onOk?: Function;

showOk?: boolean;

onChange?: Function;

};

type IState = {

stateOpen: boolean;

year: string;

selectTime: string;

selectionTime: string;

oneDisplay: string;

twoDisplay: string;

};

const quarterData = [{

value: 'Q1',

label: '第一季度'

}, {

value: 'Q2',

label: '第二季度'

}, {

value: 'Q3',

label: '第三季度'

}, {

value: 'Q4',

label: '第四季度'

}];

const _defaultProps = {

showOk: false, // 是否使用确定按钮,默认不使用

disabled: false, // 组件是否禁用,默认组件可以使用

defaultValue: "请选择时间", // 默认日期 or 没有日期时的提示语

value: "",

startValue: "1970-1",

endValue: `${moment().format("YYYY")}-${moment().quarter()}`,

open: undefined,

onOk: () => {},

className: ""

}

class QuarterPicker extends Component<IProps, IState> {

private static defaultProps = _defaultProps; //主要是用 static 关联当前的class Loading

private toggleContainer: React.RefObject<HTMLDivElement>;

constructor(props: IProps) {

super(props)

this.state = {

stateOpen: false, // 是否展示弹窗

year: "", // "2020"

selectTime: `${moment().format("YYYY")}-${moment().quarter()}`, // 选中的时间, "2020-1", "-1" 代表第一季度

selectionTime: "", // 点确定后需要返回的时间

oneDisplay: "block",

twoDisplay: "block"

}

this.toggleContainer = React.createRef()

}

componentDidMount() {

const { value, open } = this.props;

let { year, selectTime } = this.state;

year = value ? value.split("-")[0] : selectTime.split("-")[0]

this.setState({

selectTime: value ? value : selectTime,

selectionTime: value ? value : "",

year

})

this.idBlock(year)

if (open === undefined) {

document.addEventListener('mousedown', this.handleClickOutside)

}

}

componentWillUnmount() {

document.removeEventListener('mousedown', this.handleClickOutside)

}

// componentWillReceiveProps 被废弃,使用 getDerivedStateFromProps 来取代

static getDerivedStateFromProps(nextProps: IProps, prevState: IState) {

// 该方法内禁止访问 this

const { value } = nextProps;

if (value !== prevState.selectionTime) {

// 通过对比nextProps和prevState,返回一个用于更新状态的对象

const year = value && value.split('-')[0];

return {

selectTime: value,

selectionTime: value,

year

};

}

// 不需要更新状态,返回null

return null;

}

onclick = (ev: any) => {

// ...

this.setState({

stateOpen: !this.state.stateOpen,

})

}

handleClickOutside = (ev: MouseEvent) => {

if (!(this && this.toggleContainer && this.toggleContainer.current)) {

return;

}

if (this.state.stateOpen && !this.toggleContainer.current.contains(ev.target as Node)) {

this.setState({ stateOpen: false });

}

};

ulliclick = (index: number) => {

// ...

}

iconLeftClick = () => {

// ...

const year = parseInt(this.state.year);

this.setState({

year: (year - 1).toString()

})

}

iconRightClick = () => {

// ...

const year = parseInt(this.state.year);

this.setState({

year: (year + 1).toString()

})

}

idBlock = (year: string) => {

// ...

}

okBut = (ev: any) => {

// ...

}

textChange = () => {

// ...

}

changeQuarter = (item: any) => {

this.props.onChange && this.props.onChange(`${this.state.year}-${item.value}`);

this.setState({

stateOpen: false,

})

}

render() {

const { oneDisplay, twoDisplay, selectTime, year, selectionTime, stateOpen } = this.state;

const { className, defaultValue, disabled, showOk, open } = this.props;

let openOnOff = false;

if (typeof (this.props.open) === "boolean") {

openOnOff = !!open;

} else {

openOnOff = stateOpen;

}

return (

<div

className={`QuarterlyPicker ${className}`}

style={this.props.style}

ref={this.toggleContainer}>

<div className="begin">

<input className={selectionTime ? "zjl-input" : "zjl-input default_input"}

value={selectionTime ? selectionTime : defaultValue}

disabled={disabled}

onClick={(ev) => { disabled ? null : this.onclick(ev) }}

onChange={() => { this.textChange() }}

/>

<i className="img" ></i>

</div>

<div className="child" style={{ display: openOnOff ? "block" : "none" }}>

<header className="zjl-timehear">

<span>{selectTime}</span>

</header>

<div className="con">

<ul className="content-one">

<li className="lefticon" onClick={this.iconLeftClick} style={{ display: oneDisplay }}>{"<<"}</li>

<li className="righticon" onClick={this.iconRightClick} style={{ display: twoDisplay }}>{">>"}</li>

<li>{year}</li>

</ul>

</div>

<div className="TimerXhlleft">

<ul className="quaterleft">

{

quarterData && quarterData.map(item => {

return <li

key={item.value}

className={`quaterleftli ${this.props.value === item.value ? 'active' : ''}`}

onClick={this.changeQuarter.bind(this, item)}>

{item.label}

</li>

})

}

</ul>

</div>

{

showOk ?

<div className="zjl-but">

<span onClick={this.okBut}>确定</span>

</div> : null

}

</div>

</div>

)

}

}

export default QuarterPicker;

样式

:global {

.QuarterlyPicker{

height: 100%;

min-height: 22px;

min-width: 90px;

box-sizing: border-box;

margin: 0;

padding: 0;

color: rgba(0, 0, 0, 0.65);

font-size: 14px;

font-variant: tabular-nums;

line-height: 1.5;

list-style: none;

font-feature-settings: 'tnum';

position: relative;

display: inline-block;

outline: none;

cursor: text;

transition: opacity 0.3s;

.begin{

position: relative;

height: 100%;

.zjl-input{

text-overflow: ellipsis;

touch-action: manipulation;

box-sizing: border-box;

margin: 0;

padding: 0;

font-variant: tabular-nums;

list-style: none;

font-feature-settings: 'tnum';

position: relative;

display: inline-block;

width: 100%;

height: 100%;

padding: 4px 11px;

color: rgba(0, 0, 0, 0.65);

font-size: 14px;

line-height: 1.5;

background-color: #fff;

background-image: none;

border: 1px solid #d9d9d9;

border-radius: 4px;

transition: all 0.3s;

&:hover{

border-color: #40a9ff;

border-right-width: 1px !important;

}

&:focus {

border-color: #40a9ff;

border-right-width: 1px !important;

outline: 0;

box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);

}

}

.zjl-input[disabled] {

color: rgba(0, 0, 0, 0.25);

background-color: #f5f5f5;

cursor: not-allowed;

opacity: 1;

}

.default_input{

color: rgba(0, 0, 0, 0.25);

}

.img{

display: inline-block;

position: absolute;

top: 50%;

right: 12px;

height: 14px;

width: 14px;

margin-top: -7px;

// background: url("../../assets/imgs/日历1.png") no-repeat center;

background-size: 100% 100%;

color: rgba(0, 0, 0, 0.25);

font-size: 14px;

line-height: 1;

z-index: 1;

transition: all 0.3s;

user-select: none;

}

}

.child{

box-sizing: border-box;

margin: 0;

padding: 0;

color: rgba(0, 0, 0, 0.65);

font-variant: tabular-nums;

line-height: 1.5;

list-style: none;

font-feature-settings: 'tnum';

position: absolute;

z-index: 1050;

width: 280px;

font-size: 14px;

line-height: 1.5;

text-align: left;

list-style: none;

background-color: #fff;

background-clip: padding-box;

border: 1px solid #fff;

border-radius: 4px;

outline: none;

box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);

.zjl-but {

position: relative;

height: auto;

text-align: right;

padding: 0 12px;

line-height: 38px;

border-top: 1px solid #e8e8e8;

span{

position: relative;

display: inline-block;

font-weight: 400;

white-space: nowrap;

text-align: center;

background-image: none;

border: 1px solid transparent;

box-shadow: 0 2px 0 rgba(0, 0, 0, 0.015);

cursor: pointer;

transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);

user-select: none;

touch-action: manipulation;

height: 32px;

padding: 0 15px;

color: #fff;

background-color: #1890ff;

border-color: #1890ff;

text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12);

-webkit-box-shadow: 0 2px 0 rgba(0, 0, 0, 0.045);

box-shadow: 0 2px 0 rgba(0, 0, 0, 0.045);

height: 24px;

padding: 0 7px;

font-size: 14px;

border-radius: 4px;

line-height: 22px;

&:hover{

color: #fff;

background-color: #40a9ff;

border-color: #40a9ff;

}

}

}

.zjl-timehear{

height: 34px;

padding: 6px 10px;

border-bottom: 1px solid #e8e8e8;

span{

display: inline-block;

width: 100%;

margin: 0;

cursor: default;

}

}

.TimerXhlleft{

width: 100%;

padding: 20px;

.quaterleft{

display: flex;

flex-direction: row;

flex-wrap: wrap;

justify-content: space-between;

padding: 0;

.quaterleftli{

width: 50%;

text-align: center;

line-height: 50px;

height: 50px;

color: #333;

padding: 0;

margin: 0;

list-style: none;

cursor: pointer;

&:hover{

background: #e6f7ff;

cursor: pointer;

}

&.active{

background: #bae7ff;

border-radius: 1px;

// color: #fff;

}

&.warnnodata{

background: #F5f5f5;

color: rgba(0, 0, 0, 0.25);

cursor: not-allowed;

}

}

}

}

.con{

height: 40px;

line-height: 40px;

text-align: center;

border-bottom: 1px solid #e8e8e8;

user-select: none;

.content-one{

white-space: nowrap;

overflow: hidden;

position: relative;

padding: 0;

.lefticon{

position: absolute;

z-index: 100;

top: 0;

left: 0;

font-size: 18px;

cursor: pointer;

width: 30px;

margin-left: 20px;

&:hover{

color: #40a9ff;

}

}

.righticon{

position: absolute;

z-index: 100;

top: 0;

right: 0;

font-size: 18px;

cursor: pointer;

width: 30px;

margin-right: 20px;

&:hover{

color: #40a9ff;

}

}

li {

display: inline-block;

text-align: center;

cursor: default;

}

}

}

}

}

}

以上是 react 仿 antd 风格的季度选择组件 的全部内容, 来源链接: utcz.com/z/383521.html

回到顶部