|
@@ -1,5 +1,10 @@
|
|
|
<template>
|
|
<template>
|
|
|
<view class="scan-page">
|
|
<view class="scan-page">
|
|
|
|
|
+ <!-- H5 端摄像头预览容器(由 JS 动态创建 video) -->
|
|
|
|
|
+ <!-- #ifdef H5 -->
|
|
|
|
|
+ <view id="h5-video-wrap" class="h5-video-wrap"></view>
|
|
|
|
|
+ <!-- #endif -->
|
|
|
|
|
+
|
|
|
<!-- 顶部黑色遮罩 -->
|
|
<!-- 顶部黑色遮罩 -->
|
|
|
<view class="overlay top-overlay"></view>
|
|
<view class="overlay top-overlay"></view>
|
|
|
<view class="overlay bottom-overlay"></view>
|
|
<view class="overlay bottom-overlay"></view>
|
|
@@ -16,7 +21,7 @@
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
<view class="scan-tip">
|
|
<view class="scan-tip">
|
|
|
- <text>请将摄像头对准巡检点位的二维码</text>
|
|
|
|
|
|
|
+ <text>{{ tipText }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
<view class="scan-btn-row">
|
|
<view class="scan-btn-row">
|
|
@@ -26,7 +31,7 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
-import { ref, onMounted, onUnmounted } from 'vue'
|
|
|
|
|
|
|
+import { ref, onUnmounted } from 'vue'
|
|
|
import { onLoad } from '@dcloudio/uni-app'
|
|
import { onLoad } from '@dcloudio/uni-app'
|
|
|
import {
|
|
import {
|
|
|
calculateDistance,
|
|
calculateDistance,
|
|
@@ -37,6 +42,7 @@ import { verifyQrCode } from '@/api/inspection'
|
|
|
|
|
|
|
|
const taskId = ref<number>(0)
|
|
const taskId = ref<number>(0)
|
|
|
const scanResult = ref<string>('')
|
|
const scanResult = ref<string>('')
|
|
|
|
|
+const tipText = ref<string>('请将摄像头对准巡检点位的二维码')
|
|
|
|
|
|
|
|
onLoad((q) => {
|
|
onLoad((q) => {
|
|
|
taskId.value = Number(q?.taskId || 0)
|
|
taskId.value = Number(q?.taskId || 0)
|
|
@@ -44,10 +50,12 @@ onLoad((q) => {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
let scanTimer: any = null
|
|
let scanTimer: any = null
|
|
|
|
|
+let h5Stream: MediaStream | null = null
|
|
|
|
|
+let h5RafId: number | null = null
|
|
|
|
|
|
|
|
function startScan() {
|
|
function startScan() {
|
|
|
- // uniapp 跨端扫码(仅 APP 与小程序支持原生扫码,H5 退化为手动输入)
|
|
|
|
|
// #ifdef APP-PLUS || MP-WEIXIN
|
|
// #ifdef APP-PLUS || MP-WEIXIN
|
|
|
|
|
+ // APP/小程序:调起原生扫码(uni.scanCode 会自动开摄像头 + 弹原生 UI)
|
|
|
scanTimer = setTimeout(() => {
|
|
scanTimer = setTimeout(() => {
|
|
|
uni.scanCode({
|
|
uni.scanCode({
|
|
|
onlyFromCamera: true,
|
|
onlyFromCamera: true,
|
|
@@ -57,15 +65,112 @@ function startScan() {
|
|
|
onScanSuccess(res.result)
|
|
onScanSuccess(res.result)
|
|
|
},
|
|
},
|
|
|
fail: () => {
|
|
fail: () => {
|
|
|
- // 扫码失败:用户取消
|
|
|
|
|
|
|
+ // 用户取消
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
}, 300)
|
|
}, 300)
|
|
|
// #endif
|
|
// #endif
|
|
|
|
|
+
|
|
|
|
|
+ // #ifdef H5
|
|
|
|
|
+ // H5 端:用浏览器原生 BarcodeDetector + <video> 预览
|
|
|
|
|
+ // 浏览器不支持(iOS Safari < 16.4)→ 提示降级到"手动输入"
|
|
|
|
|
+ h5StartBarcodeScanner()
|
|
|
|
|
+ // #endif
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ============================== H5 端扫码实现 ==============================
|
|
|
|
|
+async function h5StartBarcodeScanner() {
|
|
|
|
|
+ // 1. 能力检测
|
|
|
|
|
+ const BarcodeDetectorCtor: any = (window as any).BarcodeDetector
|
|
|
|
|
+ if (typeof BarcodeDetectorCtor !== 'function') {
|
|
|
|
|
+ tipText.value = '当前浏览器不支持原生扫码,请点击下方"手动输入"'
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ // 2. 检测摄像头权限 API
|
|
|
|
|
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
|
|
|
+ tipText.value = '浏览器不支持获取摄像头,请点击下方"手动输入"'
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 3. 申请摄像头(优先后置)
|
|
|
|
|
+ h5Stream = await navigator.mediaDevices.getUserMedia({
|
|
|
|
|
+ video: { facingMode: { ideal: 'environment' } },
|
|
|
|
|
+ audio: false
|
|
|
|
|
+ })
|
|
|
|
|
+ } catch (e: any) {
|
|
|
|
|
+ if (e?.name === 'NotAllowedError' || e?.name === 'PermissionDeniedError') {
|
|
|
|
|
+ tipText.value = '摄像头权限被拒绝,请在浏览器设置中允许后重试'
|
|
|
|
|
+ } else if (e?.name === 'NotFoundError') {
|
|
|
|
|
+ tipText.value = '未找到可用摄像头,请点击下方"手动输入"'
|
|
|
|
|
+ } else {
|
|
|
|
|
+ tipText.value = '摄像头启动失败,请点击下方"手动输入"'
|
|
|
|
|
+ }
|
|
|
|
|
+ console.warn('[H5 扫码] getUserMedia 失败', e)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 创建 <video> 元素并播放
|
|
|
|
|
+ const wrap = document.getElementById('h5-video-wrap')
|
|
|
|
|
+ if (!wrap) return
|
|
|
|
|
+ wrap.innerHTML = ''
|
|
|
|
|
+ const video = document.createElement('video')
|
|
|
|
|
+ video.setAttribute('playsinline', 'true') // iOS 防止自动全屏
|
|
|
|
|
+ video.setAttribute('autoplay', 'true')
|
|
|
|
|
+ video.setAttribute('muted', 'true')
|
|
|
|
|
+ video.muted = true
|
|
|
|
|
+ video.style.cssText =
|
|
|
|
|
+ 'position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover;z-index:0;'
|
|
|
|
|
+ video.srcObject = h5Stream
|
|
|
|
|
+ wrap.appendChild(video)
|
|
|
|
|
+ try {
|
|
|
|
|
+ await video.play()
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn('[H5 扫码] video.play 失败', e)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 循环 detect 二维码
|
|
|
|
|
+ let detector: any
|
|
|
|
|
+ try {
|
|
|
|
|
+ detector = new BarcodeDetectorCtor({ formats: ['qr_code'] })
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ tipText.value = '浏览器不支持 QR 码识别,请点击下方"手动输入"'
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ 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) {
|
|
|
|
|
+ // 单帧 detect 失败忽略(视频流可能还没准备好)
|
|
|
|
|
+ }
|
|
|
|
|
+ h5RafId = requestAnimationFrame(tick)
|
|
|
|
|
+ }
|
|
|
|
|
+ h5RafId = requestAnimationFrame(tick)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function h5StopScanner() {
|
|
|
|
|
+ if (h5RafId != null) {
|
|
|
|
|
+ cancelAnimationFrame(h5RafId)
|
|
|
|
|
+ h5RafId = null
|
|
|
|
|
+ }
|
|
|
|
|
+ if (h5Stream) {
|
|
|
|
|
+ h5Stream.getTracks().forEach((t) => t.stop())
|
|
|
|
|
+ h5Stream = null
|
|
|
|
|
+ }
|
|
|
|
|
+ const wrap = typeof document !== 'undefined' ? document.getElementById('h5-video-wrap') : null
|
|
|
|
|
+ if (wrap) wrap.innerHTML = ''
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
onUnmounted(() => {
|
|
|
if (scanTimer) clearTimeout(scanTimer)
|
|
if (scanTimer) clearTimeout(scanTimer)
|
|
|
|
|
+ h5StopScanner()
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
async function onScanSuccess(qrContent: string) {
|
|
async function onScanSuccess(qrContent: string) {
|
|
@@ -138,9 +243,21 @@ function onManualInput() {
|
|
|
background: #000000;
|
|
background: #000000;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
}
|
|
}
|
|
|
|
|
+/* H5 端:video 预览容器铺满底层,扫码框在上层 */
|
|
|
|
|
+.h5-video-wrap {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ z-index: 0;
|
|
|
|
|
+ background: #000000;
|
|
|
|
|
+}
|
|
|
.overlay {
|
|
.overlay {
|
|
|
position: absolute;
|
|
position: absolute;
|
|
|
background: rgba(0, 0, 0, 0.6);
|
|
background: rgba(0, 0, 0, 0.6);
|
|
|
|
|
+ z-index: 2;
|
|
|
|
|
+ pointer-events: none;
|
|
|
}
|
|
}
|
|
|
.top-overlay {
|
|
.top-overlay {
|
|
|
top: 0;
|
|
top: 0;
|
|
@@ -174,6 +291,7 @@ function onManualInput() {
|
|
|
height: 400rpx;
|
|
height: 400rpx;
|
|
|
background: transparent;
|
|
background: transparent;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
|
|
+ z-index: 3;
|
|
|
}
|
|
}
|
|
|
.scan-corner {
|
|
.scan-corner {
|
|
|
position: absolute;
|
|
position: absolute;
|
|
@@ -183,7 +301,7 @@ function onManualInput() {
|
|
|
&.tl { top: 0; left: 0; border-right: none; border-bottom: none; }
|
|
&.tl { top: 0; left: 0; border-right: none; border-bottom: none; }
|
|
|
&.tr { top: 0; right: 0; border-left: none; border-bottom: none; }
|
|
&.tr { top: 0; right: 0; border-left: none; border-bottom: none; }
|
|
|
&.bl { bottom: 0; left: 0; border-right: none; border-top: none; }
|
|
&.bl { bottom: 0; left: 0; border-right: none; border-top: none; }
|
|
|
- &.br { bottom: 0; right: 0; border-left: none; border-top: none; }
|
|
|
|
|
|
|
+ &.br { bottom: 0; right: 0; border-right: none; border-top: none; }
|
|
|
}
|
|
}
|
|
|
.scan-line {
|
|
.scan-line {
|
|
|
position: absolute;
|
|
position: absolute;
|
|
@@ -207,6 +325,7 @@ function onManualInput() {
|
|
|
text-align: center;
|
|
text-align: center;
|
|
|
color: #FFFFFF;
|
|
color: #FFFFFF;
|
|
|
font-size: $font-sm;
|
|
font-size: $font-sm;
|
|
|
|
|
+ z-index: 4;
|
|
|
}
|
|
}
|
|
|
.scan-btn-row {
|
|
.scan-btn-row {
|
|
|
position: absolute;
|
|
position: absolute;
|
|
@@ -215,6 +334,7 @@ function onManualInput() {
|
|
|
right: 0;
|
|
right: 0;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
justify-content: center;
|
|
justify-content: center;
|
|
|
|
|
+ z-index: 4;
|
|
|
.btn {
|
|
.btn {
|
|
|
width: 280rpx;
|
|
width: 280rpx;
|
|
|
height: 80rpx;
|
|
height: 80rpx;
|