You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

440 lines
12 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div>
<CardContainer>
<div>
<el-button v-if="isShowDuty" type="primary" style="display: block;margin: auto auto 20px;" @click="startDuty">开始值班</el-button>
<button class="sign-btn" @click="clockIn">
<span>{{ isOutSign ? ' 外勤' : '' }}打卡</span>
</button>
<div class="sign-info">
<div class="sign-statue">
<div>打卡状态 <el-tag size="small" effect="dark" type="primary">{{ isGetLocation ? isOutSign ? '外勤打卡' : '可打卡' : '不可打卡' }}</el-tag></div>
<div>当前位置: <span v-if="isGetLocation">
<el-tag size="small" type="primary" effect="dark">{{pos.address}}</el-tag>
{{pos.lng}},{{pos.lat}}</span></div>
<div>当前距离:{{ nowDistance }}千米</div>
<div>最大打卡范围:{{ maxDistance }}千米</div>
</div>
<div class="sign-log" v-if="todayAttendance.length > 0">
<el-timeline>
<el-timeline-item
v-for="(item) in todayAttendance"
:key="item.id"
:type="item.sign_at_image ? 'warning' : 'primary'"
:timestamp="item.sign_at">
{{ item.sign_at_address }}{{ item.sign_at_image ? '(外勤)' : '' }}
</el-timeline-item>
</el-timeline>
</div>
</div>
</div>
<MonthStatics ref="MonthStatics" @today-attendance="e => todayAttendance = e"></MonthStatics>
</CardContainer>
<vxe-modal
v-model="isShow"
show-footer
:z-index="zIndex"
title="外勤打卡"
show-confirm-button
:width="560"
:height="440"
esc-closable
:fullscreen="$store.getters.device === 'mobile'"
>
<div style="line-height: 3;color: #333;font-weight: 600;">打卡照片</div>
<van-uploader capture="camera" :max-size="20 * 1024 * 1024" preview-size="140px" :max-count="1" v-model="fileList" @delete="imageId = ''"/>
<p>照片大小不能超过20Mb</p>
<div style="line-height: 3;color: #333;font-weight: 600;">描述</div>
<el-input v-model="remark" type="textarea" :autosize="{ minRows: 2 }"></el-input>
<template #footer>
<el-button type="primary" :loading="loading" @click="outClockIn"
>确认打卡</el-button
>
</template>
</vxe-modal>
</div>
</template>
<script>
import { index, save } from '@/api/onDutySchedules'
import { sign, preDistance } from '@/api/attendance'
import { throttle } from '@/utils'
import { getToken } from '@/utils/auth'
import MonthStatics from './components/MonthStatics'
import axios from "axios";
import * as uni from "@/assets/uni.webview.1.5.6";
import { PopupManager } from "element-ui/lib/utils/popup";
export default {
components: {
MonthStatics
},
data() {
return {
isInUni: false,
// start 外勤打卡
loading: false,
action: process.env.VUE_APP_UPLOAD_API,
fileList: [],
isShow: false,
zIndex: PopupManager.nextZIndex(),
imageId: '',
remark: '',
// end
isGetLocation: false,
isOutSign: false,
pos: {
lng: '',
lat: '',
address: ''
},
maxDistance: '',
nowDistance: '',
todayAttendance: [],
// 值班
isShowDuty: false,
dutyDetail: {},
}
},
computed: {
},
watch: {
isShow(newVal) {
if(newVal) {
this.zIndex = PopupManager.nextZIndex()
}
}
},
methods: {
uploadFile(file) {
const _this = this
let data = new FormData()
data.append('file', file)
return axios.post(this.action, data, {
'Content-type' : 'multipart/form-data',
headers: {
Authorization: `Bearer ${getToken()}`
}
}).then(res => {
if(res.status === 200) {
if(res.data.code) {
Promise.reject(res.data.msg)
return
}
this.imageId = res.data.data.id
}
})
},
outClockIn: throttle(async function() {
try {
this.loading = true
if(!(this.fileList[0]?.file || this.remark)) {
this.$message.warning('外勤打卡请拍照或者填写描述!')
throw Error('外勤打卡请拍照或者填写描述!')
}
if (this.fileList[0]?.file) {
await this.uploadFile(this.fileList[0].file)
}
const res = await sign({
location: `${this.pos.lng},${this.pos.lat}`,
address: this.pos.address,
image_id: this.imageId,
remark: this.remark
})
this.$message.success('打卡成功')
this.loading = false
this.isShow = false
this.fileList = []
await this.$refs['MonthStatics'].getData()
} catch (err) {
console.error(err)
this.loading = false
}
}, 1000, true),
clockIn: throttle(async function() {
try {
if (!this.isInUni) {
await this.getLocation()
} else {
uni.postMessage({
data: {
action: 'getLocation'
}
});
return
}
if(!this.isGetLocation) return
if(this.isOutSign) {
this.isShow = true
return
}
const res = await sign({
location: `${this.pos.lng},${this.pos.lat}`,
address: this.pos.address
})
this.$message.success('打卡成功')
await this.$refs['MonthStatics'].getData()
console.log(res)
} catch (err) {
console.error(err)
}
}, 1000, true),
isAuthPermission() {
if(!navigator.geolocation) {
this.isGetLocation = false
this.$msgbox.alert("您的浏览器不支持获取定位", "提示")
} else {
this.getLocation()
}
},
async getMyDuty () {
try {
const res = await index({
page: 1,
page_size: 999,
'filter[0][key]': 'user_id',
'filter[0][op]': 'eq',
'filter[0][value]': this.$store.state.user.adminId,
'filter[1][key]': 'date',
'filter[1][op]': 'like',
'filter[1][value]': this.$moment().format('YYYY-MM-DD'),
'filter[2][key]': 'status',
'filter[2][op]': 'eq',
'filter[2][value]': '0',
})
if (res?.data[0]) {
this.dutyDetail = res.data[0]
this.isShowDuty = true
}
} catch(err) {
console.error(err)
}
},
async startDuty () {
try {
this.dutyDetail.status = 1
await save(this.dutyDetail)
this.getMyDuty()
this.$refs['MonthStatics'].getData()
} catch (err) {
console.error(err)
}
},
async pos2Address(lat, lng) {
try {
const res = await this.$jsonp('https://apis.map.qq.com/ws/geocoder/v1/',{
location: lat + "," + lng,
key: "D5EBZ-C3BWP-HZIDG-VO6BE-P2MN5-ESFZO",
output: "jsonp"
})
if(!res.status) {
this.pos.address = res.result.address
}
} catch (err) {
console.error(err)
}
},
async getAppLocation(latitude, longitude, first = false) {
try {
this.pos.lng = longitude
this.pos.lat = latitude
this.isGetLocation = true
if(this.pos.lat && this.pos.lng && this.isGetLocation) {
await this.pos2Address(this.pos.lat, this.pos.lng)
const res = await preDistance({
location: `${this.pos.lng},${this.pos.lat}`
})
this.isOutSign = res.distance > Number(res.max_distance)
this.maxDistance = res.max_distance
this.nowDistance = res.distance
if (first) return
if(!this.isGetLocation) return
if(this.isOutSign) {
this.isShow = true
return
}
const res1 = await sign({
location: `${this.pos.lng},${this.pos.lat}`,
address: this.pos.address
})
this.$message.success('打卡成功')
await this.$refs['MonthStatics'].getData()
console.log(res1)
}
} catch (err) {
}
},
getLocation() {
return new Promise((resolve, reject) => {
if (/uni-app/.test(navigator.userAgent)) {
this.isInUni = true
uni.postMessage({
data: {
action: 'getLocation'
}
});
} else {
navigator.geolocation.getCurrentPosition((pos) => {
this.isGetLocation = true
console.log('经度', pos.coords.latitude);
console.log('纬度', pos.coords.longitude);
this.pos.lng = pos.coords.longitude
this.pos.lat = pos.coords.latitude
if(this.pos.lat && this.pos.lng && this.isGetLocation) {
this.pos2Address(this.pos.lat, this.pos.lng)
preDistance({
location: `${this.pos.lng},${this.pos.lat}`
}).then(res => {
this.isOutSign = res.distance > Number(res.max_distance)
this.maxDistance = res.max_distance
this.nowDistance = res.distance
resolve()
}).catch(err => reject(err))
}
}, (error) => {
console.log(error)
reject(error)
if (error.code) {
switch (error.code) {
case error.PERMISSION_DENIED:
this.$msgbox.confirm("需授权定位后进行打卡", "提示",{
confirmButtonText: '重试'
}).then(_ => {
this.getLocation()
})
this.isGetLocation = false
break;
case error.POSITION_UNAVAILABLE:
this.$msgbox.confirm("无法获取当前位置,请重试", "提示",{
confirmButtonText: '重试'
}).then(_ => {
this.getLocation()
})
this.isGetLocation = false
break;
case error.TIMEOUT:
this.$msgbox.confirm("获取位置超时,请重试", "提示",{
confirmButtonText: '重试'
}).then(_ => {
this.getLocation()
})
this.isGetLocation = false
break;
case error.UNKNOWN_ERROR:
this.$msgbox.confirm("获取位置错误,请重试", "提示",{
confirmButtonText: '重试'
}).then(_ => {
this.getLocation()
})
this.isGetLocation = false
break;
default:
this.$msgbox.confirm("获取位置错误,请重试", "提示",{
confirmButtonText: '重试'
}).then(_ => {
this.getLocation()
})
this.isGetLocation = false
break;
}
}
});
}
})
},
},
mounted() {
this.getMyDuty()
this.isAuthPermission()
},
created() {
window.getAppLocation = this.getAppLocation
}
}
</script>
<style scoped lang="scss">
.sign-btn {
width: 6.5rem;
height: 6.5rem;
border-radius: 100%;
border: none;
display: block;
background: var(--theme-color);
position: relative;
transition: all .2s;
margin: auto;
& > span {
font-size: 1rem;
font-weight: 600;
text-align: center;
color: #fff;
position: relative;
}
&:active {
filter: brightness(0.88);
}
&::before {
content: "";
background: var(--theme-color);
opacity: 0.45;
border-radius: 100%;
animation: scale 5s infinite linear;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
}
.sign-info {
margin-top: 20px;
display: flex;
justify-content: center;
padding: 0 10%;
.sign-statue {
line-height: 2;
text-align: center;
flex-basis: 50%;
}
.sign-log {
flex-basis: 50%;
}
}
@keyframes scale {
0%,100% {
transform: scale(1, 1);
}
50% {
transform: scale(1.1, 1.1);
}
}
@media (max-width: 992px) {
.sign-info {
display: block;
}
.sign-log {
display: flex;
justify-content: center;
margin-top: 10px;
}
}
</style>