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- 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;
         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- 1px !important;
        }
        &:focus {
          border-color: #40a9ff;
          border-right- 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;
         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;
       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;
           100%;
          margin: 0;
          cursor: default;
        }
      }
      .TimerXhlleft{
         100%;
        padding: 20px;
        .quaterleft{
          display: flex;
          flex-direction: row;
          flex-wrap: wrap;
          justify-content: space-between;
          padding: 0;
          .quaterleftli{
             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;
             30px;
            margin-left: 20px;
            &:hover{
              color: #40a9ff;
            }
          }
          .righticon{
            position: absolute;
            z-index: 100;
            top: 0;
            right: 0;
            font-size: 18px;
            cursor: pointer;
             30px;
            margin-right: 20px;
            &:hover{
              color: #40a9ff;
            }
          }
          li {
            display: inline-block;
            text-align: center;
            cursor: default;
          }
        }
      }
    }
  }

}

原文地址:https://www.cnblogs.com/everlose/p/12516461.html