161 lines
3.7 KiB
Plaintext
161 lines
3.7 KiB
Plaintext
<template>
|
|
<view class="qr-scanner">
|
|
<!-- #ifdef WEB -->
|
|
<view :id="videoContainerId" class="qr-scanner-video"></view>
|
|
<!-- #endif -->
|
|
<!-- #ifndef WEB -->
|
|
<camera
|
|
class="qr-scanner-camera"
|
|
mode="scanCode"
|
|
device-position="back"
|
|
@scancode="handleCameraScan"
|
|
@error="handleCameraError"
|
|
></camera>
|
|
<!-- #endif -->
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
type Props = {
|
|
cooldown?: number
|
|
active?: boolean
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
cooldown: 5000,
|
|
active: true
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'scan', result: string): void
|
|
(e: 'error'): void
|
|
}>()
|
|
|
|
let lastScannedAt = 0
|
|
// #ifdef WEB
|
|
const videoContainerId = `qr-scanner-${Date.now()}`
|
|
let scanVideo: HTMLVideoElement | null = null
|
|
let scanStream: MediaStream | null = null
|
|
let isPageActive = false
|
|
let scanTimer: number | null = null
|
|
// #endif
|
|
|
|
function emitScan(result: string) {
|
|
const now = Date.now()
|
|
if (!props.active || !result || now - lastScannedAt < props.cooldown) return
|
|
lastScannedAt = now
|
|
emit('scan', result)
|
|
}
|
|
|
|
function handleCameraScan(event: any) {
|
|
emitScan(event.detail?.result as string ?? '')
|
|
}
|
|
|
|
function handleCameraError() {
|
|
emit('error')
|
|
}
|
|
|
|
// #ifdef WEB
|
|
function getScanVideo(): HTMLVideoElement | null {
|
|
if (scanVideo != null) return scanVideo
|
|
const container = document.getElementById(videoContainerId)
|
|
if (container == null) return null
|
|
|
|
const video = document.createElement('video') as HTMLVideoElement
|
|
video.autoplay = true
|
|
video.muted = true
|
|
video.playsInline = true
|
|
video.setAttribute('style', 'display:block;width:100%;height:100%;object-fit:cover;background-color:#000000;')
|
|
container.appendChild(video)
|
|
scanVideo = video
|
|
return video
|
|
}
|
|
|
|
async function startWebScanner() {
|
|
if (!isPageActive || !props.active || scanStream != null) return
|
|
const video = getScanVideo()
|
|
if (video == null) {
|
|
handleCameraError()
|
|
return
|
|
}
|
|
|
|
try {
|
|
const BarcodeDetectorClass = (window as any).BarcodeDetector
|
|
if (BarcodeDetectorClass == null || navigator.mediaDevices == null) {
|
|
handleCameraError()
|
|
return
|
|
}
|
|
|
|
scanStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } })
|
|
video.srcObject = scanStream
|
|
await video.play()
|
|
const detector = new BarcodeDetectorClass({ formats: ['qr_code'] })
|
|
void scanWebFrame(detector)
|
|
} catch (_error) {
|
|
stopWebScanner()
|
|
handleCameraError()
|
|
}
|
|
}
|
|
|
|
async function scanWebFrame(detector: any) {
|
|
if (!isPageActive || !props.active || scanVideo == null || scanStream == null) return
|
|
|
|
try {
|
|
const results = await detector.detect(scanVideo)
|
|
if (results.length > 0) {
|
|
emitScan(results[0].rawValue as string)
|
|
}
|
|
} catch (_error) {
|
|
handleCameraError()
|
|
return
|
|
}
|
|
|
|
scanTimer = setTimeout(() => {
|
|
scanTimer = null
|
|
void scanWebFrame(detector)
|
|
}, 250)
|
|
}
|
|
|
|
function stopWebScanner() {
|
|
if (scanTimer != null) {
|
|
clearTimeout(scanTimer)
|
|
scanTimer = null
|
|
}
|
|
if (scanStream != null) {
|
|
scanStream.getTracks().forEach((track) => {
|
|
track.stop()
|
|
})
|
|
scanStream = null
|
|
}
|
|
if (scanVideo != null) {
|
|
scanVideo.srcObject = null
|
|
}
|
|
}
|
|
|
|
watch(() => props.active, (active) => {
|
|
if (active) {
|
|
void startWebScanner()
|
|
} else {
|
|
stopWebScanner()
|
|
}
|
|
})
|
|
|
|
onMounted(() => {
|
|
isPageActive = true
|
|
void startWebScanner()
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
isPageActive = false
|
|
stopWebScanner()
|
|
scanVideo = null
|
|
})
|
|
// #endif
|
|
</script>
|
|
|
|
<style>
|
|
.qr-scanner { width: 100%; height: 224px; overflow: hidden; border-radius: 16px; border: 2px solid #6C63FF; background-color: #000000; }
|
|
.qr-scanner-camera { width: 100%; height: 100%; }
|
|
.qr-scanner-video { width: 100%; height: 100%; }
|
|
</style>
|