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
+1 -1
View File
@@ -5,7 +5,7 @@
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Вечерний детектив</title> <title>Вечерний детектив</title>
<script type="module" crossorigin src="/assets/index-VgViCwpY.js"></script> <script type="module" crossorigin src="/assets/index-Bj8g3wkx.js"></script>
<link rel="modulepreload" crossorigin href="/assets/client-Dr2LhGo3.js"> <link rel="modulepreload" crossorigin href="/assets/client-Dr2LhGo3.js">
<link rel="modulepreload" crossorigin href="/assets/light-DXa4IKh6.js"> <link rel="modulepreload" crossorigin href="/assets/light-DXa4IKh6.js">
<link rel="modulepreload" crossorigin href="/assets/light-4JJV7N1X.js"> <link rel="modulepreload" crossorigin href="/assets/light-4JJV7N1X.js">
+48 -176
View File
@@ -112,21 +112,13 @@ const screenPlan = ref<ScreenPlan>()
const screenPlanDraft = ref<ScreenPlanStep[]>([]) const screenPlanDraft = ref<ScreenPlanStep[]>([])
const screenPlanDirty = ref(false) const screenPlanDirty = ref(false)
const screenPlanSaving = ref(false) const screenPlanSaving = ref(false)
const screenPlanMode = ref<'use' | 'edit'>('use')
const previewQuestions = ref<ResultsQuestion[]>([]) const previewQuestions = ref<ResultsQuestion[]>([])
const previewTeams = ref<TeamResult[]>([]) const previewTeams = ref<TeamResult[]>([])
const mapItems = ref<MapItem[]>([]) const mapItems = ref<MapItem[]>([])
const mapSaving = ref(false) const mapSaving = ref(false)
const screenVolume = ref(100) const screenVolume = ref(100)
const screenPosition = ref(0) 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 previewScreen = computed(() => screens.value.find((screen) => screen.status === 'connected') || screens.value[0])
const hasDisconnectedScreen = computed(() => screens.value.some((screen) => screen.status === 'disconnected')) const hasDisconnectedScreen = computed(() => screens.value.some((screen) => screen.status === 'disconnected'))
const screenPlanModeOptions = [ const screenPlanModeOptions = [
@@ -139,22 +131,6 @@ const screenPlanModeOptions = [
{ label: 'Таймер', value: 'SCREEN_MODE_TIMER' }, { label: 'Таймер', value: 'SCREEN_MODE_TIMER' },
{ label: 'Текст', value: 'SCREEN_MODE_TEXT' }, { 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 mapScale = ref(1)
const mapOffset = ref({ x: 0, y: 0 }) const mapOffset = ref({ x: 0, y: 0 })
let dragState: let dragState:
@@ -196,11 +172,6 @@ getGame(Number(gameId))
async function getGameScreens() { async function getGameScreens() {
const res = await client.GetGameScreens({ gameID: Number(gameId) }) const res = await client.GetGameScreens({ gameID: Number(gameId) })
screens.value = res.screens || [] 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') { if (previewScreen.value?.mode === 'SCREEN_MODE_RESULTS') {
const result = await client.GetGameResults({ gameID: Number(gameId) }) const result = await client.GetGameResults({ gameID: Number(gameId) })
previewQuestions.value = result.questions || [] previewQuestions.value = result.questions || []
@@ -229,6 +200,20 @@ function addScreenPlanStep() {
timerDurationSeconds: 600, timerDurationSeconds: 600,
}) })
screenPlanDirty.value = true 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) { function removeScreenPlanStep(index: number) {
@@ -271,7 +256,7 @@ async function saveScreenPlan() {
screenPlan.value = res.plan screenPlan.value = res.plan
screenPlanDraft.value = res.plan?.steps?.map((step) => ({ ...step })) || [] screenPlanDraft.value = res.plan?.steps?.map((step) => ({ ...step })) || []
screenPlanDirty.value = false screenPlanDirty.value = false
await getGameScreens() screenPlanMode.value = 'use'
message.success(res.plan ? 'План экранов сохранён' : 'План экранов удалён') message.success(res.plan ? 'План экранов сохранён' : 'План экранов удалён')
} catch (err) { } catch (err) {
message.error(err instanceof Error ? err.message : 'Не удалось сохранить план экранов') message.error(err instanceof Error ? err.message : 'Не удалось сохранить план экранов')
@@ -502,37 +487,6 @@ async function unbindScreen(screen: ScreenState) {
await getGameScreens() 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) { async function toTeamStory(id: number, password: string, newTab: boolean = false) {
const uri = '/team-story/' + id + '?password=' + password const uri = '/team-story/' + id + '?password=' + password
if (newTab) { if (newTab) {
@@ -980,115 +934,26 @@ useSimplePolling(() => {
</n-button> </n-button>
</n-flex> </n-flex>
</div> </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"> <div class="screen-plan">
<n-flex align="center" justify="space-between"> <n-flex align="center" justify="space-between">
<p class="screens-title">План экранов</p> <p class="screens-title">План экранов</p>
<n-space> <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>
<n-button <template v-if="screenPlan && screenPlanMode === 'use'">
v-if="screenPlan || screenPlanDirty" <n-button size="small" @click="editScreenPlan">Редактировать</n-button>
size="small" </template>
type="primary" <template v-if="screenPlanMode === 'edit'">
:loading="screenPlanSaving" <n-button size="small" @click="cancelScreenPlanEdit">Отменить</n-button>
@click="saveScreenPlan" <n-button size="small" type="primary" :loading="screenPlanSaving" @click="saveScreenPlan">
> Сохранить план
Сохранить план </n-button>
</n-button> </template>
</n-space> </n-space>
</n-flex> </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-space v-for="(step, index) in screenPlanDraft" :key="index" align="center" wrap>
<n-text>{{ index + 1 }}</n-text> <n-text>{{ index + 1 }}</n-text>
<n-select <n-select
@@ -1133,15 +998,32 @@ useSimplePolling(() => {
<n-button size="small" type="error" ghost @click="removeScreenPlanStep(index)"> <n-button size="small" type="error" ghost @click="removeScreenPlanStep(index)">
Удалить Удалить
</n-button> </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 <n-button
size="small" size="small"
:type="screenPlan?.currentStep === index ? 'primary' : 'default'" :type="screenPlan.currentStep === index ? 'primary' : 'default'"
:disabled="screenPlanDirty || screenPlan?.currentStep === index" :disabled="screenPlan.currentStep === index"
@click="setScreenPlanStep(index)" @click="setScreenPlanStep(index)"
> >
Показать Показать
</n-button> </n-button>
<template v-if="screenPlan?.currentStep === index && !screenPlanDirty"> <template v-if="screenPlan.currentStep === index">
<template v-if="step.mode === 'SCREEN_MODE_TIMER'"> <template v-if="step.mode === 'SCREEN_MODE_TIMER'">
<n-button size="small" @click="sendScreenPlanCommand(step.mode, 'SCREEN_COMMAND_TIMER_START')"> <n-button size="small" @click="sendScreenPlanCommand(step.mode, 'SCREEN_COMMAND_TIMER_START')">
Старт Старт
@@ -1183,12 +1065,6 @@ useSimplePolling(() => {
</template> </template>
</template> </template>
</n-space> </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>
</div> </div>
</template> </template>
@@ -1926,10 +1802,6 @@ useSimplePolling(() => {
width: 100%; width: 100%;
} }
.screen-mode-controls {
margin-top: 12px;
}
.map-preview-layout { .map-preview-layout {
display: grid; display: grid;
gap: 12px; gap: 12px;