generated from VLADIMIR/template_frontend
up
This commit is contained in:
+79
-409
@@ -1,35 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NFlex, NInput, NInputNumber, NSelect, NSpace, NText } from 'naive-ui'
|
||||
import { NButton, NFlex, NSpace, NText } from 'naive-ui'
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
|
||||
import type {
|
||||
Application,
|
||||
MapItem,
|
||||
ResultsQuestion,
|
||||
ScreenCommand,
|
||||
ScreenMode,
|
||||
ScreenPlan,
|
||||
ScreenPlanStep,
|
||||
ScreenState,
|
||||
Team,
|
||||
TeamResult,
|
||||
} from '@/api/generated/crabs/evening_detective_server'
|
||||
import ScreenPreview from '@/components/ScreenPreview.vue'
|
||||
import type { Application, MapItem, Team } from '@/api/generated/crabs/evening_detective_server'
|
||||
import UiBlock from '@/components/UiBlock.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
mapItems: MapItem[]
|
||||
teams: Team[]
|
||||
screens: ScreenState[]
|
||||
previewQuestions: ResultsQuestion[]
|
||||
previewTeams: TeamResult[]
|
||||
screenPlan?: ScreenPlan
|
||||
screenPlanDraft: ScreenPlanStep[]
|
||||
screenPlanDirty: boolean
|
||||
screenPlanSaving: boolean
|
||||
screenPlanMode: 'use' | 'edit'
|
||||
screenVolume: number
|
||||
screenPosition: number
|
||||
}>()
|
||||
|
||||
const GRID_SIZE = 20
|
||||
@@ -38,40 +16,17 @@ const emit = defineEmits<{
|
||||
saveItem: [item: MapItem, hidden?: boolean]
|
||||
dragging: [value: boolean]
|
||||
giveApplication: [team: Team, application: Application]
|
||||
addStep: []
|
||||
edit: []
|
||||
cancel: []
|
||||
removeStep: [index: number]
|
||||
moveStep: [index: number, offset: number]
|
||||
dirty: []
|
||||
clearPlan: []
|
||||
save: []
|
||||
setStep: [index: number]
|
||||
command: [mode: ScreenMode | undefined, command: ScreenCommand]
|
||||
'update:screenVolume': [value: number | null]
|
||||
'update:screenPosition': [value: number | null]
|
||||
}>()
|
||||
|
||||
const mapScale = ref(1)
|
||||
const mapOffset = ref({ x: 0, y: 0 })
|
||||
const mapField = ref<HTMLElement>()
|
||||
const previewScreen = computed(
|
||||
() => props.screens.find((screen) => screen.status === 'connected') || props.screens[0],
|
||||
)
|
||||
const teamsNeedingApplications = computed(
|
||||
() =>
|
||||
new Map(props.teams.filter((team) => team.applications?.length).map((team) => [team.ID, team])),
|
||||
)
|
||||
const screenPlanModeOptions = [
|
||||
{ label: 'Обложка', value: 'SCREEN_MODE_COVER' },
|
||||
{ label: 'Введение', value: 'SCREEN_MODE_INTRO' },
|
||||
{ label: 'Закрывашка', value: 'SCREEN_MODE_CONCLUSION' },
|
||||
{ label: 'Результаты', value: 'SCREEN_MODE_RESULTS' },
|
||||
{ label: 'Таймер', value: 'SCREEN_MODE_TIMER' },
|
||||
{ label: 'Текст', value: 'SCREEN_MODE_TEXT' },
|
||||
]
|
||||
let dragState:
|
||||
{ item: MapItem; offsetX: number; offsetY: number } | undefined
|
||||
|
||||
let dragState: { item: MapItem; offsetX: number; offsetY: number } | undefined
|
||||
let panState: { startX: number; startY: number; offsetX: number; offsetY: number } | undefined
|
||||
|
||||
function getMapCoordinate(event: PointerEvent) {
|
||||
@@ -172,346 +127,90 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="map-preview-layout">
|
||||
<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>
|
||||
<n-text>{{ Math.round(mapScale * 100) }}%</n-text>
|
||||
<n-button size="small" @click="changeMapScale(0.1)">+</n-button>
|
||||
<n-button size="small" @click="resetMapView">Сбросить</n-button>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
<div ref="mapField" class="map-field" @pointerdown="startMapPan" @wheel="zoomMap">
|
||||
<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>
|
||||
<n-text>{{ Math.round(mapScale * 100) }}%</n-text>
|
||||
<n-button size="small" @click="changeMapScale(0.1)">+</n-button>
|
||||
<n-button size="small" @click="resetMapView">Сбросить</n-button>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
<div ref="mapField" class="map-field" @pointerdown="startMapPan" @wheel="zoomMap">
|
||||
<div
|
||||
class="map-canvas"
|
||||
:style="{ transform: `translate(${mapOffset.x}px, ${mapOffset.y}px) scale(${mapScale})` }"
|
||||
>
|
||||
<div
|
||||
class="map-canvas"
|
||||
:style="{ transform: `translate(${mapOffset.x}px, ${mapOffset.y}px) scale(${mapScale})` }"
|
||||
v-for="item in mapItems"
|
||||
v-show="!item.hidden"
|
||||
:key="`${item.kind}-${item.itemID}`"
|
||||
class="map-item"
|
||||
:class="[
|
||||
item.kind === 'screen'
|
||||
? 'map-screen'
|
||||
: item.kind === 'organizer_desk'
|
||||
? 'map-organizer-desk'
|
||||
: 'map-team',
|
||||
{
|
||||
'map-team-needs-applications':
|
||||
item.kind === 'team' && teamsNeedingApplications.has(item.itemID),
|
||||
},
|
||||
]"
|
||||
:style="{ left: `${item.x || 0}px`, top: `${item.y || 0}px` }"
|
||||
@pointerdown.stop="item.kind !== 'team' && startMapDrag(item, $event)"
|
||||
>
|
||||
<div
|
||||
v-for="item in mapItems"
|
||||
v-show="!item.hidden"
|
||||
:key="`${item.kind}-${item.itemID}`"
|
||||
class="map-item"
|
||||
:class="[
|
||||
item.kind === 'screen'
|
||||
? 'map-screen'
|
||||
: item.kind === 'organizer_desk'
|
||||
? 'map-organizer-desk'
|
||||
: 'map-team',
|
||||
{
|
||||
'map-team-needs-applications':
|
||||
item.kind === 'team' && teamsNeedingApplications.has(item.itemID),
|
||||
},
|
||||
]"
|
||||
:style="{ left: `${item.x || 0}px`, top: `${item.y || 0}px` }"
|
||||
@pointerdown.stop="item.kind !== 'team' && startMapDrag(item, $event)"
|
||||
v-if="item.kind === 'team'"
|
||||
class="map-team-handle"
|
||||
@pointerdown.stop="startMapDrag(item, $event)"
|
||||
/>
|
||||
<span>{{ item.label }}</span>
|
||||
<n-space
|
||||
v-if="item.kind === 'team' && teamsNeedingApplications.has(item.itemID)"
|
||||
class="map-team-applications"
|
||||
vertical
|
||||
>
|
||||
<div
|
||||
v-if="item.kind === 'team'"
|
||||
class="map-team-handle"
|
||||
@pointerdown.stop="startMapDrag(item, $event)"
|
||||
/>
|
||||
<span>{{ item.label }}</span>
|
||||
<n-space
|
||||
v-if="item.kind === 'team' && teamsNeedingApplications.has(item.itemID)"
|
||||
class="map-team-applications"
|
||||
vertical
|
||||
<n-button
|
||||
v-for="application in teamsNeedingApplications.get(item.itemID)?.applications"
|
||||
:key="application.name"
|
||||
size="small"
|
||||
@click="
|
||||
emit('giveApplication', teamsNeedingApplications.get(item.itemID)!, application)
|
||||
"
|
||||
>
|
||||
<n-button
|
||||
v-for="application in teamsNeedingApplications.get(item.itemID)?.applications"
|
||||
:key="application.name"
|
||||
size="small"
|
||||
@click="
|
||||
emit('giveApplication', teamsNeedingApplications.get(item.itemID)!, application)
|
||||
"
|
||||
>
|
||||
Выдать: {{ application.name }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
<small v-if="item.kind === 'screen'"
|
||||
>{{ item.status }}{{ item.soundEnabled ? ' · звук' : '' }}</small
|
||||
>
|
||||
<button
|
||||
class="map-item-hide"
|
||||
type="button"
|
||||
@pointerdown.stop
|
||||
@click.stop="toggleMapItem(item)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
Выдать: {{ application.name }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
<small v-if="item.kind === 'screen'"
|
||||
>{{ item.status }}{{ item.soundEnabled ? ' · звук' : '' }}</small
|
||||
>
|
||||
<button
|
||||
class="map-item-hide"
|
||||
type="button"
|
||||
@pointerdown.stop
|
||||
@click.stop="toggleMapItem(item)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<n-space v-if="mapItems.some((item) => item.hidden)" class="map-hidden-items">
|
||||
<n-button
|
||||
v-for="item in mapItems.filter((item) => item.hidden)"
|
||||
:key="`hidden-${item.kind}-${item.itemID}`"
|
||||
size="small"
|
||||
@click="toggleMapItem(item)"
|
||||
>
|
||||
Добавить {{ item.label }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</UiBlock>
|
||||
<UiBlock tag="aside" class="screen-preview-panel">
|
||||
<p class="screens-title">Превью экрана</p>
|
||||
<ScreenPreview
|
||||
v-if="previewScreen?.status === 'connected'"
|
||||
:state="previewScreen"
|
||||
:questions="previewQuestions"
|
||||
:teams="previewTeams"
|
||||
/>
|
||||
<div v-else class="screen-preview-placeholder">Экран не подключён</div>
|
||||
<div class="screen-plan">
|
||||
<n-flex align="center" justify="space-between" class="screen-header">
|
||||
<p class="screens-title">План экранов</p>
|
||||
<n-space>
|
||||
<n-button
|
||||
v-if="!screenPlan && screenPlanMode === 'use'"
|
||||
size="small"
|
||||
@click="emit('addStep')"
|
||||
>
|
||||
Создать план
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="screenPlan && screenPlanMode === 'use'"
|
||||
size="small"
|
||||
@click="emit('edit')"
|
||||
>
|
||||
Редактировать
|
||||
</n-button>
|
||||
<template v-if="screenPlanMode === 'edit'">
|
||||
<n-button size="small" :disabled="screenPlanSaving" @click="emit('cancel')">
|
||||
Отменить
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="screenPlanSaving"
|
||||
:disabled="screenPlanSaving"
|
||||
@click="emit('save')"
|
||||
>
|
||||
Сохранить план
|
||||
</n-button>
|
||||
</template>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
<p v-if="!screenPlan && screenPlanMode === 'use'" class="screen-empty">
|
||||
План экранов ещё не создан.
|
||||
</p>
|
||||
<template v-else-if="screenPlanMode === 'edit'">
|
||||
<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-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-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"
|
||||
: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')">
|
||||
Добавить шаг
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="screenPlanDraft.length"
|
||||
size="small"
|
||||
:disabled="screenPlanSaving"
|
||||
@click="emit('clearPlan')"
|
||||
>
|
||||
Удалить план
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
<template v-else-if="screenPlan">
|
||||
<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>
|
||||
<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>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</template>
|
||||
</div>
|
||||
</UiBlock>
|
||||
</div>
|
||||
</div>
|
||||
<n-space v-if="mapItems.some((item) => item.hidden)" class="map-hidden-items">
|
||||
<n-button
|
||||
v-for="item in mapItems.filter((item) => item.hidden)"
|
||||
:key="`hidden-${item.kind}-${item.itemID}`"
|
||||
size="small"
|
||||
@click="toggleMapItem(item)"
|
||||
>
|
||||
Добавить {{ item.label }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</UiBlock>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.map-preview-layout {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(280px, 420px);
|
||||
}
|
||||
|
||||
.screens-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
@@ -522,31 +221,8 @@ onBeforeUnmount(() => {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.screen-preview-placeholder {
|
||||
align-items: center;
|
||||
aspect-ratio: 16 / 9;
|
||||
background-color: #111;
|
||||
color: #aaa;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
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;
|
||||
aspect-ratio: 4;
|
||||
background-color: #151515;
|
||||
border: 1px dashed rgb(255 255 255 / 20%);
|
||||
cursor: grab;
|
||||
@@ -648,10 +324,4 @@ onBeforeUnmount(() => {
|
||||
.map-hidden-items {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@media (width < 1024px) {
|
||||
.map-preview-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+49
-51
@@ -46,6 +46,7 @@ import type {
|
||||
} from '@/api/generated/crabs/evening_detective_server'
|
||||
import notificationSoundURL from '@/assets/icq-uh-oh.wav'
|
||||
import GameMapPanel from '@/components/GameMapPanel.vue'
|
||||
import GameScreenPlanPanel from '@/components/GameScreenPlanPanel.vue'
|
||||
import GameScreensPanel from '@/components/GameScreensPanel.vue'
|
||||
import GameStatePanel from '@/components/GameStatePanel.vue'
|
||||
import GameTeamsPanel from '@/components/GameTeamsPanel.vue'
|
||||
@@ -72,10 +73,10 @@ const router = useRouter()
|
||||
const route = useRoute()
|
||||
const gameId = route.params.id
|
||||
|
||||
type GameTab = 'settings' | 'teams' | 'screens' | 'game' | 'payment' | 'answers' | 'state'
|
||||
type GameTab = 'settings' | 'screens' | 'game' | 'payment' | 'answers' | 'state'
|
||||
|
||||
function isGameTab(value: string): value is GameTab {
|
||||
return ['settings', 'teams', 'screens', 'game', 'payment', 'answers', 'state'].includes(value)
|
||||
return ['settings', 'screens', 'game', 'payment', 'answers', 'state'].includes(value)
|
||||
}
|
||||
|
||||
const queryTab = typeof route.query.tab === 'string' ? route.query.tab : ''
|
||||
@@ -1362,33 +1363,6 @@ useSimplePolling(() => {
|
||||
</UiBlock>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="canManageGame" name="teams" tab="Команды">
|
||||
<div class="game-tab-content">
|
||||
<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 || []"
|
||||
:can-print-receipts="uiStore.environment === 'venue'"
|
||||
:participation-options="participationOptions"
|
||||
@add="showAddTeamModal = true"
|
||||
@open-qr="openTeamQR"
|
||||
@print="openTeamReceipt"
|
||||
@preview="(id, password) => toTeamStory(id, password, true)"
|
||||
@delete="deleteTeam"
|
||||
@give-application="giveApplication"
|
||||
/>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="canManageGame" name="screens" tab="Экраны">
|
||||
<div class="game-tab-content">
|
||||
<GameScreensPanel
|
||||
@@ -1405,6 +1379,29 @@ useSimplePolling(() => {
|
||||
@upload-track="uploadTrack"
|
||||
@delete-track="deleteTrack"
|
||||
/>
|
||||
<GameScreenPlanPanel
|
||||
:screens="screens"
|
||||
:preview-questions="previewQuestions"
|
||||
:preview-teams="previewTeams"
|
||||
:screen-plan="screenPlan"
|
||||
:screen-plan-draft="screenPlanDraft"
|
||||
:screen-plan-saving="screenPlanSaving"
|
||||
:screen-plan-mode="screenPlanMode"
|
||||
:screen-volume="screenVolume"
|
||||
:screen-position="screenPosition"
|
||||
@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>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="canManageGame" name="game" tab="Игра">
|
||||
@@ -1482,31 +1479,32 @@ useSimplePolling(() => {
|
||||
<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"
|
||||
@dragging="mapDragging = $event"
|
||||
@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"
|
||||
/>
|
||||
<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 || []"
|
||||
:can-print-receipts="uiStore.environment === 'venue'"
|
||||
:participation-options="participationOptions"
|
||||
@add="showAddTeamModal = true"
|
||||
@open-qr="openTeamQR"
|
||||
@print="openTeamReceipt"
|
||||
@preview="(id, password) => toTeamStory(id, password, true)"
|
||||
@delete="deleteTeam"
|
||||
@give-application="giveApplication"
|
||||
/>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NFlex, NInput, NInputNumber, NSelect, NSpace, NText } from 'naive-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type {
|
||||
ResultsQuestion,
|
||||
ScreenCommand,
|
||||
ScreenMode,
|
||||
ScreenPlan,
|
||||
ScreenPlanStep,
|
||||
ScreenState,
|
||||
TeamResult,
|
||||
} from '@/api/generated/crabs/evening_detective_server'
|
||||
import ScreenPreview from '@/components/ScreenPreview.vue'
|
||||
import UiBlock from '@/components/UiBlock.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
screens: ScreenState[]
|
||||
previewQuestions: ResultsQuestion[]
|
||||
previewTeams: TeamResult[]
|
||||
screenPlan?: ScreenPlan
|
||||
screenPlanDraft: ScreenPlanStep[]
|
||||
screenPlanSaving: boolean
|
||||
screenPlanMode: 'use' | 'edit'
|
||||
screenVolume: number
|
||||
screenPosition: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
addStep: []
|
||||
edit: []
|
||||
cancel: []
|
||||
removeStep: [index: number]
|
||||
moveStep: [index: number, offset: number]
|
||||
dirty: []
|
||||
clearPlan: []
|
||||
save: []
|
||||
setStep: [index: number]
|
||||
command: [mode: ScreenMode | undefined, command: ScreenCommand]
|
||||
'update:screenVolume': [value: number | null]
|
||||
'update:screenPosition': [value: number | null]
|
||||
}>()
|
||||
|
||||
const previewScreen = computed(
|
||||
() => props.screens.find((screen) => screen.status === 'connected') || props.screens[0],
|
||||
)
|
||||
const screenPlanModeOptions = [
|
||||
{ label: 'Обложка', value: 'SCREEN_MODE_COVER' },
|
||||
{ label: 'Введение', value: 'SCREEN_MODE_INTRO' },
|
||||
{ label: 'Закрывашка', value: 'SCREEN_MODE_CONCLUSION' },
|
||||
{ label: 'Результаты', value: 'SCREEN_MODE_RESULTS' },
|
||||
{ label: 'Таймер', value: 'SCREEN_MODE_TIMER' },
|
||||
{ label: 'Текст', value: 'SCREEN_MODE_TEXT' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiBlock class="screen-preview-panel">
|
||||
<p class="screens-title">Превью экрана</p>
|
||||
<ScreenPreview
|
||||
v-if="previewScreen?.status === 'connected'"
|
||||
:state="previewScreen"
|
||||
:questions="previewQuestions"
|
||||
:teams="previewTeams"
|
||||
/>
|
||||
<div v-else class="screen-preview-placeholder">Экран не подключён</div>
|
||||
<div class="screen-plan">
|
||||
<n-flex align="center" justify="space-between" class="screen-header">
|
||||
<p class="screens-title">План экранов</p>
|
||||
<n-space>
|
||||
<n-button
|
||||
v-if="!screenPlan && screenPlanMode === 'use'"
|
||||
size="small"
|
||||
@click="emit('addStep')"
|
||||
>
|
||||
Создать план
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="screenPlan && screenPlanMode === 'use'"
|
||||
size="small"
|
||||
@click="emit('edit')"
|
||||
>
|
||||
Редактировать
|
||||
</n-button>
|
||||
<template v-if="screenPlanMode === 'edit'">
|
||||
<n-button size="small" :disabled="screenPlanSaving" @click="emit('cancel')">
|
||||
Отменить
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="screenPlanSaving"
|
||||
:disabled="screenPlanSaving"
|
||||
@click="emit('save')"
|
||||
>
|
||||
Сохранить план
|
||||
</n-button>
|
||||
</template>
|
||||
</n-space>
|
||||
</n-flex>
|
||||
<p v-if="!screenPlan && screenPlanMode === 'use'" class="screen-empty">
|
||||
План экранов ещё не создан.
|
||||
</p>
|
||||
<template v-else-if="screenPlanMode === 'edit'">
|
||||
<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-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-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"
|
||||
: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')">
|
||||
Добавить шаг
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="screenPlanDraft.length"
|
||||
size="small"
|
||||
:disabled="screenPlanSaving"
|
||||
@click="emit('clearPlan')"
|
||||
>
|
||||
Удалить план
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
<template v-else-if="screenPlan">
|
||||
<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>
|
||||
<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>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</template>
|
||||
</div>
|
||||
</UiBlock>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.screens-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.screen-header {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.screen-preview-placeholder {
|
||||
align-items: center;
|
||||
aspect-ratio: 16 / 9;
|
||||
background-color: var(--brand-background);
|
||||
color: var(--brand-text-muted);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.screen-preview-panel > .screens-title {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.screen-plan {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.screen-plan-step {
|
||||
border-top: 1px solid var(--color-border);
|
||||
padding: 8px 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user