获取当前位置,以及对比是否在签到范围内

<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>

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