This commit is contained in:
2026-10-19 01:47:23 +07:00
parent aa48965268
commit 5b964a1b46
7 changed files with 68 additions and 60 deletions
+25 -21
View File
@@ -254,18 +254,20 @@ const gameOptions = computed(() => {
</n-card> </n-card>
<n-card v-if="bridge && isReady" title="Материалы" data-testid="game-materials"> <n-card v-if="bridge && isReady" title="Материалы" data-testid="game-materials">
<template #header-extra> <template #header-extra>
<n-tag <div class="server-actions">
role="status" <n-tag
data-testid="game-selection-status" role="status"
:type="selectedKey ? 'success' : 'error'" data-testid="game-selection-status"
> :type="selectedKey ? 'success' : 'error'"
{{ selectedKey ? 'Игра выбрана' : 'Игра не выбрана' }} >
</n-tag> {{ selectedKey ? 'Игра выбрана' : 'Игра не выбрана' }}
</n-tag>
<n-button v-if="selectedKey" size="small" :disabled="isBusy || state.busy || outputOpen" @click="emit('resetGame')">Сменить игру</n-button>
</div>
</template> </template>
<div class="connection-content"> <div class="connection-content">
<div v-if="selectedKey" class="current-game"> <div v-if="selectedKey" class="current-game">
<span data-testid="control-game">Текущая игра: {{ selectedName }}</span> <span data-testid="control-game">Текущая игра: {{ selectedName }}</span>
<n-button :disabled="isBusy || state.busy || outputOpen" @click="emit('resetGame')">Сменить игру</n-button>
</div> </div>
<template v-if="!selectedKey"> <template v-if="!selectedKey">
<span>Скачанные игры</span> <span>Скачанные игры</span>
@@ -481,15 +483,21 @@ const gameOptions = computed(() => {
</div> </div>
</n-card> </n-card>
<GameControlPanel v-if="bridge && isReady" :state="state" :is-busy="isBusy" :selected-key="selectedKey" :selected-name="selectedName" @action="run"> <GameControlPanel v-if="bridge && isReady" :state="state" :is-busy="isBusy" :selected-key="selectedKey" :selected-name="selectedName" @action="run">
<ScreenConnectionPanel :state="state" :is-busy="isBusy" @action="run"> <template #connection-actions>
<template v-if="state.authenticated"> <n-button
<n-button v-if="state.authenticated && state.connection !== 'connected'"
v-if="state.connection !== 'connected'" size="small"
:disabled="!canConnectCurrent || isBusy || state.busy" :disabled="!canConnectCurrent || isBusy || state.busy"
@click="run({ action: 'connect' })" @click="run({ action: 'connect' })"
>Подключиться</n-button> >Подключиться</n-button>
</template> <n-button
</ScreenConnectionPanel> v-if="state.screenId"
size="small"
:disabled="isBusy || state.busy"
@click="run({ action: 'disconnect' })"
>Отключиться</n-button>
</template>
<ScreenConnectionPanel :state="state" :is-busy="isBusy" @action="run" />
</GameControlPanel> </GameControlPanel>
</template> </template>
@@ -561,10 +569,6 @@ const gameOptions = computed(() => {
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.current-game > button {
flex-shrink: 0;
}
.game-selection { .game-selection {
align-items: center; align-items: center;
display: flex; display: flex;
+24 -21
View File
@@ -279,13 +279,23 @@ onUnmounted(() => {
/> />
<n-card v-if="bridge" title="Экран"> <n-card v-if="bridge" title="Экран">
<template #header-extra> <template #header-extra>
<n-tag <n-space align="center" :size="8">
role="status" <n-tag
data-testid="display-connection-status" role="status"
:type="host.displays.length ? 'success' : 'error'" data-testid="display-connection-status"
> :type="host.displays.length ? 'success' : 'error'"
{{ host.displays.length ? 'Подключён' : 'Не подключён' }} >
</n-tag> {{ host.displays.length ? 'Подключён' : 'Не подключён' }}
</n-tag>
<n-button
v-if="!state?.outputOpen"
size="small"
:disabled="display === null || isBusy"
@click="run(() => bridge!.show(display!))"
>Открыть показ</n-button>
<n-button v-else size="small" :disabled="isBusy" @click="run(() => bridge!.closeOutput())"
>Закрыть показ</n-button>
</n-space>
</template> </template>
<div class="screen-grid"> <div class="screen-grid">
<div v-if="state" class="screen-preview"> <div v-if="state" class="screen-preview">
@@ -354,15 +364,6 @@ onUnmounted(() => {
</svg> </svg>
</n-button> </n-button>
</n-space> </n-space>
<div class="output-toggle">
<n-button
v-if="!state?.outputOpen"
:disabled="display === null || isBusy"
@click="run(() => bridge!.show(display!))"
>Открыть показ</n-button>
<n-button v-else :disabled="isBusy" @click="run(() => bridge!.closeOutput())"
>Закрыть показ</n-button>
</div>
</div> </div>
</n-space> </n-space>
</div> </div>
@@ -373,10 +374,16 @@ onUnmounted(() => {
<template #header-extra> <template #header-extra>
<n-button <n-button
v-if="!isPlanEditing" v-if="!isPlanEditing"
:square="!!state.catalog.steps.length"
:aria-label="state.catalog.steps.length ? 'Редактировать' : 'Создать план'"
:title="state.catalog.steps.length ? 'Редактировать' : 'Создать план'"
:disabled="isBusy || isPlanSaving || planStatus.key !== state.catalog.key || !planStatus.editable" :disabled="isBusy || isPlanSaving || planStatus.key !== state.catalog.key || !planStatus.editable"
@click="editPlan" @click="editPlan"
> >
{{ state.catalog.steps.length ? 'Редактировать' : 'Создать план' }} <svg v-if="state.catalog.steps.length" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="m16 3 5 5M3 21l5-1L21 7a2.8 2.8 0 0 0-4-4L4 16z" />
</svg>
<span v-else>Создать план</span>
</n-button> </n-button>
<n-space v-else> <n-space v-else>
<n-button <n-button
@@ -523,10 +530,6 @@ onUnmounted(() => {
gap: 12px; gap: 12px;
} }
.output-toggle {
margin-left: auto;
}
.preview { .preview {
background: var(--brand-background); background: var(--brand-background);
display: block; display: block;
+11 -8
View File
@@ -49,19 +49,22 @@ function run(command: GameCommand) {
<template #header> <template #header>
<div class="game-heading"> <div class="game-heading">
<span>Игра</span> <span>Игра</span>
<n-tag role="status" data-testid="game-state" :type="state.gameError ? 'warning' : status?.type ?? 'default'"> <n-tag role="status" data-testid="game-state" :type="state.gameError ? 'warning' : status?.type ?? 'info'">
{{ !selectedKey ? 'Игра не выбрана' : status?.label ?? (!state.authenticated ? 'Требуется вход' : state.gameError ? 'Нет данных' : 'Загрузка…') }} {{ !selectedKey ? 'Игра не выбрана' : status?.label ?? (!state.authenticated ? 'Требуется вход' : state.gameError ? 'Нет данных' : 'Загрузка…') }}
</n-tag> </n-tag>
</div> </div>
</template> </template>
<template #header-extra> <template #header-extra>
<n-tag <n-space align="center" :size="8">
role="status" <n-tag
data-testid="game-connection-status" role="status"
:type="state.connection === 'connected' ? 'success' : 'error'" data-testid="game-connection-status"
> :type="state.connection === 'connected' ? 'success' : 'error'"
{{ state.connection === 'connected' ? 'Подключено' : 'Не подключено' }} >
</n-tag> {{ state.connection === 'connected' ? 'Подключено' : 'Не подключено' }}
</n-tag>
<slot name="connection-actions" />
</n-space>
</template> </template>
<div class="game-grid"> <div class="game-grid">
<n-space vertical> <n-space vertical>
+6 -2
View File
@@ -133,7 +133,11 @@ function remove(trackId?: number) {
<template> <template>
<n-card title="План музыки" data-testid="music-plan"> <n-card title="План музыки" data-testid="music-plan">
<template #header-extra> <template #header-extra>
<n-button v-if="!isEditing" :disabled="isDisabled" @click="edit">Редактировать</n-button> <n-button v-if="!isEditing" square aria-label="Редактировать" title="Редактировать" :disabled="isDisabled" @click="edit">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="m16 3 5 5M3 21l5-1L21 7a2.8 2.8 0 0 0-4-4L4 16z" />
</svg>
</n-button>
<n-space v-else> <n-space v-else>
<template v-if="isDirty || hasConflict"> <template v-if="isDirty || hasConflict">
<n-button <n-button
@@ -380,7 +384,7 @@ function remove(trackId?: number) {
<span>{{ track.name || `Трек ${index + 1}` }}</span> <span>{{ track.name || `Трек ${index + 1}` }}</span>
<n-button <n-button
data-testid="select-track" data-testid="select-track"
:type="isCurrent(index) ? 'primary' : 'default'" size="small"
:disabled="isBusy || isCurrent(index)" :disabled="isBusy || isCurrent(index)"
@click="emit('command', { action: 'track', channel: 'music', trackIndex: index })" @click="emit('command', { action: 'track', channel: 'music', trackIndex: index })"
>{{ isCurrent(index) ? 'Выбран' : 'Выбрать' }}</n-button >{{ isCurrent(index) ? 'Выбран' : 'Выбрать' }}</n-button
-6
View File
@@ -10,7 +10,6 @@ function run(intent: OnlineIntent) {
<template> <template>
<section class="screen-connection" aria-label="Экран игры"> <section class="screen-connection" aria-label="Экран игры">
<span>Экран игры</span> <span>Экран игры</span>
<slot />
<n-space v-if="state.screenId" align="center"> <n-space v-if="state.screenId" align="center">
<span>Экран №{{ state.screenId }}</span> <span>Экран №{{ state.screenId }}</span>
<span role="status" data-testid="sound-assignment"> <span role="status" data-testid="sound-assignment">
@@ -25,11 +24,6 @@ function run(intent: OnlineIntent) {
· Последнее состояние · Последнее состояние
</template> </template>
</span> </span>
<n-button
:disabled="isBusy || state.busy"
@click="run({ action: 'disconnect' })"
>Отключиться</n-button
>
</n-space> </n-space>
<n-space v-if="state.screenId"> <n-space v-if="state.screenId">
<n-button <n-button
+1 -1
View File
@@ -147,7 +147,7 @@ function timerLabel(index: number) {
> >
<n-button <n-button
data-testid="show-step" data-testid="show-step"
:type="currentIndex === index ? 'primary' : 'default'" size="small"
:disabled="isBusy || currentIndex === index" :disabled="isBusy || currentIndex === index"
@click="emit('command', { action: 'step', index })" @click="emit('command', { action: 'step', index })"
>{{ currentIndex === index ? 'Показан' : 'Показать' }}</n-button >{{ currentIndex === index ? 'Показан' : 'Показать' }}</n-button
+1 -1
View File
@@ -1,7 +1,7 @@
import type { GameCommand, GameStatus } from '../types/online.js' import type { GameCommand, GameStatus } from '../types/online.js'
export const gameStatuses = { export const gameStatuses = {
draft: { label: 'Черновик', type: 'default' }, draft: { label: 'Черновик', type: 'info' },
run: { label: 'Идёт', type: 'success' }, run: { label: 'Идёт', type: 'success' },
pause: { label: 'Пауза', type: 'warning' }, pause: { label: 'Пауза', type: 'warning' },
finish: { label: 'Завершена', type: 'info' }, finish: { label: 'Завершена', type: 'info' },