Files
App.k12.Parents/components/qr-scanner.uvue
T

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>