2024-12-04 18:37:56 +08:00

824 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="fullHeight">
<headers :showBack="true" :themeType="'white'">
<view class="headerName"> 考勤打卡 </view>
</headers>
<!-- 打卡 -->
<view v-if="!tabActive">
<view class="box personInfoBox">
<image v-if="fieldAcquisitionUrl" :src="url_config + 'image/' + fieldAcquisitionUrl" class="profile_photo"></image>
<image v-else src="/static/profile_photo.png" class="profile_photo"></image>
<view class="info">
<view class="personName">
{{ realName }}
</view>
<view class="groupName"> {{ enterpriseName }}&nbsp;&nbsp;{{ teamName }} </view>
</view>
</view>
<view class="box" :style="{ height: screenHeight - statusBarHeight - 44 - 105 - 15 - 30 - 50 + 'px' }">
<view class="top">
<view class="item">
<view class="type"> 进场 </view>
<view class="time" v-if="attendenceData.enterTime">
<uni-icons2 class="backImg" type="checkbox-filled" size="14" color="#5181F6"></uni-icons2>
{{ enterTime }} 已打卡
</view>
<view class="time" v-else> 未打卡 </view>
</view>
<view class="item">
<view class="type"> 出场 </view>
<view class="time" v-if="attendenceData.outTime">
<uni-icons2 class="backImg" type="checkbox-filled" size="14" color="#5181F6"></uni-icons2>
{{ outTime }}已打卡
</view>
<view class="time" v-else> 未打卡 </view>
</view>
</view>
<view class="bottom">
<view class="">
<view class="circleBox" :class="canClick ? '' : 'grey'" @click="checkInOpen">
<view class="name"> 考勤打卡 </view>
<view class="time">
{{ nowTime }}
</view>
</view>
<view class="address" v-if="canClick">
<uni-icons2 class="backImg" type="checkbox-filled" size="14" color="#707070"></uni-icons2>已进入考勤范围:{{
electricFence.addr
}}
</view>
<view class="address" v-else>
<uni-icons2 class="backImg" type="location-filled" size="14" color="#707070"></uni-icons2>未进入考勤区域
</view>
</view>
</view>
</view>
</view>
<!-- 统计 -->
<view v-else>
<view class="box flex-c" :style="{ height: screenHeight - statusBarHeight - 44 - 15 - 30 - 50 + 'px' }">
<view class="personInfoBox" style="padding-bottom: 24px">
<image v-if="fieldAcquisitionUrl" :src="url_config + 'image/' + fieldAcquisitionUrl" class="profile_photo"></image>
<image v-else src="/static/profile_photo.png" class="profile_photo"></image>
<view class="info">
<view class="personName">
{{ realName }}
</view>
<view class="groupName"> {{ enterpriseName }}&nbsp;&nbsp;{{ teamName }} </view>
</view>
</view>
<view class="calendarBox">
<scroll-view scroll-y="true" style="height: 100%">
<view class="calendarStatus">
<view class="item" v-for="item in clockOptions">
<view class="circle" :style="{ backgroundColor: item.color }"></view>
<text class="name">{{ item.name }}</text>
</view>
</view>
<view :class="{ calendar: true, hide: openIcon === 'down' }">
<uni-calendar
class="uni-calendar--hook"
:selected="selected"
:showMonth="false"
@change="dayChange"
@monthSwitch="monthChange"
/>
</view>
<!-- 展开 收起 -->
<view class="open" @click="handleOpen">
<view class="line"></view>
<uni-icons2 class="ic" :type="openIcon" size="20" color="#CCCCCC"></uni-icons2>
<view class="line"></view>
</view>
<view class="detail">
<view class="info">
当日班次:
{{ attendenceRule.ruleName || '固定上下班' }}
{{ attendenceRule.startTime || '9:00' }}-{{ attendenceRule.endTime || '18:00' }}
</view>
<view class="info" v-if="attendanceInfo.length">出勤统计:打卡{{ attendanceInfo.length }}次</view>
<u-time-line>
<u-time-line-item v-for="item in attendanceInfo" :key="item.createTime">
<template v-slot:content>
<view class="u-wrap">
<view class="left">
<view class="u-order-time"> {{ item.passType === 1 ? '进场' : '出场' }} {{ item.createTime.split(' ')[1] }} </view>
<view class="u-order-desc">
<image class="img" src="@/static/checkIn/position.png" />
{{ item.address }}
</view>
</view>
<view class="u-image">
<image
class="image"
:src="url_config + 'image/' + item.imageUrl"
@click="previewImage(url_config + 'image/' + item.imageUrl)"
></image>
</view>
</view>
</template>
</u-time-line-item>
</u-time-line>
<view class="info" v-if="attendanceInfo.length"
>统计截至{{ attendanceInfo.length && attendanceInfo[attendanceInfo.length - 1].createTime }}</view
>
<view class="noData" v-if="!attendanceInfo.length">
<image src="@/static/checkIn/nodata.png"></image>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
<u-tabbar :value="tabActive" @change="handleChangeTab" :list="tabList" activeColor="#4496fc" inactiveColor="#707070"></u-tabbar>
<!-- 确认打卡弹框 -->
<u-popup v-model="checkInShow" :round="10" mode="bottom" @close="checkInClose" closeable>
<view class="popup_content">
<text class="popup_title">打卡备注</text>
<view class="form_item flex">
<view class="name">请选择:</view>
<u-radio-group v-model="checkInform.type">
<u-radio :name="1"> 进场 </u-radio>
<u-radio :name="2"> 出场 </u-radio>
</u-radio-group>
</view>
<view class="form_item flex">
<view class="name">{{ phoneClockImageType ? '人脸识别' : '上传图片' }}</view>
<image
class="p_image"
v-if="checkInform.photoUrl"
:src="url_config + 'image/' + checkInform.photoUrl"
@click="uploadImage"
></image>
<image class="p_image" v-else src="@/static/checkIn/camera.png" @click="uploadImage"></image>
</view>
<u-button type="primary" @click="clockIn">打卡</u-button>
</view>
</u-popup>
</view>
</template>
<script>
import UTimeLine from '@/components/u-time-line/u-time-line.vue'
import UTimeLineItem from '@/components/u-time-line-item/u-time-line-item.vue'
import UTabbar from '@/components/u-tabbar/u-tabbar.vue'
import uniIcons from '@/components/uni-icons/uni-icons.vue'
import { getPersonAttTime } from '@/api/index.js'
import locationIcon from '@/static/checkIn/location.png'
import locationActiveIcon from '@/static/checkIn/location-active.png'
import statisticsIcon from '@/static/checkIn/statistics.png'
import statisticsActiveIcon from '@/static/checkIn/statistics-active.png'
export default {
components: { uniIcons, UTabbar, UTimeLine, UTimeLineItem },
data() {
return {
openIcon: 'up',
clockOptions: [
{
name: '正常出勤',
color: '#3a7bff'
},
{
name: '缺卡',
color: '#1d1e37'
},
{
name: '迟到',
color: '#ff6e5f'
},
{
name: '早退',
color: '#45c5a2'
},
{
name: '未出勤',
color: '#a3a3a3'
}
],
// 0无考勤1、正常 2、迟到 3、早退 4、加班5、缺卡
colors: ['#a3a3a3', '#3a7bff', '#ff6e5f', '#45c5a2', '#ffb85a', '#1d1e37'],
selected: [
// {
// date: '2024-11-21',
// // info: '打卡',
// color: '#667ED6'
// }
],
checkInform: {
type: 1,
photoUrl: ''
},
checkInShow: false,
tabList: [
{
text: '打卡',
iconPath: locationIcon,
selectedIconPath: locationActiveIcon
},
{
text: '统计',
iconPath: statisticsIcon,
selectedIconPath: statisticsActiveIcon
}
],
tabActive: 0,
statusBarHeight: 0,
screenHeight: 667,
fieldAcquisitionUrl: '',
canClick: false,
projectSn: '',
enterpriseName: '',
teamName: '',
workerId: '',
personSn: '',
ruleId: '',
electricFence: {},
attendenceData: {
enterTime: null,
outTime: null
},
currentTime: '',
photoUrl: '',
timer: null,
realName: '',
phoneClockImageType: 0,
faceScore: 0,
isMobileAttendance: 0,
timer: '', //定义一个定时器
nowTime: '',
attendanceInfo: [],
attendenceRule: {},
curMonth: new Date().getFullYear() + '-' + (new Date().getMonth() + 1),
// yyyy-mm-dd
curDay: new Date().getFullYear() + '-' + (new Date().getMonth() + 1) + '-' + new Date().getDate()
}
},
computed: {
enterTime: function () {
if (this.attendenceData.enterTime) {
var a = this.attendenceData.enterTime.split(' ')[1]
return a
} else {
return null
}
},
outTime: function () {
if (this.attendenceData.outTime) {
var a = this.attendenceData.outTime.split(' ')[1]
return a
} else {
return null
}
}
},
created() {
this.getTime()
},
beforeDestroy() {
if (this.timer) {
clearInterval(this.timer)
}
},
onLoad() {
this.statusBarHeight = uni.getStorageSync('systemInfo').statusBarHeight
this.screenHeight = uni.getStorageSync('systemInfo').screenHeight
console.info(uni.getStorageSync('userInfo'), 'userInfo')
var userInfo = JSON.parse(uni.getStorageSync('userInfo'))
this.projectSn = userInfo.projectSn
this.workerId = userInfo.id
this.personSn = userInfo.personSn
this.enterpriseName = userInfo.enterpriseName
this.teamName = userInfo.teamName
this.realName = userInfo.workerName
this.ruleId = userInfo.ruleId
this.fieldAcquisitionUrl = userInfo.fieldAcquisitionUrl
this.GetLOcation()
this.getAttendenceData()
this.getAttendenceTypeFn()
this.getAttendenceRule()
},
onShow() {
if (this.phoneClockImageType == 1 && this.photoUrl) {
this.checkInform.photoUrl = this.photoUrl
this.addPhoneAttendanceFn()
}
},
methods: {
//预览图片
previewImage(url) {
uni.previewImage({
urls: [url]
})
},
handleOpen() {
this.openIcon = this.openIcon == 'up' ? 'down' : 'up'
},
dayChange(data) {
console.info(data, '====')
this.curDay = data.fulldate
this.getWorkerDayAttendanceList()
},
monthChange(data) {
this.curMonth = data.month
this.getWorkerMonthAttendanceTotal()
},
uploadImage() {
if (this.phoneClockImageType) {
return uni.navigateTo({
url: './faceRecognition?faceScore=' + this.faceScore + '&url_config=' + this.url_config
})
}
var that = this
uni.chooseImage({
count: 1,
sourceType: ['camera'],
success(res) {
const tempFilePaths = res.tempFilePaths[0]
uni.uploadFile({
url: that.url_config + 'upload/image',
filePath: tempFilePaths,
name: 'files',
success: uploadFileRes => {
const imageUrl = JSON.parse(uploadFileRes.data).data[0].imageUrl
console.info(imageUrl)
that.checkInform.photoUrl = imageUrl
},
fail(e) {
console.log(e)
}
})
}
})
},
checkInOpen() {
if (!this.canClick) {
return uni.showToast({
title: '请进入考勤范围',
icon: 'none'
})
}
if (!this.isMobileAttendance) {
return uni.showToast({
title: 'APP考勤已关闭请联系管理员',
icon: 'none'
})
}
// this.checkInform.type = !this.attendenceData.enterTime ? 1 : 2
this.checkInShow = true
},
checkInClose() {
this.checkInShow = false
},
handleChangeTab(val) {
console.info(val, 'val')
this.tabActive = val
if (this.tabActive) {
this.curMonth = new Date().getFullYear() + '-' + (new Date().getMonth() + 1)
this.curDay = new Date().getFullYear() + '-' + (new Date().getMonth() + 1) + '-' + new Date().getDate()
this.getWorkerMonthAttendanceTotal()
this.getWorkerDayAttendanceList()
}
},
//判断是否需要人脸识别
getAttendenceTypeFn() {
var that = this
this.sendRequest({
url: 'xmgl/projectConfig/getProjectConfigList',
data: { projectSn: this.projectSn },
method: 'POST',
success(res) {
if (res.result) {
that.phoneClockImageType = res.result[0].phoneClockImageType
that.faceScore = res.result[0].faceScore
that.isMobileAttendance = res.result[0].isMobileAttendance // 是否启用APP打卡
}
}
})
},
getTime() {
this.timer = setInterval(() => {
let timeDate = new Date()
let year = timeDate.getFullYear()
let mounth = timeDate.getMonth() + 1
let day = timeDate.getDate()
let hours = timeDate.getHours()
hours = hours >= 10 ? hours : '0' + hours
let minutes = timeDate.getMinutes()
minutes = minutes >= 10 ? minutes : '0' + minutes
let seconds = timeDate.getSeconds()
seconds = seconds >= 10 ? seconds : '0' + seconds
let week = timeDate.getDay()
let weekArr = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
this.nowTime = ` ${hours}:${minutes}:${seconds}`
}, 1000)
},
// 查询人员指定月份考勤统计
getWorkerMonthAttendanceTotal() {
var that = this
that.selected = []
this.sendRequest({
url: 'xmgl/workerMonthAttendanceStatistics/selectMonthAttendanceByWorkerId',
data: { workerId: this.workerId, projectSn: this.projectSn, monthTime: this.curMonth },
method: 'POST',
success(res) {
const dates = Object.entries(res.result)
for (const item of dates) {
const [key, value] = item
if (key.includes('day')) {
console.info(value, that.curMonth + '-' + String(key.split('day')[1]).padStart(2, 0))
that.selected.push({
date: that.curMonth + '-' + String(key.split('day')[1]).padStart(2, 0),
color: that.colors[value]
})
}
}
}
})
},
// 查询个人具体某一天的考勤情况
getWorkerDayAttendanceList() {
var that = this
this.sendRequest({
url: 'xmgl/workerAttendance/viewDayAttendanceList',
data: { personSn: this.personSn, queryTime: this.curDay },
method: 'POST',
success(res) {
console.info(res)
that.attendanceInfo = res.result || []
}
})
},
// 获取考勤规则 /xmgl/workerAttendanceRule/queryById
getAttendenceRule() {
var that = this
this.sendRequest({
url: 'xmgl/workerAttendanceRule/queryById',
data: { id: this.ruleId },
method: 'POST',
success(res) {
console.info(res)
that.attendenceRule = res.result || {}
}
})
},
//获取考勤信息
getAttendenceData() {
var that = this
getPersonAttTime({ workerId: this.workerId }).then(res => {
console.log('getAttendenceData', res)
that.attendenceData = res.result
})
},
GetLOcation: function () {
var that = this
uni.getLocation({
type: 'gcj02',
success: res => {
console.log('success', res)
var latitude = parseFloat(res.latitude)
var longitude = parseFloat(res.longitude)
that.isInArea(longitude, latitude)
},
fail(res) {
console.log('fail', res)
// uni.showModal({
// title:'提示',
// content:res.errMsg
// })
}
})
},
// 校验坐标位置是否在考勤区域中
isInArea(longitude, latitude) {
var that = this
this.sendRequest({
url: 'xmgl/electricFence/checkLocation',
data: { longitude: longitude, projectSn: this.projectSn, latitude: latitude, workerId: this.workerId },
method: 'POST',
hideLoading: true,
success(res) {
console.log('isInArea', res)
if (res.result.checkType == 0) {
console.log('不在考勤范围内')
that.canClick = false
} else {
console.log('在考勤范围内')
that.canClick = true
that.electricFence = res.result.electricFence
}
}
})
},
//手机打卡
addPhoneAttendanceFn() {
var that = this
// var type = 1
// if (this.attendenceData.enterTime) {
// type = 2
// }
this.sendRequest({
url: 'xmgl/workerAttendance/addPhoneAttendance',
data: { workerId: this.workerId, address: this.electricFence.addr, ...this.checkInform },
method: 'POST',
success(res) {
console.log('addPhoneAttendanceFn', res)
uni.showToast({
title: '打卡成功',
icon: 'none'
})
that.checkInClose()
that.getAttendenceData()
that.checkInform = {
type: 1,
photoUrl: ''
}
}
})
},
clockIn() {
if (!this.canClick) {
return
}
if (!this.checkInform.photoUrl) {
return uni.showToast({
title: '请上传照片',
icon: 'none'
})
}
this.addPhoneAttendanceFn()
}
}
}
</script>
<style scoped lang="scss">
.profile_photo {
width: 48px;
height: 48px;
border-radius: 8px;
margin-right: 10px;
}
.box {
box-shadow: 0 4px 24px 0px rgba(212, 220, 236, 0.69);
border-radius: 8px;
margin: 15px;
padding: 15px;
box-sizing: border-box;
}
.personInfoBox {
display: flex;
align-items: center;
flex-direction: row;
.personName {
// color: rgba(72, 141, 236, 1);
// font-size: 17px;
font-weight: bold;
font-size: 16px;
color: #000000;
}
.groupName {
color: rgba(42, 43, 91, 0.8);
font-size: 13px;
}
}
.top {
display: flex;
align-items: center;
flex-direction: row;
.item {
background-color: #ededed;
border-radius: 12px;
padding: 8px 12px;
flex: 1;
font-size: 15px;
font-family: PingFang SC;
// width: calc(50% - 29px);
// display: inline-flex;
&:first-child {
margin-right: 10px;
}
.type {
font-size: 16px;
color: #000000;
}
.time {
font-size: 13px;
color: #707070;
display: flex;
align-items: center;
margin-top: 3px;
flex-direction: row;
}
}
}
.backImg {
margin-right: 3px;
}
.bottom {
text-align: center;
display: flex;
align-items: center;
justify-content: center;
height: calc(100% - 60px);
.address {
font-size: 13px;
margin-top: 20px;
color: #707070;
flex-direction: row;
display: flex;
align-items: baseline;
justify-content: center;
}
}
.circleBox {
width: 140px;
height: 140px;
border-radius: 50%;
background-image: linear-gradient(#0093f9, #066af8);
color: white;
display: inline-block;
box-shadow: 0 4px 24px 0px rgba(81, 129, 246, 0.5);
&.grey {
background-image: linear-gradient(#f1f1f1, #c3c3c3);
box-shadow: 0 4px 24px 0px rgba(195, 195, 195, 0.5);
}
.name {
padding-top: 45px;
font-size: 20px;
}
.time {
font-size: 15px;
color: rgba(255, 255, 255, 0.7);
}
}
.popup_content {
padding: 16px;
.popup_title {
font-size: 16px;
color: #171717;
font-weight: bold;
display: flex;
justify-content: center;
}
}
.flex {
display: flex;
align-items: center;
}
.flex-c {
display: flex;
flex-direction: column;
}
.form_item {
font-size: 30rpx;
line-height: 40px;
margin: 8px 0;
/* border-bottom: 1px solid rgba(194, 194, 194, 0.2); */
.p_image {
width: 52px;
height: 52px;
}
}
.form_item .name {
margin-right: 6px;
width: 185rpx;
text-align: left;
white-space: nowrap;
&.large {
width: 300rpx;
}
&.large-2 {
width: 240rpx;
}
}
.calendarBox {
width: 100%;
height: calc(100% - 78px - -24px);
// height: 100%;
.calendarTitle {
// background-color: #fff;
font-size: 18px;
font-weight: bold;
color: #000000;
padding: 10px 0;
}
.calendarStatus {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin-bottom: 12px;
.item {
display: flex;
align-items: center;
flex-shrink: 0;
.circle {
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 8px;
}
.text {
font-size: 12px;
color: #707070;
}
}
}
.calendar {
width: 100%;
overflow: hidden;
height: auto;
transition: all 0.3s;
&.hide {
height: 150px;
}
}
}
.open {
display: flex;
align-items: center;
.line {
flex: 1;
height: 1px;
background: #dbdbdb;
}
.ic {
flex-shrink: 0;
margin: 0 2px;
width: 20px;
height: 20px;
}
}
.detail {
padding: 20px 4px;
.info {
font-family: PingFang SC;
font-weight: 400;
font-size: 13px;
color: #707070;
line-height: 16px;
text-align: left;
margin-bottom: 16px;
}
/deep/.u-dot {
background: #707070;
}
/deep/.u-time-axis::before {
border-left: 1px solid #707070;
}
.u-wrap {
display: flex;
justify-content: space-between;
align-items: flex-start;
.left {
flex: 1;
}
.u-image {
width: 42px;
height: 42px;
margin-left: 8px;
.image {
width: 42px;
height: 42px;
border-radius: 8px;
}
}
}
.u-order-time {
font-weight: 500;
font-size: 16px;
color: #000000;
line-height: 16px;
margin-bottom: 10px;
}
.u-order-desc {
font-weight: 400;
font-size: 13px;
color: #707070;
line-height: 16px;
display: flex;
align-items: center;
.img {
width: 32rpx;
height: 16px;
margin-right: 10px;
flex-shrink: 0;
}
}
.noData {
width: 100%;
height: 200px;
display: flex;
justify-content: center;
}
}
/deep/.uni-calendar-item__weeks-box-item {
width: auto;
}
</style>