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