gaoluyang
97 分钟以前 b98107c79f5198354bbd5f81c500fdf700c88a60
feat(hrm): 排班页展示请假标记与颜色图例

- 新增请假接口调用,按员工+日期打散渲染紫色「假」标签
- 新增日期标记图例:周末/节假日休息/调休上班/请假/班次
- 自动排班跳过审批通过的请假日

Co-Authored-By: Claude Code <noreply@anthropic.com>
已修改2个文件
198 ■■■■■ 文件已修改
src/api/hrm/schedule/index.ts 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/schedule/index.vue 173 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/schedule/index.ts
@@ -47,6 +47,20 @@
    employeeCount?: number;
    itemCount?: number;
  }
  /** 排班日历展示用的请假记录(仅审批通过) */
  export interface ScheduleLeave {
    id?: number;
    employeeId?: number;
    employeeName?: string;
    leaveType?: number;
    leaveTypeName?: string;
    /** 开始时间 yyyy-MM-dd HH:mm:ss */
    startTime?: string;
    /** 结束时间 yyyy-MM-dd HH:mm:ss */
    endTime?: string;
    duration?: number;
  }
}
/** 查询某月排班列表 */
@@ -73,3 +87,14 @@
    data,
  );
}
/** 查询日期范围内审批通过的请假记录(排班日历展示用) */
export function getEmployeeScheduleLeaveList(params: {
  startDate: string;
  endDate: string;
}) {
  return requestClient.get<HrmEmployeeScheduleApi.ScheduleLeave[]>(
    '/hrm/employee-schedule/leave-list',
    { params },
  );
}
src/views/hrm/schedule/index.vue
@@ -30,6 +30,7 @@
import { getShiftList } from '#/api/hrm/shift';
import {
  batchSaveEmployeeSchedule,
  getEmployeeScheduleLeaveList,
  getEmployeeScheduleList,
  saveEmployeeSchedule,
} from '#/api/hrm/schedule';
@@ -70,6 +71,12 @@
 * 当前视图日期范围内的节假日配置: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);
/** 是否已执行过查询 */
@@ -158,6 +165,10 @@
    }
    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();
@@ -388,12 +399,63 @@
  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();
    }
  },
);
@@ -664,6 +726,35 @@
      </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"
@@ -755,11 +846,22 @@
              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="{
@@ -859,7 +961,7 @@
            checkedEmployeeIds.size > 0
              ? '(仅勾选的员工)'
              : '(未勾选时为当前页全部员工)'
          }},生成后可再手动调整。
          }},生成后可再手动调整。请假日(审批通过)会自动跳过、不覆盖。
        </div>
        <div class="flex justify-end gap-2">
          <Button @click="autoOpen = false">取消</Button>
@@ -947,7 +1049,7 @@
            batchShiftId === 0
              ? '选择「休息」将清除该范围内已有排班。'
              : '按上方休息模式跳过周末,其余日期排所选班次。'
          }}已配置节假日的日期以节假日为准:休息日强制休息,工作日强制排班(含调休上班的周末)。
          }}已配置节假日的日期以节假日为准:休息日强制休息,工作日强制排班(含调休上班的周末);员工已审批通过的请假日自动跳过,保留原排班不动。
        </div>
        <div class="flex justify-end gap-2">
          <Button @click="batchOpen = false">取消</Button>
@@ -1054,5 +1156,66 @@
    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>