RangePicker 组件选择不超过七天的范围

2,502 阅读4分钟

时间范围的选择是十分常见的需求,在最近开发的一个系统中,就有这样的需求,要求时间范围选择从开始到结束最长不得超过七天。

下面,我们就一步一步来实现这个功能。

组件选择

项目使用的是React + Antd + dva技术栈,因此我们选用Antd的 DatePicker.RangePicker 组件来实现这个功能。

<RangePicker
  ref={dateRangePicker}
  showTime
  format="YYYY-MM-DD HH:mm:ss"
  value={hackValue || value}
	// 不可选择的日期
  disabledDate={disabledDateHandle}
	// input 框获取焦点时的回调
  onFocus={dateOnFocusHandle}
  // 不可选择的时间
  disabledTime={disabledTimeHandle}
  onChange={(val) => setValue(val)}
  // 待选日期发生变化的回调
  onCalendarChange={(val) => setDates(val)}
  // 弹出日历和关闭日历的回调
  onOpenChange={onOpenChangeHandle}
  // onOk={dateOnOkHandle}
/>

不可选择的日期

disabledDate 属性可用来禁止选择部分日期,通过disabledDate 和 onCalendarChange 的一起使用,可以限制动态的日期区间选择。

const disabledDateHandle = (current) => {

  if (!dates || dates.length === 0) {
    return false;
  }
  // 不可选择的日期
  const tooLate = dates[0] && current.diff(dates[0], "days") > 7;
  const tooEarly = dates[1] && dates[1].diff(current, "days") >= 7;
  return tooEarly || tooLate;
};

不可选择的时间

disabledTime 属性用来禁止选择部分时间。RangePicker 组件默认只有日期选择功能,因此需要添加showTime属性来添加时间选择功能。通过 disabledTime 和 showTime 的一起使用,可以限制部分时间的选择。

const disabledTimeHandle = (current, type) => {
  function range(start, end) {
    const result = [];
    for (let i = start; i < end; i++) {
      result.push(i);
    }
    return result;
  }

  if (type === "start") {
    if (!dates?.length) {
      // 全部时间都可选择
      return {
        disabledHours: () => [-1, 24],
        disabledMinutes: () => [-1, 60],
        disabledSeconds: () => [-1, 60]
      };
    } else if (dates[1]) {
      const hours = moment(dates[1]).hours();
      const minute = moment(dates[1]).minutes();
      const second = moment(dates[1]).seconds();
      // 部分可选择的时间
      return {
        disabledHours: () => range(0, 24).splice(0, hours),
        disabledMinutes: () => range(0, 60).splice(0, minute),
        disabledSeconds: () => range(0, 60).splice(0, second)
      };
    }
    return {
      disabledHours: () => [-1, 24],
      disabledMinutes: () => [-1, 60],
      disabledSeconds: () => [-1, 60]
    };
  } else {
    if (!dates?.length) {
      // 全部时间都可选择
      return {
        disabledHours: () => [-1, 24],
        disabledMinutes: () => [-1, 60],
        disabledSeconds: () => [-1, 60]
      };
    } else if (dates[0]) {
      const hours = moment(dates[0]).hours();
      const minute = moment(dates[0]).minutes();
      const second = moment(dates[0]).seconds();
      // 部分可选择的时间
      return {
        disabledHours: () => range(0, 24).splice(hours),
        disabledMinutes: () => range(0, 60).splice(minute),
        disabledSeconds: () => range(0, 60).splice(second)
      };
    }
    return {
      disabledHours: () => [-1, 24],
      disabledMinutes: () => [-1, 60],
      disabledSeconds: () => [-1, 60]
    };
  }
};

onFocus

在实际开发中发现,当光标在日期选择组件的input框来回切换时,无法触发动态日期区间的选择限制,其原因是当input框获取焦点时,无法获取到已经选择的日期,Antd 的 RangePicker 组件也并未提供相关的接口来获取已经选择的日期,因此使用原生DOM方法document.getElementById方法获取input元素,然后使用原生DOM方法getAttribute 获取input的 value 属性,即已经选择的日期。

