相关文章推荐
乐观的青蛙  ·  Audio/Video ...·  2 周前    · 
坏坏的板栗  ·  具有不同ORDER ...·  1 年前    · 
耍酷的脸盆  ·  被SpringBoot整合OpenFeign ...·  1 年前    · 

产品也真是够了,周选择、月份选择、年份选择都是 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 : "",
        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,
        // 不需要更新状态,返回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 (
            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 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>
              <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}
                showOk ?
                  <div className="zjl-but">
                    <span onClick={this.okBut}>确定</span>
                  </div> : null
    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;
                display: inline-block;
                text-align: center;
                cursor: default;