uniapp h5在线签到
uniapp h5在线签到
·
获取当前位置,以及对比是否在签到范围内
<template>
<view>
<view class="header">
<view class="back-button" @click="goBack">
<u-icon name="arrow-left"></u-icon>
</view>
<view class="title">在线签到</view>
<view class="right-button" @click="goCheckInCalendar">签到日历</view>
</view>
<!-- 页面内容 -->
<view class="content" style="margin-top: 100rpx">
<!-- 顶部地图 -->
<view class="map-container">
<map
id="mapContainer"
style="height: 400rpx; width: 100%"
:longitude="longitude"
:latitude="latitude"
:scale="scale"
:markers="markers"
:circles="circles"
></map>
</view>
<!-- 表单 -->
<view class="form-container">
<view class="form-item">
<view class="form-label">我的位置</view>
<view class="form-value">{{ currentAddress }}</view>
</view>
<!-- 培训班选择 -->
<view class="form-item">
<view class="form-label">培训班</view>
<view class="form-value">
<view class="select-input" @click="showTrainingSelect = true">
{{ selectedTraining || "请选择培训班" }}
<u-icon name="arrow-down" color="#999" size="28rpx" />
</view>
<u-select
v-model="showTrainingSelect"
:list="trainingList"
@confirm="confirmTrainingSelect"
></u-select>
</view>
</view>
<!-- 签到图片 -->
<view class="form-item">
<view class="form-label">签到图片</view>
<view class="form-value">
<view class="image-upload">
<view class="upload-btn" @click="takePhoto">
<u-icon name="camera" color="#999" size="48rpx" />
</view>
<view class="image-list">
<view
class="image-item"
v-for="(image, index) in checkInImages"
:key="index"
>
<image :src="image" mode="aspectFill" />
<view class="image-delete" @click="deleteImage(index)">
<u-icon name="close-circle" color="#ff3b30" size="32rpx" />
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 备注 -->
<view class="form-item">
<view class="form-label">备注</view>
<view class="form-value">
<textarea
v-model="remark"
placeholder="请填写300字以内备注内容"
class="remark-input"
maxlength="300"
></textarea>
</view>
</view>
<!-- 签到按钮和时间 -->
<view class="checkin-section">
<view
class="checkin-button"
:class="{ checked: count > 0 }"
@click="submitCheckIn"
>
<text class="button-text">签到</text>
<text class="current-time">{{ currentTime }}</text>
</view>
<view class="checkin-status" v-if="count === 0">今日未签到</view>
<view class="checkin-status" v-else>
今日已签到<span style="color: red">{{ count }}</span
>次
</view>
</view>
</view>
</view>
<!-- 人脸识别弹窗 -->
<view v-if="showFaceModal" class="face-modal-overlay">
<view class="face-modal">
<view class="face-modal-content">
<div ref="faceContainer"></div>
<div v-if="!detectionPaused" style="margin-top: 20rpx">
<button class="face-button confirm-button" @click="confirmCapture">
确认选择
</button>
</div>
<div v-if="detectionPaused" style="margin-top: 20rpx">
<p class="face-info">已选择图像,点击重新检测可继续识别</p>
<button class="face-button resume-button" @click="resumeDetection">
重新检测
</button>
<button class="face-button verify-button" @click="metaVerify">
身份核验
</button>
</div>
</view>
</view>
</view>
</view>
</template>
<script>
import { userMedia } from "../../../../utils/utils.js";
import { getUrl } from "@/config/index";
require("tracking/build/tracking-min.js");
require("tracking/build/data/face-min.js");
export default {
data() {
return {
// 地图相关
map: null, // 地图实例
AMap: null, // AMap对象,用于调用API
markers: [], // 存储标记实例
circles: [], // 存储签到范围圆形
locationList: [], // 签到范围列表
longitude: null, // 经度
latitude: null, // 纬度
scale: 14.5, // 地图缩放级别
currentAddress: "",
// 表单相关
showTrainingSelect: false,
trainingList: [],
selectedTraining: "",
classId: "",
className: "",
checkInImages: [],
imagesUrl: "",
remark: "",
count: 0,
// 时间相关
currentTime: "",
action: "",
// action: "/api/gsyf/file/anyone/upload",
isOpen1: false,
isOpen2: false,
isOpen3: false,
isOpen4: false,
isUploading: false,
// 人脸识别相关
showFaceModal: false,
userInfo: {
id: "",
name: "",
phone: "",
identity: "",
training: "",
capturedUrl: "",
},
formData: {
bizType: 1,
bucket: "hezedatabackups-0218",
storageType: "ALI_OSS",
},
videoObj: null,
trackerTask: null,
container: null,
canvas: null,
faceDetected: false, // 人脸检测状态标志
detectionPaused: false, // 检测暂停标志
capturedImage: null, // 存储捕获的图像数据
signId: "", // 签到配置ID
};
},
methods: {
goBack() {
uni.switchTab({
url: "/pages/me/index",
});
},
goCheckInCalendar() {
uni.navigateTo({
url: "/pages/me/page/checkInCalendar/index",
});
},
// 更新当前时间
updateCurrentTime() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
const hours = String(now.getHours()).padStart(2, "0");
const minutes = String(now.getMinutes()).padStart(2, "0");
const seconds = String(now.getSeconds()).padStart(2, "0");
this.currentTime = `${hours}:${minutes}:${seconds}`;
},
// 确认培训班选择
confirmTrainingSelect(e) {
const selected = e[0];
this.selectedTraining = selected?.label || "";
this.classId = selected?.value || "";
this.className = selected?.label || "";
const found = this.trainingList.find(
(item) => item.value === selected?.value,
);
this.signId = found?.signId || "";
this.$api
.getCheck({
current: 1,
extra: {},
model: {},
order: "descending",
size: 10,
sort: "id",
id: this.signId,
})
.then((res) => {
if (res.data.enableLocation == 1) {
console.log("定位");
this.isOpen1 = res.data.enableLocation == 1 ? true : false;
}
if (res.data.enablePhoto == 1) {
console.log("签到图片");
this.isOpen2 = res.data.enablePhoto == 1 ? true : false;
}
if (res.data.enableFace == 1) {
console.log("人脸");
this.isOpen3 = res.data.enableFace == 1 ? true : false;
}
if (res.data.phoneSign == 1) {
console.log("手机端");
this.isOpen4 = res.data.phoneSign == 1 ? true : false;
}
console.log(res.data.locationList);
this.locationList = res.data.locationList || [];
if (this.isOpen1) {
uni.getLocation({
type: "gcj02",
isHighAccuracy: true,
success: (res) => {
console.log("当前位置的经度:" + res.longitude);
console.log("当前位置的纬度:" + res.latitude);
this.longitude = res.longitude;
this.latitude = res.latitude;
// 添加标记到地图
this.addMarker();
// 转换经纬度为详细地址
this.getAddressFromCoords(res.longitude, res.latitude);
},
fail: (err) => {
uni.showToast({ title: "请打开定位权限", icon: "none" });
// console.log("获取位置失败:", err);
},
});
}
});
},
// 拍照
async takePhoto() {
const _this = this;
// iOS 设备检测
const isIOS =
/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
try {
// 检查是否有媒体设备访问权限
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
console.error("浏览器不支持 getUserMedia");
_this.fallbackToInputMethod();
return;
}
// 尝试使用 getUserMedia API
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "environment" },
});
// 初始摄像头方向
let currentFacingMode = "environment"; // 默认后置摄像头
// 创建摄像头预览界面
const cameraContainer = document.createElement("div");
cameraContainer.style.position = "fixed";
cameraContainer.style.top = "0";
cameraContainer.style.left = "0";
cameraContainer.style.width = "100%";
cameraContainer.style.height = "100%";
cameraContainer.style.backgroundColor = "black";
cameraContainer.style.zIndex = "9999";
cameraContainer.style.display = "flex";
cameraContainer.style.flexDirection = "column";
// 创建视频元素
const video = document.createElement("video");
// iOS Safari 兼容性设置
video.setAttribute("playsinline", "true");
video.setAttribute("webkit-playsinline", "true");
video.setAttribute("autoplay", "true");
video.muted = true;
video.playsInline = true;
video.srcObject = stream;
video.style.width = "100%";
video.style.height = "100%";
video.style.objectFit = "cover";
video.style.backgroundColor = "black";
// 确保视频播放
video
.play()
.then(() => {
console.log("视频播放成功");
})
.catch((err) => {
console.error("视频播放失败:", err);
});
cameraContainer.appendChild(video);
// 创建拍照按钮
const captureButton = document.createElement("button");
captureButton.textContent = "拍照";
captureButton.style.position = "absolute";
captureButton.style.bottom = "50px";
captureButton.style.left = "50%";
captureButton.style.transform = "translateX(-50%)";
captureButton.style.width = "80px";
captureButton.style.height = "80px";
captureButton.style.borderRadius = "50%";
captureButton.style.backgroundColor = "white";
captureButton.style.border = "4px solid #ddd";
captureButton.style.fontSize = "16px";
captureButton.style.fontWeight = "bold";
captureButton.style.color = "#333";
captureButton.style.cursor = "pointer";
cameraContainer.appendChild(captureButton);
// 创建取消按钮
const cancelButton = document.createElement("button");
cancelButton.textContent = "取消";
cancelButton.style.position = "absolute";
cancelButton.style.top = "20px";
cancelButton.style.right = "20px";
cancelButton.style.padding = "10px 20px";
cancelButton.style.borderRadius = "20px";
cancelButton.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
cancelButton.style.border = "none";
cancelButton.style.fontSize = "14px";
cancelButton.style.color = "white";
cancelButton.style.cursor = "pointer";
cameraContainer.appendChild(cancelButton);
// 创建切换摄像头按钮
const switchCameraButton = document.createElement("button");
switchCameraButton.textContent = "切换摄像头";
switchCameraButton.style.position = "absolute";
switchCameraButton.style.bottom = "50px";
switchCameraButton.style.right = "20px";
switchCameraButton.style.padding = "10px 20px";
switchCameraButton.style.borderRadius = "20px";
switchCameraButton.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
switchCameraButton.style.border = "none";
switchCameraButton.style.fontSize = "14px";
switchCameraButton.style.color = "white";
switchCameraButton.style.cursor = "pointer";
cameraContainer.appendChild(switchCameraButton);
// 切换摄像头函数
async function switchCamera() {
// 停止当前视频流
stream.getTracks().forEach((track) => track.stop());
// 切换摄像头方向
currentFacingMode =
currentFacingMode === "environment" ? "user" : "environment";
try {
// 获取新的视频流
const newStream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: currentFacingMode },
});
// 更新视频元素
video.srcObject = newStream;
// iOS Safari 兼容性设置
video.setAttribute("playsinline", "true");
video.setAttribute("webkit-playsinline", "true");
video.setAttribute("autoplay", "true");
video.muted = true;
await video.play();
stream = newStream;
} catch (err) {
console.error("切换摄像头失败:", err);
}
}
// 绑定切换摄像头事件
switchCameraButton.addEventListener("click", switchCamera);
// 添加到页面
document.body.appendChild(cameraContainer);
// 等待视频加载完成,带超时处理
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("视频加载超时"));
}, 10000);
if (video.onloadedmetadata) {
const originalHandler = video.onloadedmetadata;
video.onloadedmetadata = () => {
clearTimeout(timeout);
originalHandler();
resolve();
};
} else {
clearTimeout(timeout);
resolve();
}
});
// 确保视频正在播放
await video.play();
// 拍照功能
return new Promise((resolve) => {
// 点击拍照按钮
captureButton.addEventListener("click", async () => {
// 创建画布元素
const canvas = document.createElement("canvas");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
// 绘制视频帧到画布
const context = canvas.getContext("2d");
context.drawImage(video, 0, 0, canvas.width, canvas.height);
// 停止视频流
stream.getTracks().forEach((track) => track.stop());
// 移除预览界面
document.body.removeChild(cameraContainer);
// 将画布转换为图片数据
const imageData = canvas.toDataURL("image/png");
// 显示图片
_this.checkInImages = [imageData];
// 上传图片
_this.uploadImageFromDataURL(imageData);
resolve();
});
// 点击取消按钮
cancelButton.addEventListener("click", () => {
// 停止视频流
stream.getTracks().forEach((track) => track.stop());
// 移除预览界面
document.body.removeChild(cameraContainer);
resolve();
});
});
} catch (err) {
console.error("无法访问摄像头:", err);
// 降级到 uni.chooseImage 方式
_this.fallbackToInputMethod();
}
},
// 降级到 uni.chooseImage 方式
fallbackToInputMethod() {
const _this = this;
uni.chooseImage({
count: 1,
sizeType: ["original", "compressed"],
sourceType: ["camera"],
success: (res) => {
console.log("chooseImage success:", res);
_this.checkInImages = _this.checkInImages.concat(res.tempFilePaths);
_this.uploadFile(res.tempFiles[0].path);
},
fail: (err) => {
console.error("chooseImage fail:", err);
uni.showToast({
title: "无法访问摄像头,请检查权限",
icon: "none",
duration: 3000,
});
},
});
},
// 从 DataURL 上传图片
uploadImageFromDataURL(dataURL) {
const _this = this;
// 设置上传状态
_this.isUploading = true;
// 将 DataURL 转换为 Blob
fetch(dataURL)
.then((res) => res.blob())
.then((blob) => {
// 创建 FormData
const formData = new FormData();
formData.append("file", blob, "capture.png");
formData.append("bizType", "1");
formData.append("bucket", "hezedatabackups-0218");
formData.append("storageType", "ALI_OSS");
// 使用 XMLHttpRequest 上传
const xhr = new XMLHttpRequest();
xhr.open("POST", this.action);
// 设置请求头
xhr.setRequestHeader("ApplicationId", "1");
xhr.setRequestHeader(
"Authorization",
"bGFtcF93ZWI6bGFtcF93ZWJfc2VicmV0",
);
xhr.setRequestHeader(
"TenantId",
uni.getStorageSync("userinfo").tenantId,
);
xhr.setRequestHeader("Token", uni.getStorageSync("userinfo").token);
xhr.onload = function () {
// 重置上传状态
_this.isUploading = false;
if (xhr.status === 200) {
try {
const data = JSON.parse(xhr.responseText);
console.log("解析后的上传结果:", data.data.url);
_this.imagesUrl = data.data.url;
} catch (e) {
console.error("解析上传结果失败:", e);
uni.showToast({ title: "上传失败", icon: "none" });
}
} else {
console.error("上传失败:", xhr.status);
uni.showToast({ title: "上传失败", icon: "none" });
}
};
xhr.onerror = function () {
// 重置上传状态
_this.isUploading = false;
console.error("上传失败: 网络错误");
uni.showToast({ title: "上传失败", icon: "none" });
};
xhr.send(formData);
})
.catch((err) => {
// 重置上传状态
_this.isUploading = false;
console.error("上传失败:", err);
uni.showToast({ title: "上传失败", icon: "none" });
});
},
// 上传文件
uploadFile(filePath) {
const _this = this;
uni.uploadFile({
url: this.action,
filePath: filePath,
name: "file",
formData: {
bizType: "1",
bucket: "hezedatabackups-0218",
storageType: "ALI_OSS",
},
header: {
ApplicationId: 1,
Authorization: "bGFtcF93ZWI6bGFtcF93ZWJfc2VicmV0",
TenantId: uni.getStorageSync("userinfo").tenantId,
Token: uni.getStorageSync("userinfo").token,
},
success: (uploadRes) => {
try {
const data = JSON.parse(uploadRes.data);
console.log("解析后的上传结果:", data.data.url);
_this.imagesUrl = data.data.url;
} catch (e) {
console.error("解析上传结果失败:", e);
uni.showToast({ title: "上传失败", icon: "none" });
}
},
fail: (err) => {
console.error("上传失败:", err);
uni.showToast({ title: "上传失败", icon: "none" });
},
});
},
// 删除图片
deleteImage(index) {
this.checkInImages.splice(index, 1);
},
// 添加标记到地图
addMarker() {
console.log("添加标记到地图");
console.log("标记位置:", this.longitude, this.latitude);
this.markers = [
{
id: 1,
longitude: this.longitude,
latitude: this.latitude,
title: "我的位置",
iconPath: "/static/image/index/user.png",
width: 30,
height: 30,
},
];
if (this.locationList && this.locationList.length > 0) {
this.circles = this.locationList.map((item, index) => ({
id: index + 1,
latitude: item.latitude,
longitude: item.longitude,
radius: item.radius,
strokeWidth: 2,
strokeColor: "#0052d9",
fillColor: "#0052d920",
}));
}
},
// 转换经纬度为详细地址
getAddressFromCoords(longitude, latitude) {
console.log("转换经纬度为详细地址:", longitude, latitude);
// 使用高德地图的逆地理编码 API(按照官方文档格式,默认返回 JSON)
uni.request({
url: `https://restapi.amap.com/v3/geocode/regeo?location=${longitude},${latitude}&key=0cdfc7185a83660ca2c227d5901d70f0&radius=500&extensions=base`,
success: (response) => {
console.log("高德地图逆地理编码响应:", response);
if (response.data && response.data.status === "1") {
this.currentAddress = response.data.regeocode.formatted_address;
console.log("地址获取成功:", this.currentAddress);
} else {
console.log("逆地理编码失败:", response.data.info);
this.currentAddress = "获取地址失败";
}
},
fail: (error) => {
console.log("逆地理编码请求失败:", error);
this.currentAddress = "获取地址失败";
},
});
},
getCount() {
// 格式化日期为 YYYY-MM-DD 格式
const formatDate = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
};
this.$api
.checkCount({
time: formatDate(new Date()),
})
.then((res) => {
console.log(res.data);
this.count = res.data;
});
},
// 提交签到
submitCheckIn() {
// 表单验证
if (!this.selectedTraining) {
uni.showToast({ title: "请选择培训班", icon: "none" });
return;
}
if (this.checkInImages.length === 0 && this.isOpen2) {
uni.showToast({ title: "请拍摄签到图片", icon: "none" });
return;
}
// 检查图片是否正在上传
if (this.isUploading) {
uni.showToast({ title: "图片正在上传中,请稍候", icon: "none" });
return;
}
// 检查图片是否已上传成功
if (this.checkInImages.length > 0 && !this.imagesUrl) {
uni.showToast({ title: "图片上传失败,请重新拍摄", icon: "none" });
return;
}
if (this.isOpen3) {
// 显示人脸识别弹窗
this.showFaceModal = true;
this.$nextTick(() => {
this.getUserInfo();
this.initElements();
this.openCamera();
});
} else {
// 直接提交签到
this.doSubmitCheckIn();
}
},
// 计算两点之间的距离(米)
getDistance(lat1, lon1, lat2, lon2) {
const R = 6371000; // 地球半径(米)
const φ1 = (lat1 * Math.PI) / 180;
const φ2 = (lat2 * Math.PI) / 180;
const Δφ = ((lat2 - lat1) * Math.PI) / 180;
const Δλ = ((lon2 - lon1) * Math.PI) / 180;
const a =
Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
},
// 检查当前位置是否在签到范围内
isWithinSignInRange() {
if (!this.locationList || this.locationList.length === 0) {
return true; // 没有范围限制,直接通过
}
for (const location of this.locationList) {
const distance = this.getDistance(
this.latitude,
this.longitude,
location.latitude,
location.longitude,
);
if (distance <= location.radius) {
return true;
}
}
return false;
},
// 实际提交签到
doSubmitCheckIn() {
// 检查是否在签到范围内
if (this.isOpen1 && !this.isWithinSignInRange()) {
uni.showToast({
title: "不在签到地点范围内",
icon: "none",
});
return;
}
console.log("提交签到数据:", {
address: this.currentAddress,
classId: this.selectedTraining,
picture: this.imagesUrl,
notes: this.remark,
});
this.$api
.check({
address: this.currentAddress,
classId: this.classId,
className: this.className,
picture: this.imagesUrl,
notes: this.remark,
identity: uni.getStorageSync("userinfo").identity,
realName: uni.getStorageSync("userinfo").name,
tenantId: uni.getStorageSync("userinfo").tenantId,
})
.then((res) => {
if (res) {
uni.showToast({ title: "签到成功", icon: "success" });
this.getCount();
// 重置表单
this.selectedTraining = "";
this.checkInImages = [];
this.remark = "";
// 关闭人脸弹窗
this.showFaceModal = false;
this.handleCancel();
} else {
uni.showToast({ title: "签到失败", icon: "none" });
}
});
},
// 人脸识别相关方法
async getUserInfo() {
await this.$api.getMyUserInfo().then((res) => {
this.userInfo.id = res.data.id;
this.userInfo.name = res.data.name;
this.userInfo.phone = res.data.phone;
this.userInfo.identity = res.data.identity;
});
},
initElements() {
// 创建容器
this.container = this.$refs.faceContainer;
if (!this.container) return;
// 清空容器
this.container.innerHTML = "";
// 创建标题
const canvasTitle = document.createElement("p");
canvasTitle.textContent = "人脸识别";
canvasTitle.style.textAlign = "center";
canvasTitle.style.fontSize = "18px";
canvasTitle.style.fontWeight = "bold";
canvasTitle.style.color = "#333";
canvasTitle.style.marginBottom = "20px";
this.container.appendChild(canvasTitle);
// 创建圆形识别区域容器
const containerDiv = document.createElement("div");
containerDiv.style.position = "relative";
containerDiv.style.width = "280px";
containerDiv.style.height = "280px";
containerDiv.style.margin = "0 auto 30px";
containerDiv.style.borderRadius = "50%";
containerDiv.style.overflow = "hidden";
containerDiv.style.boxShadow = "0 0 20px rgba(0, 0, 0, 0.2)";
containerDiv.style.border = "4px solid #1890ff";
// 创建视频元素(圆形,放在画布下面)
const videoElement = document.createElement("video");
videoElement.id = "video";
videoElement.style.position = "absolute";
videoElement.style.top = "0";
videoElement.style.left = "0";
videoElement.style.width = "100%";
videoElement.style.height = "100%";
videoElement.style.transform = "rotateY(180deg)";
videoElement.autoplay = true;
containerDiv.appendChild(videoElement);
this.videoObj = videoElement;
// 创建画布元素(圆形,放在视频上面)
const canvasElement = document.createElement("canvas");
canvasElement.id = "canvas";
canvasElement.width = 280;
canvasElement.height = 280;
canvasElement.style.position = "absolute";
canvasElement.style.top = "0";
canvasElement.style.left = "0";
canvasElement.style.width = "100%";
canvasElement.style.height = "100%";
canvasElement.style.transform = "rotateY(180deg)";
canvasElement.style.zIndex = "1";
containerDiv.appendChild(canvasElement);
this.canvas = canvasElement;
// 添加到容器
this.container.appendChild(containerDiv);
// 添加提示文字
const hintText = document.createElement("p");
hintText.textContent = "请将脸部对准圆形区域";
hintText.style.textAlign = "center";
hintText.style.fontSize = "14px";
hintText.style.color = "#666";
hintText.style.marginBottom = "20px";
this.container.appendChild(hintText);
},
openCamera() {
this.$nextTick(() => {
if (!this.canvas) {
console.error("Canvas element not initialized");
return;
}
const context = this.canvas.getContext("2d");
if (!context) {
console.error("Unable to get 2D context from canvas");
return;
}
if (!this.videoObj) {
console.error("Video element not initialized");
return;
}
const constraints = {
video: {
width: 280,
height: 280,
facingMode: "user",
},
audio: false,
};
// eslint-disable-next-line no-undef
const tracker = new tracking.ObjectTracker("face"); // 检测人脸
tracker.setInitialScale(4);
tracker.setStepSize(2);
tracker.setEdgesDensity(0.1);
tracker.on("track", (event) => {
if (this.detectionPaused) return;
// 清空画布
context.clearRect(0, 0, this.canvas.width, this.canvas.height);
// 绘制视频帧
context.drawImage(
this.videoObj,
0,
0,
this.canvas.width,
this.canvas.height,
);
// 绘制人脸检测框
event.data.forEach((rect) => {
context.font = "16px Helvetica";
context.strokeStyle = "#1890ff";
context.lineWidth = 2;
context.strokeRect(rect.x, rect.y, rect.width, rect.height);
});
if (event.data.length !== 0) {
console.log("已识别");
this.faceDetected = true;
} else {
this.faceDetected = false;
}
});
// 先获取视频流,成功后再开始追踪
userMedia(
constraints,
(stream) => {
console.log("视频流获取成功:", stream);
if (this.videoObj) {
this.videoObj.srcObject = stream;
this.videoObj.play();
console.log("视频开始播放");
// 延迟一下确保视频已经开始播放
setTimeout(() => {
console.log("开始人脸追踪");
console.log("tracking对象:", typeof tracking);
console.log("tracker对象:", tracker);
// 启动追踪
// eslint-disable-next-line no-undef
tracking.track(this.videoObj, tracker);
console.log("追踪已启动");
}, 1000);
}
},
(error) => {
console.error("视频流获取失败:", error);
this.error(error);
},
);
});
},
// 确认选择当前检测到的人脸图像
confirmCapture() {
if (this.canvas) {
this.capturedImage = this.canvas.toDataURL("image/png");
this.detectionPaused = true;
this.uploadCapturedImage(this.capturedImage);
}
},
dataURLtoBlob(dataurl) {
return new Promise((resolve) => {
var arr = dataurl.split(",");
var mime = arr[0].match(/:(.*?);/)[1];
var bstr = atob(arr[1]);
var n = bstr.length;
var u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
resolve(new Blob([u8arr], { type: mime }));
});
},
async uploadCapturedImage(base64Data) {
try {
// 将base64转换为Blob
const blob = await this.dataURLtoBlob(base64Data);
// 创建FormData
const formData = new FormData();
formData.append("file", blob, `face_capture_${Date.now()}.png`);
Object.keys(this.formData).forEach((key) => {
formData.append(key, this.formData[key]);
});
// 使用XMLHttpRequest上传(H5原生支持)
const xhr = new XMLHttpRequest();
xhr.open("POST", this.action, true);
// 设置请求头
xhr.setRequestHeader("ApplicationId", "1");
xhr.setRequestHeader(
"Authorization",
"bGFtcF93ZWI6bGFtcF93ZWJfc2VicmV0",
);
xhr.setRequestHeader(
"TenantId",
uni.getStorageSync("userinfo").tenantId,
);
xhr.setRequestHeader("Token", uni.getStorageSync("userinfo").token);
xhr.onload = () => {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
this.userInfo.capturedUrl = data.data.url;
}
};
xhr.onerror = function () {
uni.showToast({
title: "网络错误",
icon: "none",
});
};
xhr.send(formData);
} catch (error) {
uni.showToast({
title: "上传异常",
icon: "none",
});
}
},
// 恢复人脸检测
resumeDetection() {
this.detectionPaused = false;
this.faceDetected = false;
this.capturedImage = null;
},
// 身份核验
async metaVerify() {
let res = null;
console.log(this.userInfo);
try {
res = await this.$api.postId3MetaVerify({
faceUrl: this.userInfo.capturedUrl,
userName: this.userInfo.name,
identifyNum: this.userInfo.identity,
paramType: "normal",
});
console.log(res);
if (res && res.isSuccess) {
uni.showToast({
title: "身份核验成功",
icon: "success",
});
// 身份核验成功后提交签到
this.doSubmitCheckIn();
} else {
uni.showToast({
title: "身份核验失败",
icon: "none",
});
}
} catch (e) {
uni.showToast({
title: "核验异常",
icon: "none",
});
}
},
// 关闭人脸弹窗
closeFaceModal() {
this.showFaceModal = false;
this.handleCancel();
},
handleCancel() {
if (this.videoObj && this.videoObj.srcObject) {
const tracks = this.videoObj.srcObject.getTracks();
if (tracks.length > 0) {
tracks[0].stop();
}
}
if (this.trackerTask) {
this.trackerTask.stop();
}
},
// 成功显示
success(stream) {
if (this.videoObj) {
this.videoObj.srcObject = stream;
this.videoObj.play();
}
},
// 失败抛出错误,可能用户电脑没有摄像头,或者摄像头权限没有打开
error(error) {
console.log(`访问用户媒体设备失败${error.name}, ${error.message}`);
// 检查是否是权限错误
if (
error.name === "NotAllowedError" ||
error.name === "PermissionDeniedError"
) {
uni.showModal({
title: "权限提醒",
content: "请在浏览器设置中开启摄像头权限,以便进行人脸识别",
confirmText: "知道了",
showCancel: false,
});
} else if (error.name === "NotFoundError") {
uni.showModal({
title: "设备提醒",
content: "未检测到摄像头设备,请确保您的设备有摄像头并已正确连接",
confirmText: "知道了",
showCancel: false,
});
} else {
uni.showModal({
title: "错误提醒",
content: "访问摄像头失败,请稍后重试",
confirmText: "知道了",
showCancel: false,
});
}
},
},
onLoad() {
// 设置上传地址
this.action = getUrl().VUE_APP_BASE_URL + "/file/file/anyone/upload";
// 更新当前时间
this.updateCurrentTime();
setInterval(this.updateCurrentTime, 1000);
this.getCount();
// 获取培训班列表
this.$api
.getUserBookingClassSelect({
userId: uni.getStorageSync("userinfo").id,
})
.then((res) => {
console.log("培训班列表:", res.data);
this.trainingList = res.data.map((item) => ({
value: item.id.toString(),
label: item.class_name,
signId: item.sign_config_id,
}));
console.log("转换后的培训列表:", this.trainingList);
});
},
onUnload() {
// 页面卸载时销毁地图,释放内存
if (this.map) {
this.map.destroy();
}
},
};
</script>
<style scoped>
/* 标题栏样式 */
.header {
display: flex;
align-items: center;
justify-content: space-between;
height: 100rpx;
padding: 0 30rpx;
background-color: #dceeff;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
border-bottom: 1rpx solid #e6e6e6;
}
.back-button {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
margin-top: 20rpx;
}
.right-button {
width: 120rpx;
text-align: right;
color: #0052d9;
}
/* 地图样式 */
.map-container {
position: relative;
margin-bottom: 20rpx;
}
.location-info {
position: absolute;
bottom: 20rpx;
left: 20rpx;
right: 20rpx;
background-color: rgba(255, 255, 255, 0.9);
padding: 15rpx;
border-radius: 10rpx;
display: flex;
align-items: center;
gap: 10rpx;
}
/* 表单样式 */
.form-container {
padding: 0 50rpx;
}
.form-item {
display: flex;
align-items: center;
padding: 30rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.form-label {
width: 150rpx;
font-size: 32rpx;
color: #333;
}
.form-value {
flex: 1;
font-size: 32rpx;
color: #666;
}
.select-input {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10rpx 0;
}
/* 图片上传样式 */
.image-upload {
display: flex;
align-items: center;
gap: 20rpx;
}
.upload-btn {
width: 100rpx;
height: 100rpx;
border: 1rpx dashed #ddd;
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: center;
}
.image-list {
display: flex;
gap: 15rpx;
}
.image-item {
position: relative;
width: 100rpx;
height: 100rpx;
}
.image-item image {
width: 100%;
height: 100%;
border-radius: 10rpx;
}
.image-delete {
position: absolute;
top: -10rpx;
right: -10rpx;
background-color: white;
border-radius: 50%;
}
/* 备注输入框样式 */
.remark-input {
width: 100%;
height: 150rpx;
border: 1rpx solid #ddd;
border-radius: 10rpx;
padding: 15rpx;
font-size: 30rpx;
color: #666;
resize: none;
}
/* 签到按钮样式 */
.checkin-section {
margin-top: 50rpx;
margin-bottom: 50rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 20rpx;
}
.checkin-button {
width: 200rpx;
height: 200rpx;
border-radius: 50%;
background-color: #f59a23;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
box-shadow: 0 4rpx 15rpx rgba(255, 107, 53, 0.3);
}
.checkin-button.checked {
background-color: #1676fd;
box-shadow: 0 4rpx 15rpx rgba(0, 82, 217, 0.3);
}
.button-text {
font-size: 40rpx;
font-weight: 600;
margin-bottom: 10rpx;
}
.current-time {
font-size: 28rpx;
}
.checkin-status {
font-size: 32rpx;
color: #000;
display: flex;
align-items: center;
gap: 10rpx;
}
/* 分享弹窗样式 */
.share-popup {
padding: 30rpx;
text-align: center;
}
.share-popup-title {
font-size: 36rpx;
font-weight: 600;
margin-bottom: 20rpx;
}
.share-image {
width: 400rpx;
height: 400rpx;
margin: 20rpx 0;
}
.share-tip {
font-size: 28rpx;
color: #666;
margin-bottom: 30rpx;
}
/* 人脸识别弹窗样式 */
.face-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.face-modal {
width: 90%;
max-width: 600rpx;
background-color: white;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15);
}
.face-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid #e6e6e6;
background-color: #f5f5f5;
}
.face-modal-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.face-modal-close {
padding: 10rpx;
cursor: pointer;
}
.face-modal-content {
padding: 30rpx;
max-height: 70vh;
overflow-y: auto;
}
.u_box {
padding: 20rpx;
background-color: #f5f5f5;
display: flex;
align-items: center;
margin-bottom: 30rpx;
border-radius: 10rpx;
}
.avatar {
width: 100rpx;
height: 120rpx;
background-color: #dceeff;
margin-right: 20rpx;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
color: #0052d9;
font-size: 24rpx;
}
.user_info {
flex: 1;
}
.user_info div {
margin-bottom: 8rpx;
font-size: 26rpx;
color: #333;
}
#video,
#canvas {
width: 100%;
max-width: 200px;
height: auto;
margin: 20rpx auto;
display: block;
border: 2rpx solid #e6e6e6;
border-radius: 8rpx;
}
.face-info {
text-align: center;
margin-bottom: 20rpx;
color: #666;
font-size: 26rpx;
}
.face-button {
padding: 20rpx 30rpx;
border: none;
border-radius: 8rpx;
font-size: 28rpx;
cursor: pointer;
margin: 0 10rpx;
min-width: 150rpx;
}
.confirm-button {
background-color: #0052d9;
color: white;
}
.resume-button {
background-color: #f5f5f5;
color: #333;
border: 1rpx solid #e6e6e6;
}
.verify-button {
background-color: #0052d9;
color: white;
}
/* 响应式调整 */
@media (max-width: 750rpx) {
.face-modal {
width: 95%;
}
.face-modal-content {
padding: 20rpx;
}
.face-button {
padding: 15rpx 20rpx;
font-size: 26rpx;
}
}
</style>
更多推荐
所有评论(0)