generated from VLADIMIR/template_frontend
up
This commit is contained in:
@@ -43,6 +43,7 @@ import GameMapPanel from '@/components/GameMapPanel.vue'
|
||||
import GameScreensPanel from '@/components/GameScreensPanel.vue'
|
||||
import GameTeamsPanel from '@/components/GameTeamsPanel.vue'
|
||||
import HeaderMenu from '@/components/HeaderMenu.vue'
|
||||
import TicketCheckModal from '@/components/TicketCheckModal.vue'
|
||||
import UiBlock from '@/components/UiBlock.vue'
|
||||
import { useSimplePolling } from '@/composables/useSimplePolling'
|
||||
import { applicationFileType, applicationFileURL } from '@/utils/application'
|
||||
@@ -54,10 +55,10 @@ const isDownloadingQR = ref(false)
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const gameId = route.params.id
|
||||
type GameTab = 'settings' | 'teams' | 'screens' | 'game' | 'answers'
|
||||
type GameTab = 'settings' | 'teams' | 'screens' | 'game' | 'payment' | 'answers'
|
||||
|
||||
function isGameTab(value: string): value is GameTab {
|
||||
return ['settings', 'teams', 'screens', 'game', 'answers'].includes(value)
|
||||
return ['settings', 'teams', 'screens', 'game', 'payment', 'answers'].includes(value)
|
||||
}
|
||||
|
||||
const queryTab = typeof route.query.tab === 'string' ? route.query.tab : ''
|
||||
@@ -85,6 +86,9 @@ const deletedGameName = ref('')
|
||||
const showQR = ref(false)
|
||||
const urlQR = ref('')
|
||||
const titleQR = ref('')
|
||||
const showTicketCheckModal = ref(false)
|
||||
const paymentURL =
|
||||
'https://www.sberbank.ru/ru/choise_bank?requisiteNumber=79609361994&bankCode=100000000111'
|
||||
const ticketCount = ref(1)
|
||||
const issuedTickets = ref<Ticket[]>([])
|
||||
const isIssuingTickets = ref(false)
|
||||
@@ -750,7 +754,14 @@ useSimplePolling(() => {
|
||||
<n-input-number v-model:value="ticketCount" :min="1" :max="100" />
|
||||
<div v-for="ticket in issuedTickets" :key="ticket.ID" class="issued-ticket">
|
||||
<div>Билет №{{ ticket.ID }}</div>
|
||||
<a :href="ticket.URL" target="_blank" rel="noreferrer">Открыть страницу билета</a>
|
||||
<a
|
||||
:href="ticket.URL"
|
||||
class="payment-link"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Открыть страницу билета
|
||||
</a>
|
||||
<n-button size="small" @click="openQR(ticket.URL!, `Билет №${ticket.ID}`)">
|
||||
Показать QR
|
||||
</n-button>
|
||||
@@ -875,6 +886,34 @@ useSimplePolling(() => {
|
||||
/>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="payment" tab="Оплата">
|
||||
<div class="game-tab-content">
|
||||
<UiBlock>
|
||||
<n-flex align="center" justify="space-between">
|
||||
<p class="screens-title">Билеты</p>
|
||||
<n-button size="small" @click="showTicketCheckModal = true">
|
||||
Проверить билет
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</UiBlock>
|
||||
<UiBlock>
|
||||
<p class="screens-title">Оплата игры</p>
|
||||
<div class="qr-block">
|
||||
<n-qr-code :value="paymentURL" :size="300" :padding="0" />
|
||||
</div>
|
||||
<p>
|
||||
<a
|
||||
:href="paymentURL"
|
||||
class="payment-link"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Открыть ссылку оплаты
|
||||
</a>
|
||||
</p>
|
||||
</UiBlock>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="answers" tab="Проверка ответов">
|
||||
<div class="game-tab-content">
|
||||
<div v-if="answersQuestions.length == 0" class="answers-empty">
|
||||
@@ -911,6 +950,8 @@ useSimplePolling(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TicketCheckModal v-model:show="showTicketCheckModal" />
|
||||
|
||||
<!-- Окно выдачи улик -->
|
||||
<n-modal v-model:show="showGiveApplicationModal">
|
||||
<n-card
|
||||
@@ -1211,6 +1252,15 @@ useSimplePolling(() => {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.payment-link {
|
||||
color: #63e2b7;
|
||||
}
|
||||
|
||||
.payment-link:focus,
|
||||
.payment-link:hover {
|
||||
color: #8ff0cc;
|
||||
}
|
||||
|
||||
.give-application-image-block {
|
||||
background-image: url('@/assets/images/paper_white.jpg');
|
||||
background-size: cover;
|
||||
|
||||
@@ -88,14 +88,6 @@ const openMenu = ref(false)
|
||||
>
|
||||
Игры
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="menu-item-block"
|
||||
v-if="hasPermission('games_page')"
|
||||
@click="router.push('/tickets/check')"
|
||||
>
|
||||
Проверка билетов
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="menu-item-block"
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
<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 { NAlert, NButton, NCard, NInput, NModal, NSpace, useMessage } from 'naive-ui'
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
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 show = defineModel<boolean>('show', { required: true })
|
||||
type BarcodeDetector = { detect(video: HTMLVideoElement): Promise<{ rawValue: string }[]> }
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
const client = getAuthClient()
|
||||
const message = useMessage()
|
||||
const value = ref('')
|
||||
@@ -20,7 +16,7 @@ const ticket = ref<TicketPage>()
|
||||
const error = ref('')
|
||||
const loading = ref(false)
|
||||
const video = ref<HTMLVideoElement>()
|
||||
const cameraOn = ref(false)
|
||||
const scanning = ref(false)
|
||||
let stream: MediaStream | undefined
|
||||
let frame = 0
|
||||
|
||||
@@ -66,66 +62,91 @@ async function acceptTicket() {
|
||||
}
|
||||
}
|
||||
|
||||
async function scanCamera() {
|
||||
const DetectorClass = (window as Window & { BarcodeDetector?: new () => Detector }).BarcodeDetector
|
||||
async function startScanning() {
|
||||
const DetectorClass = (window as Window & { BarcodeDetector?: new () => BarcodeDetector })
|
||||
.BarcodeDetector
|
||||
if (!DetectorClass || !navigator.mediaDevices?.getUserMedia) {
|
||||
error.value = 'Сканирование камерой не поддерживается. Введите ссылку вручную.'
|
||||
return
|
||||
}
|
||||
error.value = ''
|
||||
try {
|
||||
stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } })
|
||||
cameraOn.value = true
|
||||
scanning.value = true
|
||||
await nextTick()
|
||||
if (video.value) {
|
||||
video.value.srcObject = stream
|
||||
await video.value.play()
|
||||
}
|
||||
if (!video.value) return
|
||||
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
|
||||
if (!scanning.value || !video.value) return
|
||||
try {
|
||||
const code = (await detector.detect(video.value))[0]?.rawValue
|
||||
if (code) {
|
||||
value.value = code
|
||||
stopScanning()
|
||||
await findTicket()
|
||||
return
|
||||
}
|
||||
frame = requestAnimationFrame(scan)
|
||||
} catch {
|
||||
error.value = 'Не удалось распознать QR-код.'
|
||||
stopScanning()
|
||||
}
|
||||
frame = requestAnimationFrame(scan)
|
||||
}
|
||||
frame = requestAnimationFrame(scan)
|
||||
} catch {
|
||||
error.value = 'Не удалось открыть камеру. Введите ссылку вручную.'
|
||||
stopScanning()
|
||||
}
|
||||
}
|
||||
|
||||
function stopCamera() {
|
||||
cameraOn.value = false
|
||||
function stopScanning() {
|
||||
cancelAnimationFrame(frame)
|
||||
stream?.getTracks().forEach((track) => track.stop())
|
||||
stream = undefined
|
||||
scanning.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!authStore.isAuthenticated) router.replace('/login')
|
||||
function close() {
|
||||
stopScanning()
|
||||
show.value = false
|
||||
}
|
||||
|
||||
watch(show, (isShown) => {
|
||||
if (isShown) startScanning()
|
||||
else stopScanning()
|
||||
})
|
||||
onBeforeUnmount(stopCamera)
|
||||
|
||||
onBeforeUnmount(stopScanning)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<HeaderMenu active="games" />
|
||||
<div class="center-block-custom center-middle-block-custom scanner-page">
|
||||
<n-card title="Проверка билета" class="scanner-card">
|
||||
<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 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>
|
||||
<video v-if="scanning" ref="video" class="ticket-scanner-video" muted playsinline />
|
||||
<n-alert v-if="error" type="error">{{ error }}</n-alert>
|
||||
<n-button v-if="!scanning" @click="startScanning">Сканировать QR-код</n-button>
|
||||
<n-button v-else @click="stopScanning">Остановить камеру</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>
|
||||
<n-button type="primary" :loading="loading" @click="findTicket">
|
||||
Проверить билет
|
||||
</n-button>
|
||||
<template v-if="ticket">
|
||||
<n-alert :type="ticket.status === 'active' ? 'success' : 'warning'">
|
||||
{{ ticket.gameName }}<br />
|
||||
@@ -145,22 +166,14 @@ onBeforeUnmount(stopCamera)
|
||||
</template>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</div>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.scanner-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.scanner-card {
|
||||
max-width: 700px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scanner-video {
|
||||
.ticket-scanner-video {
|
||||
aspect-ratio: 1;
|
||||
background: #000;
|
||||
background-color: #000;
|
||||
max-height: 60vh;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -8,11 +8,6 @@ const router = createRouter({
|
||||
name: 'participation',
|
||||
component: () => import('../views/ParticipationView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/tickets/check',
|
||||
name: 'ticket-check',
|
||||
component: () => import('../views/TicketScannerView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/tickets/:token',
|
||||
name: 'ticket',
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import TicketScannerPage from '@/components/TicketScannerPage.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TicketScannerPage />
|
||||
</template>
|
||||
Reference in New Issue
Block a user