一、效果展示

在这里插入图片描述

二、核心代码实现

<template>
  <view class="login-container container">
   <wd-button
          plain
          class="login-button"
          :round="false"
          size="large"
          @tap="scanQRCode"
        >
          <wd-icon name="scan" size="22px" class="m-r-2"></wd-icon>
          扫一扫登录
        </wd-button>
  </view>
</template>

<script setup lang="ts">
import { onLoad, onShow } from "@dcloudio/uni-app";
import { ref, onMounted } from "vue";
import { login } from "@/api/user.ts";
import VersionInfo from "@/components/VersionInfo.vue";
import { App_CONFIG } from "@/config/appConfig.ts";
// 引入jsQR库
import jsQR from "jsqr";

 // 通过环境判断平台类型
function scanQRCode() {
 
  const isH5 = typeof window !== "undefined";

  if (isH5) {
    try {
      if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
        uni.showToast({
          title: "当前浏览器不支持此功能",
          icon: "fail",
        });
        return;
      }

      scanQRCodeOnH5(); // H5环境使用自定义扫码实现
    } catch (error) {}
  } else {
    scanQRCodeOnApp(); // App环境使用uni.scanCode
  }
}

// H5环境下扫描二维码实现
function scanQRCodeOnH5() {
  // 创建视频和画布元素
  const video = document.createElement("video");
  const canvas = document.createElement("canvas");
  const canvasContext = canvas.getContext("2d");

  video.style.cssText = `
    
    width: 100%;
    height: 60%;
    
  `;
  // 创建扫码弹窗
  const scanContainer = document.createElement("div");
  scanContainer.style.cssText = `
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.8);
    z-index: 9999;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    color: white;
  `;

  const scanTip = document.createElement("div");
  scanTip.innerText = "将二维码放入框内,即可自动扫描";
  scanTip.style.cssText = `
    margin-bottom: 10px;
    font-size: 16px;
  `;

  const closeBtn = document.createElement("button");
  closeBtn.innerText = "取消";
  closeBtn.style.cssText = `
    margin-top: 10px;
    padding: 10px 20px;
    background: #ff5555;
    color: white;
    border: none;
    border-radius: 4px;
    font-size: 16px;
  `;

  scanContainer.appendChild(scanTip);
  scanContainer.appendChild(video);
  scanContainer.appendChild(closeBtn);
  document.body.appendChild(scanContainer);

  // 关闭扫码界面
  closeBtn.onclick = () => {
    if (stream) {
      stream.getTracks().forEach((track) => track.stop());
    }
    document.body.removeChild(scanContainer);
  };

  let stream: MediaStream | null = null;

  // 获取摄像头权限
  navigator.mediaDevices
    .getUserMedia({ video: { facingMode: "environment" } })
    .then((mediaStream) => {
      var videoTracks = mediaStream.getVideoTracks();
      let hasCamera = false;
      videoTracks.forEach((track) => {
        if (track.kind == "video" && track.label !== "Intel Virtual Camera") {
          hasCamera = true;
        }
      });
      if (!hasCamera) {
        uni.showToast({
          title: "请检查是否已打开摄像头权限",
          icon: "fail",
        });
        return;
      }

      video.setAttribute("autoplay", "");
      video.setAttribute("muted", ""); /*静音播放属性,微信环境中可删除*/
      video.setAttribute("playsinline", "true"); /*IOS微信浏览器支持小窗内播放*/
      video.setAttribute("preload", "auto"); /*是否自动播放*/
      video.setAttribute(
        "webkit-playsinline",
        "true"
      ); /*这个属性是ios 10中设置可以让视频在小窗内播放,即不全屏播放*/

      stream = mediaStream;
      video.srcObject = mediaStream;
      video.play();

      // 设置画布尺寸
      video.addEventListener("loadedmetadata", () => {
        canvas.width = video.videoWidth;
        canvas.height = video.videoHeight;

        // 开始扫描
        scanFrame();
      });
    })
    .catch((err) => {
      console.error("无法访问摄像头:", err);
      let errorMsg = "无法访问摄像头";
      let showPermissionGuide = false;

      if (
        err.name === "NotAllowedError" ||
        err.name === "PermissionDeniedError"
      ) {
        errorMsg = "摄像头权限被拒绝";
        showPermissionGuide = true;
      } else if (err.name === "NotReadableError") {
        errorMsg = "摄像头无法读取,请检查权限设置";
        showPermissionGuide = true;
      } else if (
        err.name === "NotFoundError" ||
        err.name === "OverconstrainedError"
      ) {
        errorMsg = "未找到可用的摄像头设备";
      } else if (err.name === "AbortError") {
        errorMsg = "摄像头访问被中断";
      }

      if (showPermissionGuide) {
        uni.showModal({
          title: "需要摄像头权限",
          content: "扫码功能需要访问摄像头,请在系统设置中找到本应用并开启相机权限",
          confirmText: "去设置",
          cancelText: "取消",
          success: function (res) {
            if (res.confirm) {
              gotoAppPermissionSetting();
            }
          },
        });
      } else {
        uni.showToast({
          title: errorMsg,
          icon: "fail",
        });
      }

      if (scanContainer && document.body.contains(scanContainer)) {
        document.body.removeChild(scanContainer);
      }
    });
  // 添加跳转到应用权限设置的函数
  function gotoAppPermissionSetting() {
    // 判断平台
    const platform = uni.getSystemInfoSync().platform;

    // Android平台
    if (platform === "android") {
      // uni-app 提供的打开系统设置方法
      plus.runtime.openURL("package:" + plus.runtime.appid);
    }
    // iOS平台
    else if (platform === "ios") {
      // iOS需要通过特定方式打开设置
      plus.runtime.openURL("app-settings:");
    }
    // 其他情况提示用户手动设置
    else {
      uni.showToast({
        title: "请在系统设置中找到本应用并开启相机权限",
        icon: "none",
        duration: 3000,
      });
    }
  }
  // 扫描帧
  function scanFrame() {
    if (video.readyState === video.HAVE_ENOUGH_DATA) {
      canvasContext.drawImage(video, 0, 0, canvas.width, canvas.height);
      const imageData = canvasContext.getImageData(
        0,
        0,
        canvas.width,
        canvas.height
      );

      // 使用jsQR解析二维码
      const code = jsQR(imageData.data, imageData.width, imageData.height);

      if (code) {
        // 扫码成功
        handleQRCodeResult(code.data);
        // 停止摄像头并关闭界面
        if (stream) {
          stream.getTracks().forEach((track) => track.stop());
        }
        document.body.removeChild(scanContainer);
        return;
      }
    }

    // 继续扫描下一帧
    requestAnimationFrame(scanFrame);
  }
}
function scanQRCodeOnApp() {
  uni.scanCode({
    success: (res) => {
      console.log("扫码结果:", res.result);
      handleQRCodeResult(res.result); // 处理扫码结果
    },
    fail: (err) => {
      console.error("扫码失败:", err);
      uni.showToast({
        title: "扫码失败",
        icon: "none",
      });
    },
  });
}
// 处理二维码扫描结果
function handleQRCodeResult(result: string) {
  console.log("二维码内容:", result);

}

