This commit is contained in:
2026-10-04 14:48:25 +07:00
parent 557a80ba01
commit 08389ce0f5
2 changed files with 49 additions and 177 deletions
+48 -176
View File
@@ -112,21 +112,13 @@ const screenPlan = ref<ScreenPlan>()
const screenPlanDraft = ref<ScreenPlanStep[]>([])
const screenPlanDirty = ref(false)
const screenPlanSaving = ref(false)
const screenPlanMode = ref<'use' | 'edit'>('use')
const previewQuestions = ref<ResultsQuestion[]>([])
const previewTeams = ref<TeamResult[]>([])
const mapItems = ref<MapItem[]>([])
const mapSaving = ref(false)
const screenVolume = ref(100)
const screenPosition = ref(0)
const timerDurationMinutes = ref<number | null>(10)
const timerLabel = ref('Перерыв')
const timerFormInitialized = ref(false)
const currentScreenMode = computed<ScreenMode | undefined>(() => {
const modes = screens.value.map((screen) => screen.mode).filter(Boolean)
if (!modes.length || modes.some((mode) => mode !== modes[0])) return undefined
return modes[0] as ScreenMode
})
const timerScreen = computed(() => screens.value[0])
const previewScreen = computed(() => screens.value.find((screen) => screen.status === 'connected') || screens.value[0])
const hasDisconnectedScreen = computed(() => screens.value.some((screen) => screen.status === 'disconnected'))
const screenPlanModeOptions = [
@@ -139,22 +131,6 @@ const screenPlanModeOptions = [
{ label: 'Таймер', value: 'SCREEN_MODE_TIMER' },
{ label: 'Текст', value: 'SCREEN_MODE_TEXT' },
]
const timerStateLabel = computed(() => {
switch (timerScreen.value?.timerState) {
case 'SCREEN_TIMER_STATE_RUNNING':
return 'Идёт'
case 'SCREEN_TIMER_STATE_PAUSED':
return 'Пауза'
case 'SCREEN_TIMER_STATE_FINISHED':
return 'Завершён'
default:
return 'Остановлен'
}
})
const timerRemainingLabel = computed(() => {
const seconds = Math.max(0, timerScreen.value?.timerRemainingSeconds || 0)
return `${String(Math.floor(seconds / 60)).padStart(2, '0')}:${String(seconds % 60).padStart(2, '0')}`
})
const mapScale = ref(1)
const mapOffset = ref({ x: 0, y: 0 })
let dragState:
@@ -196,11 +172,6 @@ getGame(Number(gameId))
async function getGameScreens() {
const res = await client.GetGameScreens({ gameID: Number(gameId) })
screens.value = res.screens || []
if (!timerFormInitialized.value && screens.value[0]) {
timerDurationMinutes.value = Math.max(1, (screens.value[0].timerDurationSeconds || 600) / 60)
timerLabel.value = screens.value[0].timerLabel || 'Перерыв'
timerFormInitialized.value = true
}
if (previewScreen.value?.mode === 'SCREEN_MODE_RESULTS') {
const result = await client.GetGameResults({ gameID: Number(gameId) })
previewQuestions.value = result.questions || []
@@ -229,6 +200,20 @@ function addScreenPlanStep() {
timerDurationSeconds: 600,
})
screenPlanDirty.value = true
screenPlanMode.value = 'edit'
}
function editScreenPlan() {
if (!screenPlan.value) return
screenPlanDraft.value = screenPlan.value.steps?.map((step) => ({ ...step })) || []
screenPlanMode.value = 'edit'
}
function cancelScreenPlanEdit() {
if (screenPlanDirty.value && !window.confirm('Отменить несохранённые изменения плана?')) return
screenPlanDraft.value = screenPlan.value?.steps?.map((step) => ({ ...step })) || []
screenPlanDirty.value = false
screenPlanMode.value = 'use'
}
function removeScreenPlanStep(index: number) {
@@ -271,7 +256,7 @@ async function saveScreenPlan() {
screenPlan.value = res.plan
screenPlanDraft.value = res.plan?.steps?.map((step) => ({ ...step })) || []
screenPlanDirty.value = false
await getGameScreens()
screenPlanMode.value = 'use'
message.success(res.plan ? 'План экранов сохранён' : 'План экранов удалён')
} catch (err) {
message.error(err instanceof Error ? err.message : 'Не удалось сохранить план экранов')
@@ -502,37 +487,6 @@ async function unbindScreen(screen: ScreenState) {
await getGameScreens()
}
async function setScreenMode(mode: ScreenMode) {
await client.SetScreenMode({ gameID: Number(gameId), mode })
}
async function configureScreenTimer() {
const durationMinutes = timerDurationMinutes.value
const label = timerLabel.value.trim()
if (!durationMinutes || durationMinutes <= 0 || !label) {
message.error('Укажите длительность и надпись таймера')
return
}
await client.SetScreenTimer({
gameID: Number(gameId),
durationSeconds: Math.round(durationMinutes * 60),
label,
})
await getGameScreens()
}
async function sendScreenCommand(command: ScreenCommand) {
if (command === 'SCREEN_COMMAND_PLAY' || command === 'SCREEN_COMMAND_REPLAY') {
if (!window.confirm('Запустить медиа на всех экранах?')) return
}
await client.SendScreenCommand({
gameID: Number(gameId),
command,
positionMs: Math.max(0, Math.round(screenPosition.value * 1000)),
volume: Math.max(0, Math.min(100, Math.round(screenVolume.value))),
})
}
async function toTeamStory(id: number, password: string, newTab: boolean = false) {
const uri = '/team-story/' + id + '?password=' + password
if (newTab) {
@@ -980,115 +934,26 @@ useSimplePolling(() => {
</n-button>
</n-flex>
</div>
<template v-if="!screenPlan && !screenPlanDirty">
<n-space class="screen-mode-controls">
<n-button
size="small"
:type="currentScreenMode === 'SCREEN_MODE_COVER' ? 'primary' : 'default'"
@click="setScreenMode('SCREEN_MODE_COVER')"
>Обложка</n-button
>
<n-button
size="small"
:type="currentScreenMode === 'SCREEN_MODE_INTRO' ? 'primary' : 'default'"
@click="setScreenMode('SCREEN_MODE_INTRO')"
>Введение</n-button
>
<n-button
size="small"
:type="currentScreenMode === 'SCREEN_MODE_INTRO_DONE' ? 'primary' : 'default'"
@click="setScreenMode('SCREEN_MODE_INTRO_DONE')"
>Успехов</n-button
>
<n-button
size="small"
:type="currentScreenMode === 'SCREEN_MODE_CONCLUSION' ? 'primary' : 'default'"
@click="setScreenMode('SCREEN_MODE_CONCLUSION')"
>Закрывашка</n-button
>
<n-button
size="small"
:type="currentScreenMode === 'SCREEN_MODE_CONCLUSION_DONE' ? 'primary' : 'default'"
@click="setScreenMode('SCREEN_MODE_CONCLUSION_DONE')"
>Спасибо за игру!</n-button
>
<n-button
v-if="game.status === 'finish'"
size="small"
:type="currentScreenMode === 'SCREEN_MODE_RESULTS' ? 'primary' : 'default'"
@click="setScreenMode('SCREEN_MODE_RESULTS')"
>Результаты</n-button
>
<n-button
:type="currentScreenMode === 'SCREEN_MODE_TIMER' ? 'primary' : 'default'"
@click="setScreenMode('SCREEN_MODE_TIMER')"
>Таймер</n-button
>
</n-space>
<n-space class="screen-mode-controls" align="center">
<n-input-number
v-model:value="timerDurationMinutes"
:min="1"
:max="1440"
:step="1"
size="small"
placeholder="Минуты"
/>
<n-input v-model:value="timerLabel" size="small" placeholder="Надпись таймера" />
<n-button size="small" @click="configureScreenTimer">Настроить таймер</n-button>
<n-tag type="info">{{ timerStateLabel }} · {{ timerRemainingLabel }}</n-tag>
</n-space>
<n-space class="screen-mode-controls">
<n-button size="small" @click="sendScreenCommand('SCREEN_COMMAND_TIMER_START')">Старт таймера</n-button>
<n-button size="small" @click="sendScreenCommand('SCREEN_COMMAND_TIMER_PAUSE')">Пауза таймера</n-button>
<n-button size="small" @click="sendScreenCommand('SCREEN_COMMAND_TIMER_RESUME')"
>Продолжить таймер</n-button
>
<n-button size="small" @click="sendScreenCommand('SCREEN_COMMAND_TIMER_RESET')">Сбросить таймер</n-button>
</n-space>
<n-space class="screen-mode-controls">
<n-button size="small" @click="sendScreenCommand('SCREEN_COMMAND_PLAY')">Запуск</n-button>
<n-button size="small" @click="sendScreenCommand('SCREEN_COMMAND_PAUSE')">Пауза</n-button>
<n-button size="small" @click="sendScreenCommand('SCREEN_COMMAND_RESUME')"
>Продолжить</n-button
>
<n-button size="small" @click="sendScreenCommand('SCREEN_COMMAND_STOP')">Стоп</n-button>
<n-button size="small" @click="sendScreenCommand('SCREEN_COMMAND_REPLAY')"
>Повтор</n-button
>
</n-space>
<n-space class="screen-mode-controls" align="center">
<n-text>Громкость</n-text>
<n-input-number v-model:value="screenVolume" :min="0" :max="100" :step="5" size="small" />
<n-button size="small" @click="sendScreenCommand('SCREEN_COMMAND_VOLUME')"
>Применить</n-button
>
<n-text>Позиция, сек.</n-text>
<n-input-number v-model:value="screenPosition" :min="0" :step="5" size="small" />
<n-button size="small" @click="sendScreenCommand('SCREEN_COMMAND_SEEK')"
>Перемотать</n-button
>
</n-space>
</template>
<div class="screen-plan">
<n-flex align="center" justify="space-between">
<p class="screens-title">План экранов</p>
<n-space>
<n-button v-if="!screenPlan && !screenPlanDirty" size="small" @click="addScreenPlanStep">
<n-button v-if="!screenPlan && screenPlanMode === 'use'" size="small" @click="addScreenPlanStep">
Создать план
</n-button>
<n-button
v-if="screenPlan || screenPlanDirty"
size="small"
type="primary"
:loading="screenPlanSaving"
@click="saveScreenPlan"
>
Сохранить план
</n-button>
<template v-if="screenPlan && screenPlanMode === 'use'">
<n-button size="small" @click="editScreenPlan">Редактировать</n-button>
</template>
<template v-if="screenPlanMode === 'edit'">
<n-button size="small" @click="cancelScreenPlanEdit">Отменить</n-button>
<n-button size="small" type="primary" :loading="screenPlanSaving" @click="saveScreenPlan">
Сохранить план
</n-button>
</template>
</n-space>
</n-flex>
<template v-if="screenPlan || screenPlanDirty">
<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" align="center" wrap>
<n-text>{{ index + 1 }}</n-text>
<n-select
@@ -1133,15 +998,32 @@ useSimplePolling(() => {
<n-button size="small" type="error" ghost @click="removeScreenPlanStep(index)">
Удалить
</n-button>
</n-space>
<n-space>
<n-button size="small" @click="addScreenPlanStep">Добавить шаг</n-button>
<n-button v-if="screenPlanDraft.length" size="small" @click="screenPlanDraft = []; markScreenPlanDirty()">
Удалить план
</n-button>
</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="screenPlanDirty || screenPlan?.currentStep === index"
:type="screenPlan.currentStep === index ? 'primary' : 'default'"
:disabled="screenPlan.currentStep === index"
@click="setScreenPlanStep(index)"
>
Показать
</n-button>
<template v-if="screenPlan?.currentStep === index && !screenPlanDirty">
<template v-if="screenPlan.currentStep === index">
<template v-if="step.mode === 'SCREEN_MODE_TIMER'">
<n-button size="small" @click="sendScreenPlanCommand(step.mode, 'SCREEN_COMMAND_TIMER_START')">
Старт
@@ -1183,12 +1065,6 @@ useSimplePolling(() => {
</template>
</template>
</n-space>
<n-space>
<n-button size="small" @click="addScreenPlanStep">Добавить шаг</n-button>
<n-button v-if="screenPlanDraft.length" size="small" @click="screenPlanDraft = []; markScreenPlanDirty()">
Удалить план
</n-button>
</n-space>
</template>
</div>
</template>
@@ -1926,10 +1802,6 @@ useSimplePolling(() => {
width: 100%;
}
.screen-mode-controls {
margin-top: 12px;
}
.map-preview-layout {
display: grid;
gap: 12px;