const dateOnFocusHandle = (e) => {
  // 获取 RangePicker 组件的第一个 input 元素
  const contentFormTimeNode = document.getElementById("contentForm_time") || document.getElementById('basic_time');
  const parnetNode = contentFormTimeNode?.parentNode;
  // 获取 RangePicker 组件的第二个 input 元素
  const siblingNode = parnetNode?.nextSibling?.nextSibling;
  const endDateNode = siblingNode?.firstChild;

  // 获取第一个 input 元素的value值,即开始时间
  let startDate = contentFormTimeNode?.getAttribute("value") || null;
  // 获取第二个 input 元素的value值,即结束时间
  let endDate = endDateNode?.getAttribute("value") || null;

  startDate = (startDate && moment(startDate)) || null;
  endDate = (endDate && moment(endDate)) || null;

  // 设置 state,触发 disabledDate 和 disabledTime
  setDates([startDate, endDate]);
};

示例代码

import React, { useState, useRef, useEffect } from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";
import "./index.css";
import { Form, Button, DatePicker, Select, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import moment from "moment";
const { RangePicker } = DatePicker;

export const citys = [
  {
    name: "北京",
    city: "bj",
    // 相比北京时间的小时时差
    offset: 0
  },
  {
    name: "马来西亚",
    city: "mlxy",
    offset: 0
  },
  {
    name: "菲律宾",
    city: "flb",
    offset: 0
  },
  {
    name: "新加坡",
    city: "xjp",
    offset: 0
  },
  {
    name: "台湾",
    city: "tw",
    offset: 0
  },
  {
    name: "香港",
    city: "hk",
    offset: 0
  },
  {
    name: "东京",
    city: "dj",
    offset: 1
  },
  {
    name: "首尔",
    city: "se",
    offset: 1
  },
  {
    name: "悉尼",
    city: "xn",
    offset: 2
  },
  {
    name: "惠灵顿",
    city: "hld",
    offset: 4
  },
  {
    name: "曼谷",
    city: "mg",
    offset: -1
  },
  {
    name: "河内",
    city: "hn",
    offset: -1
  },
  {
    name: "雅加达",
    city: "yjd",
    offset: -1
  },
  {
    name: "孟加拉国",
    city: "mjlg",
    offset: -2
  },
  {
    name: "巴基斯坦",
    city: "bjst",
    offset: -3
  },
  {
    name: "迪拜",
    city: "db",
    offset: -4
  },
  {
    name: "莫斯科",
    city: "msk",
    offset: -5
  },
  {
    name: "以色列",
    city: "ysl",
    offset: -6
  },
  {
    name: "南非",
    city: "nf",
    offset: -6
  },
  {
    name: "巴黎",
    city: "bl",
    offset: -7
  },
  {
    name: "柏林",
    city: "bolin",
    offset: -7
  },
  {
    name: "伦敦",
    city: "ld",
    offset: -8
  },
  {
    name: "阿根廷",
    city: "agt",
    offset: -11
  },
  {
    name: "智利",
    city: "zl",
    offset: -12
  },
  {
    name: "纽约",
    city: "ny",
    offset: -13
  },
  {
    name: "多伦多",
    city: "dld",
    offset: -13
  },
  {
    name: "芝加哥",
    city: "zjg",
    offset: -14
  },
  {
    name: "洛杉矶",
    city: "lsj",
    offset: -16
  },
  {
    name: "旧金山",
    city: "jjs",
    offset: -16
  }
];

export const converTimesStamp = (city, time) => {
  const utcOffset = citys.find((item) => item.city === city)?.offset;
  let timeZone = utcOffset + 8;
  if (timeZone >= 0) {
    if (timeZone.toString().length === 1) {
      timeZone = `+0${timeZone}`;
    } else {
      timeZone = `+${timeZone}`;
    }
  } else {
    if (timeZone.toString().replace("-", "").length === 1) {
      timeZone = `-0${timeZone.toString().replace("-", "")}`;
    }
  }

  const startTimeStr =
    moment(time[0]).format("YYYY-MM-DDTHH:mm:ss") + timeZone + ":00";
  const expireTimeStr =
    (time[1] &&
      moment(time[1]).format("YYYY-MM-DDTHH:mm:ss") + timeZone + ":00") ||
    "";

  const startTime = moment.parseZone(startTimeStr).valueOf();
  const expireTime =
    (expireTimeStr && moment.parseZone(expireTimeStr).valueOf()) || 0;

  return [startTime, expireTime];
};

const Demo = () => {
  const [form] = Form.useForm();

  const dateRangePicker = useRef();
  const [hackValue, setHackValue] = useState();
  const [dates, setDates] = useState([]);
  const [value, setValue] = useState();

  useEffect(() => {
    const time = [moment(), moment().add(24, "hours")];

    form.setFieldsValue({
      time,
      city: "bj"
    });

    setDates(time);
  }, []);

  const onFinish = (values) => {
    console.log("Success:", values);
  };

  const onFinishFailed = (errorInfo) => {
    console.log("Failed:", errorInfo);
  };

  const cityChangeHandle = (city) => {
    // let diffHours = citys.find((item) => item.city === city)?.offset;
    let date = [moment(), moment().add(24, "hour")];
    // date = [moment(date[0]).add(diffHours, 'hour'), moment(date[1]).add(diffHours, 'hour')]
    form.setFieldsValue({ time: date });
    setDates(date);
  };

  const disabledDateHandle = (current) => {

    if (!dates || dates.length === 0) {
      return false;
    }
    // 不可选择的日期
    const tooLate = dates[0] && current.diff(dates[0], "days") > 7;
    const tooEarly = dates[1] && dates[1].diff(current, "days") >= 7;
    return tooEarly || tooLate;
  };

  const disabledTimeHandle = (current, type) => {
    function range(start, end) {
      const result = [];
      for (let i = start; i < end; i++) {
        result.push(i);
      }
      return result;
    }

    if (type === "start") {
      if (!dates?.length) {
        // 全部时间都可选择
        return {
          disabledHours: () => [-1, 24],
          disabledMinutes: () => [-1, 60],
          disabledSeconds: () => [-1, 60]
        };
      } else if (dates[1]) {
        const hours = moment(dates[1]).hours();
        const minute = moment(dates[1]).minutes();
        const second = moment(dates[1]).seconds();
        // 部分可选择的时间
        return {
          disabledHours: () => range(0, 24).splice(0, hours),
          disabledMinutes: () => range(0, 60).splice(0, minute),
          disabledSeconds: () => range(0, 60).splice(0, second)
        };
      }
      return {
        disabledHours: () => [-1, 24],
        disabledMinutes: () => [-1, 60],
        disabledSeconds: () => [-1, 60]
      };
    } else {
      if (!dates?.length) {
        // 全部时间都可选择
        return {
          disabledHours: () => [-1, 24],
          disabledMinutes: () => [-1, 60],
          disabledSeconds: () => [-1, 60]
        };
      } else if (dates[0]) {
        const hours = moment(dates[0]).hours();
        const minute = moment(dates[0]).minutes();
        const second = moment(dates[0]).seconds();
        // 部分可选择的时间
        return {
          disabledHours: () => range(0, 24).splice(hours),
          disabledMinutes: () => range(0, 60).splice(minute),
          disabledSeconds: () => range(0, 60).splice(second)
        };
      }
      return {
        disabledHours: () => [-1, 24],
        disabledMinutes: () => [-1, 60],
        disabledSeconds: () => [-1, 60]
      };
    }
  };

  const onOpenChangeHandle = (open) => {
    // 弹出日历时重置 dates 和 hackValue
    if (open) {
      setHackValue([]);
      setDates([]);
      // 重置form表单
      form.setFieldsValue({ time: [] });
    } else {
       // 关闭日历时重置 dates 和 form表单
      setHackValue(undefined);
      const selectTime = form.getFieldValue("time");
      form.setFieldsValue({
        time: selectTime.length
          ? selectTime
          : [moment(), moment().add(24, "hours")]
      });
    }
  };

  // const dateOnOkHandle = dates => {
  //   // 7天   604800000毫秒
  //   if (dates[1]?.diff(dates[0], 'days') > 7) {
  //     message.destroy()
  //     message.error('您选择的时间超过7天,请重新选择')
  //   }
  // }

  const dateOnFocusHandle = (e) => {
    // 获取 RangePicker 组件的第一个 input 元素
    const contentFormTimeNode = document.getElementById("contentForm_time") || document.getElementById('basic_time');
    const parnetNode = contentFormTimeNode?.parentNode;
    // 获取 RangePicker 组件的第二个 input 元素
    const siblingNode = parnetNode?.nextSibling?.nextSibling;
    const endDateNode = siblingNode?.firstChild;

    // 获取第一个 input 元素的value值,即选择的时间
    let startDate = contentFormTimeNode?.getAttribute("value") || null;
    // 获取第二个 input 元素的value值,即选择的时间
    let endDate = endDateNode?.getAttribute("value") || null;

    startDate = (startDate && moment(startDate)) || null;
    endDate = (endDate && moment(endDate)) || null;

    // 设置 state,触发 disabledDate 和 disabledTime
    setDates([startDate, endDate]);
  };

  return (
    <Form
      name="basic"
      form={form}
      labelCol={{
        span: 8
      }}
      wrapperCol={{
        span: 16
      }}
      initialValues={{
        remember: true
      }}
      onFinish={onFinish}
      onFinishFailed={onFinishFailed}
      autoComplete="off"
    >
      <Form.Item
        noStyle
        shouldUpdate={(pre, cur) =>
          pre.city !== cur.city || pre.time !== cur.time
        }
      >
        {({ getFieldValue }) => (
          <Form.Item
            label="生效时间"
            required
            extra={
              <span>
                {getFieldValue("time")
                  ? `北京时间:${moment(
                      converTimesStamp(
                        form.getFieldValue("city"),
                        form.getFieldValue("time")
                      )[0]
                    )
                      .utcOffset(8)
                      .format("YYYY-MM-DD HH:mm:ss")}~${moment(
                      converTimesStamp(
                        form.getFieldValue("city"),
                        form.getFieldValue("time")
                      )[1]
                    )
                      .utcOffset(8)
                      .format("YYYY-MM-DD HH:mm:ss")}`
                  : null}
              </span>
            }
          >
            <Form.Item
              name="city"
              initialValue="bj"
              style={{
                display: "inline-block",
                width: 120,
                marginBottom: 0,
                marginRight: 10
              }}
            >
              <Select
                onChange={(value) => {
                  cityChangeHandle(value);
                }}
              >
                {citys.map(({ city, name, offset }) => (
                  <Select.Option key={city} value={city}>
                    {name}({offset >= 0 ? `+${offset}` : offset}:00)
                  </Select.Option>
                ))}
              </Select>
            </Form.Item>

            <Form.Item
              name="time"
              initialValue={[moment(), moment().add(24, "hours")]}
              noStyle
              rules={[{ type: "array", required: true, message: "请选择时间" }]}
            >
              <RangePicker
                ref={dateRangePicker}
                showTime
                format="YYYY-MM-DD HH:mm:ss"
                value={hackValue || value}
                disabledDate={disabledDateHandle}
                onFocus={dateOnFocusHandle}
                disabledTime={disabledTimeHandle}
                onChange={(val) => setValue(val)}
                onCalendarChange={(val) => {
                  setDates(val)
                }}
                onOpenChange={onOpenChangeHandle}
                // onOk={dateOnOkHandle}
              />
            </Form.Item>
            <Tooltip title="本功能默认生效时间范围为24小时,最长不超过7天。">
              <InfoCircleOutlined
                style={{
                  fontSize: 20,
                  marginLeft: 10,
                  transform: "translateY(3px)",
                  color: "orange"
                }}
              />
            </Tooltip>
          </Form.Item>
        )}
      </Form.Item>

      {/* <Form.Item
        wrapperCol={{
          offset: 8,
          span: 16
        }}
      >
        <Button type="primary" htmlType="submit">
          Submit
        </Button>
      </Form.Item> */}
    </Form>
  );
};

ReactDOM.render(<Demo />, document.getElementById("container"));

项目效果及代码地址:codesandbox.io/s/xuan-ze-b…