generated from VLADIMIR/template_frontend
fix
This commit is contained in:
@@ -78,6 +78,13 @@ const openMenu = ref(false)
|
||||
>
|
||||
Игры
|
||||
</div>
|
||||
<div
|
||||
class="menu-item-block"
|
||||
v-if="hasPermission('games_page')"
|
||||
@click="router.push('/tickets/check')"
|
||||
>
|
||||
Проверка билетов
|
||||
</div>
|
||||
<div
|
||||
class="menu-item-block"
|
||||
:class="{ active: props.active == 'catalog' }"
|
||||
|
||||
@@ -64,7 +64,9 @@ onMounted(load)
|
||||
<p>Покажите этот QR-код организатору.</p>
|
||||
|
||||
<p v-if="ticket.status === 'used'" class="used" role="status">
|
||||
Билет уже использован{{ ticket.usedAt ? ` ${formatGameDateTime(ticket.usedAt)}` : '' }}.
|
||||
Билет уже использован{{ ticket.usedAt ? ` ${formatGameDateTime(ticket.usedAt)}` : '' }}{{
|
||||
ticket.usedBy ? ` организатором ${ticket.usedBy}` : ''
|
||||
}}.
|
||||
</p>
|
||||
<p v-else-if="ticket.gameStatus === 'finish'" class="used" role="status">
|
||||
Игра уже завершена.
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
<script setup lang="ts">
|
||||
import { NAlert, NButton, NCard, NInput, NSpace, useMessage } from 'naive-ui'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { getAuthClient } from '@/api/auth_client'
|
||||
import type { TicketPage } from '@/api/generated/crabs/evening_detective_server'
|
||||
import HeaderMenu from '@/components/HeaderMenu.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { formatGameDateTime } from '@/utils/datetime'
|
||||
|
||||
type Detector = { detect(video: HTMLVideoElement): Promise<{ rawValue: string }[]> }
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
const client = getAuthClient()
|
||||
const message = useMessage()
|
||||
const value = ref('')
|
||||
const ticket = ref<TicketPage>()
|
||||
const error = ref('')
|
||||
const loading = ref(false)
|
||||
const video = ref<HTMLVideoElement>()
|
||||
const cameraOn = ref(false)
|
||||
let stream: MediaStream | undefined
|
||||
let frame = 0
|
||||
|
||||
const token = computed(() => {
|
||||
try {
|
||||
return new URL(value.value.trim()).pathname.match(/\/tickets\/([^/]+)/)?.[1] || ''
|
||||
} catch {
|
||||
return value.value.trim()
|
||||
}
|
||||
})
|
||||
|
||||
async function findTicket() {
|
||||
if (!token.value || loading.value) return
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
ticket.value = undefined
|
||||
try {
|
||||
const result = await client.GetTicket({ token: token.value })
|
||||
if (result.error || !result.ticket) error.value = result.error || 'Билет не найден'
|
||||
else ticket.value = result.ticket
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Не удалось проверить билет'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function acceptTicket() {
|
||||
if (!token.value || loading.value) return
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const result = await client.AcceptTicket({ token: token.value })
|
||||
if (result.error || !result.ticket) error.value = result.error || 'Не удалось принять билет'
|
||||
else {
|
||||
ticket.value = result.ticket
|
||||
message.success('Билет принят')
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Не удалось принять билет'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function scanCamera() {
|
||||
const DetectorClass = (window as Window & { BarcodeDetector?: new () => Detector }).BarcodeDetector
|
||||
if (!DetectorClass || !navigator.mediaDevices?.getUserMedia) {
|
||||
error.value = 'Сканирование камерой не поддерживается. Введите ссылку вручную.'
|
||||
return
|
||||
}
|
||||
try {
|
||||
stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } })
|
||||
cameraOn.value = true
|
||||
await nextTick()
|
||||
if (video.value) {
|
||||
video.value.srcObject = stream
|
||||
await video.value.play()
|
||||
}
|
||||
const detector = new DetectorClass()
|
||||
const scan = async () => {
|
||||
if (!cameraOn.value || !video.value) return
|
||||
const codes = await detector.detect(video.value)
|
||||
if (codes[0]?.rawValue) {
|
||||
value.value = codes[0].rawValue
|
||||
stopCamera()
|
||||
await findTicket()
|
||||
return
|
||||
}
|
||||
frame = requestAnimationFrame(scan)
|
||||
}
|
||||
frame = requestAnimationFrame(scan)
|
||||
} catch {
|
||||
error.value = 'Не удалось открыть камеру. Введите ссылку вручную.'
|
||||
}
|
||||
}
|
||||
|
||||
function stopCamera() {
|
||||
cameraOn.value = false
|
||||
cancelAnimationFrame(frame)
|
||||
stream?.getTracks().forEach((track) => track.stop())
|
||||
stream = undefined
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!authStore.isAuthenticated) router.replace('/login')
|
||||
})
|
||||
onBeforeUnmount(stopCamera)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<HeaderMenu active="games" />
|
||||
<div class="center-block-custom center-middle-block-custom scanner-page">
|
||||
<n-card title="Проверка билета" class="scanner-card">
|
||||
<n-space vertical>
|
||||
<video v-if="cameraOn" ref="video" class="scanner-video" muted playsinline />
|
||||
<n-button v-if="!cameraOn" @click="scanCamera">Сканировать камерой</n-button>
|
||||
<n-button v-else @click="stopCamera">Остановить камеру</n-button>
|
||||
<n-input
|
||||
v-model:value="value"
|
||||
placeholder="Или вставьте ссылку на билет"
|
||||
@keyup.enter="findTicket"
|
||||
/>
|
||||
<n-button type="primary" :loading="loading" @click="findTicket">Проверить билет</n-button>
|
||||
<n-alert v-if="error" type="error">{{ error }}</n-alert>
|
||||
<template v-if="ticket">
|
||||
<n-alert :type="ticket.status === 'active' ? 'success' : 'warning'">
|
||||
{{ ticket.gameName }}<br />
|
||||
Статус: {{ ticket.status === 'active' ? 'действителен' : 'уже использован' }}
|
||||
<span v-if="ticket.usedBy"> · {{ ticket.usedBy }}</span>
|
||||
<br v-if="ticket.usedAt" />
|
||||
<span v-if="ticket.usedAt">Использован: {{ formatGameDateTime(ticket.usedAt) }}</span>
|
||||
</n-alert>
|
||||
<n-button
|
||||
v-if="ticket.status === 'active' && ticket.gameStatus !== 'finish'"
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
@click="acceptTicket"
|
||||
>
|
||||
Принять билет
|
||||
</n-button>
|
||||
</template>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.scanner-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.scanner-card {
|
||||
max-width: 520px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scanner-video {
|
||||
background: #000;
|
||||
max-height: 50dvh;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user