react 仿 antd 风格的季度选择组件
产品也真是够了,周选择、月份选择、年份选择都是 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}`}
id="QuarterlyPicker"
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;
}
}
}
}
}
}