generated from VLADIMIR/template_frontend
add screen
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
<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'
|
||||
|
||||
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: 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="Подключение экрана">
|
||||
<n-qr-code v-if="qrValue" :value="qrValue" :size="260" />
|
||||
<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%;
|
||||
}
|
||||
|
||||
.ready-title {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.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>
|
||||
Reference in New Issue
Block a user