时间范围的选择是十分常见的需求,在最近开发的一个系统中,就有这样的需求,要求时间范围选择从开始到结束最长不得超过七天。
下面,我们就一步一步来实现这个功能。
组件选择
项目使用的是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…