| | |
| | | import { getShiftList } from '#/api/hrm/shift'; |
| | | import { |
| | | batchSaveEmployeeSchedule, |
| | | getEmployeeScheduleLeaveList, |
| | | getEmployeeScheduleList, |
| | | saveEmployeeSchedule, |
| | | } from '#/api/hrm/schedule'; |
| | |
| | | * 当前视图日期范围内的节假日配置:key = yyyy-MM-dd,value = 1-休息日 / 2-工作日 |
| | | */ |
| | | const holidayMap = ref(new Map<string, number>()); |
| | | /** |
| | | * 当前视图日期范围内的请假:key = `${employeeId}_${yyyy-MM-dd}`,value = 请假记录 |
| | | */ |
| | | const leaveMap = ref( |
| | | new Map<string, HrmEmployeeScheduleApi.ScheduleLeave>(), |
| | | ); |
| | | const loading = ref(false); |
| | | const saving = ref(false); |
| | | /** 是否已执行过查询 */ |
| | |
| | | } |
| | | for (const day of days.value) { |
| | | const date = formatDate(month.value, day); |
| | | // 请假期内不排班(后端批量排班同样跳过,保留原排班不动) |
| | | if (getLeaveByCell(emp.id, date)) { |
| | | continue; |
| | | } |
| | | const week = new Date( |
| | | `${month.value}-${String(day).padStart(2, '0')}`, |
| | | ).getDay(); |
| | |
| | | holidayMap.value = map; |
| | | } |
| | | |
| | | /** 视图日期变化(切月/切周/切视图)时重新拉取节假日 */ |
| | | /** 加载当前视图日期范围内的已审批请假,按「员工_日期」打散 */ |
| | | async function loadLeaves() { |
| | | const list = viewDays.value; |
| | | const first = list[0]; |
| | | const last = list[list.length - 1]; |
| | | if (!first || !last) { |
| | | leaveMap.value = new Map(); |
| | | return; |
| | | } |
| | | const leaves = await getEmployeeScheduleLeaveList({ |
| | | startDate: first.date, |
| | | endDate: last.date, |
| | | }); |
| | | const map = new Map<string, HrmEmployeeScheduleApi.ScheduleLeave>(); |
| | | for (const leave of leaves) { |
| | | if (leave.employeeId == null || !leave.startTime || !leave.endTime) { |
| | | continue; |
| | | } |
| | | const start = dayjs(leave.startTime).format('YYYY-MM-DD'); |
| | | const end = dayjs(leave.endTime).format('YYYY-MM-DD'); |
| | | const from = start < first.date ? first.date : start; |
| | | const to = end > last.date ? last.date : end; |
| | | for ( |
| | | let date = dayjs(from); |
| | | !date.isAfter(dayjs(to), 'day'); |
| | | date = date.add(1, 'day') |
| | | ) { |
| | | map.set(`${leave.employeeId}_${date.format('YYYY-MM-DD')}`, leave); |
| | | } |
| | | } |
| | | leaveMap.value = map; |
| | | } |
| | | |
| | | /** 某员工某日的请假记录 */ |
| | | function getLeaveByCell( |
| | | employeeId: number, |
| | | date: string, |
| | | ): HrmEmployeeScheduleApi.ScheduleLeave | undefined { |
| | | return leaveMap.value.get(`${employeeId}_${date}`); |
| | | } |
| | | |
| | | /** 请假单元格悬浮提示 */ |
| | | function leaveTitle(leave: HrmEmployeeScheduleApi.ScheduleLeave): string { |
| | | const range = `${dayjs(leave.startTime).format('MM-DD HH:mm')} ~ ${dayjs( |
| | | leave.endTime, |
| | | ).format('MM-DD HH:mm')}`; |
| | | const duration = leave.duration == null ? '' : `,共 ${leave.duration} 天`; |
| | | return `请假:${leave.leaveTypeName ?? ''}(审批通过)\n${range}${duration}`; |
| | | } |
| | | |
| | | /** 视图日期变化(切月/切周/切视图)时重新拉取节假日与请假 */ |
| | | watch( |
| | | () => viewDays.value.map((vd) => vd.date).join(','), |
| | | (key) => { |
| | | if (key) { |
| | | loadHolidays(); |
| | | loadLeaves(); |
| | | } |
| | | }, |
| | | ); |
| | |
| | | </template> |
| | | </div> |
| | | |
| | | <!-- 日期标记图例 --> |
| | | <div |
| | | class="mb-4 flex flex-wrap items-center gap-4 rounded-lg bg-white px-4 py-3 text-sm" |
| | | > |
| | | <span class="text-gray-500">日期标记图例:</span> |
| | | <span class="inline-flex items-center gap-1.5"> |
| | | <span class="legend-swatch bg-red-50" /> |
| | | 周末 |
| | | </span> |
| | | <span class="inline-flex items-center gap-1.5"> |
| | | <span class="legend-swatch legend-swatch-rest" /> |
| | | 节假日休息 |
| | | <span class="legend-tag legend-tag-rest">休</span> |
| | | </span> |
| | | <span class="inline-flex items-center gap-1.5"> |
| | | <span class="legend-swatch legend-swatch-work" /> |
| | | 调休上班 |
| | | <span class="legend-tag legend-tag-work">班</span> |
| | | </span> |
| | | <span class="inline-flex items-center gap-1.5"> |
| | | <span class="legend-tag legend-tag-leave">假</span> |
| | | 请假(审批通过,不参与排班) |
| | | </span> |
| | | <span class="inline-flex items-center gap-1.5"> |
| | | <span class="legend-swatch legend-swatch-shift" /> |
| | | 班次(颜色见班次图例) |
| | | </span> |
| | | </div> |
| | | |
| | | <!-- 班次图例 --> |
| | | <div |
| | | v-if="shiftList.length" |
| | |
| | | v-for="vd in viewDays" |
| | | :key="vd.date" |
| | | class="schedule-cell" |
| | | :class="[vd.isWeekend ? 'bg-red-50/40' : '', holidayClass(vd.date)]" |
| | | :class="[ |
| | | vd.isWeekend ? 'bg-red-50/40' : '', |
| | | holidayClass(vd.date), |
| | | getLeaveByCell(emp.id!, vd.date) ? 'leave-cell' : '', |
| | | ]" |
| | | @click="openPicker(emp, vd.date)" |
| | | > |
| | | <span |
| | | v-if="getShiftByCell(emp.id!, vd.date)" |
| | | v-if="getLeaveByCell(emp.id!, vd.date)" |
| | | class="leave-chip" |
| | | :title="leaveTitle(getLeaveByCell(emp.id!, vd.date)!)" |
| | | > |
| | | 假·{{ getLeaveByCell(emp.id!, vd.date)!.leaveTypeName || '请假' }} |
| | | </span> |
| | | <span |
| | | v-else-if="getShiftByCell(emp.id!, vd.date)" |
| | | class="shift-chip" |
| | | :title="getShiftByCell(emp.id!, vd.date)!.name" |
| | | :style="{ |
| | |
| | | checkedEmployeeIds.size > 0 |
| | | ? '(仅勾选的员工)' |
| | | : '(未勾选时为当前页全部员工)' |
| | | }},生成后可再手动调整。 |
| | | }},生成后可再手动调整。请假日(审批通过)会自动跳过、不覆盖。 |
| | | </div> |
| | | <div class="flex justify-end gap-2"> |
| | | <Button @click="autoOpen = false">取消</Button> |
| | |
| | | batchShiftId === 0 |
| | | ? '选择「休息」将清除该范围内已有排班。' |
| | | : '按上方休息模式跳过周末,其余日期排所选班次。' |
| | | }}已配置节假日的日期以节假日为准:休息日强制休息,工作日强制排班(含调休上班的周末)。 |
| | | }}已配置节假日的日期以节假日为准:休息日强制休息,工作日强制排班(含调休上班的周末);员工已审批通过的请假日自动跳过,保留原排班不动。 |
| | | </div> |
| | | <div class="flex justify-end gap-2"> |
| | | <Button @click="batchOpen = false">取消</Button> |
| | |
| | | white-space: nowrap; |
| | | border-radius: 3px; |
| | | } |
| | | |
| | | /* 请假:紫色底 + 紫色标签,与班次/节假日区分 */ |
| | | .leave-cell { |
| | | background-color: #f0ebff !important; |
| | | } |
| | | |
| | | .leave-chip { |
| | | display: inline-block; |
| | | max-width: 100%; |
| | | padding: 1px 6px; |
| | | overflow: hidden; |
| | | color: #fff; |
| | | font-size: 12px; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | background-color: #722ed1; |
| | | border-radius: 3px; |
| | | } |
| | | } |
| | | |
| | | /* 图例色块(图例不在 .schedule-table 内) */ |
| | | .legend-swatch { |
| | | display: inline-block; |
| | | width: 12px; |
| | | height: 12px; |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | .legend-swatch-rest { |
| | | background-color: #fff1e6; |
| | | border: 1px solid #ffd591; |
| | | } |
| | | |
| | | .legend-swatch-work { |
| | | background-color: #e8f8ec; |
| | | border: 1px solid #b7eb8f; |
| | | } |
| | | |
| | | .legend-swatch-shift { |
| | | background-color: #1677ff; |
| | | } |
| | | |
| | | .legend-tag { |
| | | display: inline-block; |
| | | padding: 0 3px; |
| | | color: #fff; |
| | | font-size: 10px; |
| | | line-height: 14px; |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | .legend-tag-rest { |
| | | background-color: #fa8c16; |
| | | } |
| | | |
| | | .legend-tag-work { |
| | | background-color: #52c41a; |
| | | } |
| | | |
| | | .legend-tag-leave { |
| | | background-color: #722ed1; |
| | | } |
| | | </style> |