背景
整体由四部分组成,报名时间、报名周期、上课时间、上课周期
通过选择报名时间、报名周期、以及上课时间,去计算在培训周期内总的培训课时,并当上课时间冲突时,给出提示。
需求:
- 报名时间(日期+时分),不可选今日之前的日期
- 培训周期(日期),不可选报名结束时间之前的日期,如果未选择报名时间,则不可选今日之前的日期
- 上课时间:每周一到每周日,时间:24小时可选,可添加多条上课时间,一条上课时间为一次课
- 报名开始时间必须大于当前时间
- 培训周期开始时间必须大于报名时间
- 上课时间不能冲突,任意两节课的开始时间或者结束时间相等也视为上课冲突。
- 计算出培训周期范围内的总课时。
设计思想
当选择完培训周期后,计算出培训周期范围内,各个星期出现的次数,再遍历上课时间列表,通过列表的每周几作为key去进行匹配,当匹配成功时,将星期出现的次数加入总课时中。
时间冲突判断,通过将时间转换为数字,通过数字大小进行比较,通过双重循环判断是否存在数字相等或者数字重叠的情况。
界面搭建
自定义组件封装
上课时间是由一个选择框 和 一个 时间选择框共同组成,因此我们只需要对这两个组件进行封装形成一个新的selectTime组件即可。
SelectTime
import { Button, Form, Input, Select, TimePicker } from 'antd';
import moment from 'moment';
import React, { useEffect, useState } from 'react';
const { Option } = Select;
const SelectTime = ({ value = {}, onChange, }) => {
const options = [{ value: 1, label: "每周一" }, { value: 2, label: "每周二" }, { value: 3, label: "每周三" }, { value: 4, label: "每周四" }, { value: 5, label: "每周五" }, { value: 6, label: "每周六" }, { value: 7, label: "每周日" }]
const [week, setWeek] = useState();
const [timeRange, setTimeRange] = useState([]);
const triggerChange = (changedValue) => {
onChange?.({
week,
timeRange,
...value,
...changedValue,
});
};
const onNumberChange = (value) => {
setWeek(value);
triggerChange({
week: value,
});
};
const onCurrencyChange = (newTimeRange) => {
setTimeRange(newTimeRange);
triggerChange({
timeRange: newTimeRange,
});
};
return (
<>
<Select
style={{
width: 100,
margin: '0 8px',
}}
options={options}
allowClear={true}
placeholder={"每周几"}
onChange={onNumberChange}
value={value.week || week}
>
</Select>
<TimePicker.RangePicker format={"HH:mm"} onChange={onCurrencyChange} value={value.timeRange || timeRange} />
</>
)
}
export default SelectTime;
通过将SelectTime 组件放在Form.List内部即可实现组件的多个添加。当添加到第二条数据的时候,由于没有label,所以需要对样式进行调整,所以引入formItemLayoutWithOutLabel。
formItemLayoutWithOutLabel
const formItemLayoutWithOutLabel = {
wrapperCol: {
xs: {
span: 24,
offset: 0,
},
sm: {
span: 20,
offset: 6,
},
},
};
formItemLayout
const formItemLayout = {
labelCol: {
xs: {
span: 24,
},
sm: {
span: 6,
},
},
wrapperCol: {
xs: {
span: 24,
},
sm: {
span: 20,
},
},
};
报名时间
<FormItem name={"signRange"} label={"报名时间"} rules={[{ required: true }]}>
<RangePicker
disabledDate={disabledDate}
// disabledTime={disabledRangeTime}
showTime={{
format: 'HH:mm',
}}
format="YYYY-MM-DD HH:mm"
onChange={getEndDate}
/>
</FormItem>
培训周期
<FormItem name={"trainRange"} label={"培训周期"} rules={[{ required: true }]}>
<RangePicker disabledDate={disabledDate2} onChange={(value, dateString) => { isIncludeWeeks(dateString[0], dateString[1]); }} />
</FormItem>
上课时间
<Form.List
name="trainingTime"
initialValue={['1']}
rules={[
{
validator: async (_, trainingTime) => {
if (!trainingTime || trainingTime.length < 1) {
return Promise.reject(new Error('至少一条数据'));
}
},
},
]}
>
{(fields, { add, remove }, { errors }) => (
<>
{fields.map((field, index) => (
<Form.Item
label={index === 0 ? '上课时间' : ''}
{...(index === 0 ? formItemLayout : formItemLayoutWithOutLabel)}
rules={[{ required: true, message: "请输入上课时间" }]}
key={field.key}
name={[field.name, 'timeRange']}
validateTrigger={['onChange', 'onBlur']}
>
<Form.Item
{...field}
validateTrigger={['onChange', 'onBlur']}
rules={[
{
required: true,
message: "请输入上课时间",
},
]}
noStyle
>
<SelectTime onChange={() => { onChangetime(); }}></SelectTime>
</Form.Item>
<PlusCircleOutlined style={{ margin: "0 10px" }} onClick={() => add()} />
{fields.length > 1 ? (
<MinusCircleOutlined
className="dynamic-delete-button"
onClick={() => {
remove(field.name);
if (form.getFieldValue('trainRange') !== undefined) {
isIncludeWeeks(form.getFieldValue('trainRange')[0], form.getFieldValue('trainRange')[1]);
}
}}
/>
) : null}
</Form.Item>
))}
</>
)}
</Form.List>
培训总课时
<FormItem name={"totalTrainingHours"} label={"培训总课时"} rules={[{ required: true }]}>
<Input readOnly style={{ border: 'none', boxShadow: "none" }}></Input>
</FormItem>
功能实现
报名时间与培训时间的限制
添加报名时间限制——disabledDate
const disabledDate = (current) => {
var time = current.clone();
return time.startOf('day') < moment().startOf('day');
};
添加培训周期时间限制——disabledDate2
注意特殊情况:当报名时间未选择时,限制范围为前一日
const disabledDate2 = (current) => {
if (form.getFieldValue('signRange') !== undefined && endDate !== null) {
return current.format('YYYY-MM-DD') < endDate.format('YYYY-MM-DD');
} else {
return current < moment().subtract(1, "days");
}
};
当报名时间选择后,培训周期的可选时间范围受限,因为我们需要对报名时间添加onchange事件——getEndDate
const getEndDate = () => {
if (form.getFieldValue('signRange') !== null) {
endDate = form.getFieldValue('signRange')[1].clone();
endDate = endDate.add(1, "days")
}
}
培训总课时与上课时间以及培训时间有关,所以当我们选择对应的培训周期后,就应该计算出这段时间内周一到周日出现的次数。添加onchange事件——isIncludeWeeks
将培训周期内的星期放入weeksArr中(1代表星期一,....,7代表星期日)
const isIncludeWeeks = (startDate, endDate) => {
let beginDateObj = new Date(startDate);
let endDateObj = new Date(endDate);
let weeksArr = [];
while (beginDateObj <= endDateObj) {
//直到结束日期,跳出循环
let todayWeekNum = beginDateObj.getDay();
if (todayWeekNum == 0) {
//如果为0,则对应“周日”
todayWeekNum = 7;
}
weeksArr.push(todayWeekNum);
beginDateObj.setTime(beginDateObj.getTime() + 24 * 60 * 60 * 1000); //每次递增1天
}
console.log(getEleNums(weeksArr));
weekNum = getEleNums(weeksArr);
// setWeekNum(getEleNums(weeksArr));
canClassTotalTime();
}
计算周一到周日出现的次数
const getEleNums = (data) => {
var map = {}
for (var i = 0; i < data.length; i++) {
var key = data[i]
if (map[key]) {
map[key] += 1
} else {
map[key] = 1
}
}
return map;
}
当选择上课时间后,计算该课时在周期内出现次数,即星期出现次数。因此,对于课时的计算只需考虑对应的每周几即可,时间用于判断上课时间是否冲突。对于上课时间添加onchange事件——onChangetime
对于几种特殊情况通过if判断排除出去,避免报错。
- 未选每周几,只选了上课时间
- 未选上课时间,只选了每周几
- 未选培训周期
// 当选择上课时间时 计算总课时以及判断冲突
const onChangetime = () => {
// canClassTotalTime();
if (form.getFieldValue('trainRange') !== undefined) {
isIncludeWeeks(form.getFieldValue('trainRange')[0], form.getFieldValue('trainRange')[1]);
}
let rangetime = [];
console.log(form.getFieldValue('trainingTime'));
if (form.getFieldValue('trainingTime') !== undefined) {
rangetime = form.getFieldValue('trainingTime').map((item) => {
if (item.week !== undefined && item.timeRange[0] !== undefined) {
return {
week: item.week,
timeRange: [item.timeRange[0].format('HH:mm'), item.timeRange[1].format('HH:mm')]
}
}
}).filter(item => item !== undefined)
judgmentTimeConflict(rangetime);
}
}
canClassTotalTime
const canClassTotalTime = () => {
let total = 0;
form.getFieldValue('trainingTime')
console.log(form.getFieldValue('trainingTime'), weekNum);
if (form.getFieldValue('trainRange') !== undefined) {
form.getFieldValue('trainingTime').map((item) => {
if (item.week !== undefined && weekNum[item.week] !== undefined)
total += weekNum[item.week];
})
}
console.log('total', total);
form.setFieldsValue({ totalTrainingHours: total })
}
报名时间冲突判断——judgmentTimeConflict
通过将时间转成字符串再转成数字类型,通过数字类型进行大小比较完成范围冲突的判断。
几种冲突情况分类讨论 (范围A[0]-A[1],范围B[0]-B[1])
- A[0]<A[1]=B[0]<B[1]
- A[0]<B[0]<A[1]<B[1]
- A[0]<B[0]<A[1]=B[1]
- A[0]<B[0]<B[1]<A[1]
- A[0]=B[0]<B[1]<A[1]
- B[0]<A[0]<B[1]<A[1]
- A[0]<B[0]<A[1]=B[0]
- B[0]<A[0]<A[1]<B[1]
judgmentTimeConflict
// 判断时间是否冲突
const judgmentTimeConflict = (timeRangeList) => {
let newrange = timeRangeList.map((item) => {
return {
week: item.week,
timeRange: [parseInt(item.timeRange[0].replace(':', '')), parseInt(item.timeRange[1].replace(':', ''))]
}
})
for (let i = 0; i < newrange.length; i++) {
for (let j = i; j < newrange.length; j++) {
if (i !== j && newrange[i].week === newrange[j].week) {
if ((newrange[i].timeRange[1] === newrange[j].timeRange[0]) || (newrange[j].timeRange[1] === newrange[i].timeRange[0])) {
message.error("上课时间冲突,请重新选择!")
return false;
} else if ((newrange[i].timeRange[0] < newrange[j].timeRange[0]) && (newrange[j].timeRange[0] < newrange[i].timeRange[1])) {
message.error("上课时间冲突,请重新选择!")
return false;
} else if ((newrange[i].timeRange[0] < newrange[j].timeRange[1]) && (newrange[j].timeRange[1] < newrange[i].timeRange[1])) {
message.error("上课时间冲突,请重新选择!")
return false;
} else if ((newrange[i].timeRange[0] === newrange[j].timeRange[0]) || (newrange[i].timeRange[1] === newrange[j].timeRange[1])) {
message.error("上课时间冲突,请重新选择!")
return false;
}else if((newrange[i].timeRange[0] > newrange[j].timeRange[0]) && (newrange[i].timeRange[1] < newrange[j].timeRange[1])){
message.error("上课时间冲突,请重新选择!")
return false;
}
}
}
}
}
效果展示