
- 发布:2025-08-28 13:54
- 更新:2025-08-28 13:54
- 阅读:15
产品分类: uniapp/App
PC开发环境操作系统: Mac
PC开发环境操作系统版本号: 14.1 (23B2073)
HBuilderX类型: 正式
HBuilderX版本号: 4.75
手机系统: 全部
手机厂商: 华为
页面类型: vue
vue版本: vue3
打包方式: 云端
项目创建方式: HBuilderX
测试过的手机:
示例代码:
<!--
- @Author: error: error: git config user.name & please set dead value or install git && error: git config user.email & please set dead value or install git & please set dead value or install git
- @Date: 2025-07-28 08:53:56
- @Description: 考勤打卡
-->
<template>
<view class="content">
<!-- #ifdef APP -->
<view class="title-wrap" :style="{height: (StatusBar + 30) + 'px'}">
<view
class="back"
style="{ top: StatusBar + 'px',zIndex: 9 }"
@click="backs"
>
<image
src="/static/tabBar/navi_back.png"
mode="scaleToFill"
></image>
</view>
<view class="title"
style="{ top: StatusBar + 'px',zIndex: 8 }">
考勤打卡
</view>
</view>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<view class="title-wrap" :style="{height:CustomBar + 'px'}">
<view
class="back"
style="{ top: StatusBar + 10 + 'px',zIndex: 9 }"
@click="backs"
>
<image
src="/static/tabBar/navi_back.png"
mode="scaleToFill"
></image>
</view>
<view class="title"
style="{ top: StatusBar + 10 + 'px',zIndex: 8 }">
考勤打卡
</view>
</view>
<!-- #endif -->
<!-- #ifdef APP -->
<view class="content-box" :style="{ 'padding-top': StatusBar + 40 + 'px' }">
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<view class="content-box" :style="{ 'padding-top': CustomBar + 10 + 'px' }">
<!-- #endif -->
<view class="detail-title-wrap">
<view class="staticInfo-wrap">
<view class="top-wrap">
<view class="userInfo-wrap">
<u-avatar
src="userInfo.avatar || 'https://white-shark-1962.oss-cn-beijing.aliyuncs.com/prd/miniIcon/other/user-bg2.png'"
size="86rpx"></u-avatar>
<view class="name-wrap">
<view class="user-name">
{{ userInfo.userName }}
</view>
<view class="dept-name">
<u--text :lines="1" :text="userInfo.deptName"></u--text>
</view>
</view>
</view>
<view class="date-wrap">
<view class="nowrap">{{ date }}</view>
<view class="nowrap">{{ weekDay }}</view>
</view>
</view>
</view>
</view>
<view class="clock-info-wrap">
<view>
班次:{{ "白班" }} {{ '8:00-17:00' }}
</view>
<view class="time-step-wrap">
<view class="time-step">
<view class="time-label">首次打卡</view>
<view class="time-value-wrap" v-if="stepList.length > 0 && stepList[0].attendanceTime">
<up-icon name="checkmark-circle-fill" color="#4492FF" style="margin-right: 5rpx;"></up-icon>
{{ stepList[0].attendanceTime }}
<view class="time-value">{{ '已打卡' }}</view>
<view class="replacement-card" v-if="stepList[0].attendanceType == 3">补卡</view>
</view>
<view class="time-value" v-else>{{ '未打卡' }}</view>
</view>
<view class="time-step">
<view class="time-label">末次打卡</view>
<view class="time-value-wrap" v-if="stepList.length > 1 && stepList[stepList.length - 1].attendanceTime">
<up-icon name="checkmark-circle-fill" color="#4492FF"></up-icon>
{{ stepList[stepList.length - 1].attendanceTime }}
<view class="time-value">{{ '已打卡' }}</view>
<view class="replacement-card" v-if="stepList[stepList.length - 1].attendanceType == 3">补卡</view>
</view>
<view class="time-value" v-else>{{ '未打卡' }}</view>
</view>
</view>
<view class="BLE-wrap" v-if="!hasPosition || !hasCamera || !hasBLE || !openPosition || !openBLE">
<up-icon name="info-circle-fill" color="#4492FF" style="margin-right: 5rpx;"></up-icon>
<view class="tips-wrap">
<view>未开启</view>
<view style="color: #4492FF;" v-if="!hasPosition">
{{ '定位权限' }}、
</view>
<view style="color: #4492FF;" v-if="!openPosition">
{{ '定位开关' }}、
</view>
<view style="color: #4492FF;" v-if="!hasCamera">
{{ '相机权限' }}、
</view>
<view style="color: #4492FF;" v-if="!hasBLE">
{{ '蓝牙权限' }}
</view>
<view style="color: #4492FF;" v-if="!openBLE">
{{ '蓝牙开关' }}
</view>
<view>无法打卡</view>
</view>
</view>
<view class="click-btn-wrap" @click="handleClockInWithEffect">
<view
class="[
'click-btn',
canClock && !isClockInDebouncing ? '' : 'click-btn-disabled',
btnActive ? 'active' : '',
isClockInDebouncing ? 'click-btn-loading' : ''
]"
@touchstart="handleBtnDown"
@touchend="handleBtnUp"
@touchcancel="handleBtnUp"
<view>
{{ isClockInDebouncing ? '打卡中...' : (canClock ? '打卡' : '无法打卡') }}
</view>
<view>
{{ currentTime }}
</view>
</view>
<view class="canClock-tips" v-show="canClock">
已进入考勤范围
</view>
<view class="canClock-tips" v-show="!hasConnectBLE">
未搜索到正确的考勤设备
</view>
</view>
</view>
</view>
<up-modal :show="showResult" title="" confirmText="我知道了" @confirm="showResult = false">
<view class="slot-content" v-if="clockResult.status == 'success'">
<image
src="https://white-shark-1962.oss-cn-beijing.aliyuncs.com/prd/miniIcon/other/clockin-success.png"
mode="scaleToFill"
class="face-image"
/>
<view class="clockResult-message">
{{ clockResult.message }}
</view>
</view>
<view class="slot-content" v-if="clockResult.status == 'error'">
<image
src="https://white-shark-1962.oss-cn-beijing.aliyuncs.com/prd/miniIcon/other/faceclockin-error.png"
mode="scaleToFill"
class="face-image"
/>
<view class="clockResult-message">
{{ clockResult.message }}
</view>
</view>
</up-modal>
<view class="footer">
<view class="btn-wrap">
<image
src="https://white-shark-1962.oss-cn-beijing.aliyuncs.com/prd/miniIcon/other/kaoqindaka-zhong.png"
mode="scaleToFill"
/>
<view style="color: #4492FF;">
考勤打卡
</view>
</view>
<view class="btn-wrap" @click="toMe">
<image
src="https://white-shark-1962.oss-cn-beijing.aliyuncs.com/prd/miniIcon/other/wodekaoqin.png"
mode="scaleToFill"
/>
<view>
我的考勤
</view>
</view>
<view class="btn-wrap" @click="toTeam" v-if="showTeamBtn">
<image
src="https://white-shark-1962.oss-cn-beijing.aliyuncs.com/prd/miniIcon/other/tuanduikaoqin.png"
mode="scaleToFill"
/>
<view>
团队考勤
</view>
</view>
</view>
<!----水印---->
<zui-watermark />
<!-- 人脸认证弹窗 -->
<faceAuthDialog
v-model:show="faceDialogShow"
isRequired="isRequired"
@auth="handleAuth"
/>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { onLoad, onShow, onUnload, onHide, onBackPress } from '@dcloudio/uni-app'
import { requestAndroidPermission } from '@/uni_modules/x-perm-apply-instr-v2/js_sdk/index.js'
import { clockIn, checkIfNeedFaceVerifyBeforeClockIn, listBluetoothInfo, listMyRecordsByMonth } from "@/services";
import { isPointInPolygon } from "@/utils";
const StatusBar = uni.getStorageSync("StatusBar");
const CustomBar = uni.getStorageSync("CustomBar");
const showTeamBtn = ref(false);
const openBLE = ref(false);
const openPosition = ref(false);
const hasPosition = ref(false);
const hasCamera = ref(false);
const hasBLE = ref(false);
const hasConnectBLE = ref(false);
const hasPointInside = ref(true); // 不再进入页面进行电子围栏判断了,改成点击按钮的时候判断,提高按钮响应速度
// 状态数据
const statusBarHeight = ref(0)
const navigationBarHeight = ref(0)
const userInfo = ref({
avatar: '',
userName: '',
deptName: ''
})
const currentDate = ref('')
const date = ref(new Date().toISOString().split('T')[0]);
const week = ref(new Date().getDay() === 0 ? '星期日' : new Date().getDay() === 1 ? '星期一' : new Date().getDay() === 2 ? '星期二' : new Date().getDay() === 3 ? '星期三' : new Date().getDay() === 4 ? '星期四' : new Date().getDay() === 5 ? '星期五' : '星期六');
const weekDay = ref('')
const currentTime = ref('')
const firstClockIn = ref(false)
const lastClockIn = ref(false)
const firstClockTime = ref('')
const lastClockTime = ref('')
const canMakeupCard = ref(false)
const location = ref('光山白鲨针布有限公司-综合楼')
const showResult = ref(false) // 打卡结果
const devicesUUIDList = ref(['00001000-0000-1000-8000-00805F9B34FB']) //公司蓝牙网关设备类型
const deviceIdList = ref(['F1:CC:2F:F9:58:78']) // 公司蓝牙网关设备ID(MAC地址)
const deviceNameList = ref(['BS001']) // 公司蓝牙网关设备名称
const findDeviceId = ref(''); //已搜索到的第一个设备id
const firstMatchedName = ref(''); // 已搜索到的第一个设备名称
const pointList = ref([
[114.892074, 31.968128],
[114.891581, 31.972124],
[114.892622, 31.972178],
[114.893174, 31.971846],
[114.893389, 31.971227],
[114.894810, 31.971059],
[114.895320, 31.968788]
]); //园区电子围栏坐标
const currentLongitude = ref('') // 实时经纬度
const currentLatitude = ref('') // 实时经纬度
const clockResult = ref({
status: 'success',
message: '打卡成功',
})
const hasFaceImg = ref(false) // 是否已经录入人脸
const faceDialogShow = ref(false) // 人脸认证弹窗
const isRequired = ref(true) // 是否强制认证
const checkNeedFace = ref(false) // 是否需要人脸验证
const isFaceVerify = ref(false) // 是否人脸验证入参
const isClockInDebouncing = ref(false) // 打卡防抖状态
const timer = ref(null)
const pointInsidetimmer = ref(null)
const permissiontimmer = ref(null)
const permissiontimmer2 = ref(null)
const systemInfo = ref(null)
const currentYearMonth = ref(new Date().getFullYear() + "-" + (new Date().getMonth() + 1).toString().padStart(2, '0'));
const stepList = ref([]) //打卡记录
const canClock = computed(() => {
// console.log("canClock", hasPosition.value , hasCamera.value , hasBLE.value , hasConnectBLE.value, hasPointInside.value, openBLE.value, openPosition.value)
return hasPosition.value && hasCamera.value && hasBLE.value && hasConnectBLE.value && hasPointInside.value && openBLE.value && openPosition.value
})
// 添加权限状态管理
const permissionStatus = ref({
camera: 'unknown', // 'unknown', 'granted', 'denied', 'permanently_denied'
location: 'unknown',
bluetooth: 'unknown'
})
const permissionRequested = ref({
camera: false,
location: false,
bluetooth: false
})
const lastPermissionCheckTime = ref(0) // 上次权限检查时间
const permissionCheckInterval = 30000 // 权限检查间隔30秒
// 添加定位相关的状态变量
const lastLocationTime = ref(0) // 上次定位时间
const locationCacheTime = 30000 // 定位缓存时间30秒
const isLocating = ref(false) // 是否正在定位中
// 获取权限 - 优化版本
const getPermission = () => {
// 如果所有权限都已获取,直接返回
if (hasBLE.value && hasCamera.value && hasPosition.value) {
return
}
// 检查权限检查间隔,避免过于频繁
const now = Date.now()
if (now - lastPermissionCheckTime.value < permissionCheckInterval) {
return
}
lastPermissionCheckTime.value = now
// console.log('获取权限-----', hasBLE.value, hasCamera.value, hasPosition.value)
// #ifdef MP-WEIXIN
// 检测蓝牙权限
uni.getSetting({
withSubscriptions: true,
success: (res) => {
// console.log("resgetSetting--", res)
// 检查位置权限
if (!res.authSetting['scope.userLocation'] && !permissionRequested.value.location) {
permissionRequested.value.location = true
uni.authorize({
scope: 'scope.userLocation',
success: (success) => {
hasPosition.value = true
permissionStatus.value.location = 'granted'
},
fail: (fail) => {
hasPosition.value = false
permissionStatus.value.location = 'denied'
},
})
} else if (res.authSetting['scope.userLocation']) {
hasPosition.value = true
permissionStatus.value.location = 'granted'
}
// 检查蓝牙权限
if (!res.authSetting['scope.bluetooth'] && !permissionRequested.value.bluetooth) {
permissionRequested.value.bluetooth = true
uni.openBluetoothAdapter({
success: (success) => {
hasBLE.value = true
permissionStatus.value.bluetooth = 'granted'
},
fail: (fail) => {
hasBLE.value = false
permissionStatus.value.bluetooth = 'denied'
},
})
} else if (res.authSetting['scope.bluetooth']) {
hasBLE.value = true
permissionStatus.value.bluetooth = 'granted'
}
// 检查相机权限
if (!res.authSetting['scope.camera'] && !permissionRequested.value.camera) {
permissionRequested.value.camera = true
uni.authorize({
scope: 'scope.camera',
success: (success) => {
hasCamera.value = true
permissionStatus.value.camera = 'granted'
},
fail: (fail) => {
hasCamera.value = false
permissionStatus.value.camera = 'denied'
},
})
} else if (res.authSetting['scope.camera']) {
hasCamera.value = true
permissionStatus.value.camera = 'granted'
}
}
})
// #endif
// #ifdef APP
// 相机权限
if (!permissionRequested.value.camera && !hasCamera.value) {
permissionRequested.value.camera = true
requestAndroidPermission('android.permission.CAMERA', {
title: '相机权限申请说明',
content: '应用需要访问您的相机,以便拍照签到'
}).then(status => {
// console.log('相机权限status', status);
if (status == 1) {
hasCamera.value = true
permissionStatus.value.camera = 'granted'
} else if (status == 0) {
hasCamera.value = false
permissionStatus.value.camera = 'denied'
} else if (status == -1) {
hasCamera.value = false
permissionStatus.value.camera = 'permanently_denied'
}
})
}
// 位置权限
if (!permissionRequested.value.location && !hasPosition.value) {
permissionRequested.value.location = true
requestAndroidPermission('android.permission.ACCESS_FINE_LOCATION', {
title: '精确定位权限申请说明',
content: '应用需要获取您的精确位置信息,以便进行打卡签到'
}).then(status => {
// console.log('位置权限status', status);
if (status == 1) {
hasPosition.value = true
permissionStatus.value.location = 'granted'
} else if (status == 0) {
hasPosition.value = false
permissionStatus.value.location = 'denied'
} else if (status == -1) {
hasPosition.value = false
permissionStatus.value.location = 'permanently_denied'
}
})
}
// 蓝牙权限
if (!permissionRequested.value.bluetooth && !hasBLE.value) {
permissionRequested.value.bluetooth = true
requestAndroidPermission('android.permission.ACCESS_FINE_LOCATION', {
title: '蓝牙权限申请说明',
content: '应用需要访问蓝牙功能,以便进行打卡签到'
}).then(status => {
// console.log('蓝牙权限status', status);
if (status == 1) {
hasBLE.value = true
permissionStatus.value.bluetooth = 'granted'
} else if (status == 0) {
hasBLE.value = false
permissionStatus.value.bluetooth = 'denied'
} else if (status == -1) {
hasBLE.value = false
permissionStatus.value.bluetooth = 'permanently_denied'
}
})
}
// #endif
}
// 检查权限状态,只在必要时申请
const checkPermissionStatus = () => {
// 如果所有权限都已获取,不需要检查
if (hasBLE.value && hasCamera.value && hasPosition.value) {
return
}
// 检查是否有权限被永久拒绝,如果有则提示用户手动开启
const hasPermanentlyDenied = Object.values(permissionStatus.value).some(status => status === 'permanently_denied')
if (hasPermanentlyDenied) {
// 权限被永久拒绝,提示用户手动开启
uni.showModal({
title: '权限提示',
content: '部分权限被拒绝,请在系统设置中手动开启相机、位置和蓝牙权限',
showCancel: false,
success: () => {
// 可以引导用户到设置页面
// #ifdef APP-PLUS
if (systemInfo.value.platform === 'android') {
const Intent = plus.android.importClass('android.content.Intent');
const Settings = plus.android.importClass('android.provider.Settings');
const Uri = plus.android.importClass('android.net.Uri');
const main = plus.android.runtimeMainActivity();
const intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
const uri = Uri.fromParts('package', main.getPackageName(), null);
intent.setData(uri);
main.startActivity(intent);
}
// #endif
}
})
return
}
// 检查是否有权限被拒绝但可以重新申请
const canRetry = Object.values(permissionStatus.value).some(status => status === 'denied')
if (canRetry) {
// 重置被拒绝的权限状态,允许重新申请
Object.keys(permissionStatus.value).forEach(key => {
if (permissionStatus.value[key] === 'denied') {
permissionStatus.value[key] = 'unknown'
permissionRequested.value[key] = false
}
})
}
// 申请权限
getPermission()
}
// 添加手动权限检查方法
const manualCheckPermission = () => {
// 重置所有权限状态,强制重新检查
Object.keys(permissionStatus.value).forEach(key => {
permissionStatus.value[key] = 'unknown'
permissionRequested.value[key] = false
})
// 重置权限状态
hasBLE.value = false
hasCamera.value = false
hasPosition.value = false
// 重新检查权限
checkPermissionStatus()
}
// 初始化系统信息
onLoad(async () => {
showTeamBtn.value = Boolean(uni.getStorageSync('mini_attendance_team') || '');
userInfo.value = uni.getStorageSync("userInfo");
systemInfo.value = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight
navigationBarHeight.value = statusBarHeight.value + 44
initDateTime()
startTimeUpdate()
// 获取所有蓝牙网关信息
await listBluetoothInfoFn()
// 初始化权限检查
checkPermissionStatus()
// 权限检查定时器 - 降低频率,避免频繁弹框
permissiontimmer.value = setInterval(() => {
// 只在权限不完整时才检查
if (!hasBLE.value || !hasCamera.value || !hasPosition.value) {
checkPermissionStatus()
}
}, 30000); // 改为30秒检查一次,大幅减少弹框频率
// 检测是否需要人脸识别
await checkIfNeedFaceVerifyBeforeClockInFn()
// 判断当前运行环境是iosapp、androidapp、微信小程序
// #ifdef MP-WEIXIN
hasConnectBLE.value = false
// #endif
// #ifdef APP
if (systemInfo.platform == "android") {
hasConnectBLE.value = false
}
// #endif
// 立即检查一次位置
checkLocation()
uni.removeStorageSync('bleSearching')
// 只在没有连接蓝牙设备时启动搜索
if (!hasConnectBLE.value) {
startBLEFn()
}
permissiontimmer2.value = setInterval(() => {
// 只在没有连接蓝牙设备时重新搜索
if (!hasConnectBLE.value && !uni.getStorageSync('bleSearching')) {
startBLEFn()
}
}, 5000); // 改为10秒检查一次,减少蓝牙搜索频率
// 单独设置定位检测定时器,频率更低
pointInsidetimmer.value = setInterval(() => {
// 检测电子围栏 - 降低频率
if (!isLocating.value) {
checkLocation()
}
}, 10000); // 改为15秒检查一次,减少定位频率
getMyAttendanceFn() // 获取我的考勤数据
})
onShow(async () => {
isFaceVerify.value = uni.getStorageSync('faceCheckStatus')
if (isFaceVerify.value && isFaceVerify.value == 1) {
await handleClockIn()
// 人脸打卡成功
// clockResult.value = {
// status: 'success',
// message: '打卡成功'
// }
// showResult.value = true
uni.removeStorageSync('faceCheckStatus')
// 获取打卡信息
getMyAttendanceFn()
} else if(isFaceVerify.value && isFaceVerify.value == 2) {
// 人脸打卡失败
clockResult.value = {
status: 'error',
message: '人脸打卡失败'
}
showResult.value = true
uni.removeStorageSync('faceCheckStatus')
}
hasFaceImg.value = await hasFacePromise()
// 获取所有蓝牙网关信息
await listBluetoothInfoFn()
// getPermission() // 移除此行,权限检查已移至onLoad
// permissiontimmer.value = setInterval(() => { // 移除此行,权限检查已移至onLoad
// // 获取权限
// getPermission()
// }, 3000);
// 检测是否需要人脸识别
await checkIfNeedFaceVerifyBeforeClockInFn()
// 判断当前运行环境是iosapp、androidapp、微信小程序
// #ifdef MP-WEIXIN
hasConnectBLE.value = false
// #endif
// #ifdef APP
if (systemInfo.platform == "android") {
hasConnectBLE.value = false
}
// #endif
// // 检测电子围栏
// checkLocation()
uni.removeStorageSync('bleSearching')
// 只在没有连接蓝牙设备时启动搜索
if (!hasConnectBLE.value) {
startBLEFn()
}
permissiontimmer2.value = setInterval(() => {
// // 检测电子围栏
// checkLocation()
// 只在没有连接蓝牙设备时重新搜索
if (!hasConnectBLE.value && !uni.getStorageSync('bleSearching')) {
startBLEFn()
}
}, 3000); // 改为5秒检查一次,减少频率
getMyAttendanceFn() // 获取我的考勤数据
})
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value)
}
clearInterval(pointInsidetimmer.value)
clearInterval(permissiontimmer.value)
clearInterval(permissiontimmer2.value)
// #ifdef MP-WEIXIN
wx.stopBluetoothDevicesDiscovery()
// #endif
// #ifdef APP
uni.stopBluetoothDevicesDiscovery()
// #endif
uni.closeBluetoothAdapter()
uni.removeStorageSync('faceCheckVerify')
})
onUnload(() => {
if (timer.value) {
clearInterval(timer.value)
}
clearInterval(pointInsidetimmer.value)
clearInterval(permissiontimmer.value)
clearInterval(permissiontimmer2.value)
// #ifdef MP-WEIXIN
wx.stopBluetoothDevicesDiscovery()
// #endif
// #ifdef APP
uni.stopBluetoothDevicesDiscovery()
// #endif
uni.removeStorageSync('faceCheckVerify')
})
onBackPress((event) => {
// console.log("AAAAA", event)
if (event.from === 'backbutton') {
backs()
return true; // 阻止默认返回行为
}
return false;
})
const backs = () => {
// #ifdef MP-WEIXIN
uni.navigateBack({
delta: 1,
});
// #endif
// #ifdef APP
plus.runtime.restart();
// #endif
}
// 获取我的考勤
const getMyAttendanceFn = async () => {
let params = {
yearMonth: currentYearMonth.value,
};
console.log("params", params)
const { code, data } = await listMyRecordsByMonth(params);
console.log("data", data)
if (code == 200 && data.length > 0) {
stepList.value = data.find(el => el.date == currentDate.value).attendanceData || []
} else {
stepList.value = [];
}
};
// 获取蓝牙设备信息
const listBluetoothInfoFn = async () => {
const {code, data} = await listBluetoothInfo()
// console.log("所有蓝牙设备列表", data)
if (code === 200 && data) {
deviceIdList.value = data.map(item => item.mac)
deviceNameList.value = data.map(el => el.name)
}
}
// 检测员工打卡前是否需要人脸识别
const checkIfNeedFaceVerifyBeforeClockInFn = async () => {
const {code, data} = await checkIfNeedFaceVerifyBeforeClockIn()
if (code == 200 && data) {
checkNeedFace.value = true
} else {
checkNeedFace.value = false
}
}
// 检测是否在电子围栏内 - 优化版本
const checkLocation = () => {
if (!hasPosition.value) {
return false
}
// 如果正在定位中,避免重复调用
if (isLocating.value) {
return false
}
// 检查定位缓存,如果距离上次定位时间不足30秒,直接使用缓存
const now = Date.now()
if (now - lastLocationTime.value < locationCacheTime &&
currentLongitude.value && currentLatitude.value) {
// console.log("使用定位缓存,避免重复定位")
hasPointInside.value = isPointInPolygon([currentLongitude.value, currentLatitude.value], pointList.value)
return
}
isLocating.value = true
// 设置定位超时
const locationTimeout = setTimeout(() => {
isLocating.value = false
// console.log("定位超时")
}, 8000) // 8秒超时
let plant = ''
// #ifdef APP
plant = systemInfo.value.platform
// #endif
uni.getLocation({
type: 'gcj02',
isHighAccuracy: plant == 'ios' ? false : true, // 改为false,提高定位速度
success: (success) => {
openPosition.value = true
clearTimeout(locationTimeout)
isLocating.value = false
// console.log("当前经纬度", success)
currentLongitude.value = success.longitude
currentLatitude.value = success.latitude
lastLocationTime.value = now
hasPointInside.value = isPointInPolygon([success.longitude, success.latitude], pointList.value)
// console.log("是否在园区内", hasPointInside.value)
// console.log("开启GPS", hasPosition.value)
// console.log("开启相机", hasCamera.value)
// console.log("hasBLE", hasBLE.value)
// console.log("hasConnectBLE", hasConnectBLE.value)
},
fail: (error) => {
openPosition.value = false
clearTimeout(locationTimeout)
isLocating.value = false
// console.log("定位失败", error)
// 如果定位失败,尝试使用缓存的位置信息
if (currentLongitude.value && currentLatitude.value) {
// console.log("定位失败,使用缓存位置信息")
// hasPointInside.value = isPointInPolygon([currentLongitude.value, currentLatitude.value], pointList.value)
}
}
})
}
// 初始化日期时间
const initDateTime = () => {
const date = new Date()
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const weeks = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
currentDate.value = ${year}-${month}-${day}
weekDay.value = weeks[date.getDay()]
updateCurrentTime()
}
// 更新当前时间
const updateCurrentTime = () => {
const date = new 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')
currentTime.value = ${hours}:${minutes}:${seconds}
return ${year}-${month}-${day} ${hours}:${minutes}:${seconds}
}
// 开始时间更新
const startTimeUpdate = () => {
timer.value = setInterval(updateCurrentTime, 1000)
}
// 处理打卡
const handleClockIn = async () => {
if (!canClock) return
try {
uni.showLoading({
title: '打卡中...'
})
let params = {
clockInTime: updateCurrentTime(),
longitude: currentLongitude.value,
latitude: currentLatitude.value,
platform: systemInfo.value.platform,
bluetoothData: {
mac: findDeviceId.value || undefined,
name: firstMatchedName.value || undefined,
},
location: undefined,
isFaceVerify: isFaceVerify.value,
}
console.log('提交打卡params', params)
const {code, data} = await clockIn(params)
uni.hideLoading()
if (code == 200) {
// 更新考勤记录
await getMyAttendanceFn()
clockResult.value = {
status: 'success',
message: '打卡成功',
}
showResult.value = true
}
} catch (error) {
uni.hideLoading()
}
}
// 补卡处理
const handleMakeup = () => {
uni.showToast({
title: '补卡申请已提交',
icon: 'none'
})
}
const toTeam = () => {
uni.redirectTo({
url: '/pagesAttendance/teamAttendance/index'
})
}
const toMe = () => {
uni.redirectTo({
url: '/pagesAttendance/myAttendance/index'
})
}
const btnActive = ref(false)
const handleBtnDown = () => {
if (!canClock.value) return
btnActive.value = true
}
const handleBtnUp = () => {
if (!canClock.value) return
btnActive.value = false
}
const handleClockInWithEffect = async () => {
// 防抖处理:如果正在打卡中,直接返回
if (isClockInDebouncing.value) {
// console.log('打卡操作正在进行中,请勿重复点击')
return
}
// 设置防抖状态
isClockInDebouncing.value = true
try {
getPermission()
// 判断是否已经录入人脸
if (!hasFaceImg.value) {
// if (true) {
faceDialogShow.value = true
return
}
// 判断是否允许打卡
if (!canClock.value) return
// 判断是否需要人脸验证
if (checkNeedFace.value) {
// if (true) {
// 人脸验证
let backUrl = '/pagesAttendance/clockIn/index'
// #ifdef MP-WEIXIN
uni.navigateTo({
url: /pagesFaceCheck/faceCheckVerify/wechatPageFaceVerify?backUrl=${backUrl}
})
// #endif
// #ifdef APP
let plant = systemInfo.value.platform
if (plant == "ios") {
uni.navigateTo({
url: /pagesFaceCheck/faceCheckVerify/iosFaceVerify?backUrl=${backUrl}
})
} else {
uni.navigateTo({
url: /pagesFaceCheck/faceCheckVerify/androidFaceVerify?backUrl=${backUrl}
})
}
// #endif
} else {
handleBtnDown()
await handleClockIn()
setTimeout(handleBtnUp, 150)
}
} finally {
// 延迟重置防抖状态,确保操作完成
setTimeout(() => {
isClockInDebouncing.value = false
}, 1000) // 1秒防抖时间
}
}
// 判断是否已经录入人脸
const hasFacePromise = () => {
return new Promise((resolve, reject) => {
if (userInfo.value.enableFaceVerify == 1) {
resolve(true)
} else {
resolve(false)
}
})
}
// 前往录入人脸
const handleAuth = () => {
// 判断是微信小程序还是app平台
let backUrl = '/pagesAttendance/clockIn/index'
uni.navigateTo({
url: /pagesFaceCheck/faceCheckStepOne/wechatPageRegister?backUrl=${backUrl}
})
// // #ifdef APP
// let plant = systemInfo.value.platform
// if (plant == "ios") {
// uni.navigateTo({
// url: /pagesFaceCheck/faceCheckStepOne/indexIOSRegister?backUrl=${backUrl}
// })
// } else {
// uni.navigateTo({
// url: /pagesFaceCheck/faceCheckStepOne/indexAndroidRegister?backUrl=${backUrl}
// })
// }
// // #endif
}
const startBLEFn = () => {
if (!hasBLE.value || hasConnectBLE.value) {
return false
}
// 如果已经在搜索中,不要重复启动
if (uni.getStorageSync('bleSearching')) {
return false
}
uni.openBluetoothAdapter({
success: () => {
// console.log('蓝牙打开了');
openBLE.value = true
// GPS定位检测
checkOpenGPSServiceByAndroidIOS()
// 标记开始搜索
uni.setStorageSync('bleSearching', true)
// console.log('调用蓝牙设备搜索');
uni.startBluetoothDevicesDiscovery({
services: devicesUUIDList.value,
// 安卓和微信小程序使用false,ios使用true
// allowDuplicatesKey: systemInfo.value.platform === 'ios' ? true : false,
allowDuplicatesKey: true,
success: (res) => {
// console.log('开始搜索蓝牙设备', res);
// 设置搜索超时,避免无限搜索
const searchTimeout = setTimeout(() => {
if (!hasConnectBLE.value) {
// console.log('蓝牙搜索超时,停止搜索');
// #ifdef MP-WEIXIN
wx.stopBluetoothDevicesDiscovery()
// #endif
// #ifdef APP
uni.stopBluetoothDevicesDiscovery()
// #endif
uni.setStorageSync('bleSearching', false)
}
}, 10000) // 10秒超时
uni.onBluetoothDeviceFound(async (devicesObj) => {
console.log('搜索到蓝牙设备---', devicesObj.devices[0]);
let plantName = systemInfo.value.platform
if(plantName == 'android') {
const deviceIds = new Set(devicesObj.devices.map(device => device.deviceId));
const deviceLocalNames = new Set(devicesObj.devices.map(device => device.localName));
// 检查是否找到目标设备
const foundTargetDevice = deviceIdList.value.some(id => deviceIds.has(id)) ||
deviceNameList.value.some(name => deviceLocalNames.has(name));
if (foundTargetDevice) {
hasConnectBLE.value = true;
findDeviceId.value = deviceIdList.value.find(id => deviceIds.has(id)) || '';
firstMatchedName.value = deviceNameList.value.find(name => deviceLocalNames.has(name)) || '';
// 找到设备后立即停止搜索
// console.log('找到目标蓝牙设备,停止搜索');
// #ifdef MP-WEIXIN
wx.stopBluetoothDevicesDiscovery()
// #endif
// #ifdef APP
uni.stopBluetoothDevicesDiscovery()
// #endif
uni.setStorageSync('bleSearching', false)
clearTimeout(searchTimeout)
// // 显示成功提示
// uni.showToast({
// title: '蓝牙设备连接成功',
// icon: 'success',
// duration: 2000
// })
}
} else {
// iOS设备处理
const deviceLocalNames = new Set(devicesObj.devices.map(device => device.localName));
const foundTargetDevice = deviceNameList.value.some(name => deviceLocalNames.has(name));
if (foundTargetDevice) {
hasConnectBLE.value = true;
firstMatchedName.value = deviceNameList.value.find(name => deviceLocalNames.has(name)) || '';
// 找到设备后立即停止搜索
// console.log('找到目标蓝牙设备,停止搜索');
uni.stopBluetoothDevicesDiscovery()
uni.setStorageSync('bleSearching', false)
clearTimeout(searchTimeout)
// // 显示成功提示
// uni.showToast({
// title: '蓝牙设备连接成功',
// icon: 'success',
// duration: 2000
// })
}
}
});
},
fail: () => {
// console.log('开始搜索蓝牙设备失败');
uni.setStorageSync('bleSearching', false)
}
})
},
fail: res => {
// console.log('蓝牙失败:', res);
openBLE.value = false
uni.setStorageSync('bleSearching', false)
}
});
}
// 获取GPS定位服务
const checkOpenGPSServiceByAndroidIOS = () => {
// #ifdef MP-WEIXIN
if (systemInfo.value.hostName == 'WeChat') {
//2、判断微信小程序是否授权位置信息
uni.getSetting({
success(res) {
let scopeUserLocation = res.authSetting["scope.userLocation"];
if (scopeUserLocation) {
// 微信小程序已授权位置信息
} else {
// 微信小程序未授权位置信息
// uni.showModal({
// title: '提示',
// content: '请允许使用位置信息',
// showCancel: false, // 不显示取消按钮
// success() {
// uni.navigateBack({
// delta: 1, //返回层数,2则上上页
// })
// }
// })
}
},
fail() {
// 微信小程序未授权位置信息
uni.showModal({
title: '提示',
content: '获取位置信息失败',
showCancel: false, // 不显示取消按钮
success() {
uni.navigateBack({
delta: 1, //返回层数,2则上上页
})
}
})
}
});
}
// #endif
// #ifdef APP-PLUS
if (systemInfo.value.platform === 'android') { // 判断平台
var context = plus.android.importClass("android.content.Context");
var locationManager = plus.android.importClass("android.location.LocationManager");
var main = plus.android.runtimeMainActivity();
var mainSvr = main.getSystemService(context.LOCATION_SERVICE);
if (!mainSvr.isProviderEnabled(locationManager.GPS_PROVIDER)) {
hasPosition.value = false
uni.showModal({
title: '提示',
content: '请打开定位服务功能',
showCancel: false, // 不显示取消按钮
success() {
if (!mainSvr.isProviderEnabled(locationManager.GPS_PROVIDER)) {
var Intent = plus.android.importClass('android.content.Intent');
var Settings = plus.android.importClass('android.provider.Settings');
var intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
main.startActivity(intent); // 打开系统设置GPS服务页面
} else {
// console.log('GPS功能已开启');
}
}
});
} else {
hasPosition.value = true
}
} else if (systemInfo.value.platform === 'ios') {
// console.log("苹果");
var cllocationManger = plus.ios.import("CLLocationManager");
var enable = cllocationManger.locationServicesEnabled();
var status = cllocationManger.authorizationStatus();
plus.ios.deleteObject(cllocationManger);
if (enable && status != 2) {
// console.log("手机系统的定位已经打开");
hasPosition.value = true
} else {
hasPosition.value = false
// console.log("手机系统的定位没有打开");
uni.showModal({
title: '提示',
content: '请前往设置-隐私-定位服务打开定位服务功能',
showCancel: false, // 不显示取消按钮
success() {
var UIApplication = plus.ios.import("UIApplication");
var application2 = UIApplication.sharedApplication();
var NSURL2 = plus.ios.import("NSURL");
// var setting2 = NSURL2.URLWithString("prefs:root=LOCATION_SERVICES");
// var setting2 = NSURL2.URLWithString("App-Prefs:root=LOCATION_SERVICES");
var setting2 = NSURL2.URLWithString("app-settings:");
//var setting2 = NSURL2.URLWithString("App-Prefs:root=Privacy&path=LOCATION");
// var setting2 = NSURL2.URLWithString("App-Prefs:root=Privacy&path=LOCATION_SERVICES");
application2.openURL(setting2);
plus.ios.deleteObject(setting2);
plus.ios.deleteObject(NSURL2);
plus.ios.deleteObject(application2);
}
});
}
}
// #endif
}
</script>
<style lang="scss" scoped>
.content {
position: relative;
}
.title-wrap {
z-index: 9;
width: 100%;
height: 480rpx;
position: fixed;
top: 0;
left: 0;
background-image: url("https://white-shark-1962.oss-cn-beijing.aliyuncs.com/prd/miniIcon/other/approve-detailHeader-bg.png");
background-repeat: no-repeat;
background-size: 100% auto;
}
.back {
position: absolute;
left: 30rpx;
width: 56rpx;
image {
display: block;
width: 30rpx;
height: 38rpx;
margin: 0 auto;
}
.toe {
width: 56rpx;
height: 56rpx;
image {
width: 56rpx;
height: 56rpx;
display: block;
}
}
}
.title {
position: fixed;
display: flex;
justify-content: center;
font-size: 36rpx;
font-weight: 500;
color: #333333;
width: 100%;
color: #fff;
}
.content-box {
background-image: url("https://white-shark-1962.oss-cn-beijing.aliyuncs.com/prd/miniIcon/other/approve-detailHeader-bg.png");
background-repeat: no-repeat;
background-size: 100% auto;
width: 100%;
height: 100vh;
overflow-y: auto;
padding-bottom: 130rpx;
display: flex;
flex-direction: column;
.detail-title-wrap {
// padding: 0 30rpx;
width: calc(100vw - 60rpx);
margin: 0 30rpx;
background-color: #F6F6F6FF;
position: relative;
border-radius: 16rpx;
.staticInfo-wrap {
background-color: #FFFFFFD9;
border-radius: 16rpx;
.top-wrap {
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 16rpx 16rpx 0rpx 0rpx;
padding: 20rpx 30rpx 10rpx 30rpx;
background-color: #e2f0ff;
.userInfo-wrap {
display: flex;
align-items: center;
.name-wrap {
display: flex;
flex-direction: column;
margin-left: 20rpx;
.user-name {
font-weight: 500;
font-size: 32rpx;
color: #333333;
}
.dept-name {
font-weight: 400;
font-size: 28rpx;
color: #666666;
}
}
}
.date-wrap {
display: flex;
align-items: center;
font-weight: 400;
font-size: 28rpx;
color: #666666;
.nowrap {
white-space: nowrap;
}
}
}
.static-card {
background-color: #fff;
border-radius: 16rpx;
padding: 20rpx 30rpx;
.static-list {
display: flex;
padding-top: 20rpx;
margin-top: 20rpx;
border-top: 1px solid #00000014;
.static-item {
width: 33.33%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.static-item-value {
color: #333333FF;
}
.static-item-title {
color: #999999FF;
}
}
.border-right {
border-right: 1px solid #00000014;
}
}
}
}
.calendar-wrap {
background-color: #fff;
padding: 20rpx;
border-radius: 16rpx 16rpx 16rpx 16rpx;
// margin-top: 20rpx;
.steps-wrap {
margin-top: 20rpx;
.steps-item {
display: flex;
.step-left-wrap {
width: 50rpx;
height: 120rpx;
display: flex;
flex-direction: column;
align-items: center;
.step-line {
flex: 1;
width: 2rpx;
height: 100%;
background: #E6E6E6;
}
}
.step-right-wrap {
padding-left: 20rpx;
display: flex;
flex-direction: column;
.step-time-wrap {
display: flex;
align-items: center;
font-weight: 500;
font-size: 32rpx;
color: #333333;
margin-bottom: 10rpx;
}
.step-address-wrap {
display: flex;
align-items: center;
font-weight: 400;
font-size: 28rpx;
color: #999999;
.apply-btn {
width: 132rpx;
height: 50rpx;
border-radius: 8rpx 8rpx 8rpx 8rpx;
border: 2rpx solid #999999;
color: #333333FF;
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
}
}
}
}
.footer {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
display: flex;
align-items: center;
justify-content: space-around;
font-size: 26rpx;
color: #333;
background: #fff;
padding: 16rpx 20rpx 20rpx;
// padding-bottom: constant(safe-area-inset-bottom);
// padding-bottom: env(safe-area-inset-bottom);
box-sizing: border-box;
.btn-wrap {
font-family: PingFang SC, PingFang SC;
font-weight: 400;
font-size: 26rpx;
color: #666666;
display: flex;
flex-direction: column;
align-items: center;
width: 112rpx;
// height: 74rpx;
image {
width: 32rpx;
height: 32rpx;
margin-bottom: 10rpx;
}
}
}
.clock-info-wrap {
margin: 0 30rpx;
padding: 30rpx;
background-color: #fff;
border-radius: 0rpx 0rpx 8rpx 8rpx;
flex: 1;
}
.time-step-wrap {
display: flex;
align-items: center;
justify-content: space-between;
margin: 20rpx 0;
.time-step {
display: flex;
flex-direction: column;
width: 48%;
background-color: #F6F8FE;
padding: 16rpx 20rpx;
border-radius: 8rpx 8rpx 8rpx 8rpx;
.time-label {
font-weight: 500;
font-size: 32rpx;
color: #3D3D3D;
}
.time-value-wrap {
display: flex;
color: #999999;
font-size: 28rpx;
.time-value {
font-weight: 400;
font-size: 28rpx;
color: #999999;
margin-right: 5rpx;
white-space: nowrap;
}
.replacement-card {
font-size: 28rpx;
color: #FFFFFF;
width: 76rpx;
height: 36rpx;
background: #00B578;
border-radius: 4rpx 4rpx 4rpx 4rpx;
text-align: center;
white-space: nowrap;
}
}
}
}
.click-btn-wrap {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-top: 200rpx;
}
.click-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-weight: 500;
font-size: 48rpx;
color: #FFFFFF;
background-color: #4492FF;
width: 270rpx;
height: 270rpx;
border-radius: 50%;
margin-bottom: 50rpx;
position: relative;
overflow: hidden;
}
/ 只有非禁用状态才有光晕效果 /
.click-btn:not(.click-btn-disabled) {
box-shadow: 0 0 20rpx rgba(68, 146, 255, 0.3);
animation: pulse 2s infinite;
}
.click-btn:not(.click-btn-disabled)::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
background: radial-gradient(circle, rgba(68, 146, 255, 0.2) 0%, transparent 70%);
transform: translate(-50%, -50%);
animation: ripple 3s infinite;
}
.click-btn:not(.click-btn-disabled)::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 120%;
height: 120%;
background: radial-gradient(circle, rgba(68, 146, 255, 0.1) 0%, transparent 70%);
transform: translate(-50%, -50%);
animation: ripple 3s infinite 1.5s;
}
.click-btn-disabled {
background-color: #CDCDCD !important;
/ 禁用状态下移除所有光晕效果 /
box-shadow: none !important;
}
.click-btn.active {
transform: scale(0.95);
transition: transform 0.1s;
}
.click-btn-loading {
background-color: #A0C4FF !important;
cursor: not-allowed;
position: relative;
overflow: hidden;
}
.click-btn-loading::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
animation: loading-shimmer 1.5s infinite;
}
@keyframes loading-shimmer {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
.canClock-tips {
font-weight: 400;
font-size: 28rpx;
color: #999999;
}
.BLE-wrap {
display: flex;
align-items: center;
padding: 20rpx;
margin: 30rpx 0;
background-color: #E3EFFF;
.tips-wrap {
display: flex;
align-items: center;
flex-wrap: wrap;
font-size: 28rpx;
font-weight: 400;
view {
white-space: nowrap;
}
}
}
.slot-content {
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
padding: 100rpx 0;
.face-image {
width: 210rpx;
height: 210rpx;
}
.clockResult-message {
font-weight: 500;
font-size: 48rpx;
color: #3D3D3D;
}
}
/ 光晕动画关键帧 /
@keyframes pulse {
0% {
box-shadow: 0 0 20rpx rgba(68, 146, 255, 0.3);
}
50% {
box-shadow: 0 0 40rpx rgba(68, 146, 255, 0.6);
}
100% {
box-shadow: 0 0 20rpx rgba(68, 146, 255, 0.3);
}
}
@keyframes ripple {
0% {
transform: translate(-50%, -50%) scale(0.8);
opacity: 1;
}
100% {
transform: translate(-50%, -50%) scale(2);
opacity: 0;
}
}
</style>
<!--
- @Author: error: error: git config user.name & please set dead value or install git && error: git config user.email & please set dead value or install git & please set dead value or install git
- @Date: 2025-07-28 08:53:56
- @Description: 考勤打卡
-->
>
<image
src="/static/tabBar/navi_back.png"
mode="scaleToFill"
></image>
</view>
<view class="title" style="{ top: StatusBar + 'px',zIndex: 8 }"> 考勤打卡
</view>
</view>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<view class="title-wrap" :style="{height:CustomBar + 'px'}">
<view
class="back" style="{ top: StatusBar + 10 + 'px',zIndex: 9 }" @click="backs"
>
<image
src="/static/tabBar/navi_back.png"
mode="scaleToFill"
></image>
</view>
<view class="title"
style="{ top: StatusBar + 10 + 'px',zIndex: 8 }">
考勤打卡
</view>
</view>
<!-- #endif -->
<!-- #ifdef APP -->
<view class="content-box" :style="{ 'padding-top': StatusBar + 40 + 'px' }">
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<view class="content-box" :style="{ 'padding-top': CustomBar + 10 + 'px' }">
<!-- #endif -->
<view class="detail-title-wrap">
<view class="staticInfo-wrap">
<view class="top-wrap">
<view class="userInfo-wrap">
<u-avatar
src="userInfo.avatar || 'https://white-shark-1962.oss-cn-beijing.aliyuncs.com/prd/miniIcon/other/user-bg2.png'"
size="86rpx"></u-avatar>
<view class="name-wrap">
<view class="user-name">
{{ userInfo.userName }}
</view>
<view class="dept-name">
<u--text :lines="1" :text="userInfo.deptName"></u--text>
</view>
</view>
</view>
<view class="date-wrap">
<view class="nowrap">{{ date }}</view>
<view class="nowrap">{{ weekDay }}</view>
</view>
</view>
</view>
</view>
<view class="clock-info-wrap">
<view>
班次:{{ "白班" }} {{ '8:00-17:00' }}
</view>
<view class="time-step-wrap">
<view class="time-step">
<view class="time-label">首次打卡</view>
<view class="time-value-wrap" v-if="stepList.length > 0 && stepList[0].attendanceTime">
<up-icon name="checkmark-circle-fill" color="#4492FF" style="margin-right: 5rpx;"></up-icon>
{{ stepList[0].attendanceTime }}
<view class="time-value">{{ '已打卡' }}</view>
<view class="replacement-card" v-if="stepList[0].attendanceType == 3">补卡</view>
</view>
<view class="time-value" v-else>{{ '未打卡' }}</view>
</view>
<view class="time-step">
<view class="time-label">末次打卡</view>
<view class="time-value-wrap" v-if="stepList.length > 1 && stepList[stepList.length - 1].attendanceTime">
<up-icon name="checkmark-circle-fill" color="#4492FF"></up-icon>
{{ stepList[stepList.length - 1].attendanceTime }}
<view class="time-value">{{ '已打卡' }}</view>
<view class="replacement-card" v-if="stepList[stepList.length - 1].attendanceType == 3">补卡</view>
</view>
<view class="time-value" v-else>{{ '未打卡' }}</view>
</view>
</view>
<view class="BLE-wrap" v-if="!hasPosition || !hasCamera || !hasBLE || !openPosition || !openBLE">
<up-icon name="info-circle-fill" color="#4492FF" style="margin-right: 5rpx;"></up-icon>
<view class="tips-wrap">
<view>未开启</view>
<view style="color: #4492FF;" v-if="!hasPosition">
{{ '定位权限' }}、
</view>
<view style="color: #4492FF;" v-if="!openPosition">
{{ '定位开关' }}、
</view>
<view style="color: #4492FF;" v-if="!hasCamera">
{{ '相机权限' }}、
</view>
<view style="color: #4492FF;" v-if="!hasBLE">
{{ '蓝牙权限' }}
</view>
<view style="color: #4492FF;" v-if="!openBLE">
{{ '蓝牙开关' }}
</view>
<view>无法打卡</view>
</view>
</view>
<view class="click-btn-wrap" @click="handleClockInWithEffect">
<view
class="[
'click-btn',
canClock && !isClockInDebouncing ? '' : 'click-btn-disabled',
btnActive ? 'active' : '',
isClockInDebouncing ? 'click-btn-loading' : ''
]"
@touchstart="handleBtnDown"
@touchend="handleBtnUp"
@touchcancel="handleBtnUp"
<view>
{{ isClockInDebouncing ? '打卡中...' : (canClock ? '打卡' : '无法打卡') }}
</view>
<view>
{{ currentTime }}
</view>
</view>
<view class="canClock-tips" v-show="canClock">
已进入考勤范围
</view>
<view class="canClock-tips" v-show="!hasConnectBLE">
未搜索到正确的考勤设备
</view>
</view>
</view>
</view>
<up-modal :show="showResult" title="" confirmText="我知道了" @confirm="showResult = false">
<view class="slot-content" v-if="clockResult.status == 'success'">
<image
src="https://white-shark-1962.oss-cn-beijing.aliyuncs.com/prd/miniIcon/other/clockin-success.png"
mode="scaleToFill"
class="face-image"
/>
<view class="clockResult-message">
{{ clockResult.message }}
</view>
</view>
<view class="slot-content" v-if="clockResult.status == 'error'">
<image
src="https://white-shark-1962.oss-cn-beijing.aliyuncs.com/prd/miniIcon/other/faceclockin-error.png"
mode="scaleToFill"
class="face-image"
/>
<view class="clockResult-message">
{{ clockResult.message }}
</view>
</view>
</up-modal>
<view class="footer">
<view class="btn-wrap">
<image
src="https://white-shark-1962.oss-cn-beijing.aliyuncs.com/prd/miniIcon/other/kaoqindaka-zhong.png"
mode="scaleToFill"
/>
<view style="color: #4492FF;">
考勤打卡
</view>
</view>
<view class="btn-wrap" @click="toMe">
<image
src="https://white-shark-1962.oss-cn-beijing.aliyuncs.com/prd/miniIcon/other/wodekaoqin.png"
mode="scaleToFill"
/>
<view>
我的考勤
</view>
</view>
<view class="btn-wrap" @click="toTeam" v-if="showTeamBtn">
<image
src="https://white-shark-1962.oss-cn-beijing.aliyuncs.com/prd/miniIcon/other/tuanduikaoqin.png"
mode="scaleToFill"
/>
<view>
团队考勤
</view>
</view>
</view>
<!----水印---->
<zui-watermark />
<!-- 人脸认证弹窗 -->
<faceAuthDialog
v-model:show="faceDialogShow"
/>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { onLoad, onShow, onUnload, onHide, onBackPress } from '@dcloudio/uni-app'
import { requestAndroidPermission } from '@/uni_modules/x-perm-apply-instr-v2/js_sdk/index.js'
import { clockIn, checkIfNeedFaceVerifyBeforeClockIn, listBluetoothInfo, listMyRecordsByMonth } from "@/services";
import { isPointInPolygon } from "@/utils";
const StatusBar = uni.getStorageSync("StatusBar");
const CustomBar = uni.getStorageSync("CustomBar");
const showTeamBtn = ref(false);
const openBLE = ref(false);
const openPosition = ref(false);
const hasPosition = ref(false);
const hasCamera = ref(false);
const hasBLE = ref(false);
const hasConnectBLE = ref(false);
const hasPointInside = ref(true); // 不再进入页面进行电子围栏判断了,改成点击按钮的时候判断,提高按钮响应速度
// 状态数据
const statusBarHeight = ref(0)
const navigationBarHeight = ref(0)
const userInfo = ref({
avatar: '',
userName: '',
deptName: ''
})
const currentDate = ref('')
const date = ref(new Date().toISOString().split('T')[0]);
const week = ref(new Date().getDay() === 0 ? '星期日' : new Date().getDay() === 1 ? '星期一' : new Date().getDay() === 2 ? '星期二' : new Date().getDay() === 3 ? '星期三' : new Date().getDay() === 4 ? '星期四' : new Date().getDay() === 5 ? '星期五' : '星期六');
const weekDay = ref('')
const currentTime = ref('')
const firstClockIn = ref(false)
const lastClockIn = ref(false)
const firstClockTime = ref('')
const lastClockTime = ref('')
const canMakeupCard = ref(false)
const location = ref('光山白鲨针布有限公司-综合楼')
const showResult = ref(false) // 打卡结果
const devicesUUIDList = ref(['00001000-0000-1000-8000-00805F9B34FB']) //公司蓝牙网关设备类型
const deviceIdList = ref(['F1:CC:2F:F9:58:78']) // 公司蓝牙网关设备ID(MAC地址)
const deviceNameList = ref(['BS001']) // 公司蓝牙网关设备名称
const findDeviceId = ref(''); //已搜索到的第一个设备id
const firstMatchedName = ref(''); // 已搜索到的第一个设备名称
const pointList = ref([
[114.892074, 31.968128],
[114.891581, 31.972124],
[114.892622, 31.972178],
[114.893174, 31.971846],
[114.893389, 31.971227],
[114.894810, 31.971059],
[114.895320, 31.968788]
]); //园区电子围栏坐标
const currentLongitude = ref('') // 实时经纬度
const currentLatitude = ref('') // 实时经纬度
const clockResult = ref({
status: 'success',
message: '打卡成功',
})
const hasFaceImg = ref(false) // 是否已经录入人脸
const faceDialogShow = ref(false) // 人脸认证弹窗
const isRequired = ref(true) // 是否强制认证
const checkNeedFace = ref(false) // 是否需要人脸验证
const isFaceVerify = ref(false) // 是否人脸验证入参
const isClockInDebouncing = ref(false) // 打卡防抖状态
const timer = ref(null)
const pointInsidetimmer = ref(null)
const permissiontimmer = ref(null)
const permissiontimmer2 = ref(null)
const systemInfo = ref(null)
const currentYearMonth = ref(new Date().getFullYear() + "-" + (new Date().getMonth() + 1).toString().padStart(2, '0'));
const stepList = ref([]) //打卡记录
const canClock = computed(() => {
// console.log("canClock", hasPosition.value , hasCamera.value , hasBLE.value , hasConnectBLE.value, hasPointInside.value, openBLE.value, openPosition.value)
return hasPosition.value && hasCamera.value && hasBLE.value && hasConnectBLE.value && hasPointInside.value && openBLE.value && openPosition.value
})
// 添加权限状态管理
const permissionStatus = ref({
camera: 'unknown', // 'unknown', 'granted', 'denied', 'permanently_denied'
location: 'unknown',
bluetooth: 'unknown'
})
const permissionRequested = ref({
camera: false,
location: false,
bluetooth: false
})
const lastPermissionCheckTime = ref(0) // 上次权限检查时间
const permissionCheckInterval = 30000 // 权限检查间隔30秒
// 添加定位相关的状态变量
const lastLocationTime = ref(0) // 上次定位时间
const locationCacheTime = 30000 // 定位缓存时间30秒
const isLocating = ref(false) // 是否正在定位中
// 获取权限 - 优化版本
const getPermission = () => {
// 如果所有权限都已获取,直接返回
if (hasBLE.value && hasCamera.value && hasPosition.value) {
return
}
// 检查权限检查间隔,避免过于频繁
const now = Date.now()
if (now - lastPermissionCheckTime.value < permissionCheckInterval) {
return
}
lastPermissionCheckTime.value = now
// console.log('获取权限-----', hasBLE.value, hasCamera.value, hasPosition.value)
// #ifdef MP-WEIXIN
// 检测蓝牙权限
uni.getSetting({
withSubscriptions: true,
success: (res) => {
// console.log("resgetSetting--", res)
// 检查位置权限
if (!res.authSetting['scope.userLocation'] && !permissionRequested.value.location) {
permissionRequested.value.location = true
uni.authorize({
scope: 'scope.userLocation',
success: (success) => {
hasPosition.value = true
permissionStatus.value.location = 'granted'
},
fail: (fail) => {
hasPosition.value = false
permissionStatus.value.location = 'denied'
},
})
} else if (res.authSetting['scope.userLocation']) {
hasPosition.value = true
permissionStatus.value.location = 'granted'
}
// 检查蓝牙权限
if (!res.authSetting['scope.bluetooth'] && !permissionRequested.value.bluetooth) {
permissionRequested.value.bluetooth = true
uni.openBluetoothAdapter({
success: (success) => {
hasBLE.value = true
permissionStatus.value.bluetooth = 'granted'
},
fail: (fail) => {
hasBLE.value = false
permissionStatus.value.bluetooth = 'denied'
},
})
} else if (res.authSetting['scope.bluetooth']) {
hasBLE.value = true
permissionStatus.value.bluetooth = 'granted'
}
// 检查相机权限
if (!res.authSetting['scope.camera'] && !permissionRequested.value.camera) {
permissionRequested.value.camera = true
uni.authorize({
scope: 'scope.camera',
success: (success) => {
hasCamera.value = true
permissionStatus.value.camera = 'granted'
},
fail: (fail) => {
hasCamera.value = false
permissionStatus.value.camera = 'denied'
},
})
} else if (res.authSetting['scope.camera']) {
hasCamera.value = true
permissionStatus.value.camera = 'granted'
}
}
})
// #endif
// #ifdef APP
// 相机权限
if (!permissionRequested.value.camera && !hasCamera.value) {
permissionRequested.value.camera = true
requestAndroidPermission('android.permission.CAMERA', {
title: '相机权限申请说明',
content: '应用需要访问您的相机,以便拍照签到'
}).then(status => {
// console.log('相机权限status', status);
if (status == 1) {
hasCamera.value = true
permissionStatus.value.camera = 'granted'
} else if (status == 0) {
hasCamera.value = false
permissionStatus.value.camera = 'denied'
} else if (status == -1) {
hasCamera.value = false
permissionStatus.value.camera = 'permanently_denied'
}
})
}
// 位置权限
if (!permissionRequested.value.location && !hasPosition.value) {
permissionRequested.value.location = true
requestAndroidPermission('android.permission.ACCESS_FINE_LOCATION', {
title: '精确定位权限申请说明',
content: '应用需要获取您的精确位置信息,以便进行打卡签到'
}).then(status => {
// console.log('位置权限status', status);
if (status == 1) {
hasPosition.value = true
permissionStatus.value.location = 'granted'
} else if (status == 0) {
hasPosition.value = false
permissionStatus.value.location = 'denied'
} else if (status == -1) {
hasPosition.value = false
permissionStatus.value.location = 'permanently_denied'
}
})
}
// 蓝牙权限
if (!permissionRequested.value.bluetooth && !hasBLE.value) {
permissionRequested.value.bluetooth = true
requestAndroidPermission('android.permission.ACCESS_FINE_LOCATION', {
title: '蓝牙权限申请说明',
content: '应用需要访问蓝牙功能,以便进行打卡签到'
}).then(status => {
// console.log('蓝牙权限status', status);
if (status == 1) {
hasBLE.value = true
permissionStatus.value.bluetooth = 'granted'
} else if (status == 0) {
hasBLE.value = false
permissionStatus.value.bluetooth = 'denied'
} else if (status == -1) {
hasBLE.value = false
permissionStatus.value.bluetooth = 'permanently_denied'
}
})
}
// #endif
}
// 检查权限状态,只在必要时申请
const checkPermissionStatus = () => {
// 如果所有权限都已获取,不需要检查
if (hasBLE.value && hasCamera.value && hasPosition.value) {
return
}
// 检查是否有权限被永久拒绝,如果有则提示用户手动开启
const hasPermanentlyDenied = Object.values(permissionStatus.value).some(status => status === 'permanently_denied')
if (hasPermanentlyDenied) {
// 权限被永久拒绝,提示用户手动开启
uni.showModal({
title: '权限提示',
content: '部分权限被拒绝,请在系统设置中手动开启相机、位置和蓝牙权限',
showCancel: false,
success: () => {
// 可以引导用户到设置页面
// #ifdef APP-PLUS
if (systemInfo.value.platform === 'android') {
const Intent = plus.android.importClass('android.content.Intent');
const Settings = plus.android.importClass('android.provider.Settings');
const Uri = plus.android.importClass('android.net.Uri');
const main = plus.android.runtimeMainActivity();
const intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
const uri = Uri.fromParts('package', main.getPackageName(), null);
intent.setData(uri);
main.startActivity(intent);
}
// #endif
}
})
return
}
// 检查是否有权限被拒绝但可以重新申请
const canRetry = Object.values(permissionStatus.value).some(status => status === 'denied')
if (canRetry) {
// 重置被拒绝的权限状态,允许重新申请
Object.keys(permissionStatus.value).forEach(key => {
if (permissionStatus.value[key] === 'denied') {
permissionStatus.value[key] = 'unknown'
permissionRequested.value[key] = false
}
})
}
// 申请权限
getPermission()
}
// 添加手动权限检查方法
const manualCheckPermission = () => {
// 重置所有权限状态,强制重新检查
Object.keys(permissionStatus.value).forEach(key => {
permissionStatus.value[key] = 'unknown'
permissionRequested.value[key] = false
})
// 重置权限状态
hasBLE.value = false
hasCamera.value = false
hasPosition.value = false
// 重新检查权限
checkPermissionStatus()
}
// 初始化系统信息
onLoad(async () => {
showTeamBtn.value = Boolean(uni.getStorageSync('mini_attendance_team') || '');
userInfo.value = uni.getStorageSync("userInfo");
systemInfo.value = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight
navigationBarHeight.value = statusBarHeight.value + 44
initDateTime()
startTimeUpdate()
// 获取所有蓝牙网关信息
await listBluetoothInfoFn()
// 初始化权限检查
checkPermissionStatus()
// 权限检查定时器 - 降低频率,避免频繁弹框
permissiontimmer.value = setInterval(() => {
// 只在权限不完整时才检查
if (!hasBLE.value || !hasCamera.value || !hasPosition.value) {
checkPermissionStatus()
}
}, 30000); // 改为30秒检查一次,大幅减少弹框频率
// 检测是否需要人脸识别
await checkIfNeedFaceVerifyBeforeClockInFn()
// 判断当前运行环境是iosapp、androidapp、微信小程序
// #ifdef MP-WEIXIN
hasConnectBLE.value = false
// #endif
// #ifdef APP
if (systemInfo.platform == "android") {
hasConnectBLE.value = false
}
// #endif
// 立即检查一次位置
checkLocation()
uni.removeStorageSync('bleSearching')
// 只在没有连接蓝牙设备时启动搜索
if (!hasConnectBLE.value) {
startBLEFn()
}
permissiontimmer2.value = setInterval(() => {
// 只在没有连接蓝牙设备时重新搜索
if (!hasConnectBLE.value && !uni.getStorageSync('bleSearching')) {
startBLEFn()
}
}, 5000); // 改为10秒检查一次,减少蓝牙搜索频率
// 单独设置定位检测定时器,频率更低
pointInsidetimmer.value = setInterval(() => {
// 检测电子围栏 - 降低频率
if (!isLocating.value) {
checkLocation()
}
}, 10000); // 改为15秒检查一次,减少定位频率
getMyAttendanceFn() // 获取我的考勤数据
})
onShow(async () => {
isFaceVerify.value = uni.getStorageSync('faceCheckStatus')
if (isFaceVerify.value && isFaceVerify.value == 1) {
await handleClockIn()
// 人脸打卡成功
// clockResult.value = {
// status: 'success',
// message: '打卡成功'
// }
// showResult.value = true
uni.removeStorageSync('faceCheckStatus')
// 获取打卡信息
getMyAttendanceFn()
} else if(isFaceVerify.value && isFaceVerify.value == 2) {
// 人脸打卡失败
clockResult.value = {
status: 'error',
message: '人脸打卡失败'
}
showResult.value = true
uni.removeStorageSync('faceCheckStatus')
}
hasFaceImg.value = await hasFacePromise()
// 获取所有蓝牙网关信息
await listBluetoothInfoFn()
// getPermission() // 移除此行,权限检查已移至onLoad
// permissiontimmer.value = setInterval(() => { // 移除此行,权限检查已移至onLoad
// // 获取权限
// getPermission()
// }, 3000);
// 检测是否需要人脸识别
await checkIfNeedFaceVerifyBeforeClockInFn()
// 判断当前运行环境是iosapp、androidapp、微信小程序
// #ifdef MP-WEIXIN
hasConnectBLE.value = false
// #endif
// #ifdef APP
if (systemInfo.platform == "android") {
hasConnectBLE.value = false
}
// #endif
// // 检测电子围栏
// checkLocation()
uni.removeStorageSync('bleSearching')
// 只在没有连接蓝牙设备时启动搜索
if (!hasConnectBLE.value) {
startBLEFn()
}
permissiontimmer2.value = setInterval(() => {
// // 检测电子围栏
// checkLocation()
// 只在没有连接蓝牙设备时重新搜索
if (!hasConnectBLE.value && !uni.getStorageSync('bleSearching')) {
startBLEFn()
}
}, 3000); // 改为5秒检查一次,减少频率
getMyAttendanceFn() // 获取我的考勤数据
})
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value)
}
clearInterval(pointInsidetimmer.value)
clearInterval(permissiontimmer.value)
clearInterval(permissiontimmer2.value)
// #ifdef MP-WEIXIN
wx.stopBluetoothDevicesDiscovery()
// #endif
// #ifdef APP
uni.stopBluetoothDevicesDiscovery()
// #endif
uni.closeBluetoothAdapter()
uni.removeStorageSync('faceCheckVerify')
})
onUnload(() => {
if (timer.value) {
clearInterval(timer.value)
}
clearInterval(pointInsidetimmer.value)
clearInterval(permissiontimmer.value)
clearInterval(permissiontimmer2.value)
// #ifdef MP-WEIXIN
wx.stopBluetoothDevicesDiscovery()
// #endif
// #ifdef APP
uni.stopBluetoothDevicesDiscovery()
// #endif
uni.removeStorageSync('faceCheckVerify')
})
onBackPress((event) => {
// console.log("AAAAA", event)
if (event.from === 'backbutton') {
backs()
return true; // 阻止默认返回行为
}
return false;
})
const backs = () => {
// #ifdef MP-WEIXIN
uni.navigateBack({
delta: 1,
});
// #endif
// #ifdef APP
plus.runtime.restart();
// #endif
}
// 获取我的考勤
const getMyAttendanceFn = async () => {
let params = {
yearMonth: currentYearMonth.value,
};
console.log("params", params)
const { code, data } = await listMyRecordsByMonth(params);
console.log("data", data)
if (code == 200 && data.length > 0) {
stepList.value = data.find(el => el.date == currentDate.value).attendanceData || []
} else {
stepList.value = [];
}
};
// 获取蓝牙设备信息
const listBluetoothInfoFn = async () => {
const {code, data} = await listBluetoothInfo()
// console.log("所有蓝牙设备列表", data)
if (code === 200 && data) {
deviceIdList.value = data.map(item => item.mac)
deviceNameList.value = data.map(el => el.name)
}
}
// 检测员工打卡前是否需要人脸识别
const checkIfNeedFaceVerifyBeforeClockInFn = async () => {
const {code, data} = await checkIfNeedFaceVerifyBeforeClockIn()
if (code == 200 && data) {
checkNeedFace.value = true
} else {
checkNeedFace.value = false
}
}
// 检测是否在电子围栏内 - 优化版本
const checkLocation = () => {
if (!hasPosition.value) {
return false
}
// 如果正在定位中,避免重复调用
if (isLocating.value) {
return false
}
// 检查定位缓存,如果距离上次定位时间不足30秒,直接使用缓存
const now = Date.now()
if (now - lastLocationTime.value < locationCacheTime &&
currentLongitude.value && currentLatitude.value) {
// console.log("使用定位缓存,避免重复定位")
hasPointInside.value = isPointInPolygon([currentLongitude.value, currentLatitude.value], pointList.value)
return
}
isLocating.value = true
// 设置定位超时
const locationTimeout = setTimeout(() => {
isLocating.value = false
// console.log("定位超时")
}, 8000) // 8秒超时
let plant = ''
// #ifdef APP
plant = systemInfo.value.platform
// #endif
uni.getLocation({
type: 'gcj02',
isHighAccuracy: plant == 'ios' ? false : true, // 改为false,提高定位速度
success: (success) => {
openPosition.value = true
clearTimeout(locationTimeout)
isLocating.value = false
// console.log("当前经纬度", success)
currentLongitude.value = success.longitude
currentLatitude.value = success.latitude
lastLocationTime.value = now
hasPointInside.value = isPointInPolygon([success.longitude, success.latitude], pointList.value)
// console.log("是否在园区内", hasPointInside.value)
// console.log("开启GPS", hasPosition.value)
// console.log("开启相机", hasCamera.value)
// console.log("hasBLE", hasBLE.value)
// console.log("hasConnectBLE", hasConnectBLE.value)
},
fail: (error) => {
openPosition.value = false
clearTimeout(locationTimeout)
isLocating.value = false
// console.log("定位失败", error)
// 如果定位失败,尝试使用缓存的位置信息
if (currentLongitude.value && currentLatitude.value) {
// console.log("定位失败,使用缓存位置信息")
// hasPointInside.value = isPointInPolygon([currentLongitude.value, currentLatitude.value], pointList.value)
}
}
})
}
// 初始化日期时间
const initDateTime = () => {
const date = new Date()
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const weeks = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
currentDate.value = ${year}-${month}-${day}
weekDay.value = weeks[date.getDay()]
updateCurrentTime()
}
// 更新当前时间
const updateCurrentTime = () => {
const date = new 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')
currentTime.value = ${hours}:${minutes}:${seconds}
return ${year}-${month}-${day} ${hours}:${minutes}:${seconds}
}
// 开始时间更新
const startTimeUpdate = () => {
timer.value = setInterval(updateCurrentTime, 1000)
}
// 处理打卡
const handleClockIn = async () => {
if (!canClock) return
try {
uni.showLoading({
title: '打卡中...'
})
let params = {
clockInTime: updateCurrentTime(),
longitude: currentLongitude.value,
latitude: currentLatitude.value,
platform: systemInfo.value.platform,
bluetoothData: {
mac: findDeviceId.value || undefined,
name: firstMatchedName.value || undefined,
},
location: undefined,
isFaceVerify: isFaceVerify.value,
}
console.log('提交打卡params', params)
const {code, data} = await clockIn(params)
uni.hideLoading()
if (code == 200) {
// 更新考勤记录
await getMyAttendanceFn()
clockResult.value = {
status: 'success',
message: '打卡成功',
}
showResult.value = true
}
} catch (error) {
uni.hideLoading()
}
}
// 补卡处理
const handleMakeup = () => {
uni.showToast({
title: '补卡申请已提交',
icon: 'none'
})
}
const toTeam = () => {
uni.redirectTo({
url: '/pagesAttendance/teamAttendance/index'
})
}
const toMe = () => {
uni.redirectTo({
url: '/pagesAttendance/myAttendance/index'
})
}
const btnActive = ref(false)
const handleBtnDown = () => {
if (!canClock.value) return
btnActive.value = true
}
const handleBtnUp = () => {
if (!canClock.value) return
btnActive.value = false
}
const handleClockInWithEffect = async () => {
// 防抖处理:如果正在打卡中,直接返回
if (isClockInDebouncing.value) {
// console.log('打卡操作正在进行中,请勿重复点击')
return
}
// 设置防抖状态
isClockInDebouncing.value = true
try {
getPermission()
// 判断是否已经录入人脸
if (!hasFaceImg.value) {
// if (true) {
faceDialogShow.value = true
return
}
// 判断是否允许打卡
if (!canClock.value) return
// 判断是否需要人脸验证
if (checkNeedFace.value) {
// if (true) {
// 人脸验证
let backUrl = '/pagesAttendance/clockIn/index'
// #ifdef MP-WEIXIN
uni.navigateTo({
url: /pagesFaceCheck/faceCheckVerify/wechatPageFaceVerify?backUrl=${backUrl}
})
// #endif
// #ifdef APP
let plant = systemInfo.value.platform
if (plant == "ios") {
uni.navigateTo({
url: /pagesFaceCheck/faceCheckVerify/iosFaceVerify?backUrl=${backUrl}
})
} else {
uni.navigateTo({
url: /pagesFaceCheck/faceCheckVerify/androidFaceVerify?backUrl=${backUrl}
})
}
// #endif
} else {
handleBtnDown()
await handleClockIn()
setTimeout(handleBtnUp, 150)
}
} finally {
// 延迟重置防抖状态,确保操作完成
setTimeout(() => {
isClockInDebouncing.value = false
}, 1000) // 1秒防抖时间
}
}
// 判断是否已经录入人脸
const hasFacePromise = () => {
return new Promise((resolve, reject) => {
if (userInfo.value.enableFaceVerify == 1) {
resolve(true)
} else {
resolve(false)
}
})
}
// 前往录入人脸
const handleAuth = () => {
// 判断是微信小程序还是app平台
let backUrl = '/pagesAttendance/clockIn/index'
uni.navigateTo({
url: /pagesFaceCheck/faceCheckStepOne/wechatPageRegister?backUrl=${backUrl}
})
// // #ifdef APP
// let plant = systemInfo.value.platform
// if (plant == "ios") {
// uni.navigateTo({
// url: /pagesFaceCheck/faceCheckStepOne/indexIOSRegister?backUrl=${backUrl}
// })
// } else {
// uni.navigateTo({
// url: /pagesFaceCheck/faceCheckStepOne/indexAndroidRegister?backUrl=${backUrl}
// })
// }
// // #endif
}
const startBLEFn = () => {
if (!hasBLE.value || hasConnectBLE.value) {
return false
}
// 如果已经在搜索中,不要重复启动
if (uni.getStorageSync('bleSearching')) {
return false
}
uni.openBluetoothAdapter({
success: () => {
// console.log('蓝牙打开了');
openBLE.value = true
// GPS定位检测
checkOpenGPSServiceByAndroidIOS()
// 标记开始搜索
uni.setStorageSync('bleSearching', true)
// console.log('调用蓝牙设备搜索');
uni.startBluetoothDevicesDiscovery({
services: devicesUUIDList.value,
// 安卓和微信小程序使用false,ios使用true
// allowDuplicatesKey: systemInfo.value.platform === 'ios' ? true : false,
allowDuplicatesKey: true,
success: (res) => {
// console.log('开始搜索蓝牙设备', res);
// 设置搜索超时,避免无限搜索
const searchTimeout = setTimeout(() => {
if (!hasConnectBLE.value) {
// console.log('蓝牙搜索超时,停止搜索');
// #ifdef MP-WEIXIN
wx.stopBluetoothDevicesDiscovery()
// #endif
// #ifdef APP
uni.stopBluetoothDevicesDiscovery()
// #endif
uni.setStorageSync('bleSearching', false)
}
}, 10000) // 10秒超时
uni.onBluetoothDeviceFound(async (devicesObj) => {
console.log('搜索到蓝牙设备---', devicesObj.devices[0]);
let plantName = systemInfo.value.platform
if(plantName == 'android') {
const deviceIds = new Set(devicesObj.devices.map(device => device.deviceId));
const deviceLocalNames = new Set(devicesObj.devices.map(device => device.localName));
// 检查是否找到目标设备
const foundTargetDevice = deviceIdList.value.some(id => deviceIds.has(id)) ||
deviceNameList.value.some(name => deviceLocalNames.has(name));
if (foundTargetDevice) {
hasConnectBLE.value = true;
findDeviceId.value = deviceIdList.value.find(id => deviceIds.has(id)) || '';
firstMatchedName.value = deviceNameList.value.find(name => deviceLocalNames.has(name)) || '';
// 找到设备后立即停止搜索
// console.log('找到目标蓝牙设备,停止搜索');
// #ifdef MP-WEIXIN
wx.stopBluetoothDevicesDiscovery()
// #endif
// #ifdef APP
uni.stopBluetoothDevicesDiscovery()
// #endif
uni.setStorageSync('bleSearching', false)
clearTimeout(searchTimeout)
// // 显示成功提示
// uni.showToast({
// title: '蓝牙设备连接成功',
// icon: 'success',
// duration: 2000
// })
}
} else {
// iOS设备处理
const deviceLocalNames = new Set(devicesObj.devices.map(device => device.localName));
const foundTargetDevice = deviceNameList.value.some(name => deviceLocalNames.has(name));
if (foundTargetDevice) {
hasConnectBLE.value = true;
firstMatchedName.value = deviceNameList.value.find(name => deviceLocalNames.has(name)) || '';
// 找到设备后立即停止搜索
// console.log('找到目标蓝牙设备,停止搜索');
uni.stopBluetoothDevicesDiscovery()
uni.setStorageSync('bleSearching', false)
clearTimeout(searchTimeout)
// // 显示成功提示
// uni.showToast({
// title: '蓝牙设备连接成功',
// icon: 'success',
// duration: 2000
// })
}
}
});
},
fail: () => {
// console.log('开始搜索蓝牙设备失败');
uni.setStorageSync('bleSearching', false)
}
})
},
fail: res => {
// console.log('蓝牙失败:', res);
openBLE.value = false
uni.setStorageSync('bleSearching', false)
}
});
}
// 获取GPS定位服务
const checkOpenGPSServiceByAndroidIOS = () => {
// #ifdef MP-WEIXIN
if (systemInfo.value.hostName == 'WeChat') {
//2、判断微信小程序是否授权位置信息
uni.getSetting({
success(res) {
let scopeUserLocation = res.authSetting["scope.userLocation"];
if (scopeUserLocation) {
// 微信小程序已授权位置信息
} else {
// 微信小程序未授权位置信息
// uni.showModal({
// title: '提示',
// content: '请允许使用位置信息',
// showCancel: false, // 不显示取消按钮
// success() {
// uni.navigateBack({
// delta: 1, //返回层数,2则上上页
// })
// }
// })
}
},
fail() {
// 微信小程序未授权位置信息
uni.showModal({
title: '提示',
content: '获取位置信息失败',
showCancel: false, // 不显示取消按钮
success() {
uni.navigateBack({
delta: 1, //返回层数,2则上上页
})
}
})
}
});
}
// #endif
// #ifdef APP-PLUS
if (systemInfo.value.platform === 'android') { // 判断平台
var context = plus.android.importClass("android.content.Context");
var locationManager = plus.android.importClass("android.location.LocationManager");
var main = plus.android.runtimeMainActivity();
var mainSvr = main.getSystemService(context.LOCATION_SERVICE);
if (!mainSvr.isProviderEnabled(locationManager.GPS_PROVIDER)) {
hasPosition.value = false
uni.showModal({
title: '提示',
content: '请打开定位服务功能',
showCancel: false, // 不显示取消按钮
success() {
if (!mainSvr.isProviderEnabled(locationManager.GPS_PROVIDER)) {
var Intent = plus.android.importClass('android.content.Intent');
var Settings = plus.android.importClass('android.provider.Settings');
var intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
main.startActivity(intent); // 打开系统设置GPS服务页面
} else {
// console.log('GPS功能已开启');
}
}
});
} else {
hasPosition.value = true
}
} else if (systemInfo.value.platform === 'ios') {
// console.log("苹果");
var cllocationManger = plus.ios.import("CLLocationManager");
var enable = cllocationManger.locationServicesEnabled();
var status = cllocationManger.authorizationStatus();
plus.ios.deleteObject(cllocationManger);
if (enable && status != 2) {
// console.log("手机系统的定位已经打开");
hasPosition.value = true
} else {
hasPosition.value = false
// console.log("手机系统的定位没有打开");
uni.showModal({
title: '提示',
content: '请前往设置-隐私-定位服务打开定位服务功能',
showCancel: false, // 不显示取消按钮
success() {
var UIApplication = plus.ios.import("UIApplication");
var application2 = UIApplication.sharedApplication();
var NSURL2 = plus.ios.import("NSURL");
// var setting2 = NSURL2.URLWithString("prefs:root=LOCATION_SERVICES");
// var setting2 = NSURL2.URLWithString("App-Prefs:root=LOCATION_SERVICES");
var setting2 = NSURL2.URLWithString("app-settings:");
//var setting2 = NSURL2.URLWithString("App-Prefs:root=Privacy&path=LOCATION");
// var setting2 = NSURL2.URLWithString("App-Prefs:root=Privacy&path=LOCATION_SERVICES");
application2.openURL(setting2);
plus.ios.deleteObject(setting2);
plus.ios.deleteObject(NSURL2);
plus.ios.deleteObject(application2);
}
});
}
}
// #endif
}
</script>
<style lang="scss" scoped>
.content {
position: relative;
}
.title-wrap {
z-index: 9;
width: 100%;
height: 480rpx;
position: fixed;
top: 0;
left: 0;
background-image: url("https://white-shark-1962.oss-cn-beijing.aliyuncs.com/prd/miniIcon/other/approve-detailHeader-bg.png");
background-repeat: no-repeat;
background-size: 100% auto;
}
.back {
position: absolute;
left: 30rpx;
width: 56rpx;
image {
display: block;
width: 30rpx;
height: 38rpx;
margin: 0 auto;
}
.toe {
width: 56rpx;
height: 56rpx;
image {
width: 56rpx;
height: 56rpx;
display: block;
}
}
}
.title {
position: fixed;
display: flex;
justify-content: center;
font-size: 36rpx;
font-weight: 500;
color: #333333;
width: 100%;
color: #fff;
}
.content-box {
background-image: url("https://white-shark-1962.oss-cn-beijing.aliyuncs.com/prd/miniIcon/other/approve-detailHeader-bg.png");
background-repeat: no-repeat;
background-size: 100% auto;
width: 100%;
height: 100vh;
overflow-y: auto;
padding-bottom: 130rpx;
display: flex;
flex-direction: column;
.detail-title-wrap {
// padding: 0 30rpx;
width: calc(100vw - 60rpx);
margin: 0 30rpx;
background-color: #F6F6F6FF;
position: relative;
border-radius: 16rpx;
.staticInfo-wrap {
background-color: #FFFFFFD9;
border-radius: 16rpx;
.top-wrap {
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 16rpx 16rpx 0rpx 0rpx;
padding: 20rpx 30rpx 10rpx 30rpx;
background-color: #e2f0ff;
.userInfo-wrap {
display: flex;
align-items: center;
.name-wrap {
display: flex;
flex-direction: column;
margin-left: 20rpx;
.user-name {
font-weight: 500;
font-size: 32rpx;
color: #333333;
}
.dept-name {
font-weight: 400;
font-size: 28rpx;
color: #666666;
}
}
}
.date-wrap {
display: flex;
align-items: center;
font-weight: 400;
font-size: 28rpx;
color: #666666;
.nowrap {
white-space: nowrap;
}
}
}
.static-card {
background-color: #fff;
border-radius: 16rpx;
padding: 20rpx 30rpx;
.static-list {
display: flex;
padding-top: 20rpx;
margin-top: 20rpx;
border-top: 1px solid #00000014;
.static-item {
width: 33.33%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.static-item-value {
color: #333333FF;
}
.static-item-title {
color: #999999FF;
}
}
.border-right {
border-right: 1px solid #00000014;
}
}
}
}
.calendar-wrap {
background-color: #fff;
padding: 20rpx;
border-radius: 16rpx 16rpx 16rpx 16rpx;
// margin-top: 20rpx;
.steps-wrap {
margin-top: 20rpx;
.steps-item {
display: flex;
.step-left-wrap {
width: 50rpx;
height: 120rpx;
display: flex;
flex-direction: column;
align-items: center;
.step-line {
flex: 1;
width: 2rpx;
height: 100%;
background: #E6E6E6;
}
}
.step-right-wrap {
padding-left: 20rpx;
display: flex;
flex-direction: column;
.step-time-wrap {
display: flex;
align-items: center;
font-weight: 500;
font-size: 32rpx;
color: #333333;
margin-bottom: 10rpx;
}
.step-address-wrap {
display: flex;
align-items: center;
font-weight: 400;
font-size: 28rpx;
color: #999999;
.apply-btn {
width: 132rpx;
height: 50rpx;
border-radius: 8rpx 8rpx 8rpx 8rpx;
border: 2rpx solid #999999;
color: #333333FF;
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
}
}
}
}
.footer {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
display: flex;
align-items: center;
justify-content: space-around;
font-size: 26rpx;
color: #333;
background: #fff;
padding: 16rpx 20rpx 20rpx;
// padding-bottom: constant(safe-area-inset-bottom);
// padding-bottom: env(safe-area-inset-bottom);
box-sizing: border-box;
.btn-wrap {
font-family: PingFang SC, PingFang SC;
font-weight: 400;
font-size: 26rpx;
color: #666666;
display: flex;
flex-direction: column;
align-items: center;
width: 112rpx;
// height: 74rpx;
image {
width: 32rpx;
height: 32rpx;
margin-bottom: 10rpx;
}
}
}
.clock-info-wrap {
margin: 0 30rpx;
padding: 30rpx;
background-color: #fff;
border-radius: 0rpx 0rpx 8rpx 8rpx;
flex: 1;
}
.time-step-wrap {
display: flex;
align-items: center;
justify-content: space-between;
margin: 20rpx 0;
.time-step {
display: flex;
flex-direction: column;
width: 48%;
background-color: #F6F8FE;
padding: 16rpx 20rpx;
border-radius: 8rpx 8rpx 8rpx 8rpx;
.time-label {
font-weight: 500;
font-size: 32rpx;
color: #3D3D3D;
}
.time-value-wrap {
display: flex;
color: #999999;
font-size: 28rpx;
.time-value {
font-weight: 400;
font-size: 28rpx;
color: #999999;
margin-right: 5rpx;
white-space: nowrap;
}
.replacement-card {
font-size: 28rpx;
color: #FFFFFF;
width: 76rpx;
height: 36rpx;
background: #00B578;
border-radius: 4rpx 4rpx 4rpx 4rpx;
text-align: center;
white-space: nowrap;
}
}
}
}
.click-btn-wrap {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-top: 200rpx;
}
.click-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-weight: 500;
font-size: 48rpx;
color: #FFFFFF;
background-color: #4492FF;
width: 270rpx;
height: 270rpx;
border-radius: 50%;
margin-bottom: 50rpx;
position: relative;
overflow: hidden;
}
/ 只有非禁用状态才有光晕效果 /
.click-btn:not(.click-btn-disabled) {
box-shadow: 0 0 20rpx rgba(68, 146, 255, 0.3);
animation: pulse 2s infinite;
}
.click-btn:not(.click-btn-disabled)::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
background: radial-gradient(circle, rgba(68, 146, 255, 0.2) 0%, transparent 70%);
transform: translate(-50%, -50%);
animation: ripple 3s infinite;
}
.click-btn:not(.click-btn-disabled)::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 120%;
height: 120%;
background: radial-gradient(circle, rgba(68, 146, 255, 0.1) 0%, transparent 70%);
transform: translate(-50%, -50%);
animation: ripple 3s infinite 1.5s;
}
.click-btn-disabled {
background-color: #CDCDCD !important;
/ 禁用状态下移除所有光晕效果 /
box-shadow: none !important;
}
.click-btn.active {
transform: scale(0.95);
transition: transform 0.1s;
}
.click-btn-loading {
background-color: #A0C4FF !important;
cursor: not-allowed;
position: relative;
overflow: hidden;
}
.click-btn-loading::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
animation: loading-shimmer 1.5s infinite;
}
@keyframes loading-shimmer {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
.canClock-tips {
font-weight: 400;
font-size: 28rpx;
color: #999999;
}
.BLE-wrap {
display: flex;
align-items: center;
padding: 20rpx;
margin: 30rpx 0;
background-color: #E3EFFF;
.tips-wrap {
display: flex;
align-items: center;
flex-wrap: wrap;
font-size: 28rpx;
font-weight: 400;
view {
white-space: nowrap;
}
}
}
.slot-content {
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
padding: 100rpx 0;
.face-image {
width: 210rpx;
height: 210rpx;
}
.clockResult-message {
font-weight: 500;
font-size: 48rpx;
color: #3D3D3D;
}
}
/ 光晕动画关键帧 /
@keyframes pulse {
0% {
box-shadow: 0 0 20rpx rgba(68, 146, 255, 0.3);
}
50% {
box-shadow: 0 0 40rpx rgba(68, 146, 255, 0.6);
}
100% {
box-shadow: 0 0 20rpx rgba(68, 146, 255, 0.3);
}
}
@keyframes ripple {
0% {
transform: translate(-50%, -50%) scale(0.8);
opacity: 1;
}
100% {
transform: translate(-50%, -50%) scale(2);
opacity: 0;
}
}
</style>
操作步骤:
进来页面搜索蓝牙设备
进来页面搜索蓝牙设备
预期结果:
进来页面搜索蓝牙设备,离开页面停止搜索蓝牙
进来页面搜索蓝牙设备,离开页面停止搜索蓝牙
实际结果:
进来页面搜索蓝牙设备,离开页面蓝牙搜索还在进行,导致设备卡顿崩溃
进来页面搜索蓝牙设备,离开页面蓝牙搜索还在进行,导致设备卡顿崩溃
bug描述:
uniapp开发的考勤打卡功能,需要搜索到指定的蓝牙考勤设备。
问题1:uni.stopBluetoothDevicesDiscovery方法不能真正停止搜索蓝牙;
问题2:uni.getBluetoothDevices方法的advertisData数据一直是空对象,实际上用别的原生设备能够获取到蓝牙网关广播的信息内容
0 个回复