UNIAPP扫码二维码和识别上传图片的二维码

Lirioing2025/06/17UNIAPPFEVUEUNIAPP

前言

要调研H5网页扫描二维码的功能,经过两天的努力,最终选择了 html5-qrcode 库。在使用这个之前,遇到了几个问题,在此分享一下:

  1. 手机拍摄的图片上传就会识别不到,发现是图片太大导致,所以图片上传时需要压缩图片大小。
  2. 如果二维码的图片是侧着拍,也大概率识别不出来。
  3. 调用手机摄像头扫码,需要在 https 环境下或者本地调试环境 localhost

扫码组件

安装库

npm install html5-qrcode

创建组件 lr-scan.vue

<template>
  <view class="lr-scan">
     <view class="lr-scan__box">
        <!-- 扫描框 -->
        <view class="scan-frame">
          <view id="qr-reader" class="qr-reader"></view>
          <view class="corner top-left"></view>
          <view class="corner top-right"></view>
          <view class="corner bottom-left"></view>
          <view class="corner bottom-right"></view>
          <view class="scan-line" :style="{ transform: `translateY(${scanLinePosition}px)` }"></view>
        </view>
        <!-- <button @click="startScan">开始扫描</button> -->
     </view>
  </view>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
import { Html5Qrcode, Html5QrcodeScanType } from 'html5-qrcode';

const html5QrCode = ref(null)
const isScanning = ref(false)
const scanResult = ref('')
const errorMessage = ref('')

onMounted(() => {
  html5QrCode.value = new Html5Qrcode('qr-reader')
  startScan()
})
onUnmounted(() => {
  isScanning.value = false
})

// 开始扫描
const startScan = async () => {
  startScanLineAnimation()
  try {
    errorMessage.value = '';
    
    const config = {
      fps: 10,
      qrbox: { width: 250, height: 250 },
      aspectRatio: 1,
      supportedScanTypes: [Html5QrcodeScanType.SCAN_TYPE_CAMERA]
    };
    
    await html5QrCode.value.start(
      { facingMode: "environment" },  // 优先使用后置摄像头
      config,
      onScanSuccess,
      onScanFailure
    );
    
    isScanning.value = true;
  } catch (err) {
    handleError(err);
  }
}
// 结束扫描
const stopScan = async () => {
  if (html5QrCode.value && isScanning.value) {
    try {
    await html5QrCode.value.stop();
    } catch (err) {
      handleError(err);
    } finally {
      isScanning.value = false;
    }
  } 
}
const emit = defineEmits(['finish', 'quit'])
// 扫码成功
const onScanSuccess = async (decodedText, decodedResult) => {
  scanResult.value = decodedText;
  await stopScan()
  stopScanLineAnimation()
  emit('finish', decodedText)
}
// 扫码失败
const onScanFailure = (error) => {
  // uni.showToast({
  //   title: String('onScanFailure' + error)
  // })
  console.warn('扫码失败:', error);
}
// 处理报错
const handleError = async (error) => {
  errorMessage.value = getErrorMessage(error);
  if (errorMessage.value) {
    await stopScan()
    uni.showToast({
      title: errorMessage.value
    })
    emit('quit')
  }
}
// 获取错误信息
const getErrorMessage = (error) => {
  if (error.includes('NotAllowedError')) {
    return '请允许摄像头访问权限';
  } else if (error.includes('NotFoundError')) {
    return '未找到可用摄像头';
  } else {
    return '扫码功能出错: ' + error;
  }
}

// 配置参数
const scanLineSpeed = 1; // 扫描线移动速度
const scanLinePosition = ref(0);
const scanLineDirection = ref(1);
let scanLineInterval = null;

// 开始扫描线动画
const startScanLineAnimation = () => {
  scanLinePosition.value = 0;
  scanLineDirection.value = 1;
  
  scanLineInterval = setInterval(() => {
    // 更新扫描线位置
    scanLinePosition.value += scanLineSpeed * scanLineDirection.value;
    
    // 反转方向
    if (scanLinePosition.value >= 250 || scanLinePosition.value <= 0) {
      scanLineDirection.value *= -1;
    }
  }, 16); // 约60fps
};

// 停止扫描线动画
const stopScanLineAnimation = () => {
  if (scanLineInterval) {
    clearInterval(scanLineInterval);
    scanLineInterval = null;
    scanLinePosition.value = 0;
    scanLineDirection.value = 1;
  }
};
</script>
<style lang="scss" scoped>
.lr-scan {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  width: 100%;
  height: 100%;
  z-index: 9999; /* 确保在最上层 */
  background-color: #000;
}
/* 修改扫描框样式 */
#qr-reader {
  position: absolute !important;
  width: 250px !important;
  height: 250px !important;
}
.scan-frame {
  position: absolute;
  width: 250px;
  height: 250px;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%); /* 自身宽高的一半 */
  z-index: 999;
  border: 2rpx solid rgba(59, 130, 246, 0.8);
  box-shadow: 0 0 30rpx rgba(59, 130, 246, 0.5);
}

