Browse Source

feat: 更新打卡页面,增加打卡状态显示和成功提示功能。优化位置状态和考勤信息展示,增强用户体验。

master
lizhuang 1 week ago
parent
commit
82cf33400d
1 changed files with 240 additions and 152 deletions
  1. 240
    152
      src/views/m/checkin/index.vue

+ 240
- 152
src/views/m/checkin/index.vue View File

<div class="status-time"> <div class="status-time">
上班{{ attendanceGroup.workStartTime }} 上班{{ attendanceGroup.workStartTime }}
</div> </div>
<div class="status-label">未打卡</div>
<div class="status-label">
<span v-if="checkInStatus === 0">未打卡</span>
<span v-else-if="checkInStatus === 1">已打卡</span>
<span v-else-if="checkInStatus === 3">迟到打卡</span>
</div>
</div> </div>
<div class="status-card"> <div class="status-card">
<div class="status-time">下班{{ attendanceGroup.workEndTime }}</div> <div class="status-time">下班{{ attendanceGroup.workEndTime }}</div>
<transition name="fade"> <transition name="fade">
<div v-if="showSuccess" class="success-overlay"> <div v-if="showSuccess" class="success-overlay">
<div class="success-content"> <div class="success-content">
<div class="success-close" @click="closeSuccessDialog">×</div>
<div class="success-icon"> <div class="success-icon">
<svg viewBox="0 0 24 24" class="checkmark"> <svg viewBox="0 0 24 24" class="checkmark">
<path <path
<div class="success-info"> <div class="success-info">
<div class="info-item"> <div class="info-item">
<span class="label">打卡时间</span> <span class="label">打卡时间</span>
<span class="value">{{ currentTime }}</span>
<span class="value">{{ currentCompleteDate }}</span>
</div> </div>
<div class="info-item"> <div class="info-item">
<span class="label">打卡地点</span> <span class="label">打卡地点</span>
<span class="value">{{ locationStatus }}</span>
<span class="value">{{ attendanceGroup.areaName }}</span>
</div> </div>
</div> </div>
</div> </div>
<!-- 范围,地点提示 --> <!-- 范围,地点提示 -->
<div class="location-info"> <div class="location-info">
<div class="location-text"> <div class="location-text">
<em v-if="isInRange" class="text-success"
><i class="el-icon-success" />已进入打卡范围</em
>
<em v-else class="text-error"
><i class="el-icon-error" />未进入打卡范围</em
>
<span>{{ locationStatus }}</span>
<em
v-if="isInRange"
class="text-success"
><i class="el-icon-success" />已进入打卡范围</em>
<em
v-else
class="text-error"
><i class="el-icon-error" />未进入打卡范围</em>
<span>{{ attendanceGroup.areaName }}</span>
</div> </div>
</div> </div>


<p style="text-align: center"> <p style="text-align: center">
经度:{{ userLocation.longitude }} <br />
经度:{{ userLocation.longitude }} <br>
纬度:{{ userLocation.latitude }} 纬度:{{ userLocation.latitude }}
</p> </p>


