generated from VLADIMIR/template_frontend
231 lines
6.7 KiB
Vue
231 lines
6.7 KiB
Vue
<script setup lang="ts">
|
|
import { NButton, NCard, NQrCode, NSelect, useMessage } from 'naive-ui'
|
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
|
|
import { getAuthClient } from '@/api/auth_client'
|
|
import type { Game, ScreenState } from '@/api/generated/crabs/evening_detective_server'
|
|
import HeaderMenu from '@/components/HeaderMenu.vue'
|
|
import { formatGameDateTime } from '@/utils/datetime'
|
|
|
|
const sessionKey = 'evening-detective-screen-session'
|
|
const client = getAuthClient()
|
|
const message = useMessage()
|
|
const route = useRoute()
|
|
|
|
const screenId = ref('')
|
|
const accessToken = ref('')
|
|
const state = ref<ScreenState>()
|
|
const games = ref<Game[]>([])
|
|
const selectedGameId = ref<number | null>(null)
|
|
const prepared = ref(false)
|
|
const loading = ref(true)
|
|
const binding = ref(false)
|
|
const bindSuccess = ref(false)
|
|
let pollTimer: number | null = null
|
|
|
|
const bindingFlow = computed(() => Boolean(route.query.screenId && route.query.accessToken))
|
|
const qrValue = computed(() => {
|
|
if (!screenId.value || !accessToken.value) return ''
|
|
const url = new URL('/screen', window.location.origin)
|
|
url.searchParams.set('screenId', screenId.value)
|
|
url.searchParams.set('accessToken', accessToken.value)
|
|
return url.toString()
|
|
})
|
|
const gameOptions = computed(() =>
|
|
games.value.map((game) => ({
|
|
label: `${formatGameDateTime(game.startAt).split(',')[0] || 'дата не указана'} — ${game.name || `Игра ${game.id}`}`,
|
|
value: game.id!,
|
|
})),
|
|
)
|
|
|
|
function saveSession() {
|
|
sessionStorage.setItem(sessionKey, JSON.stringify({ screenId: screenId.value, accessToken: accessToken.value }))
|
|
}
|
|
|
|
function readSession() {
|
|
try {
|
|
const saved = JSON.parse(sessionStorage.getItem(sessionKey) || '{}') as {
|
|
screenId?: string
|
|
accessToken?: string
|
|
}
|
|
screenId.value = saved.screenId || ''
|
|
accessToken.value = saved.accessToken || ''
|
|
} catch {
|
|
sessionStorage.removeItem(sessionKey)
|
|
}
|
|
}
|
|
|
|
async function createSession() {
|
|
const res = await client.CreateScreenSession({})
|
|
if (res.error || !res.screenId || !res.accessToken) {
|
|
throw new Error(res.error || 'Не удалось создать сессию экрана')
|
|
}
|
|
screenId.value = res.screenId
|
|
accessToken.value = res.accessToken
|
|
saveSession()
|
|
}
|
|
|
|
async function loadState() {
|
|
if (!screenId.value || !accessToken.value) return
|
|
try {
|
|
const res = await client.GetScreenState({ screenId: screenId.value, accessToken: accessToken.value })
|
|
if (res.error) throw new Error(res.error)
|
|
state.value = res.state
|
|
} catch (err) {
|
|
message.error(err instanceof Error ? err.message : 'Не удалось получить состояние экрана')
|
|
}
|
|
}
|
|
|
|
async function loadGames() {
|
|
try {
|
|
const res = await client.GetGames({})
|
|
if (res.error) throw new Error(res.error)
|
|
games.value = res.games || []
|
|
} catch (err) {
|
|
message.error(err instanceof Error ? err.message : 'Не удалось загрузить игры')
|
|
}
|
|
}
|
|
|
|
async function bind() {
|
|
if (!selectedGameId.value || !route.query.screenId || !route.query.accessToken) return
|
|
binding.value = true
|
|
try {
|
|
const res = await client.BindScreen({
|
|
screenId: String(route.query.screenId),
|
|
accessToken: String(route.query.accessToken),
|
|
gameId: selectedGameId.value,
|
|
})
|
|
if (res.error) throw new Error(res.error)
|
|
bindSuccess.value = true
|
|
} catch (err) {
|
|
message.error(err instanceof Error ? err.message : 'Не удалось привязать экран')
|
|
} finally {
|
|
binding.value = false
|
|
}
|
|
}
|
|
|
|
async function prepareScreen() {
|
|
try {
|
|
await document.documentElement.requestFullscreen?.()
|
|
} catch {
|
|
message.warning('Полноэкранный режим недоступен в этом браузере')
|
|
}
|
|
prepared.value = true
|
|
}
|
|
|
|
onMounted(async () => {
|
|
if (bindingFlow.value) {
|
|
await loadGames()
|
|
loading.value = false
|
|
return
|
|
}
|
|
readSession()
|
|
try {
|
|
if (!screenId.value || !accessToken.value) await createSession()
|
|
await loadState()
|
|
pollTimer = window.setInterval(loadState, 1000)
|
|
} catch (err) {
|
|
message.error(err instanceof Error ? err.message : 'Не удалось подготовить экран')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
if (pollTimer != null) window.clearInterval(pollTimer)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="bindingFlow" class="screen-center">
|
|
<n-card class="binding-card" title="Привязка экрана">
|
|
<template v-if="bindSuccess">
|
|
<p>Экран привязан к игре.</p>
|
|
<p>Можно закрыть эту страницу.</p>
|
|
</template>
|
|
<template v-else>
|
|
<p>Выберите игру, к которой привязать экран.</p>
|
|
<n-select v-model:value="selectedGameId" :options="gameOptions" placeholder="Игра" />
|
|
<n-button type="primary" :loading="binding" :disabled="selectedGameId == null" @click="bind">
|
|
Привязать
|
|
</n-button>
|
|
</template>
|
|
</n-card>
|
|
</div>
|
|
|
|
<div v-else class="screen-page">
|
|
<div v-if="loading" class="screen-center">Подготовка экрана…</div>
|
|
<template v-else>
|
|
<div v-if="!state?.gameId" class="screen-center">
|
|
<n-card class="qr-card" title="Подключение экрана">
|
|
<div v-if="qrValue" class="screen-qr">
|
|
<n-qr-code :value="qrValue" :size="260" :padding="0" />
|
|
</div>
|
|
<p>Отсканируйте QR телефоном организатора и выберите игру.</p>
|
|
<n-button v-if="!prepared" type="primary" @click="prepareScreen">Подготовить экран</n-button>
|
|
</n-card>
|
|
</div>
|
|
<div v-else class="screen-center">
|
|
<n-card class="ready-card">
|
|
<p class="ready-title">Экран подключён</p>
|
|
<p>Игра №{{ state.gameId }}</p>
|
|
<n-button v-if="!prepared" type="primary" @click="prepareScreen">Подготовить экран</n-button>
|
|
</n-card>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<HeaderMenu v-if="bindingFlow" active="games" />
|
|
</template>
|
|
|
|
<style scoped>
|
|
.binding-card,
|
|
.qr-card,
|
|
.ready-card {
|
|
max-width: min(94vw, 520px);
|
|
text-align: center;
|
|
width: 100%;
|
|
}
|
|
|
|
.binding-card p,
|
|
.qr-card p,
|
|
.ready-card p {
|
|
margin: 0 0 16px;
|
|
}
|
|
|
|
.binding-card .n-select {
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.binding-card .n-button,
|
|
.qr-card .n-button,
|
|
.ready-card .n-button {
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.ready-title {
|
|
font-size: 28px;
|
|
}
|
|
|
|
.screen-qr {
|
|
background: #fff;
|
|
line-height: 0;
|
|
padding: 12px;
|
|
}
|
|
|
|
.screen-center {
|
|
align-items: center;
|
|
display: flex;
|
|
justify-content: center;
|
|
min-height: 100dvh;
|
|
padding: 20px;
|
|
}
|
|
|
|
.screen-page {
|
|
background: #111;
|
|
color: #eee;
|
|
min-height: 100dvh;
|
|
}
|
|
</style>
|