generated from VLADIMIR/template_frontend
up
This commit is contained in:
+203
-159
@@ -15,6 +15,7 @@ import type {
|
||||
TeamResult,
|
||||
} from '@/api/generated/crabs/evening_detective_server'
|
||||
import ScreenPreview from '@/components/ScreenPreview.vue'
|
||||
import UiBlock from '@/components/UiBlock.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
mapItems: MapItem[]
|
||||
@@ -154,8 +155,8 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<div class="map-preview-layout">
|
||||
<div class="map-block">
|
||||
<n-flex align="center" justify="space-between">
|
||||
<UiBlock>
|
||||
<n-flex align="center" justify="space-between" class="screen-header">
|
||||
<p class="screens-title">Карта помещения</p>
|
||||
<n-space>
|
||||
<n-button size="small" @click="changeMapScale(-0.1)">−</n-button>
|
||||
@@ -234,8 +235,8 @@ onBeforeUnmount(() => {
|
||||
Добавить {{ item.label }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
<aside class="screen-preview-panel">
|
||||
</UiBlock>
|
||||
<UiBlock tag="aside" class="screen-preview-panel">
|
||||
<p class="screens-title">Превью экрана</p>
|
||||
<ScreenPreview
|
||||
v-if="previewScreen?.status === 'connected'"
|
||||
@@ -245,7 +246,7 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
<div v-else class="screen-preview-placeholder">Экран не подключён</div>
|
||||
<div class="screen-plan">
|
||||
<n-flex align="center" justify="space-between">
|
||||
<n-flex align="center" justify="space-between" class="screen-header">
|
||||
<p class="screens-title">План экранов</p>
|
||||
<n-space>
|
||||
<n-button
|
||||
@@ -282,66 +283,77 @@ onBeforeUnmount(() => {
|
||||
План экранов ещё не создан.
|
||||
</p>
|
||||
<template v-else-if="screenPlanMode === 'edit'">
|
||||
<n-space v-for="(step, index) in screenPlanDraft" :key="index" align="center" wrap>
|
||||
<n-text>{{ index + 1 }}</n-text>
|
||||
<n-select
|
||||
v-model:value="step.mode"
|
||||
:options="screenPlanModeOptions"
|
||||
:disabled="screenPlanSaving"
|
||||
size="small"
|
||||
style="width: 180px"
|
||||
@update:value="emit('dirty')"
|
||||
/>
|
||||
<n-input
|
||||
v-if="step.mode === 'SCREEN_MODE_TEXT'"
|
||||
v-model:value="step.text"
|
||||
:disabled="screenPlanSaving"
|
||||
size="small"
|
||||
placeholder="Текст экрана"
|
||||
style="min-width: 240px"
|
||||
@update:value="emit('dirty')"
|
||||
/>
|
||||
<template v-if="step.mode === 'SCREEN_MODE_TIMER'">
|
||||
<n-space
|
||||
v-for="(step, index) in screenPlanDraft"
|
||||
:key="index"
|
||||
class="screen-plan-step"
|
||||
align="center"
|
||||
justify="space-between"
|
||||
wrap
|
||||
>
|
||||
<n-space align="center" wrap>
|
||||
<n-text>{{ index + 1 }}</n-text>
|
||||
<n-select
|
||||
v-model:value="step.mode"
|
||||
:options="screenPlanModeOptions"
|
||||
:disabled="screenPlanSaving"
|
||||
size="small"
|
||||
style="width: 180px"
|
||||
@update:value="emit('dirty')"
|
||||
/>
|
||||
<n-input
|
||||
v-model:value="step.timerLabel"
|
||||
v-if="step.mode === 'SCREEN_MODE_TEXT'"
|
||||
v-model:value="step.text"
|
||||
:disabled="screenPlanSaving"
|
||||
size="small"
|
||||
placeholder="Надпись таймера"
|
||||
placeholder="Текст экрана"
|
||||
style="min-width: 240px"
|
||||
@update:value="emit('dirty')"
|
||||
/>
|
||||
<n-input-number
|
||||
v-model:value="step.timerDurationSeconds"
|
||||
:min="1"
|
||||
:max="86400"
|
||||
:disabled="screenPlanSaving"
|
||||
<template v-if="step.mode === 'SCREEN_MODE_TIMER'">
|
||||
<n-input
|
||||
v-model:value="step.timerLabel"
|
||||
:disabled="screenPlanSaving"
|
||||
size="small"
|
||||
placeholder="Надпись таймера"
|
||||
@update:value="emit('dirty')"
|
||||
/>
|
||||
<n-input-number
|
||||
v-model:value="step.timerDurationSeconds"
|
||||
:min="1"
|
||||
:max="86400"
|
||||
:disabled="screenPlanSaving"
|
||||
size="small"
|
||||
placeholder="Секунды"
|
||||
@update:value="emit('dirty')"
|
||||
/>
|
||||
</template>
|
||||
</n-space>
|
||||
<n-space class="screen-plan-actions">
|
||||
<n-button
|
||||
size="small"
|
||||
placeholder="Секунды"
|
||||
@update:value="emit('dirty')"
|
||||
/>
|
||||
</template>
|
||||
<n-button
|
||||
size="small"
|
||||
:disabled="screenPlanSaving || index === 0"
|
||||
@click="emit('moveStep', index, -1)"
|
||||
>
|
||||
↑
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
:disabled="screenPlanSaving || index === screenPlanDraft.length - 1"
|
||||
@click="emit('moveStep', index, 1)"
|
||||
>
|
||||
↓
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
type="error"
|
||||
ghost
|
||||
:disabled="screenPlanSaving"
|
||||
@click="emit('removeStep', index)"
|
||||
>
|
||||
Удалить
|
||||
</n-button>
|
||||
:disabled="screenPlanSaving || index === 0"
|
||||
@click="emit('moveStep', index, -1)"
|
||||
>
|
||||
↑
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
:disabled="screenPlanSaving || index === screenPlanDraft.length - 1"
|
||||
@click="emit('moveStep', index, 1)"
|
||||
>
|
||||
↓
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
type="error"
|
||||
ghost
|
||||
:disabled="screenPlanSaving"
|
||||
@click="emit('removeStep', index)"
|
||||
>
|
||||
Удалить
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-space>
|
||||
<n-space>
|
||||
<n-button size="small" :disabled="screenPlanSaving" @click="emit('addStep')">
|
||||
@@ -358,119 +370,138 @@ onBeforeUnmount(() => {
|
||||
</n-space>
|
||||
</template>
|
||||
<template v-else-if="screenPlan">
|
||||
<n-space v-for="(step, index) in screenPlan.steps || []" :key="index" align="center" wrap>
|
||||
<n-text>{{ index + 1 }}</n-text>
|
||||
<n-text>
|
||||
{{ screenPlanModeOptions.find((option) => option.value === step.mode)?.label }}
|
||||
<template v-if="step.mode === 'SCREEN_MODE_TEXT'">: {{ step.text }}</template>
|
||||
<template v-else-if="step.mode === 'SCREEN_MODE_TIMER'">
|
||||
: {{ step.timerLabel }} ({{ step.timerDurationSeconds }} с)
|
||||
</template>
|
||||
</n-text>
|
||||
<n-button
|
||||
size="small"
|
||||
:type="screenPlan.currentStep === index ? 'primary' : 'default'"
|
||||
:disabled="screenPlan.currentStep === index"
|
||||
@click="emit('setStep', index)"
|
||||
>
|
||||
Показать
|
||||
</n-button>
|
||||
<template v-if="screenPlan.currentStep === index">
|
||||
<template v-if="step.mode === 'SCREEN_MODE_TIMER'">
|
||||
<n-button
|
||||
size="small"
|
||||
@click="emit('command', step.mode, 'SCREEN_COMMAND_TIMER_START')"
|
||||
>
|
||||
Старт
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
@click="emit('command', step.mode, 'SCREEN_COMMAND_TIMER_PAUSE')"
|
||||
>
|
||||
Пауза
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
@click="emit('command', step.mode, 'SCREEN_COMMAND_TIMER_RESUME')"
|
||||
>
|
||||
Продолжить
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
@click="emit('command', step.mode, 'SCREEN_COMMAND_TIMER_RESET')"
|
||||
>
|
||||
Сбросить
|
||||
</n-button>
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
step.mode === 'SCREEN_MODE_INTRO' || step.mode === 'SCREEN_MODE_CONCLUSION'
|
||||
"
|
||||
<n-space
|
||||
v-for="(step, index) in screenPlan.steps || []"
|
||||
:key="index"
|
||||
class="screen-plan-step"
|
||||
align="center"
|
||||
justify="space-between"
|
||||
wrap
|
||||
>
|
||||
<n-space align="center">
|
||||
<n-text>{{ index + 1 }}</n-text>
|
||||
<n-text>
|
||||
{{ screenPlanModeOptions.find((option) => option.value === step.mode)?.label }}
|
||||
<template v-if="step.mode === 'SCREEN_MODE_TEXT'">: {{ step.text }}</template>
|
||||
<template v-else-if="step.mode === 'SCREEN_MODE_TIMER'">
|
||||
: {{ step.timerLabel }} ({{ step.timerDurationSeconds }} с)
|
||||
</template>
|
||||
</n-text>
|
||||
</n-space>
|
||||
<n-space class="screen-plan-actions">
|
||||
<n-button
|
||||
size="small"
|
||||
:type="screenPlan.currentStep === index ? 'primary' : 'default'"
|
||||
:disabled="screenPlan.currentStep === index"
|
||||
@click="emit('setStep', index)"
|
||||
>
|
||||
<n-button size="small" @click="emit('command', step.mode, 'SCREEN_COMMAND_PLAY')">
|
||||
Запуск
|
||||
</n-button>
|
||||
<n-button size="small" @click="emit('command', step.mode, 'SCREEN_COMMAND_PAUSE')">
|
||||
Пауза
|
||||
</n-button>
|
||||
<n-button size="small" @click="emit('command', step.mode, 'SCREEN_COMMAND_RESUME')">
|
||||
Продолжить
|
||||
</n-button>
|
||||
<n-button size="small" @click="emit('command', step.mode, 'SCREEN_COMMAND_STOP')">
|
||||
Стоп
|
||||
</n-button>
|
||||
<n-button size="small" @click="emit('command', step.mode, 'SCREEN_COMMAND_REPLAY')">
|
||||
Повтор
|
||||
</n-button>
|
||||
<n-input-number
|
||||
:value="screenVolume"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="5"
|
||||
size="small"
|
||||
@update:value="emit('update:screenVolume', $event)"
|
||||
/>
|
||||
<n-button size="small" @click="emit('command', step.mode, 'SCREEN_COMMAND_VOLUME')">
|
||||
Громкость
|
||||
</n-button>
|
||||
<n-input-number
|
||||
:value="screenPosition"
|
||||
:min="0"
|
||||
:step="5"
|
||||
size="small"
|
||||
@update:value="emit('update:screenPosition', $event)"
|
||||
/>
|
||||
<n-button size="small" @click="emit('command', step.mode, 'SCREEN_COMMAND_SEEK')">
|
||||
Перемотать
|
||||
</n-button>
|
||||
Показать
|
||||
</n-button>
|
||||
<template v-if="screenPlan.currentStep === index">
|
||||
<template v-if="step.mode === 'SCREEN_MODE_TIMER'">
|
||||
<n-button
|
||||
size="small"
|
||||
@click="emit('command', step.mode, 'SCREEN_COMMAND_TIMER_START')"
|
||||
>
|
||||
Старт
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
@click="emit('command', step.mode, 'SCREEN_COMMAND_TIMER_PAUSE')"
|
||||
>
|
||||
Пауза
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
@click="emit('command', step.mode, 'SCREEN_COMMAND_TIMER_RESUME')"
|
||||
>
|
||||
Продолжить
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
@click="emit('command', step.mode, 'SCREEN_COMMAND_TIMER_RESET')"
|
||||
>
|
||||
Сбросить
|
||||
</n-button>
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
step.mode === 'SCREEN_MODE_INTRO' || step.mode === 'SCREEN_MODE_CONCLUSION'
|
||||
"
|
||||
>
|
||||
<n-button size="small" @click="emit('command', step.mode, 'SCREEN_COMMAND_PLAY')">
|
||||
Запуск
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
@click="emit('command', step.mode, 'SCREEN_COMMAND_PAUSE')"
|
||||
>
|
||||
Пауза
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
@click="emit('command', step.mode, 'SCREEN_COMMAND_RESUME')"
|
||||
>
|
||||
Продолжить
|
||||
</n-button>
|
||||
<n-button size="small" @click="emit('command', step.mode, 'SCREEN_COMMAND_STOP')">
|
||||
Стоп
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
@click="emit('command', step.mode, 'SCREEN_COMMAND_REPLAY')"
|
||||
>
|
||||
Повтор
|
||||
</n-button>
|
||||
<n-input-number
|
||||
:value="screenVolume"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="5"
|
||||
size="small"
|
||||
@update:value="emit('update:screenVolume', $event)"
|
||||
/>
|
||||
<n-button
|
||||
size="small"
|
||||
@click="emit('command', step.mode, 'SCREEN_COMMAND_VOLUME')"
|
||||
>
|
||||
Громкость
|
||||
</n-button>
|
||||
<n-input-number
|
||||
:value="screenPosition"
|
||||
:min="0"
|
||||
:step="5"
|
||||
size="small"
|
||||
@update:value="emit('update:screenPosition', $event)"
|
||||
/>
|
||||
<n-button size="small" @click="emit('command', step.mode, 'SCREEN_COMMAND_SEEK')">
|
||||
Перемотать
|
||||
</n-button>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</template>
|
||||
</div>
|
||||
</aside>
|
||||
</UiBlock>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.map-preview-layout {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
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;
|
||||
}
|
||||
|
||||
.screens-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.screen-header {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.screen-preview-placeholder {
|
||||
@@ -483,6 +514,19 @@ onBeforeUnmount(() => {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.screen-preview-panel > .screens-title {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.screen-plan {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.screen-plan-step {
|
||||
border-top: 1px solid rgb(255 255 255 / 10%);
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.map-field {
|
||||
aspect-ratio: 1;
|
||||
background-color: #151515;
|
||||
@@ -515,7 +559,7 @@ onBeforeUnmount(() => {
|
||||
border-radius: 6px;
|
||||
cursor: grab;
|
||||
min-width: 90px;
|
||||
padding: 8px 24px 8px 10px;
|
||||
padding: 8px 24px 8px 8px;
|
||||
position: absolute;
|
||||
user-select: none;
|
||||
}
|
||||
@@ -545,8 +589,8 @@ onBeforeUnmount(() => {
|
||||
background-color: #48633a;
|
||||
border-radius: 6px 6px 0 0;
|
||||
cursor: grab;
|
||||
margin: -8px -24px 8px -10px;
|
||||
padding: 8px 24px 8px 10px;
|
||||
margin: -8px -24px 8px -8px;
|
||||
padding: 8px 24px 8px 8px;
|
||||
}
|
||||
|
||||
.map-team-handle:active {
|
||||
@@ -584,7 +628,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.map-hidden-items {
|
||||
margin-top: 10px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@media (width < 1024px) {
|
||||
|
||||
+159
-165
@@ -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>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ref } from 'vue'
|
||||
|
||||
import type { ScreenState } from '@/api/generated/crabs/evening_detective_server'
|
||||
import ScreenBindingModal from '@/components/ScreenBindingModal.vue'
|
||||
import UiBlock from '@/components/UiBlock.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
screens: ScreenState[]
|
||||
@@ -20,12 +21,10 @@ const showBindingModal = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="screens-block">
|
||||
<n-flex align="center" justify="space-between">
|
||||
<UiBlock>
|
||||
<n-flex align="center" justify="space-between" class="screen-header">
|
||||
<p class="screens-title">Экраны</p>
|
||||
<n-button size="small" type="primary" @click="showBindingModal = true">
|
||||
Привязать экран
|
||||
</n-button>
|
||||
<n-button size="small" @click="showBindingModal = true"> Привязать экран </n-button>
|
||||
</n-flex>
|
||||
<n-alert
|
||||
v-if="screens.some((screen) => screen.status === 'disconnected')"
|
||||
@@ -53,7 +52,7 @@ const showBindingModal = ref(false)
|
||||
</n-flex>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</UiBlock>
|
||||
<ScreenBindingModal
|
||||
v-model:show="showBindingModal"
|
||||
:binding="props.screenBinding"
|
||||
@@ -62,26 +61,23 @@ const showBindingModal = ref(false)
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.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: 0;
|
||||
}
|
||||
|
||||
.screen-header {
|
||||
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: 8px 0;
|
||||
}
|
||||
|
||||
.screen-status {
|
||||
@@ -91,7 +87,7 @@ const showBindingModal = ref(false)
|
||||
|
||||
.screen-empty {
|
||||
color: #aaa;
|
||||
margin: 12px 0 0;
|
||||
margin: 16px 0 0;
|
||||
}
|
||||
|
||||
.screen-error {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { NButton, NFlex } from 'naive-ui'
|
||||
|
||||
import type { Application, Team } from '@/api/generated/crabs/evening_detective_server'
|
||||
import UiBlock from '@/components/UiBlock.vue'
|
||||
|
||||
defineProps<{
|
||||
teams: Team[]
|
||||
@@ -18,10 +19,10 @@ const emit = defineEmits<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="team-block team-block-hover" @click="emit('add')">
|
||||
<UiBlock class="team-block-hover" @click="emit('add')">
|
||||
<p class="place-image-plus">+</p>
|
||||
</div>
|
||||
<div v-for="team in teams" :key="team.ID" class="team-block">
|
||||
</UiBlock>
|
||||
<UiBlock v-for="team in teams" :key="team.ID">
|
||||
<div class="team-content-block">
|
||||
<n-flex justify="space-between">
|
||||
<div class="title-block">
|
||||
@@ -66,24 +67,17 @@ const emit = defineEmits<{
|
||||
Выдать: {{ application.name }}
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</UiBlock>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.team-block {
|
||||
background-color: #222;
|
||||
border: 1px solid #222;
|
||||
border-radius: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.team-block-hover:hover {
|
||||
background-color: #333;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.team-content-block {
|
||||
padding: 16px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.team-name-block {
|
||||
@@ -91,13 +85,13 @@ const emit = defineEmits<{
|
||||
}
|
||||
|
||||
.link-button {
|
||||
margin: 6px 6px 0 0;
|
||||
margin: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.place-image-plus {
|
||||
font-size: 32px;
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
tag?: string
|
||||
}>(),
|
||||
{
|
||||
tag: 'div',
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="tag" class="ui-block">
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ui-block {
|
||||
background-color: #222;
|
||||
border-radius: 10px;
|
||||
margin: 8px 0;
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user