This commit is contained in:
2026-10-04 23:46:57 +07:00
parent 68fd0760f6
commit 196dbb3b7b
6 changed files with 412 additions and 359 deletions
+159 -165
View File
@@ -44,6 +44,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 UiBlock from '@/components/UiBlock.vue'
import { useSimplePolling } from '@/composables/useSimplePolling'
import { applicationFileType, applicationFileURL } from '@/utils/application'
import { formatGameDateTime, parseDateWithoutTimezone } from '@/utils/datetime'
@@ -698,133 +699,144 @@ useSimplePolling(() => {
<div class="width1200">
<n-tabs :value="activeTab" type="line" animated @update:value="setActiveTab">
<n-tab-pane name="game" tab="Игра">
<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>
<div class="game-tab-content">
<div class="settings-block">
<n-flex justify="space-between">
<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-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>
</n-flex>
</n-flex>
</div>
</div>
</n-tab-pane>
<n-tab-pane name="teams" tab="Команды">
<n-tag :bordered="false" type="info"> Участвуют команд: {{ activeTeamCount }} </n-tag>
<GameTeamsPanel
:teams="game.teams || []"
:participation-options="participationOptions"
@add="showAddTeamModal = true"
@open-qr="openTeamQR"
@preview="(id, password) => toTeamStory(id, password, true)"
@delete="deleteTeam"
@give-application="giveApplication"
/>
<div class="game-tab-content">
<n-tag :bordered="false" type="info"> Участвуют команд: {{ activeTeamCount }} </n-tag>
<GameTeamsPanel
:teams="game.teams || []"
:participation-options="participationOptions"
@add="showAddTeamModal = true"
@open-qr="openTeamQR"
@preview="(id, password) => toTeamStory(id, password, true)"
@delete="deleteTeam"
@give-application="giveApplication"
/>
</div>
</n-tab-pane>
<n-tab-pane name="screens-map" tab="Экраны и карта">
<GameScreensPanel
:screens="screens"
:screen-binding="screenBinding"
@bind="bindScannedScreen"
@set-sound="setSoundScreen"
@unbind="unbindScreen"
/>
<GameMapPanel
:map-items="mapItems"
:teams="game.teams || []"
:screens="screens"
:preview-questions="previewQuestions"
:preview-teams="previewTeams"
:screen-plan="screenPlan"
:screen-plan-draft="screenPlanDraft"
:screen-plan-dirty="screenPlanDirty"
:screen-plan-saving="screenPlanSaving"
:screen-plan-mode="screenPlanMode"
:screen-volume="screenVolume"
:screen-position="screenPosition"
@save-item="saveMapItemAndReload"
@give-application="giveApplication"
@add-step="addScreenPlanStep"
@edit="editScreenPlan"
@cancel="cancelScreenPlanEdit"
@remove-step="removeScreenPlanStep"
@move-step="moveScreenPlanStep"
@dirty="markScreenPlanDirty"
@clear-plan="clearScreenPlan"
@save="saveScreenPlan"
@set-step="setScreenPlanStep"
@command="sendScreenPlanCommand"
@update:screen-volume="screenVolume = $event ?? 0"
@update:screen-position="screenPosition = $event ?? 0"
/>
<div class="game-tab-content">
<GameMapPanel
:map-items="mapItems"
:teams="game.teams || []"
:screens="screens"
:preview-questions="previewQuestions"
:preview-teams="previewTeams"
:screen-plan="screenPlan"
:screen-plan-draft="screenPlanDraft"
:screen-plan-dirty="screenPlanDirty"
:screen-plan-saving="screenPlanSaving"
:screen-plan-mode="screenPlanMode"
:screen-volume="screenVolume"
:screen-position="screenPosition"
@save-item="saveMapItemAndReload"
@give-application="giveApplication"
@add-step="addScreenPlanStep"
@edit="editScreenPlan"
@cancel="cancelScreenPlanEdit"
@remove-step="removeScreenPlanStep"
@move-step="moveScreenPlanStep"
@dirty="markScreenPlanDirty"
@clear-plan="clearScreenPlan"
@save="saveScreenPlan"
@set-step="setScreenPlanStep"
@command="sendScreenPlanCommand"
@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-tabs>
</div>
@@ -1123,7 +1135,7 @@ useSimplePolling(() => {
<div v-if="answersQuestions.length == 0" class="answers-empty">
Вопросы дела не заданы или команды ещё не отвечали.
</div>
<div v-for="question in answersQuestions" :key="question.code" class="answers-question-block">
<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>
@@ -1142,7 +1154,7 @@ useSimplePolling(() => {
class="answers-score-input"
/>
</div>
</div>
</UiBlock>
<template #footer>
<n-flex justify="end">
<n-button @click="saveAllScores()" :disabled="answersQuestions.length == 0">
@@ -1199,7 +1211,11 @@ useSimplePolling(() => {
<style scoped>
.reviews-summary {
margin-bottom: 20px;
margin-bottom: 16px;
}
.game-tab-content {
padding-top: 0;
}
.review-row {
@@ -1231,7 +1247,7 @@ useSimplePolling(() => {
}
.link-button {
margin-top: 20px;
margin-top: 16px;
width: 100%;
}
@@ -1253,16 +1269,16 @@ useSimplePolling(() => {
display: inline-block;
line-height: 0;
margin: 0 auto;
padding: 12px;
padding: 16px;
}
.give-application-image-block {
background-image: url('@/assets/images/paper_white.jpg');
background-size: cover;
box-shadow: 0 3px 15px rgb(98 98 98);
margin: 10px 0 5px;
margin: 8px 0;
max-width: 320px;
padding: 7px;
padding: 8px;
}
.give-application-image {
@@ -1270,13 +1286,13 @@ useSimplePolling(() => {
}
.give-application-audio {
margin: 10px 0 5px;
margin: 8px 0;
width: 100%;
}
.give-application-pdf-link {
display: inline-block;
margin: 10px 0 5px;
margin: 8px 0;
text-decoration: underline;
}
@@ -1284,29 +1300,22 @@ useSimplePolling(() => {
color: #777;
}
.answers-question-block {
background-color: #222;
border-radius: 8px;
margin: 15px 0;
padding: 12px;
}
.answers-question-title {
font-size: 17px;
font-weight: 600;
margin: 0 0 4px;
margin: 0 0 8px;
}
.answers-question-answer {
color: #aaa;
margin: 0 0 10px;
margin: 0 0 16px;
}
.correct-answer {
color: gold;
display: inline-block;
font-weight: 500;
margin-top: 4px;
margin-top: 8px;
white-space: pre-wrap;
}
@@ -1316,9 +1325,9 @@ useSimplePolling(() => {
border-radius: 6px;
display: flex;
flex-flow: row wrap;
gap: 10px;
gap: 8px;
margin: 8px 0;
padding: 8px 10px;
padding: 8px;
}
.answers-team-name {
@@ -1336,26 +1345,19 @@ useSimplePolling(() => {
width: 110px;
}
.screens-block {
background-color: #222;
border-radius: 10px;
margin: 12px 0;
padding: 16px;
}
.screens-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 12px;
margin-bottom: 8px;
}
.screen-row {
align-items: center;
border-top: 1px solid rgb(255 255 255 / 10%);
display: flex;
gap: 12px;
gap: 8px;
justify-content: space-between;
padding: 12px 0;
padding: 16px 0;
}
.screen-status {
@@ -1365,7 +1367,7 @@ useSimplePolling(() => {
.screen-empty {
color: #aaa;
margin: 12px 0 0;
margin: 16px 0 0;
}
.screen-error {
@@ -1375,18 +1377,10 @@ useSimplePolling(() => {
.map-preview-layout {
display: grid;
gap: 12px;
gap: 16px;
grid-template-columns: minmax(0, 1fr) minmax(280px, 420px);
}
.map-block,
.screen-preview-panel {
background-color: #222;
border-radius: 10px;
margin: 12px 0;
padding: 16px;
}
.screen-preview-placeholder {
align-items: center;
aspect-ratio: 16 / 9;
@@ -1428,7 +1422,7 @@ useSimplePolling(() => {
border-radius: 6px;
cursor: grab;
min-width: 90px;
padding: 8px 24px 8px 10px;
padding: 8px 24px 8px 8px;
position: absolute;
user-select: none;
}
@@ -1458,7 +1452,7 @@ useSimplePolling(() => {
background-color: #48633a;
border-radius: 6px 6px 0 0;
cursor: grab;
margin: -8px -24px 8px -10px;
margin: -8px -24px 8px -8px;
padding: 8px 24px 8px 10px;
}
@@ -1497,7 +1491,7 @@ useSimplePolling(() => {
}
.map-hidden-items {
margin-top: 10px;
margin-top: 16px;
}
@media (width < 1024px) {
@@ -1511,7 +1505,7 @@ useSimplePolling(() => {
border-top: 1px solid #777;
display: flex;
flex-wrap: wrap;
gap: 10px;
padding-top: 10px;
gap: 8px;
padding-top: 8px;
}
</style>