Преглед изворни кода

feat(attendance): 优化考勤状态处理逻辑,增强出勤日计算与异常日识别

xieyong пре 1 месец
родитељ
комит
29d049014b
1 измењених фајлова са 48 додато и 7 уклоњено
  1. 48 7
      src/pages/attendance/index.vue

+ 48 - 7
src/pages/attendance/index.vue

@@ -137,14 +137,21 @@ const weeks = ["一", "二", "三", "四", "五", "六", "日"];
 // 所以统一从状态字段里再识别一次,保证红点/红格稳定出现。
 const ABNORMAL_STATUSES = ["LATE", "EARLY", "ABSENT"];
 
-// 把后端的 status 字段统一成数组形态:兼容 string、array,也兼容 attendanceStatusList / statusList 两个字段名。
+// 把后端的 status 字段统一成数组形态:
+//   - 兼容 string / array
+//   - 兼容逗号分隔字符串 "NORMAL,LATE"(服务端 attendanceStatusList 的实际形态)
+//   - 同时兼容 attendanceStatusList / statusList 两个字段名
 function getDayStatuses(data) {
   if (!data) return [];
   const out = [];
   for (const v of [data.attendanceStatusList, data.statusList]) {
     if (!v) continue;
-    if (typeof v === "string") out.push(v);
-    else if (Array.isArray(v)) out.push(...v);
+    if (typeof v === "string") {
+      // 逗号分隔 → 拆;单一值 → 保留原值
+      out.push(...v.split(",").map((s) => s.trim()).filter(Boolean));
+    } else if (Array.isArray(v)) {
+      out.push(...v);
+    }
   }
   return out;
 }
@@ -161,6 +168,40 @@ function isAbnormalDay(data) {
   return getDayStatuses(data).some((s) => ABNORMAL_STATUSES.includes(s));
 }
 
+// 是否算出勤日:
+//   - 正常 (NORMAL)        → 算
+//   - 异常 (LATE/EARLY/ABSENT) → 算
+//   - 请假半天(status 同时含 LEAVE 与 NORMAL/LATE/EARLY/ABSENT)→ 算
+//   - 请假全天(status 只有 LEAVE)→ 不算
+// 出勤率 = attendanceDays / workDays,所以分子要把「来了 / 来了一半」都纳入。
+function isAttendedDay(data) {
+  if (!data) return false;
+  const statuses = getDayStatuses(data);
+  // 全天请假:只有一个 LEAVE → 不算出勤
+  const isOnlyLeave = statuses.length === 1 && statuses[0] === "LEAVE";
+  if (isOnlyLeave) return false;
+  // 含 NORMAL/LATE/EARLY/ABSENT 任一 → 算(含异常日、半天请假)
+  return statuses.some((s) =>
+    s === "NORMAL" || s === "LATE" || s === "EARLY" || s === "ABSENT",
+  );
+}
+
+// 计算某月按周班次表应出勤的天数(与 daily 数据是否回传无关)。
+// 用法:rate = attendanceDays / expectedWorkDays,避免「当月还没结束 / API 只回了有打卡的天」
+// 导致分母偏小、出勤率虚高。
+function getExpectedWorkDaysInMonth(month, schedule) {
+  if (!Array.isArray(schedule) || schedule.length === 0) return 0;
+  const [y, m] = month.split("-").map(Number);
+  if (!y || !m) return 0;
+  const totalDays = new Date(y, m, 0).getDate();
+  let count = 0;
+  for (let d = 1; d <= totalDays; d++) {
+    const ds = `${y}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
+    if (!isRestDay(ds, schedule)) count++;
+  }
+  return count;
+}
+
 const currentMonth = ref(formatMonth(new Date()));
 const monthList = ref([]); // ESS 月度返回数组
 const selectedDate = ref(formatDate(new Date()));
@@ -190,20 +231,20 @@ const selectedLabel = computed(() => {
 
 const summary = computed(() => {
   const list = monthList.value || [];
-  let workDays = 0;
   let attendanceDays = 0;
   let lateCount = 0;
   let abnormalCount = 0;
   const schedule = weeklySchedule.value;
+  // 应出勤天数:按当前月 + 周班次表推算(不依赖 daily 数据是否回传)
+  const workDays = getExpectedWorkDaysInMonth(currentMonth.value, schedule);
   for (const item of list) {
     const statuses = getDayStatuses(item);
     // 休息日跳过:与日历「休息日无点」保持一致;
     // 优先看状态 REST,缺失时再按周班次表兜底(月底补算没及时回写等场景)。
     const isRest = statuses.includes("REST") || isRestDay(item.attendanceDate, schedule);
     if (isRest) continue;
-    workDays++;
-    // 出勤天数 = NORMAL:与日历「绿点」同源
-    if (statuses.includes("NORMAL")) attendanceDays++;
+    // 出勤天数 = NORMAL + 异常 + 半天请假,与日历「绿点/红点/黄点」三类同源
+    if (isAttendedDay(item)) attendanceDays++;
     // 异常数:与日历「红点」同源,覆盖 LATE/EARLY/ABSENT 三类
     if (isAbnormalDay(item)) abnormalCount++;
     // 迟到:异常里的一个子集,按 lateMinutes 或 LATE 状态识别(数组形态兼容)