Просмотр исходного кода

fix(uniapp-scan): H5 端扫码加 jsQR fallback 兼容老浏览器(iOS Safari < 17 / 老 WebView)

上一版只用 BarcodeDetector,但它在以下浏览器不可用:
- iOS Safari < 17
- 老安卓 WebView / 微信内置浏览器老版本
- Firefox

修法:装 jsQR(纯 JS,50KB,无 WASM 依赖)做 fallback
- 优先用 BarcodeDetector(性能好,Chrome/Edge/iOS 17+ 走这条)
- 不可用时 → jsQR:<canvas> 抽帧 + getImageData 识别
- 命中后走同一份 verifyQrCode 校验流程

影响:inspection/scan + qualityInspection/scan 两边
依赖新增:jsqr@1.4.0
yangjingjing 16 часов назад
Родитель
Сommit
7b991a50d3
4 измененных файлов с 111 добавлено и 42 удалено
  1. 6 0
      package-lock.json
  2. 1 0
      package.json
  3. 55 24
      src/pages/inspection/scan/index.vue
  4. 49 18
      src/pages/qualityInspection/scan/index.vue

+ 6 - 0
package-lock.json

@@ -13,6 +13,7 @@
         "@dcloudio/uni-components": "3.0.0-4030620241128001",
         "@dcloudio/uni-h5": "3.0.0-4030620241128001",
         "@dcloudio/uni-mp-weixin": "3.0.0-4030620241128001",
+        "jsqr": "^1.4.0",
         "vue": "^3.4.21",
         "vue-i18n": "^9.1.9"
       },
@@ -7906,6 +7907,11 @@
         "graceful-fs": "^4.1.6"
       }
     },
+    "node_modules/jsqr": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/jsqr/-/jsqr-1.4.0.tgz",
+      "integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A=="
+    },
     "node_modules/kleur": {
       "version": "3.0.3",
       "resolved": "https://registry.npmmirror.com/kleur/-/kleur-3.0.3.tgz",

+ 1 - 0
package.json

@@ -16,6 +16,7 @@
     "@dcloudio/uni-components": "3.0.0-4030620241128001",
     "@dcloudio/uni-h5": "3.0.0-4030620241128001",
     "@dcloudio/uni-mp-weixin": "3.0.0-4030620241128001",
+    "jsqr": "^1.4.0",
     "vue": "^3.4.21",
     "vue-i18n": "^9.1.9"
   },

+ 55 - 24
src/pages/inspection/scan/index.vue

@@ -39,6 +39,8 @@ import {
   ensureLocationAuth
 } from '@/utils/location'
 import { verifyQrCode } from '@/api/inspection'
+// jsQR:纯 JS QR 识别库,作为 BarcodeDetector 不可用时的 fallback(iOS Safari < 17 / 老 WebView)
+import jsQR from 'jsqr'
 
 const taskId = ref<number>(0)
 const scanResult = ref<string>('')
