zhangwencui
3 天以前 9c83f21a3e781ab5520b5eb7ddfe35c3638a9a21
打卡签到接口对接
已添加1个文件
已修改2个文件
468 ■■■■ 文件已修改
src/api/personnelManagement/attendance.js 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/humanResources/attendance/checkin.vue 238 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/humanResources/attendance/report.vue 204 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/attendance.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
import request from '@/utils/request'
export function createPersonalAttendanceRecord(params) {
    return request({
        url: "/personalAttendanceRecords",
        method: "post",
        data: params,
    });
}
export function findPersonalAttendanceRecords(query) {
    return request({
        url: "/personalAttendanceRecords/listPage",
        method: "get",
        params: query,
    });
}
export function findTodayPersonalAttendanceRecord(query) {
    return request({
        url: "/personalAttendanceRecords/today",
        method: "get",
        params: query,
    });
}
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">白班: {{ workTimeDict.startAt }}-{{ workTimeDict.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 }"
                @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,45 +97,19 @@
<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("");
@@ -143,6 +117,12 @@
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æŸ¥è¯¢ä»Šæ—¥æ‰“卡信息
  const fetchTodayData = () => {
    findTodayPersonalAttendanceRecord({}).then(res => {
      todayRecord.value = res.data;
    });
  };
  // æ‰“卡范围状态
@@ -165,22 +145,39 @@
  // ä»Šæ—¥æ—¥æœŸ
  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 (!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 "缺勤";
    }
  });
  // å¯¼èˆªåˆ°è¯¦ç»†æŠ¥å‘Šé¡µé¢
