Files
App.k12.Parents/components/qr-scanner.uvue
T
2026-10-29 10:08:39 +08:00

150 lines
3.4 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">
// #ifdef WEB
import { BrowserQRCodeReader } from '@zxing/browser'
import type { IScannerControls } from '@zxing/browser'
// #endif
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 scannerControls: IScannerControls | null = null
let scanVideo: HTMLVideoElement | null = null
let isPageActive = false
let restartTimer: 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 || scannerControls != null) return
const video = getScanVideo()
if (video == null) {
handleCameraError()
return
}
try {
const reader = new BrowserQRCodeReader()
scannerControls = await reader.decodeFromConstraints(
{ video: { facingMode: 'environment' } },
video,
(result, _error, controls) => {
if (!result) return
controls.stop()
scannerControls = null
emitScan(result.getText())
scheduleRestart()
}
)
} catch (_error) {
handleCameraError()
}
}
function scheduleRestart() {
if (!isPageActive || !props.active || restartTimer != null) return
restartTimer = setTimeout(() => {
restartTimer = null
void startWebScanner()
}, props.cooldown)
}
function stopWebScanner() {
if (restartTimer != null) {
clearTimeout(restartTimer)
restartTimer = null
}
if (scannerControls != null) {
scannerControls.stop()
scannerControls = 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>