</div> </div>
</template> </template>
<script> <script>
import { mapGetters } from "vuex";
import { mapGetters } from 'vuex'
import { import {
queryAttendanceGroupByUserId, queryAttendanceGroupByUserId,
checkIn, checkIn,
checkOut, checkOut,
getCurrentDayRecord,
} from "@/api/checkin/punch-card";
getCurrentDayRecord
} from '@/api/checkin/punch-card'
export default { export default {
name: "MCheckin",
name: 'MCheckin',
data() { data() {
return { return {
// 当前时间 // 当前时间
currentTime: new Date().toLocaleTimeString("zh-CN", { hour12: false }),
currentTime: new Date().toLocaleTimeString('zh-CN', { hour12: false }),
// 打卡完成时间
currentCompleteDate: '',
// 用户位置 // 用户位置
userLocation: { userLocation: {
latitude: null, // 纬度 latitude: null, // 纬度
longitude: null, // 经度
longitude: null // 经度
}, },
// 位置状态 // 位置状态
locationStatus: "正在获取位置信息...",
locationStatus: '正在获取位置信息...',
// 打卡成功提示 // 打卡成功提示
showSuccess: false, showSuccess: false,
// 位置监听ID // 位置监听ID
watchId: null, watchId: null,
// 打卡状态 // 打卡状态
punchStatus: { punchStatus: {
morning: "未打卡", // 上班打卡状态:未打卡、已打卡、迟到
evening: "未打卡", // 下班打卡状态:未打卡、已打卡、早退
morning: '未打卡', // 上班打卡状态:未打卡、已打卡、迟到
evening: '未打卡' // 下班打卡状态:未打卡、已打卡、早退
}, },
// 备注对话框 // 备注对话框
showRemarkDialog: false, showRemarkDialog: false,
// 备注对话框标题 // 备注对话框标题
remarkDialogTitle: "",
remarkDialogTitle: '',
// 备注表单 // 备注表单
remarkForm: { remarkForm: {
remark: "", // 备注
type: "", // 打卡类型:morning_late 或 evening_early
remark: '', // 备注
type: '' // 打卡类型:morning_late 或 evening_early
}, },
// 是否在打卡范围内 // 是否在打卡范围内
isInRange: false, isInRange: false,
// 考勤组信息 // 考勤组信息
attendanceGroup: {}, attendanceGroup: {},
// 当前考勤状态 // 当前考勤状态
currentAttendance: {},
};
currentAttendance: null
}
}, },
computed: { computed: {
...mapGetters(["userinfo", "avatar"]),
...mapGetters(['userinfo', 'avatar']),
// 获取时区 // 获取时区
timeZone() { timeZone() {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
return Intl.DateTimeFormat().resolvedOptions().timeZone
}, },
// 打卡按钮文本 // 打卡按钮文本
punchButtonText() { punchButtonText() {
// 优先根据打卡状态判断,而不是时间 // 优先根据打卡状态判断,而不是时间
if (this.punchStatus.morning === "未打卡") {
if (this.punchStatus.morning === '未打卡') {
// 如果上班还没打卡,判断是否迟到 // 如果上班还没打卡,判断是否迟到
if (this.isLate()) { if (this.isLate()) {
return "上班打卡";
return '上班打卡'
} }
return "上班打卡";
} else if (this.punchStatus.evening === "未打卡") {
return '上班打卡'
} else if (this.punchStatus.evening === '未打卡') {
// 上班已打卡,下班未打卡,判断是否早退 // 上班已打卡,下班未打卡,判断是否早退
if (this.isEarly()) { if (this.isEarly()) {
return "下班打卡";
return '下班打卡'
} }
return "下班打卡";
return '下班打卡'
} else { } else {
// 都已经打卡了 // 都已经打卡了
return "已打卡";
return '已打卡'
} }
}, },
/**
* 获取当前考勤状态
* @returns {number} 考勤状态码
*/
checkInStatus() {
if (
this.currentAttendance == null ||
this.currentAttendance.checkInStatus == 0
) {
return 0
} else {
return Number(this.currentAttendance.checkInStatus)
}
}
}, },
mounted() { mounted() {
// 更新时间 // 更新时间
setInterval(() => { setInterval(() => {
this.currentTime = new Date().toLocaleTimeString("zh-CN", {
hour12: false,
});
}, 1000);
this.currentTime = new Date().toLocaleTimeString('zh-CN', {
hour12: false
})
}, 1000)


// 开始监听位置变化 // 开始监听位置变化
this.startLocationUpdates();
this.startLocationUpdates()


// 初始化 // 初始化
this.init();
this.init()
}, },
beforeDestroy() { beforeDestroy() {
// 组件销毁前清除位置监听 // 组件销毁前清除位置监听
if (this.watchId !== null) { if (this.watchId !== null) {
navigator.geolocation.clearWatch(this.watchId);
navigator.geolocation.clearWatch(this.watchId)
} }
}, },
methods: { methods: {
* 初始化 * 初始化
*/ */
async init() { async init() {
const { userId } = this.userinfo;
const { userId } = this.userinfo
if (!userId) { if (!userId) {
this.$message.error("用户信息获取失败");
return;
this.$message.error('用户信息获取失败')
return
} }
// 获取考勤组信息 // 获取考勤组信息
const res = await queryAttendanceGroupByUserId({ userId });
const res = await queryAttendanceGroupByUserId({ userId })
if (res.code === 200) { if (res.code === 200) {
if (res.data === null) { if (res.data === null) {
this.$message.error("未配置考勤组,请联系管理员");
return;
this.$message.error('未配置考勤组,请联系管理员')
return
}

// 获取当前考勤状态
const currentAttendance = await getCurrentDayRecord({ userId })
if (currentAttendance.code === 200) {
this.currentAttendance = currentAttendance.data
if (currentAttendance.data.length > 0) {
this.currentAttendance =
currentAttendance.data[currentAttendance.data.length - 1]
} else {
this.currentAttendance = null
}
} else {
this.currentAttendance = null
this.$message.error('获取考勤状态失败,请联系管理员')
} }


// 考勤组信息 // 考勤组信息
this.attendanceGroup = res.data;
this.attendanceGroup = res.data


// 获取考勤范围 // 获取考勤范围
const { latitude, longitude } = this.userLocation;
const { lat, lng, radius } = this.attendanceGroup;
const distance = this.getDistance(latitude, longitude, lat, lng);
console.log(distance);
const { latitude, longitude } = this.userLocation
const { lat, lng, radius } = this.attendanceGroup
const distance = this.getDistance(latitude, longitude, lat, lng)
console.log(distance)


// 判断是否在打卡范围内 // 判断是否在打卡范围内
if (distance > radius) { if (distance > radius) {
this.isInRange = false;
this.isInRange = false
} else { } else {
this.isInRange = true;
this.isInRange = true
} }
} else { } else {
this.$message.error(res.msg);
this.$message.error(res.msg)
} }
}, },


/**
* 格式化日期时间为 YYYY-MM-DD HH:mm:ss 格式
* @param {Date} date 日期对象
* @returns {string} 格式化后的日期时间字符串
*/
formatDateTime(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')

return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
},

/** /**
* 计算两个经纬度之间的距离 * 计算两个经纬度之间的距离
* @param {number} lat1 纬度1 * @param {number} lat1 纬度1
* @returns {number} 距离,单位为公里 * @returns {number} 距离,单位为公里
*/ */
getDistance(lat1, lng1, lat2, lng2) { getDistance(lat1, lng1, lat2, lng2) {
const R = 6371; // 地球半径,单位为公里
const R = 6371 // 地球半径,单位为公里
// 计算纬度差 // 计算纬度差
const dLat = (lat2 - lat1) * (Math.PI / 180);
const dLat = (lat2 - lat1) * (Math.PI / 180)
// 计算经度差 // 计算经度差
const dLng = (lng2 - lng1) * (Math.PI / 180);
const dLng = (lng2 - lng1) * (Math.PI / 180)
// 计算距离 // 计算距离
const a = const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat1 * (Math.PI / 180)) *
Math.cos(lat2 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) *
Math.sin(dLng / 2) * Math.sin(dLng / 2) *
Math.sin(dLng / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const distance = R * c; // 距离,单位为公里
return distance;
Math.sin(dLng / 2)
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
const distance = R * c // 距离,单位为公里
return distance
}, },


/** /**
const options = { const options = {
enableHighAccuracy: true, // 使用高精度定位 enableHighAccuracy: true, // 使用高精度定位
timeout: 5000, // 超时时间 timeout: 5000, // 超时时间
maximumAge: 0, // 不使用缓存的位置信息
};
maximumAge: 0 // 不使用缓存的位置信息
}


// 开始监听位置变化 // 开始监听位置变化
this.watchId = navigator.geolocation.watchPosition( this.watchId = navigator.geolocation.watchPosition(
this.handleLocationSuccess, this.handleLocationSuccess,
this.handleLocationError, this.handleLocationError,
options options
);
)
}, },


/** /**
handleLocationSuccess(position) { handleLocationSuccess(position) {
this.userLocation = { this.userLocation = {
latitude: position.coords.latitude, latitude: position.coords.latitude,
longitude: position.coords.longitude,
};
longitude: position.coords.longitude
}


// 调用后端API获取具体地址 // 调用后端API获取具体地址
this.locationStatus = ``;
this.locationStatus = this.attendanceGroup.areaName
}, },


/** /**
* @param {GeolocationPositionError} error 错误信息 * @param {GeolocationPositionError} error 错误信息
*/ */
handleLocationError(error) { handleLocationError(error) {
console.error("获取位置失败:", error);
console.error('获取位置失败:', error)
switch (error.code) { switch (error.code) {
case error.PERMISSION_DENIED: case error.PERMISSION_DENIED:
this.locationStatus = "请允许获取位置权限";
break;
this.locationStatus = '请允许获取位置权限'
break
case error.POSITION_UNAVAILABLE: case error.POSITION_UNAVAILABLE:
this.locationStatus = "位置信息不可用";
break;
this.locationStatus = '位置信息不可用'
break
case error.TIMEOUT: case error.TIMEOUT:
this.locationStatus = "获取位置超时";
break;
this.locationStatus = '获取位置超时'
break
default: default:
this.locationStatus = "获取位置失败,请检查定位权限";
this.locationStatus = '获取位置失败,请检查定位权限'
} }
}, },


navigator.geolocation.getCurrentPosition(resolve, reject, { navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true, enableHighAccuracy: true,
timeout: 5000, timeout: 5000,
maximumAge: 0,
});
});
maximumAge: 0
})
})


