generated from VLADIMIR/template_frontend
up
This commit is contained in:
+197
-259
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Flag, Pause, Play, Reset, Settings, Trophy } from '@vicons/carbon'
|
||||
import { Flag, Pause, Play, Reset, Trophy } from '@vicons/carbon'
|
||||
import { Icon } from '@vicons/utils'
|
||||
import { NButton, NTag } from 'naive-ui'
|
||||
import {
|
||||
@@ -55,25 +55,26 @@ const isDownloadingQR = ref(false)
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const gameId = route.params.id
|
||||
type GameTab = 'game' | 'teams' | 'screens-map'
|
||||
type GameTab = 'game' | 'answers' | 'teams' | 'screens' | 'screens-map'
|
||||
|
||||
function isGameTab(value: string): value is GameTab {
|
||||
return ['game', 'teams', 'screens-map'].includes(value)
|
||||
return ['game', 'answers', 'teams', 'screens', 'screens-map'].includes(value)
|
||||
}
|
||||
|
||||
const queryTab = typeof route.query.tab === 'string' ? route.query.tab : ''
|
||||
const activeTab = ref<GameTab>(isGameTab(queryTab) ? queryTab : 'game')
|
||||
const activeTab = ref<GameTab>(isGameTab(queryTab) ? queryTab : 'screens-map')
|
||||
|
||||
async function setActiveTab(value: string) {
|
||||
if (!isGameTab(value)) return
|
||||
activeTab.value = value
|
||||
await router.replace({ query: { ...route.query, tab: value } })
|
||||
if (value === 'answers') await loadGameAnswers()
|
||||
}
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const showSettingsModal = ref(false)
|
||||
const settingsDraft = ref({ name: '', description: '', format: 'online', location: '' })
|
||||
const settingsDraftInitialized = ref(false)
|
||||
const showGiveApplicationModal = ref(false)
|
||||
const showAddTeamModal = ref(false)
|
||||
const showDeleteTeamModal = ref(false)
|
||||
@@ -85,7 +86,6 @@ const deletedGameName = ref('')
|
||||
const showQR = ref(false)
|
||||
const urlQR = ref('')
|
||||
const titleQR = ref('')
|
||||
const showIssueTicketsModal = ref(false)
|
||||
const ticketCount = ref(1)
|
||||
const issuedTickets = ref<Ticket[]>([])
|
||||
const isIssuingTickets = ref(false)
|
||||
@@ -127,6 +127,15 @@ const screenPosition = ref(0)
|
||||
async function getGame(id: number) {
|
||||
const res = await client.GetGame({ ID: id })
|
||||
game.value = res.game!
|
||||
if (!settingsDraftInitialized.value) {
|
||||
settingsDraft.value = {
|
||||
name: game.value.name || '',
|
||||
description: game.value.description || '',
|
||||
format: game.value.format || 'online',
|
||||
location: game.value.location || '',
|
||||
}
|
||||
settingsDraftInitialized.value = true
|
||||
}
|
||||
game.value.teams = game.value.teams?.sort((a, b) => {
|
||||
let aLen = 0
|
||||
if ((a.applications?.length || 0) > 0) {
|
||||
@@ -384,20 +393,6 @@ function toResults() {
|
||||
router.push('/games/' + gameId + '/results')
|
||||
}
|
||||
|
||||
function openSettings() {
|
||||
settingsDraft.value = {
|
||||
name: game.value.name || '',
|
||||
description: game.value.description || '',
|
||||
format: game.value.format || 'online',
|
||||
location: game.value.location || '',
|
||||
}
|
||||
showSettingsModal.value = true
|
||||
}
|
||||
|
||||
function closeSettings() {
|
||||
showSettingsModal.value = false
|
||||
}
|
||||
|
||||
async function updateGame() {
|
||||
await client.UpdateGame({
|
||||
ID: game.value.ID,
|
||||
@@ -409,7 +404,6 @@ async function updateGame() {
|
||||
scenarioID: game.value.scenario?.ID,
|
||||
})
|
||||
await getGame(game.value.ID!)
|
||||
showSettingsModal.value = false
|
||||
}
|
||||
|
||||
const giveApplicationTeam = ref<Team>({
|
||||
@@ -518,12 +512,6 @@ async function openQR(url: string, text: string) {
|
||||
showQR.value = true
|
||||
}
|
||||
|
||||
function openIssueTickets() {
|
||||
ticketCount.value = 1
|
||||
issuedTickets.value = []
|
||||
showIssueTicketsModal.value = true
|
||||
}
|
||||
|
||||
async function issueTickets() {
|
||||
if (isIssuingTickets.value || !game.value.ID) return
|
||||
isIssuingTickets.value = true
|
||||
@@ -584,7 +572,6 @@ async function finishGame() {
|
||||
}
|
||||
|
||||
// ===== Ответы команд и оценка организатором =====
|
||||
const showAnswersModal = ref(false)
|
||||
const showReviewsModal = ref(false)
|
||||
const gameReviews = ref<GameReview[]>([])
|
||||
const reviewsAverage = ref(0)
|
||||
@@ -620,11 +607,6 @@ async function loadGameAnswers() {
|
||||
scores.value = next
|
||||
}
|
||||
|
||||
async function openAnswersModal() {
|
||||
showAnswersModal.value = true
|
||||
await loadGameAnswers()
|
||||
}
|
||||
|
||||
async function openReviewsModal() {
|
||||
showReviewsModal.value = true
|
||||
const res = await client.GetGameReviewsForModeration({ gameID: Number(gameId) })
|
||||
@@ -654,6 +636,10 @@ async function saveAllScores() {
|
||||
await loadGameAnswers()
|
||||
}
|
||||
|
||||
if (activeTab.value === 'answers') {
|
||||
loadGameAnswers()
|
||||
}
|
||||
|
||||
function getCurrentUTC(): Date {
|
||||
const now = new Date()
|
||||
return new Date(now.getTime() - now.getTimezoneOffset() * 60000)
|
||||
@@ -698,96 +684,103 @@ useSimplePolling(() => {
|
||||
<div class="center-block-custom">
|
||||
<div class="width1200">
|
||||
<n-tabs :value="activeTab" type="line" animated @update:value="setActiveTab">
|
||||
<n-tab-pane name="game" tab="Игра">
|
||||
<n-tab-pane name="game" tab="Настройки">
|
||||
<div class="game-tab-content">
|
||||
<div class="settings-block">
|
||||
<n-flex justify="space-between">
|
||||
<n-space>
|
||||
<n-tag :bordered="false" type="info">
|
||||
Участвуют команд: {{ activeTeamCount }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ timeString }}
|
||||
</n-tag>
|
||||
<n-tag v-if="formatGameDateTime(game.startAt)" type="info">
|
||||
Начало: {{ formatGameDateTime(game.startAt) }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
|
||||
<n-flex justify="end">
|
||||
<n-button quaternary @click="openQR(url, 'Админка')"> QR </n-button>
|
||||
<n-button quaternary @click="openIssueTickets()"> Выпустить билеты </n-button>
|
||||
<n-button quaternary @click="openAnswersModal()"> Ответы команд </n-button>
|
||||
<n-button v-if="game.status == 'finish'" quaternary @click="toResults()">
|
||||
<Icon class="settings-icon">
|
||||
<Trophy />
|
||||
</Icon>
|
||||
Итоги
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="activeTeamCount"
|
||||
quaternary
|
||||
:loading="isDownloadingQR"
|
||||
:disabled="isDownloadingQR"
|
||||
@click="downloadTeamQR()"
|
||||
>
|
||||
QR команд
|
||||
</n-button>
|
||||
<n-button quaternary @click="openSettings()">
|
||||
<Icon class="settings-icon">
|
||||
<Settings />
|
||||
</Icon>
|
||||
Настройки
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="(game.teams?.length || 0) == 0"
|
||||
type="error"
|
||||
quaternary
|
||||
@click="showDeleteGameModal = true"
|
||||
>
|
||||
Удалить игру
|
||||
</n-button>
|
||||
|
||||
<n-space>
|
||||
<n-button type="info" v-if="game.status != 'draft'" dashed @click="resetGame()">
|
||||
<Icon>
|
||||
<Reset />
|
||||
</Icon>
|
||||
</n-button>
|
||||
<n-button type="info" v-if="game.status == 'draft'" dashed @click="startGame()">
|
||||
<Icon>
|
||||
<Play />
|
||||
</Icon>
|
||||
</n-button>
|
||||
<n-button type="info" v-if="game.status == 'pause'" dashed @click="playGame()">
|
||||
<Icon>
|
||||
<Play />
|
||||
</Icon>
|
||||
</n-button>
|
||||
<n-button type="info" v-if="game.status == 'run'" dashed @click="pauseGame()">
|
||||
<Icon>
|
||||
<Pause />
|
||||
</Icon>
|
||||
</n-button>
|
||||
<n-button
|
||||
type="info"
|
||||
v-if="game.status != 'finish'"
|
||||
dashed
|
||||
@click="finishGame()"
|
||||
>
|
||||
<Icon>
|
||||
<Flag />
|
||||
</Icon>
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
<UiBlock>
|
||||
<n-flex justify="end">
|
||||
<n-button quaternary @click="openQR(url, 'Админка')">QR</n-button>
|
||||
<n-button v-if="game.status == 'finish'" quaternary @click="toResults()">
|
||||
<Icon class="settings-icon">
|
||||
<Trophy />
|
||||
</Icon>
|
||||
Итоги
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="(game.teams?.length || 0) == 0"
|
||||
type="error"
|
||||
quaternary
|
||||
@click="showDeleteGameModal = true"
|
||||
>
|
||||
Удалить игру
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</div>
|
||||
</UiBlock>
|
||||
<UiBlock>
|
||||
<p class="screens-title">Настройки игры</p>
|
||||
<n-space vertical>
|
||||
<p>Название</p>
|
||||
<n-input
|
||||
v-model:value="settingsDraft.name"
|
||||
type="text"
|
||||
placeholder='Дело №0 "Следствие ведут овечки"'
|
||||
/>
|
||||
|
||||
<p class="settings-header">Формат игры</p>
|
||||
<n-select
|
||||
v-model:value="settingsDraft.format"
|
||||
:options="[
|
||||
{ label: 'Online', value: 'online' },
|
||||
{ label: 'Offline', value: 'offline' },
|
||||
]"
|
||||
aria-label="Формат игры"
|
||||
/>
|
||||
|
||||
<p class="settings-header">Описание</p>
|
||||
<n-input
|
||||
v-model:value="settingsDraft.description"
|
||||
type="textarea"
|
||||
placeholder="Убийство пастуха..."
|
||||
/>
|
||||
|
||||
<p class="settings-header">Место проведения</p>
|
||||
<n-input
|
||||
v-model:value="settingsDraft.location"
|
||||
type="text"
|
||||
placeholder="Оставьте пустым для онлайн-игры"
|
||||
/>
|
||||
</n-space>
|
||||
<n-flex justify="end" class="settings-actions">
|
||||
<n-button @click="updateGame()">Сохранить изменения</n-button>
|
||||
</n-flex>
|
||||
</UiBlock>
|
||||
<UiBlock>
|
||||
<p class="screens-title">Выпуск билетов</p>
|
||||
<n-space vertical>
|
||||
<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>
|
||||
<n-button size="small" @click="openQR(ticket.URL!, `Билет №${ticket.ID}`)">
|
||||
Показать QR
|
||||
</n-button>
|
||||
</div>
|
||||
</n-space>
|
||||
<n-flex justify="end" class="settings-actions">
|
||||
<n-button
|
||||
:loading="isIssuingTickets"
|
||||
:disabled="isIssuingTickets"
|
||||
@click="issueTickets()"
|
||||
>
|
||||
Выпустить
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</UiBlock>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="teams" tab="Команды">
|
||||
<div class="game-tab-content">
|
||||
<n-tag :bordered="false" type="info"> Участвуют команд: {{ activeTeamCount }} </n-tag>
|
||||
<n-flex justify="space-between" align="center">
|
||||
<n-tag :bordered="false" type="info"> Участвуют команд: {{ activeTeamCount }} </n-tag>
|
||||
<n-button
|
||||
v-if="activeTeamCount"
|
||||
quaternary
|
||||
:loading="isDownloadingQR"
|
||||
:disabled="isDownloadingQR"
|
||||
@click="downloadTeamQR()"
|
||||
>
|
||||
QR команд
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<GameTeamsPanel
|
||||
:teams="game.teams || []"
|
||||
:participation-options="participationOptions"
|
||||
@@ -799,8 +792,58 @@ useSimplePolling(() => {
|
||||
/>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="screens-map" tab="Экраны и карта">
|
||||
<n-tab-pane name="screens" tab="Экраны">
|
||||
<div class="game-tab-content">
|
||||
<GameScreensPanel
|
||||
:screens="screens"
|
||||
:screen-binding="screenBinding"
|
||||
@bind="bindScannedScreen"
|
||||
@set-sound="setSoundScreen"
|
||||
@unbind="unbindScreen"
|
||||
/>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="screens-map" tab="Игра">
|
||||
<div class="game-tab-content">
|
||||
<UiBlock>
|
||||
<n-flex justify="space-between">
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
{{ timeString }}
|
||||
</n-tag>
|
||||
<n-tag v-if="formatGameDateTime(game.startAt)" type="info">
|
||||
Начало: {{ formatGameDateTime(game.startAt) }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
<n-flex justify="end">
|
||||
<n-button type="info" v-if="game.status != 'draft'" dashed @click="resetGame()">
|
||||
<Icon>
|
||||
<Reset />
|
||||
</Icon>
|
||||
</n-button>
|
||||
<n-button type="info" v-if="game.status == 'draft'" dashed @click="startGame()">
|
||||
<Icon>
|
||||
<Play />
|
||||
</Icon>
|
||||
</n-button>
|
||||
<n-button type="info" v-if="game.status == 'pause'" dashed @click="playGame()">
|
||||
<Icon>
|
||||
<Play />
|
||||
</Icon>
|
||||
</n-button>
|
||||
<n-button type="info" v-if="game.status == 'run'" dashed @click="pauseGame()">
|
||||
<Icon>
|
||||
<Pause />
|
||||
</Icon>
|
||||
</n-button>
|
||||
<n-button type="info" v-if="game.status != 'finish'" dashed @click="finishGame()">
|
||||
<Icon>
|
||||
<Flag />
|
||||
</Icon>
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</UiBlock>
|
||||
<GameMapPanel
|
||||
:map-items="mapItems"
|
||||
:teams="game.teams || []"
|
||||
@@ -829,72 +872,44 @@ useSimplePolling(() => {
|
||||
@update:screen-volume="screenVolume = $event ?? 0"
|
||||
@update:screen-position="screenPosition = $event ?? 0"
|
||||
/>
|
||||
<GameScreensPanel
|
||||
:screens="screens"
|
||||
:screen-binding="screenBinding"
|
||||
@bind="bindScannedScreen"
|
||||
@set-sound="setSoundScreen"
|
||||
@unbind="unbindScreen"
|
||||
/>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="answers" tab="Проверка ответов">
|
||||
<div class="game-tab-content">
|
||||
<div v-if="answersQuestions.length == 0" class="answers-empty">
|
||||
Вопросы дела не заданы или команды ещё не отвечали.
|
||||
</div>
|
||||
<UiBlock v-for="question in answersQuestions" :key="question.code">
|
||||
<p class="answers-question-title">{{ question.code }}: {{ question.text }}</p>
|
||||
<p class="answers-question-answer">
|
||||
Правильный ответ:<br /><span class="correct-answer">{{ question.answer || '—' }}</span>
|
||||
</p>
|
||||
<div v-for="team in answersTeams" :key="team.team?.ID" class="answers-team-row">
|
||||
<div class="answers-team-name">
|
||||
{{ team.team?.name }}
|
||||
</div>
|
||||
<div class="answers-team-answer">
|
||||
{{ teamAnswer(team, question.code) || '—' }}
|
||||
</div>
|
||||
<n-input-number
|
||||
:value="scores[scoreKey(team.team?.ID, question.code)]"
|
||||
@update:value="onScoreUpdate(scoreKey(team.team?.ID, question.code), $event)"
|
||||
placeholder="Балл"
|
||||
class="answers-score-input"
|
||||
/>
|
||||
</div>
|
||||
</UiBlock>
|
||||
<n-flex justify="end">
|
||||
<n-button @click="saveAllScores()" :disabled="answersQuestions.length == 0">
|
||||
Сохранить баллы
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Окно настроек -->
|
||||
<n-modal v-model:show="showSettingsModal">
|
||||
<n-card
|
||||
style="width: 600px"
|
||||
title="Настройки игры"
|
||||
:bordered="false"
|
||||
size="huge"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<template #header-extra>
|
||||
<n-button @click="closeSettings()"> x </n-button>
|
||||
</template>
|
||||
<n-space vertical>
|
||||
<p>Название</p>
|
||||
<n-input
|
||||
v-model:value="settingsDraft.name"
|
||||
type="text"
|
||||
placeholder='Дело №0 "Следствие ведут овечки"'
|
||||
/>
|
||||
|
||||
<p class="settings-header">Формат игры</p>
|
||||
<n-select
|
||||
v-model:value="settingsDraft.format"
|
||||
:options="[
|
||||
{ label: 'Online', value: 'online' },
|
||||
{ label: 'Offline', value: 'offline' },
|
||||
]"
|
||||
aria-label="Формат игры"
|
||||
/>
|
||||
|
||||
<p class="settings-header">Описание</p>
|
||||
<n-input
|
||||
v-model:value="settingsDraft.description"
|
||||
type="textarea"
|
||||
placeholder="Убийство пастуха..."
|
||||
/>
|
||||
|
||||
<p class="settings-header">Место проведения</p>
|
||||
<n-input
|
||||
v-model:value="settingsDraft.location"
|
||||
type="text"
|
||||
placeholder="Оставьте пустым для онлайн-игры"
|
||||
/>
|
||||
</n-space>
|
||||
<template #footer>
|
||||
<n-flex justify="end">
|
||||
<n-button @click="updateGame()" ghost> Сохранить изменения </n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
|
||||
<!-- Окно выдачи улик -->
|
||||
<n-modal v-model:show="showGiveApplicationModal">
|
||||
<n-card
|
||||
@@ -1062,40 +1077,6 @@ useSimplePolling(() => {
|
||||
</n-card>
|
||||
</n-modal>
|
||||
|
||||
<!-- Окно qr -->
|
||||
<n-modal v-model:show="showIssueTicketsModal">
|
||||
<n-card
|
||||
style="width: min(94vw, 700px)"
|
||||
title="Выпуск билетов"
|
||||
:bordered="false"
|
||||
size="huge"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<template #header-extra>
|
||||
<n-button @click="showIssueTicketsModal = false"> x </n-button>
|
||||
</template>
|
||||
<n-space vertical>
|
||||
<n-input-number v-model:value="ticketCount" :min="1" :max="100" />
|
||||
<n-button
|
||||
type="primary"
|
||||
:loading="isIssuingTickets"
|
||||
:disabled="isIssuingTickets"
|
||||
@click="issueTickets()"
|
||||
>
|
||||
Выпустить
|
||||
</n-button>
|
||||
<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>
|
||||
<n-button size="small" @click="openQR(ticket.URL!, `Билет №${ticket.ID}`)">
|
||||
Показать QR
|
||||
</n-button>
|
||||
</div>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
|
||||
<n-modal v-model:show="showQR">
|
||||
<n-card
|
||||
style="width: 600px"
|
||||
@@ -1118,53 +1099,6 @@ useSimplePolling(() => {
|
||||
</n-card>
|
||||
</n-modal>
|
||||
|
||||
<!-- Окно ответов команд и оценки -->
|
||||
<n-modal v-model:show="showAnswersModal">
|
||||
<n-card
|
||||
style="width: min(94vw, 900px)"
|
||||
title="Ответы команд"
|
||||
:bordered="false"
|
||||
size="huge"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
:content-style="{ maxHeight: '70vh', overflow: 'auto' }"
|
||||
>
|
||||
<template #header-extra>
|
||||
<n-button @click="showAnswersModal = false"> x </n-button>
|
||||
</template>
|
||||
<div v-if="answersQuestions.length == 0" class="answers-empty">
|
||||
Вопросы дела не заданы или команды ещё не отвечали.
|
||||
</div>
|
||||
<UiBlock v-for="question in answersQuestions" :key="question.code">
|
||||
<p class="answers-question-title">{{ question.code }}: {{ question.text }}</p>
|
||||
<p class="answers-question-answer">
|
||||
Правильный ответ:<br /><span class="correct-answer">{{ question.answer || '—' }}</span>
|
||||
</p>
|
||||
<div v-for="team in answersTeams" :key="team.team?.ID" class="answers-team-row">
|
||||
<div class="answers-team-name">
|
||||
{{ team.team?.name }}
|
||||
</div>
|
||||
<div class="answers-team-answer">
|
||||
{{ teamAnswer(team, question.code) || '—' }}
|
||||
</div>
|
||||
<n-input-number
|
||||
:value="scores[scoreKey(team.team?.ID, question.code)]"
|
||||
@update:value="onScoreUpdate(scoreKey(team.team?.ID, question.code), $event)"
|
||||
placeholder="Балл"
|
||||
class="answers-score-input"
|
||||
/>
|
||||
</div>
|
||||
</UiBlock>
|
||||
<template #footer>
|
||||
<n-flex justify="end">
|
||||
<n-button @click="saveAllScores()" :disabled="answersQuestions.length == 0">
|
||||
Сохранить баллы
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
|
||||
<n-modal v-model:show="showReviewsModal">
|
||||
<n-card
|
||||
style="width: min(94vw, 800px)"
|
||||
@@ -1246,6 +1180,10 @@ useSimplePolling(() => {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.settings-actions {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.link-button {
|
||||
margin-top: 16px;
|
||||
width: 100%;
|
||||
|
||||
Reference in New Issue
Block a user