| | |
| | | <u-icon name="calendar" |
| | | color="#348fe2" |
| | | size="16"></u-icon> |
| | | <text class="shift-text">白班: {{ todayRecord.startAt }}-{{ todayRecord.endAt }}</text> |
| | | <text class="shift-text">{{ todayRecord.shift || '-' }}: {{ todayRecord.startAt }}-{{ todayRecord.endAt }}</text> |
| | | </view> |
| | | </view> |
| | | <!-- 打卡按钮 --> |
| | |
| | | const todayRecord = ref({}); |
| | | |
| | | // 班次信息 |
| | | const workTimeDict = ref({ |
| | | startAt: "09:00", |
| | | endAt: "18:00", |
| | | }); |
| | | const workTimeDict = ref(); |
| | | |
| | | // 当前时间展示 |
| | | const nowTime = ref(""); |
| | |
| | | findTodayPersonalAttendanceRecord({}).then(res => { |
| | | if (res.data) { |
| | | todayRecord.value = res.data; |
| | | noNeedCheckIn.value = false; |
| | | // 检查startAt和endAt是否为空,为空则无需打卡 |
| | | if (!todayRecord.value.startAt || !todayRecord.value.endAt) { |
| | | noNeedCheckIn.value = true; |
| | | } else { |
| | | noNeedCheckIn.value = false; |
| | | } |
| | | updateInCheckRange(); |
| | | } else { |
| | | // 页面显示“无需打卡” |
| | | todayRecord.value = {}; |
| | | noNeedCheckIn.value = true; |
| | | updateInCheckRange(); |
| | | } |
| | | }); |
| | | }; |
| | |
| | | const m = String(now.getMinutes()).padStart(2, "0"); |
| | | const s = String(now.getSeconds()).padStart(2, "0"); |
| | | nowTime.value = `${Y}-${M}-${D} ${h}:${m}:${s}`; |
| | | updateInCheckRange(); |
| | | }; |
| | | |
| | | // 今日日期 |
| | | const todayStr = computed(() => nowTime.value.slice(0, 10)); |
| | | |
| | | const parseHmToMinutes = hm => { |
| | | if (!hm || typeof hm !== "string") return null; |
| | | const [hStr, mStr] = hm.split(":"); |
| | | const h = Number(hStr); |
| | | const m = Number(mStr); |
| | | if (!Number.isFinite(h) || !Number.isFinite(m)) return null; |
| | | if (h < 0 || h > 23 || m < 0 || m > 59) return null; |
| | | return h * 60 + m; |
| | | }; |
| | | |
| | | const parseYmdToDate = ymd => { |
| | | if (!ymd || typeof ymd !== "string") return null; |
| | | const [yStr, mStr, dStr] = ymd.slice(0, 10).split("-"); |
| | | const y = Number(yStr); |
| | | const m = Number(mStr); |
| | | const d = Number(dStr); |
| | | if (!Number.isFinite(y) || !Number.isFinite(m) || !Number.isFinite(d)) { |
| | | return null; |
| | | } |
| | | return new Date(y, m - 1, d, 0, 0, 0, 0); |
| | | }; |
| | | |
| | | const addMinutes = (date, minutes) => { |
| | | return new Date(date.getTime() + minutes * 60 * 1000); |
| | | }; |
| | | |
| | | const buildShiftWindow = () => { |
| | | const startAtMinutes = parseHmToMinutes(todayRecord.value?.startAt); |
| | | const endAtMinutes = parseHmToMinutes(todayRecord.value?.endAt); |
| | | if (startAtMinutes === null || endAtMinutes === null) return null; |
| | | |
| | | const baseYmd = todayRecord.value?.date |
| | | ? String(todayRecord.value.date).slice(0, 10) |
| | | : todayStr.value; |
| | | const baseDate = parseYmdToDate(baseYmd); |
| | | if (!baseDate) return null; |
| | | |
| | | const startDateTime = addMinutes(baseDate, startAtMinutes); |
| | | const crossDay = startAtMinutes > endAtMinutes; |
| | | const endBase = crossDay ? addMinutes(baseDate, 24 * 60) : baseDate; |
| | | const endDateTime = addMinutes(endBase, endAtMinutes); |
| | | |
| | | return { startDateTime, endDateTime }; |
| | | }; |
| | | |
| | | const updateInCheckRange = () => { |
| | | if (noNeedCheckIn.value) { |
| | | inCheckRange.value = true; |
| | | return; |
| | | } |
| | | const window = buildShiftWindow(); |
| | | if (!window) { |
| | | inCheckRange.value = true; |
| | | return; |
| | | } |
| | | const now = new Date(); |
| | | const checkInEarlyMinutes = 120; |
| | | const checkOutLateMinutes = 720; |
| | | const needAction = (() => { |
| | | if (todayRecord.value?.workEndAt) return "done"; |
| | | if (todayRecord.value?.workStartAt) return "checkOut"; |
| | | const distToStart = Math.abs( |
| | | now.getTime() - window.startDateTime.getTime() |
| | | ); |
| | | const distToEnd = Math.abs(now.getTime() - window.endDateTime.getTime()); |
| | | return distToEnd < distToStart ? "checkOut" : "checkIn"; |
| | | })(); |
| | | const start = addMinutes(window.startDateTime, -checkInEarlyMinutes); |
| | | const end = addMinutes( |
| | | window.endDateTime, |
| | | needAction === "checkOut" ? checkOutLateMinutes : 0 |
| | | ); |
| | | inCheckRange.value = now >= start && now <= end; |
| | | }; |
| | | |
| | | // 打卡按钮文本 |
| | | const checkInOutText = computed(() => { |
| | | if (noNeedCheckIn.value) { |
| | | return "无需打卡"; |
| | | } |
| | | if (!todayRecord.value || !todayRecord.value.workStartAt) { |
| | | return "上班打卡"; |
| | | } |
| | | if (!todayRecord.value.workEndAt) { |
| | | return "下班打卡"; |
| | | if (todayRecord.value.workStartAt) { |
| | | return "下班打卡"; |
| | | } |
| | | const window = buildShiftWindow(); |
| | | if (!window) return "上班打卡"; |
| | | const now = new Date(); |
| | | const distToStart = Math.abs( |
| | | now.getTime() - window.startDateTime.getTime() |
| | | ); |
| | | const distToEnd = Math.abs(now.getTime() - window.endDateTime.getTime()); |
| | | return distToEnd < distToStart ? "下班打卡" : "上班打卡"; |
| | | } |
| | | return "已打卡"; |
| | | }); |
| | |
| | | // #endif |
| | | }); |
| | | }; |
| | | const form = ref({ |
| | | longitude: "", |
| | | latitude: "", |
| | | }); |
| | | |
| | | // 获取当前位置 |
| | | const getCurrentLocation = () => { |
| | | return new Promise((resolve, reject) => { |
| | | uni.getLocation({ |
| | | type: "wgs84", |
| | | success: res => { |
| | | currentLocation.value = res; |
| | | // 模拟检查是否在打卡范围内(实际项目中应根据公司位置和允许的半径进行计算) |
| | | // 这里简单模拟为始终在范围内 |
| | | inCheckRange.value = true; |
| | | resolve(res); |
| | | }, |
| | | fail: err => { |
| | | console.error("获取位置失败:", err); |
| | | // 失败时默认允许打卡(实际项目中应根据业务需求处理) |
| | | inCheckRange.value = true; |
| | | reject(err); |
| | | }, |
| | | }); |
| | | uni.showLoading({ title: "获取位置中..." }); |
| | | |
| | | uni.getLocation({ |
| | | type: "gcj02", |
| | | success: res => { |
| | | uni.hideLoading(); |
| | | form.value.latitude = res.latitude; |
| | | form.value.longitude = res.longitude; |
| | | }, |
| | | fail: err => { |
| | | uni.hideLoading(); |
| | | console.error("获取位置失败:", err); |
| | | |
| | | // 显示错误提示并引导用户检查权限 |
| | | showToast("获取位置失败,请检查定位权限"); |
| | | |
| | | // 引导用户检查权限设置 |
| | | uni.showModal({ |
| | | title: "位置权限提示", |
| | | content: |
| | | "获取位置失败,可能是因为位置权限未开启,请在设备设置中检查并开启位置权限。", |
| | | confirmText: "知道了", |
| | | cancelText: "取消", |
| | | success: res => { |
| | | if (res.confirm) { |
| | | // 可以尝试打开设置页面(如果支持) |
| | | if (uni.openSetting) { |
| | | uni.openSetting({ |
| | | success: settingRes => { |
| | | console.log("设置结果:", settingRes); |
| | | }, |
| | | }); |
| | | } |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | // 失败时显示错误信息 |
| | | form.value.visitAddress = "位置获取失败"; |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | |
| | | } |
| | | |
| | | // 调用打卡API |
| | | createPersonalAttendanceRecord({}) |
| | | createPersonalAttendanceRecord({ |
| | | ...form.value, |
| | | }) |
| | | .then(res => { |
| | | // 记录打卡时间 |
| | | lastCheckInTime.value = Date.now(); |
| | |
| | | fetchTodayData(); |
| | | }) |
| | | .catch(error => { |
| | | console.error("打卡失败:", error); |
| | | uni.showToast({ |
| | | title: error.msg || "打卡失败,请重试", |
| | | icon: "none", |
| | |
| | | |
| | | // 获取位置权限并检查位置 |
| | | try { |
| | | await getLocationPermission(); |
| | | // await getLocationPermission(); |
| | | await getCurrentLocation(); |
| | | } catch (error) { |
| | | console.error("位置权限获取失败:", error); |
| | |
| | | .attendance-records { |
| | | animation-delay: 0.2s; |
| | | } |
| | | </style> |
| | | </style> |