Просмотр исходного кода

feat(attendance): 优化上下班时间计算逻辑,增加今日下班卡打卡拦截提示

xieyong 4 дней назад
Родитель
Сommit
215275a944
1 измененных файлов с 54 добавлено и 12 удалено
  1. 54 12
      src/pages/attendance/index.vue

+ 54 - 12
src/pages/attendance/index.vue

@@ -255,11 +255,30 @@ function cellClass(cell) {
   return cls.join(" ");
 }
 
+// 计算某一天的有效上下班时间,详情卡与下班卡校验都从这里取:
+//   1) daily 接口返回的 scheduledStart/End
+//   2) 周班次表当天非休息日时的首/末段
+//   3) shiftInfo.startTime / endTime(rule 兜底)
+// dateStr 与 selectedDate 相同时复用已加载的 dailyDetail;非选中日期时 daily 字段视为空。
+function getEffectiveShiftRange(dateStr) {
+  const isSelected = dateStr === selectedDate.value
+  const daily = isSelected ? (dailyDetail.value || {}) : {}
+  const scheduleInfo = getShiftForDate(dateStr, weeklySchedule.value)
+  let start = daily.scheduledStart || ''
+  let end = daily.scheduledEnd || ''
+  if ((!start || !end) && scheduleInfo && !scheduleInfo.isRestDay && scheduleInfo.segments.length) {
+    start = start || scheduleInfo.segments[0].start
+    end = end || scheduleInfo.segments[scheduleInfo.segments.length - 1].end
+  }
+  if (!start) start = shiftInfo.startTime
+  if (!end) end = shiftInfo.endTime
+  return { start, end, scheduleInfo, isRestDay: Boolean(scheduleInfo?.isRestDay) }
+}
+
 const detail = computed(() => {
   const d = dailyDetail.value || {};
-  // 选中日期的周班次(用于 daily 接口没返回数据时的兜底显示)
-  const scheduleInfo = getShiftForDate(selectedDate.value, weeklySchedule.value);
-  const isRest = Boolean(scheduleInfo?.isRestDay);
+  // 与 getPunchBlockReason 共用同源计算
+  const { start, end, scheduleInfo, isRestDay: isRest } = getEffectiveShiftRange(selectedDate.value);
 
   const inTime = d.actualIn || null;
   const outTime = d.actualOut || null;
@@ -271,15 +290,6 @@ const detail = computed(() => {
   const outLabel = outTime
     ? (d.earlyMinutes > 0 ? `早退 ${d.earlyMinutes} 分钟` : "正常")
     : "未打卡";
-  // 上下班时间:daily 返回 > 周班次表(当天非休息日时)> rule.shiftStartTime/End
-  let start = d.scheduledStart || '';
-  let end = d.scheduledEnd || '';
-  if ((!start || !end) && scheduleInfo && !scheduleInfo.isRestDay && scheduleInfo.segments.length) {
-    start = start || scheduleInfo.segments[0].start
-    end = end || scheduleInfo.segments[scheduleInfo.segments.length - 1].end
-  }
-  if (!start) start = shiftInfo.startTime
-  if (!end) end = shiftInfo.endTime
 
   let shiftText;
   if (ruleStatus.value === 'empty') {
@@ -439,6 +449,33 @@ function changeMonth(step) {
   loadMonth(target);
 }
 
+// 计算「今日下班卡是否允许打」:
+//   - 已在今日打过上班卡、未打下班卡 → 本次是下班卡
+//   - 当前时间 < 下班时间 → 拦截并提示
+// 下班时间取值优先级与 detail 一致:daily 返回 > 周班次表 > shiftInfo.endTime
+function getPunchBlockReason() {
+  const todayStr = formatDate(new Date());
+  // 仅当用户在查看「今日」时校验,避免误判其他日期
+  if (selectedDate.value !== todayStr) return null;
+  const detail = dailyDetail.value;
+  if (!detail) return null; // 当日详情未加载或接口失败 → 不拦截(保守放行)
+  // 与详情卡 detail 共用同源计算
+  const { end: endTime, isRestDay } = getEffectiveShiftRange(todayStr);
+  if (isRestDay) return null; // 休息日不拦
+  if (!endTime) return null; // 拿不到下班时间 → 不拦截
+  // endTime 形态多:HH:mm / HH:mm:ss / "yyyy-MM-dd HH:mm:ss" / "yyyy-MM-ddTHH:mm:ss[.SSS][+08:00]"
+  // 取首个 HH:mm 段,避免 split(":") 把 "2026-09-14 18:00:00" 拆成 NaN 导致永远不拦截
+  const m = String(endTime).match(/(\d{1,2}):(\d{2})/);
+  if (!m) return null;
+  const endMinutes = Number(m[1]) * 60 + Number(m[2]);
+  const now = new Date();
+  const nowMinutes = now.getHours() * 60 + now.getMinutes();
+  if (nowMinutes < endMinutes) {
+    return `还未到下班时间(${endTime}),暂不能打下班卡`;
+  }
+  return null;
+}
+
 async function onPunch() {
   // 规则未配置 / 不可打卡:拦截
   if (ruleStatus.value === 'empty') {
@@ -448,6 +485,11 @@ async function onPunch() {
     const reason = ruleBlockedReason.value || '不可用';
     return uni.showToast({ title: `考勤规则${reason},暂不能打卡`, icon: "none" });
   }
+  // 未到下班点 → 拦截打下班卡
+  const punchBlock = getPunchBlockReason();
+  if (punchBlock) {
+    return uni.showToast({ title: punchBlock, icon: "none" });
+  }
   uni.showLoading({ title: "定位中..." });
   try {
     const loc = await getLocationWithFallback();