fix
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
} from 'naive-ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import ScreenConnectionPanel from './ScreenConnectionPanel.vue'
|
||||
import GameControlPanel from './GameControlPanel.vue'
|
||||
import {
|
||||
DEFAULT_SERVER,
|
||||
type GameMaterial,
|
||||
@@ -209,8 +210,7 @@ const gameOptions = computed(() => {
|
||||
<n-alert v-if="state.notice" type="info">{{ state.notice }}</n-alert>
|
||||
</div>
|
||||
</n-card>
|
||||
<div v-if="bridge && isReady" class="game-grid">
|
||||
<n-card title="Игра">
|
||||
<n-card v-if="bridge && isReady" title="Материалы" data-testid="game-materials">
|
||||
<template #header-extra>
|
||||
<n-tag
|
||||
role="status"
|
||||
@@ -223,7 +223,7 @@ const gameOptions = computed(() => {
|
||||
<div class="connection-content">
|
||||
<div v-if="selectedKey" class="current-game">
|
||||
<span data-testid="control-game">Текущая игра: {{ selectedName }}</span>
|
||||
<n-button :disabled="isBusy || state.busy" @click="emit('resetGame')"> Сбросить </n-button>
|
||||
<n-button :disabled="isBusy || state.busy" @click="emit('resetGame')">Сменить игру</n-button>
|
||||
</div>
|
||||
<template v-if="state.authenticated">
|
||||
<div v-if="!selectedKey" class="game-selection">
|
||||
@@ -390,36 +390,21 @@ const gameOptions = computed(() => {
|
||||
</n-collapse>
|
||||
<p v-if="!state.authenticated">Войдите на сервер, чтобы выбрать игру.</p>
|
||||
</div>
|
||||
</n-card>
|
||||
<ScreenConnectionPanel
|
||||
:state="state"
|
||||
:is-busy="isBusy"
|
||||
@action="run"
|
||||
>
|
||||
</n-card>
|
||||
<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 v-if="state.authenticated">
|
||||
<n-button
|
||||
v-if="state.connection !== 'connected'"
|
||||
:disabled="!canConnectCurrent || isBusy || state.busy"
|
||||
@click="run({ action: 'connect' })"
|
||||
>Подключиться</n-button
|
||||
>
|
||||
>Подключиться</n-button>
|
||||
</template>
|
||||
</ScreenConnectionPanel>
|
||||
</div>
|
||||
</GameControlPanel>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.game-grid {
|
||||
align-items: start;
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 360px), 1fr));
|
||||
}
|
||||
|
||||
.game-grid > * {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.materials-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { NAlert, NButton, NCard, NSpace, NTag, useDialog } from 'naive-ui'
|
||||
import type { GameCommand, OnlineIntent, OnlineState } from '../types/online'
|
||||
import { gameCommands, gameStatuses } from '../utils/game-status'
|
||||
|
||||
const props = defineProps<{
|
||||
state: OnlineState
|
||||
isBusy: boolean
|
||||
selectedKey: string | null
|
||||
selectedName: string | null
|
||||
}>()
|
||||
const emit = defineEmits<{ action: [intent: OnlineIntent] }>()
|
||||
const dialog = useDialog()
|
||||
const game = computed(() => props.state.game?.key === props.selectedKey ? props.state.game : null)
|
||||
const status = computed(() => game.value ? gameStatuses[game.value.status] : null)
|
||||
const disabled = computed(() => props.isBusy || props.state.busy || !props.state.authenticated || !!props.state.gameError)
|
||||
const commands = {
|
||||
start: { label: 'Начать игру', path: 'm8 5 12 7-12 7z' },
|
||||
play: { label: 'Продолжить игру', path: 'm8 5 12 7-12 7z' },
|
||||
pause: { label: 'Поставить игру на паузу', path: 'M8 5v14M16 5v14' },
|
||||
finish: { label: 'Завершить игру', path: 'M5 21V3m0 1h14l-4 5 4 5H5' },
|
||||
reset: { label: 'Сбросить игру', path: 'M4 5v6h6M4 11a8 8 0 1 1 2 7' },
|
||||
}
|
||||
function run(command: GameCommand) {
|
||||
if (disabled.value || !game.value || !props.selectedKey) return
|
||||
if (command !== 'reset') {
|
||||
emit('action', { action: 'game-command', command, key: props.selectedKey })
|
||||
return
|
||||
}
|
||||
const key = props.selectedKey
|
||||
const id = game.value.id
|
||||
dialog.warning({
|
||||
title: 'Сбросить игру?',
|
||||
content: `Игра «${game.value.name}» вернётся в черновик. Время начала и завершения будет очищено. Материалы и ответы сохранятся.`,
|
||||
positiveText: 'Сбросить игру',
|
||||
negativeText: 'Отмена',
|
||||
onPositiveClick: () => {
|
||||
if (disabled.value || props.selectedKey !== key || game.value?.id !== id) return
|
||||
emit('action', { action: 'game-command', command, key })
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card title="Игра" data-testid="game-control">
|
||||
<template #header-extra>
|
||||
<n-tag role="status" data-testid="game-state" :type="state.gameError ? 'warning' : status?.type ?? 'default'">
|
||||
{{ !selectedKey ? 'Игра не выбрана' : status?.label ?? (!state.authenticated ? 'Требуется вход' : state.gameError ? 'Нет данных' : 'Загрузка…') }}
|
||||
</n-tag>
|
||||
</template>
|
||||
<div class="game-grid">
|
||||
<n-space vertical>
|
||||
<span v-if="selectedKey">{{ game?.name || selectedName }}</span>
|
||||
<span v-else>Выберите игру в блоке «Материалы».</span>
|
||||
<n-alert v-if="state.gameError" type="warning" role="status">
|
||||
{{ state.gameError }}{{ game ? ' Показано последнее полученное состояние.' : '' }}
|
||||
</n-alert>
|
||||
<n-space v-if="game && selectedKey" align="center">
|
||||
<n-button
|
||||
v-for="command in gameCommands[game.status]"
|
||||
:key="command"
|
||||
square
|
||||
:disabled="disabled"
|
||||
:type="command === 'finish' || command === 'reset' ? 'error' : 'default'"
|
||||
:aria-label="commands[command].label"
|
||||
:title="commands[command].label"
|
||||
@click="run(command)"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path :d="commands[command].path" />
|
||||
</svg>
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-space>
|
||||
<slot />
|
||||
</div>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.game-grid {
|
||||
align-items: start;
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 360px), 1fr));
|
||||
}
|
||||
|
||||
.game-grid > * {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NCard, NSpace, NTag } from 'naive-ui'
|
||||
import { NButton, NSpace, NTag } from 'naive-ui'
|
||||
import type { OnlineIntent, OnlineState } from '../types/online'
|
||||
defineProps<{ state: OnlineState; isBusy: boolean }>()
|
||||
const emit = defineEmits<{ action: [intent: OnlineIntent] }>()
|
||||
@@ -8,8 +8,9 @@ function run(intent: OnlineIntent) {
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<n-card title="Экран игры">
|
||||
<template #header-extra>
|
||||
<section class="screen-connection" aria-label="Экран игры">
|
||||
<div class="screen-heading">
|
||||
<span>Экран игры</span>
|
||||
<n-tag
|
||||
role="status"
|
||||
data-testid="game-connection-status"
|
||||
@@ -17,7 +18,7 @@ function run(intent: OnlineIntent) {
|
||||
>
|
||||
{{ state.connection === 'connected' ? 'Подключено' : 'Не подключено' }}
|
||||
</n-tag>
|
||||
</template>
|
||||
</div>
|
||||
<slot />
|
||||
<n-space v-if="state.screenId" align="center">
|
||||
<span>Экран №{{ state.screenId }}</span>
|
||||
@@ -49,5 +50,21 @@ function run(intent: OnlineIntent) {
|
||||
>Назначить звук этому экрану</n-button
|
||||
>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.screen-connection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.screen-heading {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,6 +7,8 @@ export function useOnlineConnection() {
|
||||
const message = useMessage()
|
||||
const isReady = ref(false)
|
||||
const state = ref<OnlineState>({
|
||||
game: null,
|
||||
gameError: '',
|
||||
server: DEFAULT_SERVER,
|
||||
currentGameId: null,
|
||||
materials: [],
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { ScreenStep } from './screen.js'
|
||||
|
||||
export const DEFAULT_SERVER = 'https://evening-detective.crabs-games.art'
|
||||
export type GameStatus = 'draft' | 'run' | 'pause' | 'finish'
|
||||
export type GameCommand = 'start' | 'pause' | 'play' | 'finish' | 'reset'
|
||||
|
||||
export interface MusicPlaylist {
|
||||
id: number
|
||||
@@ -17,6 +19,7 @@ export interface GameMaterial {
|
||||
}
|
||||
|
||||
export type OnlineIntent =
|
||||
| { action: 'game-command'; command: GameCommand; key: string }
|
||||
| { action: 'download-material'; key: string; assetId: string }
|
||||
| { action: 'playlists' }
|
||||
| { action: 'playlist-create'; name: string }
|
||||
@@ -33,6 +36,8 @@ export type OnlineIntent =
|
||||
| { action: 'select-game' | 'download' | 'check'; gameId: number }
|
||||
| { action: 'remove'; key: string }
|
||||
export interface OnlineState {
|
||||
game: { key: string; id: number; name: string; status: GameStatus } | null
|
||||
gameError: string
|
||||
server: string
|
||||
currentGameId: number | null
|
||||
materials: GameMaterial[]
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { GameCommand, GameStatus } from '../types/online.js'
|
||||
|
||||
export const gameStatuses = {
|
||||
draft: { label: 'Черновик', type: 'default' },
|
||||
run: { label: 'Идёт', type: 'success' },
|
||||
pause: { label: 'Пауза', type: 'warning' },
|
||||
finish: { label: 'Завершена', type: 'info' },
|
||||
} as const
|
||||
|
||||
export const gameCommands: Record<GameStatus, GameCommand[]> = {
|
||||
draft: ['start', 'finish'],
|
||||
run: ['pause', 'finish', 'reset'],
|
||||
pause: ['play', 'finish', 'reset'],
|
||||
finish: ['reset'],
|
||||
}
|
||||
Reference in New Issue
Block a user