vue3使用vue-qrcode-reader组件实现扫一扫功能

5 阅读3分钟

相关文档:www.npmjs.com/package/vue…

前置工作:

npm install vue-qrcode-reader

实现流程:

<div>
    <!-- 核心扫码组件 -->
    <qrcode-stream
      v-if="isCameraActive"
      :constraints="cameraConfig"
      @detect="onDetect"
      @error="onCameraError"
    >
      <!-- 扫码界面遮罩 -->
      <div class="overlay">
        <div class="scan-frame"></div>
        <div class="tip-text">对准二维码到框内</div>
      </div>
    </qrcode-stream>
     <!-- 权限提示 -->
    <div v-if="showPermissionAlert" class="permission-alert">
      <van-icon name="warning" size="24px" />
      <p>需要摄像头权限才能扫码</p>
      <van-button type="primary" @click="retryCamera">重新授权</van-button>
    </div>
        <!-- 操作按钮组 -->
    <div class="button-group">
      <!-- 切换摄像头按钮 -->
      <van-button
        class="switch-camera-btn"
        type="primary"
        round
        @click="switchCamera"
      >
        {{ cameraType === "user" ? "后置摄像头" : "前置摄像头" }}
      </van-button>
​
      <!-- 关闭按钮 -->
      <van-button class="close-btn" type="danger" round @click="handleClose">
        关闭
      </van-button>
    </div>
</div>
.scanner-container {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: black;
  z-index: 9999;
}
​
.overlay {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 80%;
  height: 80%;
  pointer-events: none;
}
​
.scan-frame {
  position: relative;
  border: 2px solid #07c160;
  height: 60%;
  margin: 20% auto;
  border-radius: 4px;
  box-shadow: 0 0 0 100vmax rgba(0, 0, 0, 0.5);
  clip-path: inset(0 0 0 0);
}
​
/* 扫描线动画 */
.scan-frame::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 2px;
  background: linear-gradient(to right, transparent, #07c160, transparent);
  animation: scan-line 2s linear infinite;
  box-shadow: 0 0 10px #07c160;
}
​
/* 四个角的装饰 */
.scan-frame::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  border: 2px solid transparent;
  border-image: linear-gradient(45deg, #07c160, transparent, #07c160) 1;
  animation: border-glow 2s linear infinite;
}
​
/* 扫描线动画 */
@keyframes scan-line {
  0% {
    top: 0;
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
  100% {
    top: 100%;
    opacity: 1;
  }
}
​
/* 边框发光动画 */
@keyframes border-glow {
  0% {
    box-shadow: 0 0 5px #07c160;
  }
  50% {
    box-shadow: 0 0 20px #07c160;
  }
  100% {
    box-shadow: 0 0 5px #07c160;
  }
}
​
.tip-text {
  color: white;
  text-align: center;
  margin-top: 20px;
  font-size: 14px;
  text-shadow: 0 0 5px rgba(7, 193, 96, 0.5);
}
​
.button-group {
  position: fixed;
  bottom: 30px;
  left: 0;
  width: 100%;
  display: flex;
  justify-content: center;
  gap: 20px;
  z-index: 10000;
}
​
.switch-camera-btn,
.close-btn {
  min-width: 120px;
}
​
.permission-alert {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  color: white;
  text-align: center;
  width: 80%;
}
​
.permission-alert button {
  margin-top: 20px;
}
<script setup>
const isCameraActive = ref(false);
const showPermissionAlert = ref(false);
const cameraType = ref("environment"); // 'user' 前置 | 'environment' 后置
const isScanning = ref(true); // 控制是否继续扫描
let successTimer = ref();
let errorTimer = ref();    
 // 计算摄像头配置
const cameraConfig = computed(() => ({
  facingMode: cameraType.value,
}));   
// 切换摄像头
const switchCamera = () => {
  cameraType.value = cameraType.value === "user" ? "environment" : "user";
  // 重新初始化摄像头
  initCamera();
};
// 扫码结果处理
const onDetect = (result) => {
      if (!isScanning.value) return; // 如果已停止扫描,不处理结果
      const decodedText = result[0]?.rawValue;
      console.log(decodedText)
}
// 摄像头错误处理
const onCameraError = async (error) => {
  console.error("摄像头错误:", error);
  if (error.name === "NotAllowedError") {
    showPermissionAlert.value = true;
  } else if (error.name === "NotFoundError") {
    // console.log("未找到摄像头", "未找到摄像头");
     showToast("未找到摄像头");s
  }
};
// 重新尝试授权
const retryCamera = () => {
  showPermissionAlert.value = false;
  initCamera();
};
// 初始化摄像头
const initCamera = async () => {
  try {
    let devices = await navigator.mediaDevices.enumerateDevices();
    let videoDevices = devices.filter(
        (device) => device.kind === "videoinput"
    );
/*    console.log(videoDevices.length > 0,'videoDevices');
    console.log(videoDevices[0].deviceId !== '','videoDevices');*/
    // 权限检测逻辑(通过 deviceId 是否为空判断)
    const hasPermission = videoDevices.length > 0 && videoDevices[0].deviceId !== '';
    // console.log('hasPermission-是否已获取摄像头权限',hasPermission)
    // 未获取权限时的处理
    if (!hasPermission) {
      // console.log('尚未获得摄像头权限,开始请求权限...');
      //触发权限弹窗并获取流
      const stream = await navigator.mediaDevices.getUserMedia({
        video: { facingMode: cameraType.value }
      });
      //停止初始化的媒体流(仅用于触发权限)
      stream.getTracks().forEach(track => track.stop());
      // 重新枚举获取完整设备列表
      devices = await navigator.mediaDevices.enumerateDevices();
      videoDevices = devices.filter(device => device.kind === "videoinput");
    }
    // console.log("可用摄像头:", videoDevices);
    if (videoDevices.length === 0) {
      throw new Error("NotFoundError");
    }
    isCameraActive.value = true;
    isScanning.value = true; // 确保扫描状态为开启
/*    console.log("当前选择的摄像头类型:", cameraType.value);
    console.log("可用摄像头列表:", videoDevices);*/
  } catch (error) {
    await onCameraError(error);
  }
};
// 关闭扫码
const handleClose = () => {
  isScanning.value = false;
  isCameraActive.value = false;
  router.go(-1);
};
onMounted(() => {
  initCamera();
});
</script>