From 4071a902f383275b7ed284876bbdf3e19cfa3522 Mon Sep 17 00:00:00 2001
From: zhangwencui <1064582902@qq.com>
Date: 星期一, 09 二月 2026 18:02:15 +0800
Subject: [PATCH] Merge branch 'dev_new' of http://114.132.189.42:9002/r/product-inventory-APP-before into dev_new

---
 src/pages/attendance/checkin.vue |  752 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 752 insertions(+), 0 deletions(-)

diff --git a/src/pages/attendance/checkin.vue b/src/pages/attendance/checkin.vue
new file mode 100644
index 0000000..18a87d7
--- /dev/null
+++ b/src/pages/attendance/checkin.vue
@@ -0,0 +1,752 @@
+<template>
+  <view class="attendance-checkin">
+    <!-- 椤甸潰澶撮儴 -->
+    <PageHeader title="鎵撳崱绛惧埌"
+                @back="goBack" />
+    <!-- 浠婃棩鑰冨嫟鍖哄煙 -->
+    <view class="today-attendance">
+      <view class="attendance-header">
+        <text class="attendance-title">浠婃棩鑰冨嫟</text>
+      </view>
+      <!-- 鐝淇℃伅 -->
+      <view class="shift-info">
+        <view class="shift-item">
+          <u-icon name="calendar"
+                  color="#348fe2"
+                  size="16"></u-icon>
+          <text class="shift-text">鐧界彮: 09:00-18:00</text>
+        </view>
+      </view>
+      <!-- 鎵撳崱鎸夐挳 -->
+      <view class="checkin-button-container">
+        <view class="checkin-button-wrapper">
+          <view class="checkin-button"
+                :class="{ 'disabled': checkInOutText === '宸叉墦鍗�' }"
+                @click="handleCheckInOut">
+            <text class="checkin-button-text">{{ checkInOutText }}</text>
+            <text class="checkin-time">{{ nowTime.split(' ')[1] }}</text>
+          </view>
+        </view>
+        <!-- 鎵撳崱鑼冨洿鐘舵�� -->
+      </view>
+    </view>
+    <!-- 鎴戠殑鑰冨嫟璁板綍 -->
+    <view class="attendance-records">
+      <view class="records-header">
+        <text class="records-title">浠婃棩鑰冨嫟</text>
+        <view @click="navigateToReport"
+              class="detail-button">鏌ョ湅璇︽儏</view>
+      </view>
+      <!-- 鍛樺伐淇℃伅 -->
+      <view class="employee-info">
+        <view class="info-item">
+          <text class="info-label">閮ㄩ棬</text>
+          <text class="info-value">{{ currentUser.dept }}</text>
+        </view>
+        <view class="info-item">
+          <text class="info-label">濮撳悕</text>
+          <text class="info-value">{{ currentUser.name }}</text>
+        </view>
+        <view class="info-item">
+          <text class="info-label">宸ュ彿</text>
+          <text class="info-value">{{ currentUser.no }}</text>
+        </view>
+      </view>
+      <!-- 浠婃棩鑰冨嫟鐘舵�� -->
+      <view class="today-status">
+        <u-icon :name="todayRecord ? 'checkmark-circle' : 'close-circle'"
+                :color="todayRecord ? '#4cd964' : '#ff3b30'"
+                size="16"></u-icon>
+        <text class="status-text">
+          {{ todayRecord ? `浠婃棩鑰冨嫟: 涓婄彮 ${todayRecord.checkInTime}` : '浠婃棩鏈墦鍗�' }}
+        </text>
+      </view>
+      <!-- 涓嬬彮鑰冨嫟鐘舵�� -->
+      <view v-if="todayRecord && todayRecord.checkOutTime"
+            class="today-status">
+        <u-icon :name="todayRecord ? 'checkmark-circle' : 'close-circle'"
+                :color="todayRecord ? '#4cd964' : '#ff3b30'"
+                size="16"></u-icon>
+        <text class="status-text">
+          {{ `浠婃棩鑰冨嫟: 涓嬬彮 ${todayRecord.checkOutTime}` }}
+        </text>
+      </view>
+      <!-- 鎵撳崱鐘舵�� -->
+      <view v-if="todayRecord"
+            class="today-status">
+        <u-icon :name="todayRecord.status === 'normal' ? 'checkmark-circle' : 'clock'"
+                :color="todayRecord.status === 'normal' ? '#4cd964' : '#ff3b30'"
+                size="16"></u-icon>
+        <text class="status-text">
+          {{ `鎵撳崱鐘舵��: ${todayRecord.statusText}` }}
+        </text>
+      </view>
+      <view v-else
+            class="today-status">
+        <u-icon name="clock"
+                color="#ff3b30"
+                size="16"></u-icon>
+        <text class="status-text">
+          鎵撳崱鐘舵��: 缂哄崱
+        </text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<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: "鐢熶骇涓�閮�",
+  });
+
+  // 妯℃嫙鑰冨嫟鍘熷鏁版嵁
+  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: "姝e父",
+      remark: "鍔犵彮0.5灏忔椂",
+    },
+  ]);
+
+  // 褰撳墠鏃堕棿灞曠ず
+  const nowTime = ref("");
+  let timer = null;
+  // 杩斿洖涓婁竴椤�
+  const goBack = () => {
+    uni.navigateBack();
+  };
+
+  // 鎵撳崱鑼冨洿鐘舵��
+  const inCheckRange = ref(true);
+
+  // 褰撳墠浣嶇疆
+  const currentLocation = ref(null);
+
+  const updateNowTime = () => {
+    const now = new Date();
+    const Y = now.getFullYear();
+    const M = String(now.getMonth() + 1).padStart(2, "0");
+    const D = String(now.getDate()).padStart(2, "0");
+    const h = String(now.getHours()).padStart(2, "0");
+    const m = String(now.getMinutes()).padStart(2, "0");
+    const s = String(now.getSeconds()).padStart(2, "0");
+    nowTime.value = `${Y}-${M}-${D} ${h}:${m}:${s}`;
+  };
+
+  // 浠婃棩鏃ユ湡
+  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) {
+      return "涓婄彮鎵撳崱";
+    }
+    if (!todayRecord.value.checkOutTime) {
+      return "涓嬬彮鎵撳崱";
+    }
+    return "宸叉墦鍗�";
+  });
+
+  // 瀵艰埅鍒拌缁嗘姤鍛婇〉闈�
+  const navigateToReport = () => {
+    uni.navigateTo({
+      url: "/pages/attendance/report",
+    });
+  };
+
+  // 鑾峰彇浣嶇疆鏉冮檺
+  const getLocationPermission = () => {
+    return new Promise((resolve, reject) => {
+      // #ifdef APP-PLUS
+      uni.getAppAuthorizeSetting({
+        success: res => {
+          if (res.authSetting["scope.userLocation"]) {
+            resolve(true);
+          } else {
+            uni.requestAppAuthorize({
+              scope: "scope.userLocation",
+              success: res => {
+                resolve(res.authSetting["scope.userLocation"]);
+              },
+              fail: err => {
+                reject(err);
+              },
+            });
+          }
+        },
+        fail: err => {
+          reject(err);
+        },
+      });
+      // #else
+      // 闈濧PP鐜鐩存帴杩斿洖鎴愬姛
+      resolve(true);
+      // #endif
+    });
+  };
+
+  // 鑾峰彇褰撳墠浣嶇疆
+  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);
+        },
+      });
+    });
+  };
+
+  // 鎵撳崱閫昏緫锛堜粎鍓嶇妯℃嫙锛�
+  const handleCheckInOut = async () => {
+    // 妫�鏌ユ槸鍚﹀湪鎵撳崱鑼冨洿鍐�
+    if (!inCheckRange.value) {
+      uni.showToast({
+        title: "涓嶅湪鎵撳崱鑼冨洿鍐咃紝鏃犳硶鎵撳崱",
+        icon: "none",
+      });
+      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" ? "杩熷埌" : "姝e父";
+      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",
+      });
+    } 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 = "姝e父";
+      }
+      uni.showToast({
+        title: "涓嬬彮鎵撳崱鎴愬姛",
+        icon: "success",
+      });
+    } else {
+      uni.showToast({
+        title: "浠婃棩宸插畬鎴愪笂涓嬬彮鎵撳崱",
+        icon: "none",
+      });
+    }
+  };
+
+  onMounted(async () => {
+    updateNowTime();
+    timer = setInterval(updateNowTime, 1000);
+
+    // 鑾峰彇浣嶇疆鏉冮檺骞舵鏌ヤ綅缃�
+    try {
+      await getLocationPermission();
+      await getCurrentLocation();
+    } catch (error) {
+      console.error("浣嶇疆鏉冮檺鑾峰彇澶辫触:", error);
+    }
+  });
+
+  onBeforeUnmount(() => {
+    if (timer) {
+      clearInterval(timer);
+    }
+  });
+</script>
+
+<style scoped lang="scss">
+  // 鍏ㄥ眬鍙橀噺
+  $primary-color: #2c7be5;
+  $primary-light: #4a90e2;
+  $success-color: #4cd964;
+  $warning-color: #ff9500;
+  $danger-color: #ff3b30;
+  $text-primary: #333333;
+  $text-secondary: #666666;
+  $text-tertiary: #999999;
+  $bg-color: #f5f7fa;
+  $card-bg: #ffffff;
+  $border-color: #e8e8e8;
+  $shadow-sm: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
+  $shadow-md: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
+  $shadow-lg: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
+
+  .attendance-checkin {
+    min-height: 100vh;
+    background-color: $bg-color;
+    padding-bottom: 30rpx;
+    background-image: linear-gradient(135deg, #f5f7fa 0%, #e4e8f0 100%);
+    display: flex;
+    flex-direction: column;
+  }
+
+  /* 浠婃棩鑰冨嫟鍖哄煙 */
+  .today-attendance {
+    background-color: $card-bg;
+    margin: 20rpx;
+    border-radius: 16rpx;
+    box-shadow: $shadow-md;
+    padding: 32rpx;
+    transition: all 0.3s ease;
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+  }
+
+  .today-attendance:hover {
+    box-shadow: $shadow-lg;
+    transform: translateY(-2rpx);
+  }
+
+  .attendance-header {
+    margin-bottom: 24rpx;
+    display: flex;
+    align-items: center;
+  }
+
+  .attendance-title {
+    font-size: 18px;
+    font-weight: 600;
+    color: $text-primary;
+    position: relative;
+    padding-left: 16rpx;
+  }
+
+  .attendance-title::before {
+    content: "";
+    position: absolute;
+    left: 0;
+    top: 50%;
+    transform: translateY(-50%);
+    width: 4rpx;
+    height: 20rpx;
+    background-color: $primary-color;
+    border-radius: 2rpx;
+  }
+
+  /* 鐝淇℃伅 */
+  .shift-info {
+    margin-bottom: 36rpx;
+    padding: 20rpx;
+    background-color: rgba($primary-color, 0.05);
+    border-radius: 12rpx;
+    border-left: 4rpx solid $primary-color;
+  }
+
+  .shift-item {
+    display: flex;
+    align-items: center;
+    margin-bottom: 16rpx;
+  }
+
+  .shift-item:last-child {
+    margin-bottom: 0;
+  }
+
+  .shift-item u-icon {
+    margin-right: 14rpx;
+    transition: all 0.3s ease;
+  }
+
+  .shift-item:hover u-icon {
+    transform: scale(1.1);
+  }
+
+  .shift-text {
+    font-size: 14px;
+    color: $text-secondary;
+    font-weight: 500;
+  }
+
+  /* 鎵撳崱鎸夐挳瀹瑰櫒 */
+  .checkin-button-container {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    margin-bottom: 20rpx;
+    flex: 1;
+    justify-content: center;
+  }
+
+  /* 鎵撳崱鎸夐挳 */
+  .checkin-button-wrapper {
+    position: relative;
+    margin-bottom: 36rpx;
+    margin-top: 40rpx;
+  }
+
+  .checkin-button {
+    width: 260rpx;
+    height: 260rpx;
+    border-radius: 50%;
+    background: linear-gradient(135deg, $primary-color, $primary-light);
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    box-shadow: 0 8rpx 32rpx rgba($primary-color, 0.4);
+    transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
+    cursor: pointer;
+    position: relative;
+    overflow: hidden;
+  }
+
+  .checkin-button::before {
+    content: "";
+    position: absolute;
+    top: -50%;
+    left: -50%;
+    width: 200%;
+    height: 200%;
+    background: linear-gradient(
+      45deg,
+      transparent,
+      rgba(255, 255, 255, 0.1),
+      transparent
+    );
+    transform: rotate(45deg);
+    animation: shine 3s infinite;
+    opacity: 0;
+  }
+
+  @keyframes shine {
+    0% {
+      transform: translateX(-100%) rotate(45deg);
+      opacity: 0;
+    }
+    50% {
+      opacity: 0.3;
+    }
+    100% {
+      transform: translateX(100%) rotate(45deg);
+      opacity: 0;
+    }
+  }
+
+  .checkin-button:hover:not(.disabled) {
+    transform: scale(1.08);
+    box-shadow: 0 12rpx 40rpx rgba($primary-color, 0.5);
+  }
+
+  .checkin-button:active:not(.disabled) {
+    transform: scale(0.98);
+  }
+
+  .checkin-button.disabled {
+    background: linear-gradient(135deg, #d1d1d6, #e5e5ea);
+    box-shadow: $shadow-sm;
+    cursor: not-allowed;
+  }
+
+  .checkin-button-text {
+    font-size: 22px;
+    font-weight: 700;
+    color: #ffffff;
+    margin-bottom: 12rpx;
+    text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
+    z-index: 1;
+  }
+
+  .checkin-time {
+    font-size: 16px;
+    color: rgba(255, 255, 255, 0.95);
+    font-weight: 500;
+    z-index: 1;
+  }
+
+  /* 鎵撳崱鑼冨洿鐘舵�� */
+  .location-status {
+    display: flex;
+    align-items: center;
+    margin-top: 16rpx;
+    padding: 16rpx 24rpx;
+    background-color: rgba($success-color, 0.05);
+    border-radius: 8rpx;
+    border-left: 4rpx solid $success-color;
+    transition: all 0.3s ease;
+  }
+
+  .location-status.warning {
+    background-color: rgba($danger-color, 0.05);
+    border-left-color: $danger-color;
+  }
+
+  .location-status u-icon {
+    margin-right: 10rpx;
+    animation: pulse 2s infinite;
+  }
+
+  @keyframes pulse {
+    0% {
+      transform: scale(1);
+    }
+    50% {
+      transform: scale(1.1);
+    }
+    100% {
+      transform: scale(1);
+    }
+  }
+
+  .location-text {
+    font-size: 14px;
+    color: $text-secondary;
+    font-weight: 500;
+  }
+
+  .location-text.warning {
+    color: $danger-color;
+  }
+
+  /* 鑰冨嫟璁板綍鍖哄煙 */
+  .attendance-records {
+    background-color: $card-bg;
+    margin: 0 20rpx 20rpx;
+    border-radius: 16rpx;
+    box-shadow: $shadow-md;
+    padding: 32rpx;
+    transition: all 0.3s ease;
+  }
+
+  .attendance-records:hover {
+    box-shadow: $shadow-lg;
+    transform: translateY(-2rpx);
+  }
+
+  .records-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 24rpx;
+    padding-bottom: 16rpx;
+    border-bottom: 1rpx solid $border-color;
+  }
+
+  .records-title {
+    font-size: 16px;
+    font-weight: 600;
+    color: $text-primary;
+    position: relative;
+    padding-left: 12rpx;
+    width: 300rpx;
+  }
+
+  .records-title::before {
+    content: "";
+    position: absolute;
+    left: 0;
+    top: 50%;
+    transform: translateY(-50%);
+    width: 3rpx;
+    height: 16rpx;
+    background-color: $primary-color;
+    border-radius: 1.5rpx;
+  }
+
+  .detail-button {
+    font-size: 14px;
+    color: $primary-color;
+    font-weight: 500;
+    transition: all 0.3s ease;
+    padding: 8rpx 16rpx;
+    border-radius: 6rpx;
+    float: right;
+  }
+
+  .detail-button:hover {
+    background-color: rgba($primary-color, 0.1);
+    transform: translateX(4rpx);
+  }
+
+  /* 鍛樺伐淇℃伅 */
+  .employee-info {
+    background-color: rgba($primary-color, 0.05);
+    border-radius: 12rpx;
+    padding: 20rpx;
+    margin-bottom: 24rpx;
+  }
+
+  .info-item {
+    display: flex;
+    align-items: center;
+    margin-bottom: 12rpx;
+  }
+
+  .info-item:last-child {
+    margin-bottom: 0;
+  }
+
+  .info-label {
+    font-size: 13px;
+    color: $text-secondary;
+    font-weight: 500;
+    width: 80rpx;
+  }
+
+  .info-value {
+    font-size: 13px;
+    color: $text-primary;
+    font-weight: 500;
+    flex: 1;
+  }
+
+  /* 浠婃棩鑰冨嫟鐘舵�� */
+  .today-status {
+    display: flex;
+    align-items: center;
+    margin-top: 24rpx;
+    padding: 20rpx;
+    background-color: rgba($primary-color, 0.05);
+    border-radius: 12rpx;
+    transition: all 0.3s ease;
+  }
+
+  .today-status:hover {
+    background-color: rgba($primary-color, 0.08);
+  }
+
+  .today-status u-icon {
+    margin-right: 14rpx;
+    transition: all 0.3s ease;
+  }
+
+  .today-status:hover u-icon {
+    transform: scale(1.1);
+  }
+
+  .status-text {
+    font-size: 14px;
+    color: $text-secondary;
+    font-weight: 500;
+    margin-left: 10rpx;
+  }
+
+  /* 鍝嶅簲寮忚皟鏁� */
+  @media (max-width: 375px) {
+    .today-attendance,
+    .attendance-records {
+      margin: 12rpx;
+      padding: 24rpx;
+    }
+
+    .checkin-button {
+      width: 220rpx;
+      height: 220rpx;
+    }
+
+    .checkin-button-text {
+      font-size: 20px;
+    }
+
+    .checkin-time {
+      font-size: 14px;
+    }
+  }
+
+  /* 鍔ㄧ敾鏁堟灉 */
+  @keyframes fadeInUp {
+    from {
+      opacity: 0;
+      transform: translateY(20rpx);
+    }
+    to {
+      opacity: 1;
+      transform: translateY(0);
+    }
+  }
+
+  .today-attendance,
+  .attendance-records {
+    animation: fadeInUp 0.5s ease-out;
+  }
+
+  .attendance-records {
+    animation-delay: 0.2s;
+  }
+</style>
\ No newline at end of file

--
Gitblit v1.9.3