Przeglądaj źródła

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

master
lizhuang 1 tydzień temu
rodzic
commit
82cf33400d
1 zmienionych plików z 240 dodań i 152 usunięć
  1. 240
    152
      src/views/m/checkin/index.vue

+ 240
- 152
src/views/m/checkin/index.vue Wyświetl plik

@@ -62,7 +62,11 @@
<div class="status-time">
上班{{ attendanceGroup.workStartTime }}
</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 class="status-card">
<div class="status-time">下班{{ attendanceGroup.workEndTime }}</div>
@@ -121,6 +125,7 @@
<transition name="fade">
<div v-if="showSuccess" class="success-overlay">
<div class="success-content">
<div class="success-close" @click="closeSuccessDialog">×</div>
<div class="success-icon">
<svg viewBox="0 0 24 24" class="checkmark">
<path
@@ -132,11 +137,11 @@
<div class="success-info">
<div class="info-item">
<span class="label">打卡时间</span>
<span class="value">{{ currentTime }}</span>
<span class="value">{{ currentCompleteDate }}</span>
</div>
<div class="info-item">
<span class="label">打卡地点</span>
<span class="value">{{ locationStatus }}</span>
<span class="value">{{ attendanceGroup.areaName }}</span>
</div>
</div>
</div>
@@ -146,18 +151,20 @@
<!-- 范围,地点提示 -->
<div class="location-info">
<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>

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

@@ -184,97 +191,113 @@
</div>
</template>
<script>
import { mapGetters } from "vuex";
import { mapGetters } from 'vuex'
import {
queryAttendanceGroupByUserId,
checkIn,
checkOut,
getCurrentDayRecord,
} from "@/api/checkin/punch-card";
getCurrentDayRecord
} from '@/api/checkin/punch-card'
export default {
name: "MCheckin",
name: 'MCheckin',
data() {
return {
// 当前时间
currentTime: new Date().toLocaleTimeString("zh-CN", { hour12: false }),
currentTime: new Date().toLocaleTimeString('zh-CN', { hour12: false }),
// 打卡完成时间
currentCompleteDate: '',
// 用户位置
userLocation: {
latitude: null, // 纬度
longitude: null, // 经度
longitude: null // 经度
},
// 位置状态
locationStatus: "正在获取位置信息...",
locationStatus: '正在获取位置信息...',
// 打卡成功提示
showSuccess: false,
// 位置监听ID
watchId: null,
// 打卡状态
punchStatus: {
morning: "未打卡", // 上班打卡状态:未打卡、已打卡、迟到
evening: "未打卡", // 下班打卡状态:未打卡、已打卡、早退
morning: '未打卡', // 上班打卡状态:未打卡、已打卡、迟到
evening: '未打卡' // 下班打卡状态:未打卡、已打卡、早退
},
// 备注对话框
showRemarkDialog: false,
// 备注对话框标题
remarkDialogTitle: "",
remarkDialogTitle: '',
// 备注表单
remarkForm: {
remark: "", // 备注
type: "", // 打卡类型:morning_late 或 evening_early
remark: '', // 备注
type: '' // 打卡类型:morning_late 或 evening_early
},
// 是否在打卡范围内
isInRange: false,
// 考勤组信息
attendanceGroup: {},
// 当前考勤状态
currentAttendance: {},
};
currentAttendance: null
}
},
computed: {
...mapGetters(["userinfo", "avatar"]),
...mapGetters(['userinfo', 'avatar']),
// 获取时区
timeZone() {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
return Intl.DateTimeFormat().resolvedOptions().timeZone
},
// 打卡按钮文本
punchButtonText() {
// 优先根据打卡状态判断,而不是时间
if (this.punchStatus.morning === "未打卡") {
if (this.punchStatus.morning === '未打卡') {
// 如果上班还没打卡,判断是否迟到
if (this.isLate()) {
return "上班打卡";
return '上班打卡'
}
return "上班打卡";
} else if (this.punchStatus.evening === "未打卡") {
return '上班打卡'
} else if (this.punchStatus.evening === '未打卡') {
// 上班已打卡,下班未打卡,判断是否早退
if (this.isEarly()) {
return "下班打卡";
return '下班打卡'
}
return "下班打卡";
return '下班打卡'
} else {
// 都已经打卡了
return "已打卡";
return '已打卡'
}
},
/**
* 获取当前考勤状态
* @returns {number} 考勤状态码
*/
checkInStatus() {
if (
this.currentAttendance == null ||
this.currentAttendance.checkInStatus == 0
) {
return 0
} else {
return Number(this.currentAttendance.checkInStatus)
}
}
},
mounted() {
// 更新时间
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() {
// 组件销毁前清除位置监听
if (this.watchId !== null) {
navigator.geolocation.clearWatch(this.watchId);
navigator.geolocation.clearWatch(this.watchId)
}
},
methods: {
@@ -282,39 +305,70 @@ export default {
* 初始化
*/
async init() {
const { userId } = this.userinfo;
const { userId } = this.userinfo
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.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) {
this.isInRange = false;
this.isInRange = false
} else {
this.isInRange = true;
this.isInRange = true
}
} 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
@@ -324,21 +378,21 @@ export default {
* @returns {number} 距离,单位为公里
*/
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 =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * (Math.PI / 180)) *
Math.cos(lat2 * (Math.PI / 180)) *
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
},

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

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

/**
@@ -368,11 +422,11 @@ export default {
handleLocationSuccess(position) {
this.userLocation = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
};
longitude: position.coords.longitude
}

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

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

@@ -406,15 +460,15 @@ export default {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0,
});
});
maximumAge: 0
})
})

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

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

@@ -464,24 +518,24 @@ export default {
* 判断是否迟到
*/
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() {
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)
},

/**
@@ -489,81 +543,92 @@ export default {
*/
async submitRemark() {
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) {
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
*/
async submitPunch(type) {
// 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) {
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:
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 = {
userId: this.userinfo.userId,
userName: this.userinfo.nickName,
lng: this.userLocation.longitude,
lat: this.userLocation.latitude,
checkInStatus,
clockIn: new Date().toISOString(),
clockIn: this.currentCompleteDate,
checkInType,
description: this.remarkForm.remark,
};
const res = await checkIn(params);
description: this.remarkForm.remark
}
const res = await checkIn(params)

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 {
this.$message.error(res.msg);
this.$message.error(res.msg)
}
} else {
this.punchStatus.evening = type === "evening" ? "已打卡" : "早退";
this.punchStatus.evening = type === 'evening' ? '已打卡' : '早退'
}
},
},
};
}
}
}
</script>
<style lang="scss" scoped>
.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%);
min-height: 100vh;
color: #333;
@@ -736,6 +801,7 @@ export default {
display: flex;
flex-direction: column;
gap: 6px;
align-items: center;
em {
font-style: normal;
display: flex;
@@ -836,6 +902,28 @@ export default {
text-align: center;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
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 {

Ładowanie…
Anuluj
Zapisz