generated from VLADIMIR/template_frontend
up
This commit is contained in:
+158
-17
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Flag, Pause, Play, Reset, Settings, Trophy } from '@vicons/carbon'
|
||||
import { Icon } from '@vicons/utils'
|
||||
import { NButton, NTag } from 'naive-ui'
|
||||
import { NAlert, NButton, NTag } from 'naive-ui'
|
||||
import {
|
||||
NCard,
|
||||
NFlex,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
NText,
|
||||
} from 'naive-ui'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { getAuthClient } from '@/api/auth_client'
|
||||
@@ -68,6 +68,15 @@ const showIssueTicketsModal = ref(false)
|
||||
const ticketCount = ref(1)
|
||||
const issuedTickets = ref<Ticket[]>([])
|
||||
const isIssuingTickets = ref(false)
|
||||
const showScreenScanner = ref(false)
|
||||
const screenScannerError = ref('')
|
||||
const screenScannerVideo = ref<HTMLVideoElement>()
|
||||
const screenBinding = ref(false)
|
||||
const screenScanning = ref(false)
|
||||
let screenScannerStream: MediaStream | undefined
|
||||
let screenScannerFrame = 0
|
||||
|
||||
type BarcodeDetector = { detect(video: HTMLVideoElement): Promise<{ rawValue: string }[]> }
|
||||
|
||||
const game = ref<Game>({
|
||||
ID: undefined,
|
||||
@@ -113,6 +122,7 @@ const currentScreenMode = computed<ScreenMode | undefined>(() => {
|
||||
})
|
||||
const timerScreen = computed(() => screens.value[0])
|
||||
const previewScreen = computed(() => screens.value.find((screen) => screen.status === 'connected') || screens.value[0])
|
||||
const hasDisconnectedScreen = computed(() => screens.value.some((screen) => screen.status === 'disconnected'))
|
||||
const timerStateLabel = computed(() => {
|
||||
switch (timerScreen.value?.timerState) {
|
||||
case 'SCREEN_TIMER_STATE_RUNNING':
|
||||
@@ -186,6 +196,88 @@ async function getGameScreens() {
|
||||
}
|
||||
getGameScreens()
|
||||
|
||||
function stopScreenScanner() {
|
||||
cancelAnimationFrame(screenScannerFrame)
|
||||
screenScannerStream?.getTracks().forEach((track) => track.stop())
|
||||
screenScannerStream = undefined
|
||||
screenScanning.value = false
|
||||
}
|
||||
|
||||
function closeScreenScanner() {
|
||||
stopScreenScanner()
|
||||
showScreenScanner.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 bindScannedScreen(screenID: string, accessToken: string) {
|
||||
if (screenBinding.value) return
|
||||
screenBinding.value = true
|
||||
try {
|
||||
await client.BindScreen({ screenID, accessToken, gameID: Number(gameId) })
|
||||
message.success('Экран привязан к игре')
|
||||
await getGameScreens()
|
||||
closeScreenScanner()
|
||||
} catch (err) {
|
||||
message.error(err instanceof Error ? err.message : 'Не удалось привязать экран')
|
||||
} finally {
|
||||
screenBinding.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function startScreenScanner() {
|
||||
const DetectorClass = (window as Window & { BarcodeDetector?: new () => BarcodeDetector }).BarcodeDetector
|
||||
if (!DetectorClass || !navigator.mediaDevices?.getUserMedia) {
|
||||
screenScannerError.value = 'Сканирование камерой не поддерживается в этом браузере.'
|
||||
showScreenScanner.value = true
|
||||
return
|
||||
}
|
||||
showScreenScanner.value = true
|
||||
screenScannerError.value = ''
|
||||
try {
|
||||
screenScannerStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } })
|
||||
screenScanning.value = true
|
||||
await nextTick()
|
||||
if (!screenScannerVideo.value) return
|
||||
screenScannerVideo.value.srcObject = screenScannerStream
|
||||
await screenScannerVideo.value.play()
|
||||
const detector = new DetectorClass()
|
||||
const scan = async () => {
|
||||
if (!screenScannerVideo.value || !screenScannerStream) return
|
||||
try {
|
||||
const code = (await detector.detect(screenScannerVideo.value))[0]?.rawValue
|
||||
if (code) {
|
||||
const screen = parseScreenQRCode(code)
|
||||
if (screen) {
|
||||
stopScreenScanner()
|
||||
await bindScannedScreen(screen.screenID, screen.accessToken)
|
||||
return
|
||||
}
|
||||
screenScannerError.value = 'Это не QR-код экрана.'
|
||||
}
|
||||
screenScannerFrame = requestAnimationFrame(scan)
|
||||
} catch {
|
||||
screenScannerError.value = 'Не удалось распознать QR-код.'
|
||||
stopScreenScanner()
|
||||
}
|
||||
}
|
||||
screenScannerFrame = requestAnimationFrame(scan)
|
||||
} catch {
|
||||
screenScannerError.value = 'Не удалось открыть камеру.'
|
||||
stopScreenScanner()
|
||||
}
|
||||
}
|
||||
|
||||
async function getGameMap() {
|
||||
if (dragState || mapSaving.value) return
|
||||
const res = await client.GetGameMap({ gameID: Number(gameId) })
|
||||
@@ -518,6 +610,8 @@ useSimplePolling(() => {
|
||||
getGameMap()
|
||||
}, 2000)
|
||||
|
||||
onBeforeUnmount(stopScreenScanner)
|
||||
|
||||
async function resetGame() {
|
||||
await client.ResetGame({
|
||||
ID: game.value.ID,
|
||||
@@ -744,22 +838,32 @@ useSimplePolling(() => {
|
||||
</n-flex>
|
||||
</div>
|
||||
|
||||
<div v-if="screens.length > 0" class="screens-block">
|
||||
<p class="screens-title">Экраны</p>
|
||||
<div v-for="screen in screens" :key="screen.ID" class="screen-row">
|
||||
<div>
|
||||
<div>Экран {{ screen.ID }}</div>
|
||||
<div class="screen-status">{{ screen.status }}</div>
|
||||
<div v-if="screen.error" class="screen-error">{{ screen.error }}</div>
|
||||
<div class="screens-block">
|
||||
<n-flex align="center" justify="space-between">
|
||||
<p class="screens-title">Экраны</p>
|
||||
<n-button size="small" type="primary" @click="startScreenScanner">Привязать экран</n-button>
|
||||
</n-flex>
|
||||
<n-alert v-if="hasDisconnectedScreen" type="warning" title="Проверьте подключение экрана">
|
||||
Один или несколько экранов не выходят на связь более минуты.
|
||||
</n-alert>
|
||||
<p v-if="screens.length === 0" class="screen-empty">К игре пока не привязаны экраны.</p>
|
||||
<template v-else>
|
||||
<div v-for="screen in screens" :key="screen.ID" class="screen-row">
|
||||
<div>
|
||||
<div>Экран {{ screen.ID }}</div>
|
||||
<div class="screen-status">
|
||||
{{ screen.status === 'connected' ? 'Онлайн' : 'Оффлайн' }}
|
||||
</div>
|
||||
<div v-if="screen.error" class="screen-error">{{ screen.error }}</div>
|
||||
</div>
|
||||
<n-flex justify="end">
|
||||
<n-tag v-if="screen.soundEnabled" type="success">Со звуком</n-tag>
|
||||
<n-button v-else size="small" @click="setSoundScreen(screen)">Назначить звук</n-button>
|
||||
<n-button size="small" type="error" ghost @click="unbindScreen(screen)">
|
||||
Отвязать
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</div>
|
||||
<n-flex justify="end">
|
||||
<n-tag v-if="screen.soundEnabled" type="success">Со звуком</n-tag>
|
||||
<n-button v-else size="small" @click="setSoundScreen(screen)">Назначить звук</n-button>
|
||||
<n-button size="small" type="error" ghost @click="unbindScreen(screen)"
|
||||
>Отвязать</n-button
|
||||
>
|
||||
</n-flex>
|
||||
</div>
|
||||
<n-space class="screen-mode-controls">
|
||||
<n-button
|
||||
size="small"
|
||||
@@ -848,6 +952,7 @@ useSimplePolling(() => {
|
||||
>Перемотать</n-button
|
||||
>
|
||||
</n-space>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="map-preview-layout">
|
||||
@@ -1005,6 +1110,30 @@ useSimplePolling(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n-modal v-model:show="showScreenScanner" @after-leave="stopScreenScanner">
|
||||
<n-card
|
||||
style="width: min(600px, 94vw)"
|
||||
title="Привязать экран"
|
||||
:bordered="false"
|
||||
size="huge"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<template #header-extra>
|
||||
<n-button @click="closeScreenScanner"> x </n-button>
|
||||
</template>
|
||||
<n-space vertical>
|
||||
<video ref="screenScannerVideo" class="screen-scanner-video" muted playsinline />
|
||||
<n-alert v-if="screenScannerError" type="error">{{ screenScannerError }}</n-alert>
|
||||
<n-button v-if="!screenScanning && !screenBinding" type="primary" @click="startScreenScanner">
|
||||
Сканировать QR-код
|
||||
</n-button>
|
||||
<n-button v-if="screenScanning" @click="stopScreenScanner">Остановить камеру</n-button>
|
||||
<n-button v-if="screenBinding" type="primary" loading>Привязка…</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
|
||||
<!-- Окно настроек -->
|
||||
<n-modal v-model:show="showSettingsModal">
|
||||
<n-card
|
||||
@@ -1541,11 +1670,23 @@ useSimplePolling(() => {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.screen-empty {
|
||||
color: #aaa;
|
||||
margin: 12px 0 0;
|
||||
}
|
||||
|
||||
.screen-error {
|
||||
color: #f2a2a2;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.screen-scanner-video {
|
||||
aspect-ratio: 1;
|
||||
background-color: #000;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.screen-mode-controls {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user