<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">白班: {{ workTimeDict.startAt }}-{{ workTimeDict.endAt }}</text>
|
</view>
|
</view>
|
<!-- 打卡按钮 -->
|
<view class="checkin-button-container">
|
<view class="checkin-button-wrapper">
|
<view class="checkin-button"
|
:class="{ 'disabled': todayRecord.workEndAt }"
|
@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">{{ todayRecord.deptName || '-' }}</text>
|
</view>
|
<view class="info-item">
|
<text class="info-label">姓名</text>
|
<text class="info-value">{{ todayRecord.staffName || '-' }}</text>
|
</view>
|
<view class="info-item">
|
<text class="info-label">工号</text>
|
<text class="info-value">{{ todayRecord.staffNo || '-' }}</text>
|
</view>
|
</view>
|
<!-- 今日考勤状态 -->
|
<view class="today-status">
|
<u-icon :name="todayRecord.id ? 'checkmark-circle' : 'close-circle'"
|
:color="todayRecord.id ? '#4cd964' : '#ff3b30'"
|
size="16"></u-icon>
|
<text class="status-text">
|
{{ todayRecord.id ? `今日考勤: 上班 ${todayRecord.workStartAt || '-'}` : '今日未打卡' }}
|
</text>
|
</view>
|
<!-- 下班考勤状态 -->
|
<view v-if="todayRecord.id && todayRecord.workEndAt"
|
class="today-status">
|
<u-icon name="checkmark-circle"
|
color="#4cd964"
|
size="16"></u-icon>
|
<text class="status-text">
|
{{ `今日考勤: 下班 ${todayRecord.workEndAt}` }}
|
</text>
|
</view>
|
<!-- 打卡状态 -->
|
<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">
|
{{ `打卡状态: ${todayStatusText}` }}
|
</text>
|
</view>
|
<!-- 工时信息 -->
|
<view v-if="todayRecord.id && todayRecord.workHours"
|
class="today-status">
|
<u-icon name="clock"
|
color="#348fe2"
|
size="16"></u-icon>
|
<text class="status-text">
|
{{ `工时(小时): ${todayRecord.workHours}` }}
|
</text>
|
</view>
|
</view>
|
</view>
|
</template>
|
|
<script setup>
|
import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue";
|
import PageHeader from "@/components/PageHeader.vue";
|
import { getDicts } from "@/api/system/dict/data";
|
import {
|
createPersonalAttendanceRecord,
|
findTodayPersonalAttendanceRecord,
|
} from "@/api/personnelManagement/attendance.js";
|
// 今日打卡记录
|
const todayRecord = ref({});
|
|
// 班次信息
|
const workTimeDict = ref({
|
startAt: "09:00",
|
endAt: "18:00",
|
});
|
|
// 当前时间展示
|
const nowTime = ref("");
|
let timer = null;
|
// 返回上一页
|
const goBack = () => {
|
uni.navigateBack();
|
};
|
// 查询今日打卡信息
|
const fetchTodayData = () => {
|
findTodayPersonalAttendanceRecord({}).then(res => {
|
todayRecord.value = res.data;
|
});
|
};
|
|
// 打卡范围状态
|
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 checkInOutText = computed(() => {
|
if (!todayRecord.value || !todayRecord.value.workStartAt) {
|
return "上班打卡";
|
}
|
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 "缺勤";
|
}
|
});
|
|
// 导航到详细报告页面
|
const navigateToReport = () => {
|
uni.navigateTo({
|
url: "/pages/humanResources/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
|
// 非APP环境直接返回成功
|
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 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({
|
title: "不在打卡范围内,无法打卡",
|
icon: "none",
|
});
|
return;
|
}
|
|
// 调用打卡API
|
createPersonalAttendanceRecord({})
|
.then(res => {
|
uni.showToast({
|
title: "打卡成功!",
|
icon: "success",
|
});
|
// 重新获取今日打卡信息
|
fetchTodayData();
|
})
|
.catch(error => {
|
console.error("打卡失败:", error);
|
uni.showToast({
|
title: error.msg || "打卡失败,请重试",
|
icon: "none",
|
});
|
});
|
};
|
|
onMounted(async () => {
|
fetchTodayData();
|
updateNowTime();
|
timer = setInterval(updateNowTime, 1000);
|
getWorkTimeDict();
|
|
// 获取位置权限并检查位置
|
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>
|