@@ -243,8 +240,32 @@
    });
  };
  // æ‰“卡逻辑(仅前端模拟)
  // èŽ·å–ç­æ¬¡å­—å…¸æ•°æ®
  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 (todayRecord.value.workEndAt) {
      uni.showToast({
        title: "您已经打过卡了,无需重复打卡!!!",
        icon: "none",
      });
      return;
    }
    // æ£€æŸ¥æ˜¯å¦åœ¨æ‰“卡范围内
    if (!inCheckRange.value) {
      uni.showToast({
@@ -254,75 +275,30 @@
      return;
    }
    const [dateStr, timeStr] = nowTime.value.split(" ");
    if (!dateStr || !timeStr) return;
    // ä¸Šç­æ‰“卡
    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: "",
    // è°ƒç”¨æ‰“卡API
    createPersonalAttendanceRecord({})
      .then(res => {
        uni.showToast({
          title: "打卡成功!",
          icon: "success",
        });
        // é‡æ–°èŽ·å–ä»Šæ—¥æ‰“å¡ä¿¡æ¯
        fetchTodayData();
      })
      .catch(error => {
        console.error("打卡失败:", 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 {
src/pages/humanResources/attendance/report.vue
@@ -26,33 +26,43 @@
                        @cancel="showDatePicker = false"
                        title="搜索日期" />
    <view class="record-list">
      <view v-for="(item) in tableData"
      <!-- åŠ è½½çŠ¶æ€ -->
      <view v-if="loading"
            class="loading-state">
        <u-icon name="loading"
                size="40"
                color="#348fe2"></u-icon>
        <text class="loading-text">加载中...</text>
      </view>
      <view v-else
            v-for="(item) in tableData"
            :key="item.id"
            class="record-item-card"
            :class="{ 'abnormal': item.status !== 'normal' }">
            :class="{ 'abnormal': item.status !== 0 }">
        <view class="record-item-header">
          <text class="record-date">{{ item.date }}</text>
          <u-tag :type="item.status === 'normal' ? 'success' : 'error'"
          <u-tag :type="item.status === 0 ? 'success' : 'error'"
                 size="small">
            {{ item.statusText }}
            <!-- {{ item.status === 0 ? '正常' : (item.status === 1 ? '迟到' : (item.status === 2 ? '早退' : '迟到、早退')) }} -->
            {{ getStatusText(item.status) }}
          </u-tag>
        </view>
        <view class="record-item-body">
          <view class="record-detail">
            <text class="detail-label">员工</text>
            <text class="detail-value">{{ item.name }} ({{ item.no }})</text>
            <text class="detail-value">{{ item.staffName }} ({{ item.staffNo }})</text>
          </view>
          <view class="record-detail">
            <text class="detail-label">部门</text>
            <text class="detail-value">{{ item.dept }}</text>
            <text class="detail-value">{{ item.deptName }}</text>
          </view>
          <view class="record-detail">
            <text class="detail-label">上班时间</text>
            <text class="detail-value">{{ item.checkInTime ? item.checkInTime : '缺卡' }}</text>
            <text class="detail-value">{{ item.workStartAt || '缺卡' }}</text>
          </view>
          <view class="record-detail">
            <text class="detail-label">下班时间</text>
            <text class="detail-value">{{ item.checkOutTime? item.checkOutTime : '缺卡' }}</text>
            <text class="detail-value">{{ item.workEndAt || '缺卡' }}</text>
          </view>
          <view class="record-detail">
            <text class="detail-label">工时</text>
@@ -66,11 +76,11 @@
        </view>
      </view>
      <!-- ç©ºçŠ¶æ€ -->
      <view v-if="tableData.length === 0"
      <view v-if="tableData.length === 0 && !loading"
            class="empty-state">
        <u-icon name="clock-o"
        <!-- <u-icon name="clock-o"
                size="60"
                color="#999"></u-icon>
                color="#999"></u-icon> -->
        <text class="empty-text">暂无考勤记录</text>
      </view>
    </view>
@@ -89,96 +99,25 @@
  import { ref, reactive, onMounted } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import dayjs from "dayjs";
  // æ¨¡æ‹Ÿå½“前登录员工
  const currentUser = reactive({
    id: 1,
    name: "张三",
    no: "E10001",
    dept: "生产一部",
  });
  // æ¨¡æ‹Ÿè€ƒå‹¤åŽŸå§‹æ•°æ®
  const rawAttendance = ref([
    {
      id: 1,
      date: "2026-02-09",
      userId: 1,
      name: "张三",
      no: "E10001",
      dept: "生产一部",
      checkInTime: "08:58",
      checkOutTime: "",
      workHours: null,
      status: "normal",
      statusText: "正常",
      remark: "",
    },
    {
      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小时",
    },
    {
      id: 4,
      date: "2026-02-06",
      userId: 1,
      name: "张三",
      no: "E10001",
      dept: "生产一部",
      checkInTime: "08:50",
      checkOutTime: "17:45",
      workHours: 8.9,
      status: "early",
      statusText: "早退",
      remark: "家中有事提前离开",
    },
    {
      id: 5,
      date: "2026-02-05",
      userId: 1,
      name: "张三",
      no: "E10001",
      dept: "生产一部",
      checkInTime: "08:40",
      checkOutTime: "18:20",
      workHours: 9.7,
      status: "normal",
      statusText: "正常",
      remark: "加班0.5小时",
    },
  ]);
  import { findPersonalAttendanceRecords } from "@/api/personnelManagement/attendance.js";
  // æŸ¥è¯¢è¡¨å•
  const searchForm = reactive({
    date: "",
  });
  // åˆ†é¡µå‚æ•°
  const page = reactive({
    current: -1,
    size: -1,
    total: 0,
  });
  // è¡¨æ ¼æ•°æ®
  const tableData = ref([]);
  // åŠ è½½çŠ¶æ€
  const loading = ref(false);
  // è¿”回上一页
  const goBack = () => {
@@ -196,6 +135,20 @@
    showDatePicker.value = false;
    handleQuery();
  };
  const getStatusText = status => {
    switch (status) {
      case 0:
        return "正常";
      case 1:
        return "迟到";
      case 2:
        return "早退";
      case 3:
        return "迟到、早退";
      case 4:
        return "缺勤";
    }
  };
  // æ˜¾ç¤ºæ—¥æœŸé€‰æ‹©å™¨
  const selectDate = () => {
@@ -204,27 +157,42 @@
  // æ¸…除日期选择
  const clearDate = () => {
    resetQuery();
  };
  // æŸ¥è¯¢
  const handleQuery = () => {
    loading.value = true;
    console.log(searchForm, "searchForm");
    findPersonalAttendanceRecords({
      ...page,
      ...searchForm,
    })
      .then(res => {
        tableData.value = res.data.records;
        page.total = res.data.total;
      })
      .catch(error => {
        console.error("查询失败:", error);
        uni.showToast({
          title: "查询失败,请重试",
          icon: "none",
        });
      })
      .finally(() => {
        loading.value = false;
      });
  };
  // é‡ç½®æŸ¥è¯¢
  const resetQuery = () => {
    searchForm.date = "";
    handleQuery();
  };
  // æŸ¥è¯¢
  const recomputeTable = () => {
    const list = rawAttendance.value.filter(item => {
      if (searchForm.date && item.date !== searchForm.date) {
        return false;
      }
      return true;
    });
    tableData.value = list;
  };
  const handleQuery = () => {
    recomputeTable();
  };
  onMounted(() => {
    recomputeTable();
    handleQuery();
  });
</script>
@@ -307,6 +275,24 @@
    margin: 0 20rpx 24rpx;
  }
  /* åŠ è½½çŠ¶æ€ */
  .loading-state {
    background-color: $card-bg;
    border-radius: 16rpx;
    box-shadow: $shadow-md;
    text-align: center;
    padding: 120rpx 0;
    margin: 0 20rpx;
    transition: all 0.3s ease;
  }
  .loading-text {
    font-size: 14px;
    color: $text-tertiary;
    margin-top: 24rpx;
    font-weight: 500;
  }
  .record-item-card {
    background-color: $card-bg;
    border-radius: 16rpx;