this.handleLocationSuccess(position);
return this.userLocation;
this.handleLocationSuccess(position)
return this.userLocation
} catch (error) { } catch (error) {
this.handleLocationError(error);
throw error;
this.handleLocationError(error)
throw error
} }
}, },
/** /**
*/ */
async handlePunch() { async handlePunch() {
if (!this.isInRange) { if (!this.isInRange) {
this.$message.error("请在打卡范围内进行打卡");
return;
this.$message.error('请在打卡范围内进行打卡')
return
} }


try { try {
// 根据打卡状态判断,而不是时间 // 根据打卡状态判断,而不是时间
if (this.punchStatus.morning === "未打卡") {
if (this.punchStatus.morning === '未打卡') {
// 上班打卡 // 上班打卡
if (this.isLate()) { if (this.isLate()) {
// 迟到打卡,需要填写备注 // 迟到打卡,需要填写备注
this.showRemarkDialog = true;
this.remarkDialogTitle = "迟到打卡";
this.remarkForm.type = "morning_late";
this.showRemarkDialog = true
this.remarkDialogTitle = '迟到打卡'
this.remarkForm.type = 'morning_late'
} else { } else {
// 正常打卡 // 正常打卡
await this.submitPunch("morning");
await this.submitPunch('morning')
} }
} else if (this.punchStatus.evening === "未打卡") {
} else if (this.punchStatus.evening === '未打卡') {
// 下班打卡 // 下班打卡
if (this.isEarly()) { if (this.isEarly()) {
// 早退打卡,需要填写备注 // 早退打卡,需要填写备注
this.showRemarkDialog = true;
this.remarkDialogTitle = "早退打卡";
this.remarkForm.type = "evening_early";
this.showRemarkDialog = true
this.remarkDialogTitle = '早退打卡'
this.remarkForm.type = 'evening_early'
} else { } else {
// 正常打卡 // 正常打卡
await this.submitPunch("evening");
await this.submitPunch('evening')
} }
} else { } else {
// 都已经打卡了 // 都已经打卡了
this.$message.info("今日已完成打卡");
this.$message.info('今日已完成打卡')
} }
} catch (error) { } catch (error) {
console.error("打卡失败:", error);
this.$message.error("打卡失败,请重试");
console.error('打卡失败:', error)
this.$message.error('打卡失败,请重试')
} }
}, },