@@ -80,19 +82,13 @@ function startScan() {
 
 // ============================== H5 端扫码实现 ==============================
 async function h5StartBarcodeScanner() {
-  // 1. 能力检测
-  const BarcodeDetectorCtor: any = (window as any).BarcodeDetector
-  if (typeof BarcodeDetectorCtor !== 'function') {
-    tipText.value = '当前浏览器不支持原生扫码,请点击下方"手动输入"'
-    return
-  }
-  // 2. 检测摄像头权限 API
+  // 1. 检测摄像头权限 API
   if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
     tipText.value = '浏览器不支持获取摄像头,请点击下方"手动输入"'
     return
   }
   try {
-    // 3. 申请摄像头(优先后置)
+    // 2. 申请摄像头(优先后置)
     h5Stream = await navigator.mediaDevices.getUserMedia({
       video: { facingMode: { ideal: 'environment' } },
       audio: false
@@ -109,7 +105,7 @@ async function h5StartBarcodeScanner() {
     return
   }
 
-  // 4. 创建 <video> 元素并播放
+  // 3. 创建 <video> 元素并播放
   const wrap = document.getElementById('h5-video-wrap')
   if (!wrap) return
   wrap.innerHTML = ''
@@ -128,27 +124,62 @@ async function h5StartBarcodeScanner() {
     console.warn('[H5 扫码] video.play 失败', e)
   }
 
-  // 5. 循环 detect 二维码
-  let detector: any
-  try {
-    detector = new BarcodeDetectorCtor({ formats: ['qr_code'] })
-  } catch (e) {
-    tipText.value = '浏览器不支持 QR 码识别,请点击下方"手动输入"'
+  // 4. 选 QR 识别方案:
+  //    优先 BarcodeDetector(Chrome/Edge/iOS 17+ 性能好),
+  //    不可用时 fallback jsQR(纯 JS,兼容老浏览器但稍慢)
+  const BarcodeDetectorCtor: any = (window as any).BarcodeDetector
+  if (typeof BarcodeDetectorCtor === 'function') {
+    let detector: any
+    try {
+      detector = new BarcodeDetectorCtor({ formats: ['qr_code'] })
+    } catch (e) {
+      // ignore
+    }
+    if (detector) {
+      const tick = async () => {
+        if (!h5Stream) return
+        try {
+          const codes = await detector.detect(video)
+          if (codes && codes.length && codes[0].rawValue) {
+            h5StopScanner()
+            scanResult.value = codes[0].rawValue
+            onScanSuccess(codes[0].rawValue)
+            return
+          }
+        } catch (e) {
+          // 单帧失败忽略
+        }
+        h5RafId = requestAnimationFrame(tick)
+      }
+      h5RafId = requestAnimationFrame(tick)
+      return
+    }
+  }
+
+  // 5. Fallback:jsQR(用 canvas 抽帧识别)
+  tipText.value = '正在用 jsQR 识别中…'
+  const canvas = document.createElement('canvas')
+  const ctx = canvas.getContext('2d', { willReadFrequently: true })
+  if (!ctx) {
+    tipText.value = '当前浏览器不支持 QR 码识别,请点击下方"手动输入"'
     return
   }
-  const tick = async () => {
+  const tick = () => {
     if (!h5Stream) return
-    try {
-      const codes = await detector.detect(video)
-      if (codes && codes.length && codes[0].rawValue) {
-        // 命中:停摄像头 + 触发回调
+    if (video.readyState === video.HAVE_ENOUGH_DATA) {
+      canvas.width = video.videoWidth
+      canvas.height = video.videoHeight
+      ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
+      const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
+      const code = jsQR(imageData.data, imageData.width, imageData.height, {
+        inversionAttempts: 'dontInvert'
+      })
+      if (code && code.data) {
         h5StopScanner()
-        scanResult.value = codes[0].rawValue
-        onScanSuccess(codes[0].rawValue)
+        scanResult.value = code.data
+        onScanSuccess(code.data)
         return
       }
-    } catch (e) {
-      // 单帧 detect 失败忽略(视频流可能还没准备好)
     }
     h5RafId = requestAnimationFrame(tick)
   }

+ 49 - 18
src/pages/qualityInspection/scan/index.vue

@@ -39,6 +39,8 @@ import {
   ensureLocationAuth
 } from '@/utils/location'
 import { verifyQualityQrCode, getScanRangeMeter } from '@/api/qualityInspection'
+// jsQR:纯 JS QR 识别库,作为 BarcodeDetector 不可用时的 fallback
+import jsQR from 'jsqr'
 
 const taskId = ref<number>(0)
 const scanResult = ref<string>('')
@@ -82,11 +84,6 @@ function startScan() {
 
 // ============================== H5 端扫码实现 ==============================
 async function h5StartBarcodeScanner() {
-  const BarcodeDetectorCtor: any = (window as any).BarcodeDetector
-  if (typeof BarcodeDetectorCtor !== 'function') {
-    tipText.value = '当前浏览器不支持原生扫码,请点击下方"手动输入"'
-    return
-  }
   if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
     tipText.value = '浏览器不支持获取摄像头,请点击下方"手动输入"'
     return
@@ -124,25 +121,59 @@ async function h5StartBarcodeScanner() {
   } catch (e) {
     console.warn('[H5 扫码] video.play 失败', e)
   }
-  let detector: any
-  try {
-    detector = new BarcodeDetectorCtor({ formats: ['qr_code'] })
-  } catch (e) {
-    tipText.value = '浏览器不支持 QR 码识别,请点击下方"手动输入"'
+  // 优先 BarcodeDetector,不可用则 jsQR fallback
+  const BarcodeDetectorCtor: any = (window as any).BarcodeDetector
+  if (typeof BarcodeDetectorCtor === 'function') {
+    let detector: any
+    try {
+      detector = new BarcodeDetectorCtor({ formats: ['qr_code'] })
+    } catch (e) {
+      // ignore
+    }
+    if (detector) {
+      const tick = async () => {
+        if (!h5Stream) return
+        try {
+          const codes = await detector.detect(video)
+          if (codes && codes.length && codes[0].rawValue) {
+            h5StopScanner()
+            scanResult.value = codes[0].rawValue
+            onScanSuccess(codes[0].rawValue)
+            return
+          }
+        } catch (e) {
+          // ignore
+        }
+        h5RafId = requestAnimationFrame(tick)
+      }
+      h5RafId = requestAnimationFrame(tick)
+      return
+    }
+  }
+  // jsQR fallback
+  tipText.value = '正在用 jsQR 识别中…'
+  const canvas = document.createElement('canvas')
+  const ctx = canvas.getContext('2d', { willReadFrequently: true })
+  if (!ctx) {
+    tipText.value = '当前浏览器不支持 QR 码识别,请点击下方"手动输入"'
     return
   }
-  const tick = async () => {
+  const tick = () => {
     if (!h5Stream) return
-    try {
-      const codes = await detector.detect(video)
-      if (codes && codes.length && codes[0].rawValue) {
+    if (video.readyState === video.HAVE_ENOUGH_DATA) {
+      canvas.width = video.videoWidth
+      canvas.height = video.videoHeight
+      ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
+      const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
+      const code = jsQR(imageData.data, imageData.width, imageData.height, {
+        inversionAttempts: 'dontInvert'
+      })
+      if (code && code.data) {
         h5StopScanner()
-        scanResult.value = codes[0].rawValue
-        onScanSuccess(codes[0].rawValue)
+        scanResult.value = code.data
+        onScanSuccess(code.data)
         return
       }
-    } catch (e) {
-      // 单帧失败忽略
     }
     h5RafId = requestAnimationFrame(tick)
   }