| | |
| | | <u-icon name="calendar" |
| | | color="#348fe2" |
| | | size="16"></u-icon> |
| | | <text class="shift-text">白班: 09:00-18:00</text> |
| | | <text class="shift-text">白班: {{ todayRecord.startAt }}-{{ todayRecord.endAt }}</text> |
| | | </view> |
| | | </view> |
| | | <!-- 打卡按钮 --> |
| | | <view class="checkin-button-container"> |
| | | <view class="checkin-button-wrapper"> |
| | | <view class="checkin-button" |
| | | :class="{ 'disabled': checkInOutText === '已打卡' }" |
| | | :class="{ 'disabled': todayRecord.workEndAt || noNeedCheckIn }" |
| | | @click="handleCheckInOut"> |
| | | <text class="checkin-button-text">{{ checkInOutText }}</text> |
| | | <text class="checkin-time">{{ nowTime.split(' ')[1] }}</text> |
| | |
| | | <view class="employee-info"> |
| | | <view class="info-item"> |
| | | <text class="info-label">部门</text> |
| | | <text class="info-value">{{ currentUser.dept }}</text> |
| | | <text class="info-value">{{ todayRecord.deptName || '-' }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">姓名</text> |
| | | <text class="info-value">{{ currentUser.name }}</text> |
| | | <text class="info-value">{{ todayRecord.staffName || '-' }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">工号</text> |
| | | <text class="info-value">{{ currentUser.no }}</text> |
| | | <text class="info-value">{{ todayRecord.staffNo || '-' }}</text> |
| | | </view> |
| | | </view> |
| | | <!-- 今日考勤状态 --> |
| | | <view class="today-status"> |
| | | <u-icon :name="todayRecord ? 'checkmark-circle' : 'close-circle'" |
| | | :color="todayRecord ? '#4cd964' : '#ff3b30'" |
| | | <u-icon :name="todayRecord.id ? 'checkmark-circle' : 'close-circle'" |
| | | :color="todayRecord.id ? '#4cd964' : '#ff3b30'" |
| | | size="16"></u-icon> |
| | | <text class="status-text"> |
| | | {{ todayRecord ? `今日考勤: 上班 ${todayRecord.checkInTime}` : '今日未打卡' }} |
| | | {{ todayRecord.id ? `今日考勤: 上班 ${todayRecord.workStartAt || '-'}` : '今日未打卡' }} |
| | | </text> |
| | | </view> |
| | | <!-- 下班考勤状态 --> |
| | | <view v-if="todayRecord && todayRecord.checkOutTime" |
| | | <view v-if="todayRecord.id && todayRecord.workEndAt" |
| | | class="today-status"> |
| | | <u-icon :name="todayRecord ? 'checkmark-circle' : 'close-circle'" |
| | | :color="todayRecord ? '#4cd964' : '#ff3b30'" |
| | | <u-icon name="checkmark-circle" |
| | | color="#4cd964" |
| | | size="16"></u-icon> |
| | | <text class="status-text"> |
| | | {{ `今日考勤: 下班 ${todayRecord.checkOutTime}` }} |
| | | {{ `今日考勤: 下班 ${todayRecord.workEndAt}` }} |
| | | </text> |
| | | </view> |
| | | <!-- 打卡状态 --> |
| | | <view v-if="todayRecord" |
| | | class="today-status"> |
| | | <u-icon :name="todayRecord.status === 'normal' ? 'checkmark-circle' : 'clock'" |
| | | :color="todayRecord.status === 'normal' ? '#4cd964' : '#ff3b30'" |
| | | <view class="today-status"> |
| | | <u-icon :name="todayRecord.id ? (todayRecord.status === 0 ? 'checkmark-circle' : 'clock') : 'clock'" |
| | | :color="todayRecord.id ? (todayRecord.status === 0 ? '#4cd964' : '#ff3b30') : '#ff3b30'" |
| | | size="16"></u-icon> |
| | | <text class="status-text"> |
| | | {{ `打卡状态: ${todayRecord.statusText}` }} |
| | | {{ `打卡状态: ${todayStatusText}` }} |
| | | </text> |
| | | </view> |
| | | <view v-else |
| | | <!-- 工时信息 --> |
| | | <view v-if="todayRecord.id && todayRecord.workHours" |
| | | class="today-status"> |
| | | <u-icon name="clock" |
| | | color="#ff3b30" |
| | | color="#348fe2" |
| | | size="16"></u-icon> |
| | | <text class="status-text"> |
| | | 打卡状态: 缺卡 |
| | | {{ `工时(小时): ${todayRecord.workHours}` }} |
| | | </text> |
| | | </view> |
| | | </view> |
| | |
| | | <script setup> |
| | | import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | // 模拟当前登录员工 |
| | | const currentUser = reactive({ |
| | | id: 1, |
| | | name: "张三", |
| | | no: "E10001", |
| | | dept: "生产一部", |
| | | }); |
| | | import { getDicts } from "@/api/system/dict/data"; |
| | | import { |
| | | createPersonalAttendanceRecord, |
| | | findTodayPersonalAttendanceRecord, |
| | | } from "@/api/personnelManagement/attendance.js"; |
| | | // 今日打卡记录 |
| | | const todayRecord = ref({}); |
| | | |
| | | // 模拟考勤原始数据 |
| | | const rawAttendance = ref([ |
| | | { |
| | | id: 2, |
| | | date: "2026-02-08", |
| | | userId: 1, |
| | | name: "张三", |
| | | no: "E10001", |
| | | dept: "生产一部", |
| | | checkInTime: "09:15", |
| | | checkOutTime: "18:05", |
| | | workHours: 8.8, |
| | | status: "late", |
| | | statusText: "迟到", |
| | | remark: "因交通拥堵迟到", |
| | | }, |
| | | { |
| | | id: 3, |
| | | date: "2026-02-07", |
| | | userId: 1, |
| | | name: "张三", |
| | | no: "E10001", |
| | | dept: "生产一部", |
| | | checkInTime: "08:45", |
| | | checkOutTime: "18:30", |
| | | workHours: 9.7, |
| | | status: "normal", |
| | | statusText: "正常", |
| | | remark: "加班0.5小时", |
| | | }, |
| | | ]); |
| | | // 班次信息 |
| | | const workTimeDict = ref({ |
| | | startAt: "09:00", |
| | | endAt: "18:00", |
| | | }); |
| | | |
| | | // 当前时间展示 |
| | | const nowTime = ref(""); |
| | | let timer = null; |
| | | |
| | | // 上次打卡时间 |
| | | const lastCheckInTime = ref(null); |
| | | |
| | | // 打卡冷却时间(毫秒) |
| | | const CHECKIN_COOLDOWN = 5000; |
| | | // 返回上一页 |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | // 查询今日打卡信息 |
| | | const fetchTodayData = () => { |
| | | findTodayPersonalAttendanceRecord({}).then(res => { |
| | | if (res.data) { |
| | | todayRecord.value = res.data; |
| | | // 检查startAt和endAt是否为空,为空则无需打卡 |
| | | if (!todayRecord.value.startAt || !todayRecord.value.endAt) { |
| | | noNeedCheckIn.value = true; |
| | | } else { |
| | | noNeedCheckIn.value = false; |
| | | } |
| | | } else { |
| | | // 页面显示“无需打卡” |
| | | todayRecord.value = {}; |
| | | noNeedCheckIn.value = true; |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // 打卡范围状态 |
| | | const inCheckRange = ref(true); |
| | | |
| | | // 是否无需打卡 |
| | | const noNeedCheckIn = ref(false); |
| | | |
| | | // 当前位置 |
| | | const currentLocation = ref(null); |
| | |
| | | // 今日日期 |
| | | const todayStr = computed(() => nowTime.value.slice(0, 10)); |
| | | |
| | | // 当日当前员工考勤记录 |
| | | const todayRecord = computed(() => |
| | | rawAttendance.value.find( |
| | | item => item.userId === currentUser.id && item.date === todayStr.value |
| | | ) |
| | | ); |
| | | |
| | | // 打卡按钮文本 |
| | | const checkInOutText = computed(() => { |
| | | if (!todayRecord.value || !todayRecord.value.checkInTime) { |
| | | if (noNeedCheckIn.value) { |
| | | return "无需打卡"; |
| | | } |
| | | if (!todayRecord.value || !todayRecord.value.workStartAt) { |
| | | return "上班打卡"; |
| | | } |
| | | if (!todayRecord.value.checkOutTime) { |
| | | if (!todayRecord.value.workEndAt) { |
| | | return "下班打卡"; |
| | | } |
| | | return "已打卡"; |
| | | }); |
| | | |
| | | // 今日状态标签类型 |
| | | const todayStatusTag = computed(() => { |
| | | if (!todayRecord.value.id) return "info"; |
| | | if (todayRecord.value.status === 0) return "success"; |
| | | return "danger"; |
| | | }); |
| | | |
| | | // 今日状态文本 |
| | | const todayStatusText = computed(() => { |
| | | if (!todayRecord.value.id) return "未打卡"; |
| | | switch (todayRecord.value.status) { |
| | | case 0: |
| | | return "正常"; |
| | | case 1: |
| | | return "迟到"; |
| | | case 2: |
| | | return "早退"; |
| | | case 3: |
| | | return "迟到、早退"; |
| | | case 4: |
| | | 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 = "位置获取失败"; |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | // 打卡逻辑(仅前端模拟) |
| | | // 获取班次字典数据 |
| | | const getWorkTimeDict = () => { |
| | | getDicts("sys_work_time") |
| | | .then(res => { |
| | | if (res.data && res.data.length > 0) { |
| | | const dictData = res.data; |
| | | workTimeDict.value = { |
| | | startAt: dictData[0].dictValue || "-", |
| | | endAt: dictData[1].dictValue || "-", |
| | | }; |
| | | } |
| | | }) |
| | | .catch(error => { |
| | | console.error("获取班次字典失败:", error); |
| | | }); |
| | | }; |
| | | |
| | | // 打卡逻辑 |
| | | const handleCheckInOut = async () => { |
| | | if (noNeedCheckIn.value) { |
| | | uni.showToast({ |
| | | title: "今日无需打卡", |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | if (todayRecord.value.workEndAt) { |
| | | uni.showToast({ |
| | | title: "您已经打过卡了,无需重复打卡!!!", |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | // 检查是否在打卡冷却时间内 |
| | | if (lastCheckInTime.value) { |
| | | const currentTime = Date.now(); |
| | | const timeDiff = currentTime - lastCheckInTime.value; |
| | | if (timeDiff < CHECKIN_COOLDOWN) { |
| | | const remainingTime = Math.ceil((CHECKIN_COOLDOWN - timeDiff) / 1000); |
| | | uni.showToast({ |
| | | title: `请${remainingTime}秒后再试`, |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | // 检查是否在打卡范围内 |
| | | if (!inCheckRange.value) { |
| | | uni.showToast({ |
| | |
| | | return; |
| | | } |
| | | |
| | | const [dateStr, timeStr] = nowTime.value.split(" "); |
| | | if (!dateStr || !timeStr) return; |
| | | // 调用打卡API |
| | | createPersonalAttendanceRecord({ |
| | | ...form.value, |
| | | }) |
| | | .then(res => { |
| | | // 记录打卡时间 |
| | | lastCheckInTime.value = Date.now(); |
| | | |
| | | // 上班打卡 |
| | | if (!todayRecord.value) { |
| | | const newId = rawAttendance.value.length |
| | | ? Math.max(...rawAttendance.value.map(i => i.id)) + 1 |
| | | : 1; |
| | | const status = timeStr > "09:00:00" ? "late" : "normal"; |
| | | const statusText = status === "late" ? "迟到" : "正常"; |
| | | rawAttendance.value.push({ |
| | | id: newId, |
| | | date: dateStr, |
| | | userId: currentUser.id, |
| | | name: currentUser.name, |
| | | no: currentUser.no, |
| | | dept: currentUser.dept, |
| | | checkInTime: timeStr.slice(0, 5), |
| | | checkOutTime: "", |
| | | workHours: null, |
| | | status, |
| | | statusText, |
| | | remark: "", |
| | | uni.showToast({ |
| | | title: "打卡成功!", |
| | | icon: "success", |
| | | }); |
| | | // 重新获取今日打卡信息 |
| | | fetchTodayData(); |
| | | }) |
| | | .catch(error => { |
| | | uni.showToast({ |
| | | title: error.msg || "打卡失败,请重试", |
| | | icon: "none", |
| | | }); |
| | | }); |
| | | uni.showToast({ |
| | | title: "上班打卡成功", |
| | | icon: "success", |
| | | }); |
| | | } else if (!todayRecord.value.checkOutTime) { |
| | | // 下班打卡 |
| | | todayRecord.value.checkOutTime = timeStr.slice(0, 5); |
| | | // 简单按 9:00-18:00 计算工时 |
| | | const start = todayRecord.value.checkInTime || "09:00"; |
| | | const [sh, sm] = start.split(":").map(v => parseInt(v, 10)); |
| | | const [eh, em] = todayRecord.value.checkOutTime |
| | | .split(":") |
| | | .map(v => parseInt(v, 10)); |
| | | const diff = (eh * 60 + em - (sh * 60 + sm)) / 60; |
| | | todayRecord.value.workHours = Number(Math.max(diff, 0).toFixed(1)); |
| | | |
| | | // 早退判断:18:00 前离开视为早退(只示意) |
| | | if (timeStr < "18:00:00") { |
| | | if (todayRecord.value.status === "late") { |
| | | // 既迟到又早退 |
| | | todayRecord.value.status = "late-early"; |
| | | todayRecord.value.statusText = "迟到 + 早退"; |
| | | } else { |
| | | // 仅早退 |
| | | todayRecord.value.status = "early"; |
| | | todayRecord.value.statusText = "早退"; |
| | | } |
| | | } else if (todayRecord.value.status === "normal") { |
| | | todayRecord.value.statusText = "正常"; |
| | | } |
| | | uni.showToast({ |
| | | title: "下班打卡成功", |
| | | icon: "success", |
| | | }); |
| | | } else { |
| | | uni.showToast({ |
| | | title: "今日已完成上下班打卡", |
| | | icon: "none", |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | onMounted(async () => { |
| | | fetchTodayData(); |
| | | updateNowTime(); |
| | | timer = setInterval(updateNowTime, 1000); |
| | | getWorkTimeDict(); |
| | | |
| | | // 获取位置权限并检查位置 |
| | | try { |
| | | await getLocationPermission(); |
| | | // await getLocationPermission(); |
| | | await getCurrentLocation(); |
| | | } catch (error) { |
| | | console.error("位置权限获取失败:", error); |