Files
evening_detective_frontend/src/components/ScreenBindingModal.vue
T
2026-10-04 16:07:41 +07:00

135 lines
3.9 KiB
Vue

<script setup lang="ts">
import { NAlert, NButton, NCard, NModal, NSpace } from 'naive-ui'
import { nextTick, onBeforeUnmount, ref, watch } from 'vue'
defineProps<{
binding: boolean
}>()
const show = defineModel<boolean>('show', { required: true })
const emit = defineEmits<{
scanned: [screenID: string, accessToken: string]
}>()
type BarcodeDetector = { detect(video: HTMLVideoElement): Promise<{ rawValue: string }[]> }
const scannerError = ref('')
const scannerVideo = ref<HTMLVideoElement>()
const scanning = ref(false)
let scannerStream: MediaStream | undefined
let scannerFrame = 0
function stopScanning() {
cancelAnimationFrame(scannerFrame)
scannerStream?.getTracks().forEach((track) => track.stop())
scannerStream = undefined
scanning.value = false
}
function parseScreenQRCode(rawValue: string) {
try {
const url = new URL(rawValue, window.location.origin)
const screenID = url.searchParams.get('screenId')
const accessToken = url.searchParams.get('accessToken')
if (url.pathname !== '/screen' || !screenID || !accessToken) return
return { screenID, accessToken }
} catch {
return
}
}
async function startScanning() {
const DetectorClass = (window as Window & { BarcodeDetector?: new () => BarcodeDetector })
.BarcodeDetector
if (!DetectorClass || !navigator.mediaDevices?.getUserMedia) {
scannerError.value = 'Сканирование камерой не поддерживается в этом браузере.'
return
}
scannerError.value = ''
try {
scannerStream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' },
})
scanning.value = true
await nextTick()
if (!scannerVideo.value) return
scannerVideo.value.srcObject = scannerStream
await scannerVideo.value.play()
const detector = new DetectorClass()
const scan = async () => {
if (!scannerVideo.value || !scannerStream) return
try {
const code = (await detector.detect(scannerVideo.value))[0]?.rawValue
if (code) {
const screen = parseScreenQRCode(code)
if (screen) {
stopScanning()
show.value = false
emit('scanned', screen.screenID, screen.accessToken)
return
}
scannerError.value = 'Это не QR-код экрана.'
}
scannerFrame = requestAnimationFrame(scan)
} catch {
scannerError.value = 'Не удалось распознать QR-код.'
stopScanning()
}
}
scannerFrame = requestAnimationFrame(scan)
} catch {
scannerError.value = 'Не удалось открыть камеру.'
stopScanning()
}
}
function close() {
stopScanning()
show.value = false
}
watch(show, (isShown) => {
if (isShown) startScanning()
else stopScanning()
})
onBeforeUnmount(stopScanning)
</script>
<template>
<n-modal v-model:show="show">
<n-card
style="width: min(600px, 94vw)"
title="Привязать экран"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<template #header-extra>
<n-button @click="close"> x </n-button>
</template>
<n-space vertical>
<video ref="scannerVideo" class="screen-scanner-video" muted playsinline />
<n-alert v-if="scannerError" type="error">{{ scannerError }}</n-alert>
<n-button v-if="!scanning && !binding" type="primary" @click="startScanning">
Сканировать QR-код
</n-button>
<n-button v-if="scanning" @click="stopScanning">Остановить камеру</n-button>
<n-button v-if="binding" type="primary" loading>Привязка…</n-button>
</n-space>
</n-card>
</n-modal>
</template>
<style scoped>
.screen-scanner-video {
aspect-ratio: 1;
background-color: #000;
max-height: 60vh;
object-fit: cover;
width: 100%;
}
</style>