<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: "正常",
|
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/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 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" ? "迟到" : "正常";
|
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 = "正常";
|
}
|
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>
|