项目背景
员工排班是一套ERP+POS系统内的功能,系统应用于一家连锁小商品店铺,店铺分为一家总店及多个分店,总店没有实体店,主要负责审核、财务等工作;分店是具体店铺,可以通过POS系统售卖货品。系统前端页面搭建采用HTML+SCSS语言,页面功能基于React框架+JSX实现,后端开发是Django框架+Python语言。
员工排班功能:各分店店长对其店铺员工每两周进行一次排班工作,每日排班分为三个时段,类型分为正常上班、公共假期、年假、休息等14种,排班信息保存后会按类型统计出两周内总工作时长。排班提交到总部后,总部财务人员会根据统计出来的总工时,结算员工工资。
页面功能实践
员工排班页面主要分为三大部分:
- 筛选查询,包含开始结束时间、分店、员工筛选
- 排班工时统计,按不同类型展示工时,并统计两周内总时长
- 排班信息表,按员工展示两周内排班信息
复杂点在于筛选部分中开始时间的筛选,排班信息表的展示和时间段编辑。
1. 开始时间选项的获取
店长每两周一次的排班工作,首先选择想要编辑的排班对应的时间段,所以开始时间应该是周一的日期,为了能编辑以往未提交的排班,设定开始时间下拉选择内容为当前日期前后各8周的每周周一的日期,结束时间则是根据开始时间自动计算出的下一周周日的日期。
需要运用Date对象的getTime、getDay等方法以及一些js计算逻辑,因此让我对Date对象有了更多的理解,也更熟悉了其方法的运用。
开始时间获取思路:如何计算本周周一日期呢?
- getTime方法可以返回1970年1月1日至今的毫秒数,由此获取今天的毫秒数
- 计算今天距周一的天数,就能得到距周一的毫秒数
- 二者相减即得到周一的毫秒数
有了基础的思路后,就能推算出前后各8周周一的毫秒数。再利用新建日期对象方法new Date(),传入毫秒数,就能将其转为对应的日期了;然后应用字符串split方法截取年、月、日,将日期转为显示格式。
将日期放入一个数组中,在select标签下直接循环数组,就可以实现筛选项的展示。
function getMondayDateArr() {
let date = new Date()
let dateTime = date.getTime()
let weekDay = date.getDay() || 7 // getDay返回一周中的某一天,周日会返回0,因此将其记为7
let oneDayTime = 24*60*60*1000
let mondayDateArr = []
for (let i = 0; i < 8; i++) {
// 当天的周数减1可以得到今天距离周一的天数,因此可以通过加一个7的倍数,得出距前几周的周一的毫秒数
let beforeMondayTime = dateTime - (weekDay - 1 + (i * 7)) * oneDayTime
let beforeMondayDate = new Date(beforeMondayTime)
let beforeLocalDate = beforeMondayDate.toLocaleDateString()
// getFormatDate方法将日期格式转为显示格式,如2022-01-01
let formatBeforeLocalDate = getFormatDate(beforeLocalDate)
mondayDateArr.unshift(formatBeforeLocalDate)
let afterMondayTime = dateTime + (7 - (weekDay - 1) + (i * 7)) * oneDayTime
let afterMondayDate = new Date(afterMondayTime)
let afterLocalDate = afterMondayDate.toLocaleDateString()
let formatAfterLocalDate = getFormatDate(afterLocalDate)
mondayDateArr.push(formatAfterLocalDate)
}
return mondayDateArr
}
// 日期选项数组的使用
<select>
{
mondayDateArr.map((mondayDate) => (
<option value={mondayDate}>{mondayDate}</option>
))
}
</select>
2. 排班信息表展示
排班信息表需要按各分店员工展示两周内每天排班信息,如果是新建排班或查询不到某个员工排班信息时,需要有一个默认排班表以供参考编辑。如何妥善展示这些信息很重要,因此我需要先设计好后端数据返回格式:在员工信息内包含其排班情况。前端发请求查询数据,查询不到的排班设为默认排班。页面展示时的多层嵌套循环以及循环内部的编辑修改让我对JSX语法更熟悉,积累了实现功能时前、后端总体设计编写经验。
后端数据库表设计及查询方式
数据库排班表:分为基础信息表及排班内容表,基础表记录开始时间,结束时间,分店等信息;内容表记录员工Id,排版类型,三个时段的开始结束时间等具体信息,两个表通过基础表Id关联。
查询:先通过时间、分店查询出基础表Id,再循环对应分店员工,根据员工Id和基础表Id,查询每日排班信息,按格式放入对应的员工信息中。返回的数据格式:
data: {
startDate: '',
endDate: '',
branchID: '',
status: '',
content: [
{staffID: 1, name: 'A', rosterInfo: [
{date: '', weekDay: '', hours: '', type: '', periodOneStart: '', periodOneEnd: ''...}
{date: '', weekDay: '', hours: '', type: '', periodOneStart: '', periodOneEnd: ''...}
...
]},
{staffID: 2, name: 'B', rosterInfo: []}
...
]
}
前端查询展示
前端查询:发请求查询排班信息,如果查询不到某个员工或者整个时间段暂无排班信息(即上述数据中rosterInfo为空数组),就在前端将其设为默认排班信息。
排班表展示:先循环数据中content数组,展示员工姓名,再内部循环rosterInfo,将排班内容逐条展示
content.map((staff) => (
<div>
<h3>{staff.name}</h3>
<table>
<thead>
<tr>
<th>Week</th>
<th>Date</th>
<th>Type</th>
<th>Period 1</th>
<th>Hours</th>
</tr>
</thead>
<tbody>
{
staff.rosterInfo.map((roster, index) => (
<tr>
<td>{roster.weekDay}</td>
<td>{roster.date}</td>
<td>{roster.type}</td>
<td>{roster.periodOneStart + ‘-‘ + roster.periodOneEnd}</td>
<td>{roster.hours}</td>
</tr>
))
}
</tbody>
</table>
</div>
))
3. 排班表时间段编辑
排班表时间段编辑,选用弹窗方式,时间选择选项是从早9点到晚9点且以30分钟为间隔的时间选项,应用Date对象的setHours、getHours方法以及while循环,进一步熟悉Date对象,提高了逻辑思维能力。
如何获得半小时为间隔的时间筛选项呢?
- 应用setHours和setMinutes方法设置一个初始时间,即早上9点
- 利用while循环,只要小时数小于等于21点,则将时间放到时间选项数组里,并将分钟累加30。
只要更改时间间隔,就能获得以任意时间为间隔的时间数组啦。
由于while条件是小时数小于21,会将21:30分放入数组,所以用数组的pop方法,移除时间数组最后一个元素。
时间段选择好开始结束时间,就可以通过getTime方法进行加减计算,得出时间段长度,从而统计出每天总工作时长。
function getTimeOption() {
let date = new Date()
date.setHours(9)
date.setMinutes(0)
let timeOption = []
while (date.getHours() <= 21) {
let h = date.getHours()
let m = date.getMinutes()
timeOption.push(h + ':' + (m === 0 ? m + '0' : m))
date.setMinutes(m + 30)
}
timeOption.pop()
return timeOption
}
小结
通过员工排班的时间筛选和表内时间编辑,让我对Date对象更加熟悉,也能比较熟练的运用Date的各种方法处理各种时间计算情况;排班表设计展示,积累了前后端总体设计编写经验,提升了独立完成小项目的能力。 另外,我发现每个项目开始前都应该先拆分其功能点,避免功能堆积导致无从下手;拆分后对每个部分进行细化,编写前多思考,可以少走弯路,提升编写效率。