|
|
@@ -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 状态识别(数组形态兼容)
|