|
|
@@ -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();
|