gaoluyang
昨天 fcf6e5286e72d2df817ac077b47acd2bfc1e0649
src/pages/humanResources/attendance/checkin.vue
@@ -14,14 +14,14 @@
          <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>
@@ -41,53 +41,53 @@
      <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>
@@ -97,56 +97,57 @@
<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);
@@ -165,22 +166,42 @@
  // 今日日期
  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 "缺勤";
    }
  });
  // 导航到详细报告页面
@@ -220,31 +241,104 @@
      // #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({
@@ -254,79 +348,38 @@
      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);