generated from VLADIMIR/template_frontend
135 lines
3.9 KiB
Vue
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>
|