function getUserMediaCompatible() {
  // 老的浏览器可能根本没有实现 mediaDevices,所以我们可以先设置一个空的对象
  if (navigator.mediaDevices === undefined) {
    navigator.mediaDevices = {};
  }

  // 一些浏览器部分支持 mediaDevices。我们不能直接给对象设置 getUserMedia
  // 因为这样可能会覆盖已有的属性。这里我们只会在没有 getUserMedia 属性的时候添加它。
  if (navigator.mediaDevices.getUserMedia === undefined) {
    navigator.mediaDevices.getUserMedia = function (constraints) {
      // 首先,如果有 getUserMedia 的话,就获得它
      var getUserMedia =
        navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

      // 一些浏览器根本没实现它 - 那么就返回一个 error 到 promise 的 reject 来保持一个统一的接口
      if (!getUserMedia) {
        return Promise.reject(
          new Error("getUserMedia is not implemented in this browser")
        );
      }

      // 否则,为老的 navigator.getUserMedia 方法包裹一个 Promise
      return new Promise(function (resolve, reject) {
        getUserMedia.call(navigator, constraints, resolve, reject);
      });
    };
  }
}
onMounted(() => {
  getUserMediaCompatible();
});
</script>



三、遇到的问题及解决方案

在IOS浏览器中摄像头没有成功调起,设置以下属性即可解决

      video.setAttribute("playsinline", "true"); /*IOS微信浏览器支持小窗内播放*/
      video.setAttribute("preload", "auto"); /*是否自动播放*/
      video.setAttribute(
        "webkit-playsinline",
        "true"
      ); /*这个属性是ios 10中设置可以让视频在小窗内播放,即不全屏播放*/

更多推荐