.corner {
  position: absolute;
  width: 30rpx;
  height: 30rpx;
  border-width: 6rpx;
  border-color: #3b82f6;
}

.top-left {
  top: -6rpx;
  left: -6rpx;
  border-top-style: solid;
  border-left-style: solid;
  border-radius: 6rpx 0 0 0;
}

.top-right {
  top: -6rpx;
  right: -6rpx;
  border-top-style: solid;
  border-right-style: solid;
  border-radius: 0 6rpx 0 0;
}

.bottom-left {
  bottom: -6rpx;
  left: -6rpx;
  border-bottom-style: solid;
  border-left-style: solid;
  border-radius: 0 0 0 6rpx;
}

.bottom-right {
  bottom: -6rpx;
  right: -6rpx;
  border-bottom-style: solid;
  border-right-style: solid;
  border-radius: 0 0 6rpx 0;
}

.scan-line {
  position: absolute;
  width: 100%;
  height: 2rpx;
  background: linear-gradient(90deg, rgba(59, 130, 246, 0) 0%, rgba(59, 130, 246, 0.8) 50%, rgba(59, 130, 246, 0) 100%);
  box-shadow: 0 0 15rpx rgba(59, 130, 246, 0.8);
  transition: transform 0.016s linear;
}
</style>

使用组件

<template>
  <view class="home">
    <button @click="startScan">开始扫描</button>
    <view>{{ scanResult || '暂无扫描结果' }}</view>
    <view id="qr" class="qr"></view>
    <lr-scan v-if="showScaning" @finish="getScanResult" @quit="getScanResult"></lr-scan>
    <button @click="chooseImage">上传图片</button>
  </view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { Html5Qrcode } from 'html5-qrcode';
const html5QrCode = ref(null)
onMounted(() => {
  html5QrCode.value = new Html5Qrcode('qr');
})

const showScaning = ref(false)
const scanResult = ref('')
const startScan = () => {
  showScaning.value = true
}

const getScanResult = (res) => {
  showScaning.value = false
  scanResult.value = res
}
const originalFile = ref(null)
const compressedBlob = ref(null)
const compressedFile = ref(null)
const chooseImage = async () => {
  const res = await uni.chooseImage({
    count: 1, //默认9
    sourceType: ['ablum', 'camera'], // 从相册选择图片
    sizeType: ['original'], // 使用原图
    quality: 100 // 适当压缩但保持清晰度
  });
  originalFile.value = res.tempFiles[0]
  // 压缩图片后得到的blob文件
  compressedBlob.value = await compressImage(originalFile.value, {
    quality: 0.75,
    maxWidth: 800,
    maxHeight: 800,
  })
  // 转换为 File 对象
  compressedFile.value = new File(
    [compressedBlob.value],          // Blob 数据数组
    originalFile.value.name || 'compressed-image.jpg',    // 文件名
    { type: compressedBlob.value.type } // 文件类型
  );
  compressedFile.value.path = URL.createObjectURL(compressedBlob.value)
  scanQRCode(compressedFile.value)
}
const sacnErr = ref('')
const scanQRCode = (file) => {
   // // 创建实例 (不需要挂载到DOM)
  html5QrCode.value.scanFile(file, false)
  .then(decodedText => {
    scanResult.value = decodedText
    uni.showToast({
      title: decodedText,
      icon: 'none'
    })
  }).catch(err => {
    console.log(err)
    sacnErr.value = err
    uni.showToast({
      title: String(err),
      icon: 'fail'
    })
  })
}

const compressImage = (file, options) => {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = function(event) {
      const img = new Image();
      img.onload = function() {
        const canvas = document.createElement('canvas');
        let width = img.width;
        let height = img.height;
        
        // 按比例缩放
        if (width > height) {
          if (width > options.maxWidth) {
            height *= options.maxWidth / width;
            width = options.maxWidth;
          }
        } else {
          if (height > options.maxHeight) {
            width *= options.maxHeight / height;
            height = options.maxHeight;
          }
        }
        
        canvas.width = width;
        canvas.height = height;
        
        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0, width, height);
        
        // 转换为Blob对象
        canvas.toBlob(
          blob => resolve(blob), // 这里返回的是 Blob 对象
          file.type || 'image/jpeg',
          options.quality
        );
      };
      img.onerror = reject;
      img.src = event.target.result;
    };
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
};

</script>

image-20250617102943690

上次更新 2025/12/6 16:52:48