* 判断是否迟到 * 判断是否迟到
*/ */
isLate() { isLate() {
const now = new Date();
const hour = now.getHours();
const minute = now.getMinutes();
const { workStartTime } = this.attendanceGroup;
const [hour2, minute2] = workStartTime.split(":");
return hour > hour2 || (hour === hour2 && minute > minute2);
const now = new Date()
const hour = now.getHours()
const minute = now.getMinutes()
const { workStartTime } = this.attendanceGroup
const [hour2, minute2] = workStartTime.split(':')
return hour > hour2 || (hour === hour2 && minute > minute2)
}, },


/** /**
* 判断是否早退 * 判断是否早退
*/ */
isEarly() { isEarly() {
const now = new Date();
const hour = now.getHours();
const minute = now.getMinutes();
const { workEndTime } = this.attendanceGroup;
const [hour2, minute2] = workEndTime.split(":");
return hour < hour2 || (hour === hour2 && minute < minute2);
const now = new Date()
const hour = now.getHours()
const minute = now.getMinutes()
const { workEndTime } = this.attendanceGroup
const [hour2, minute2] = workEndTime.split(':')
return hour < hour2 || (hour === hour2 && minute < minute2)
}, },


/** /**
*/ */
async submitRemark() { async submitRemark() {
try { try {
await this.submitPunch(this.remarkForm.type);
this.showRemarkDialog = false;
this.remarkForm.remark = "";
await this.submitPunch(this.remarkForm.type)
this.showRemarkDialog = false
this.remarkForm.remark = ''
} catch (error) { } catch (error) {
console.error("提交备注失败:", error);
this.$message.error("提交失败,请重试");
console.error('提交备注失败:', error)
this.$message.error('提交失败,请重试')
} }
}, },


/**
* 关闭打卡成功弹窗
*/
closeSuccessDialog() {
this.showSuccess = false
this.init()
},

/** /**
* 提交打卡 * 提交打卡
* @param {string} type 打卡类型:morning/evening/morning_late/evening_early * @param {string} type 打卡类型:morning/evening/morning_late/evening_early
*/ */
async submitPunch(type) { async submitPunch(type) {
// TODO: 调用打卡API // TODO: 调用打卡API
console.log("打卡类型:", type);
console.log("备注:", this.remarkForm.remark);
let checkInStatus = -1;
let checkInType = "clockIn";
console.log('打卡类型:', type)
console.log('备注:', this.remarkForm.remark)
let checkInStatus = -1
let checkInType = 'clockIn'
switch (type) { switch (type) {
case "morning":
case 'morning':
// 正常上班打卡 // 正常上班打卡
checkInStatus = 0;
checkInType = "clockIn";
break;
case "evening":
checkInStatus = 0
checkInType = 'clockIn'
break
case 'evening':
// 正常下班打卡 // 正常下班打卡
checkInStatus = 0;
checkInType = "下班打卡";
break;
case "morning_late":
checkInStatus = 0
checkInType = '下班打卡'
break
case 'morning_late':
// 迟到打卡 // 迟到打卡
checkInStatus = 1;
checkInType = "clockIn";
break;
case "evening_early":
checkInStatus = 1
checkInType = 'clockIn'
break
case 'evening_early':
// 早退打卡 // 早退打卡
checkInStatus = 1;
checkInType = "早退打卡";
break;
checkInStatus = 1
checkInType = '早退打卡'
break
default: default:
this.$message.error("打卡类型错误");
break;
this.$message.error('打卡类型错误')
break
} }


// 更新打卡状态 // 更新打卡状态
if (type === "morning" || type === "morning_late") {
if (type === 'morning' || type === 'morning_late') {
this.currentCompleteDate = this.formatDateTime(new Date())
const params = { const params = {
userId: this.userinfo.userId, userId: this.userinfo.userId,
userName: this.userinfo.nickName,
lng: this.userLocation.longitude, lng: this.userLocation.longitude,
lat: this.userLocation.latitude, lat: this.userLocation.latitude,
checkInStatus, checkInStatus,
clockIn: new Date().toISOString(),
clockIn: this.currentCompleteDate,
checkInType, checkInType,
description: this.remarkForm.remark,
};
const res = await checkIn(params);
description: this.remarkForm.remark
}
const res = await checkIn(params)

if (res.code === 200) { if (res.code === 200) {
this.punchStatus.morning = type === "morning" ? "已打卡" : "迟到";
//打卡成功
this.showSuccess = true;
this.init();
this.punchStatus.morning = type === 'morning' ? '已打卡' : '迟到'
// 打卡成功
this.showSuccess = true
this.init()
} else { } else {
this.$message.error(res.msg);
this.$message.error(res.msg)
} }
} else { } else {
this.punchStatus.evening = type === "evening" ? "已打卡" : "早退";
this.punchStatus.evening = type === 'evening' ? '已打卡' : '早退'
} }
},
},
};
}
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.page { .page {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh; min-height: 100vh;
color: #333; color: #333;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
align-items: center;
em { em {
font-style: normal; font-style: normal;
display: flex; display: flex;
text-align: center; text-align: center;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
animation: popIn 0.3s ease-out; animation: popIn 0.3s ease-out;
position: relative;
}

.success-close {
position: absolute;
top: 15px;
right: 20px;
font-size: 24px;
color: #999;
cursor: pointer;
padding: 4px;
border-radius: 50%;
transition: all 0.2s ease;

&:hover {
background: #f5f5f5;
color: #666;
}

&:active {
transform: scale(0.95);
}
} }


.success-icon { .success-icon {

Loading…
Cancel
Save