Files
evening_detective_frontend/src/components/GamesPage.vue
T
2026-09-03 01:28:39 +07:00

270 lines
7.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import {
NButton,
NCard,
NDatePicker,
NFlex,
NInput,
NModal,
NSelect,
NSpace,
useMessage,
} from 'naive-ui'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { getAuthClient } from '@/api/auth_client'
import type { Game, Scenario } from '@/api/generated/crabs/evening_detective_server'
import HeaderMenu from '@/components/HeaderMenu.vue'
import { formatGameDateTime } from '@/utils/datetime'
const client = getAuthClient()
const message = useMessage()
const router = useRouter()
const games = ref<Game[]>([])
async function getGames() {
games.value = []
const res = await client.GetGames({})
if (res.error != '') {
message.error(res.error!)
return
}
games.value = res.games || []
}
getGames()
async function toGame(id: number) {
router.push('/games/' + id)
}
function toResults(id: number) {
router.push('/games/' + id + '/results')
}
// ===== Создание игры =====
const showCreateGameModal = ref(false)
const newGameName = ref('')
const newGameDescription = ref('')
const newGameScenarioId = ref<number | null>(null)
const newGameStartAt = ref<number | null>(null)
const isCreating = ref(false)
const isCatalogLoading = ref(false)
const catalogError = ref(false)
const catalogScenarios = ref<Scenario[]>([])
// Опубликованные сценарии каталога — кандидаты для новой игры
const catalogOptions = computed(() =>
catalogScenarios.value
.filter((scenario) => scenario.status == 'public' && scenario.id != undefined)
.map((scenario) => ({ label: scenario.name || '', value: scenario.id! })),
)
async function getCatalogScenarios() {
isCatalogLoading.value = true
catalogError.value = false
try {
const res = await client.GetScenariosCatalog({})
if (res.error != '') {
catalogError.value = true
message.error(res.error!)
return
}
catalogScenarios.value = res.scenarios || []
} catch (err) {
catalogError.value = true
message.error(err instanceof Error ? err.message : 'Не удалось загрузить сценарии')
} finally {
isCatalogLoading.value = false
}
}
function openCreateGame() {
newGameName.value = ''
newGameDescription.value = ''
newGameScenarioId.value = null
newGameStartAt.value = null
showCreateGameModal.value = true
getCatalogScenarios()
}
async function createGame() {
const name = newGameName.value.trim()
const scenarioId = newGameScenarioId.value
const startAtMs = newGameStartAt.value
if (name == '' || scenarioId == null || startAtMs == null) {
return
}
isCreating.value = true
try {
const res = await client.AddGame({
name: name,
description: newGameDescription.value,
// Сервер парсит startAt как google.protobuf.Timestamp — нужен RFC 3339 с таймзоной
startAt: new Date(startAtMs).toISOString(),
scenarioId: scenarioId,
})
if (res.error != '') {
message.error(res.error!)
return
}
message.success('Игра создана')
showCreateGameModal.value = false
router.push('/games/' + res.id!)
} catch (err) {
message.error(err instanceof Error ? err.message : 'Не удалось создать игру')
} finally {
isCreating.value = false
}
}
</script>
<template>
<div class="center-block-custom">
<div class="width1200">
<div class="game-block game-block-hover" @click="openCreateGame()">
<div class="game-create-content">
<span class="game-create-plus">+</span>
<span>Создать игру</span>
</div>
</div>
<div v-for="game in games" :key="game.id" class="game-block" @click="toGame(game.id!)">
<div class="game-title">
{{ game.name }}
</div>
<div class="game-description">
{{ game.description }}
</div>
<div v-if="formatGameDateTime(game.startAt)" class="game-date">
Начало: {{ formatGameDateTime(game.startAt) }}
</div>
<div v-if="game.status == 'finish'" class="game-actions">
<n-button size="small" ghost @click.stop="toResults(game.id!)">Результаты</n-button>
</div>
</div>
</div>
</div>
<!-- Окно создания игры -->
<n-modal v-model:show="showCreateGameModal">
<n-card
style="width: min(94vw, 600px)"
title="Создание игры"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<template #header-extra>
<n-button @click="showCreateGameModal = false"> x </n-button>
</template>
<n-space vertical>
<p>Название</p>
<n-input
v-model:value="newGameName"
type="text"
placeholder='Дело №0 "Следствие ведут овечки"'
/>
<p>Описание</p>
<n-input
v-model:value="newGameDescription"
type="textarea"
placeholder="Убийство пастуха..."
/>
<p>Сценарий (дело из каталога)</p>
<n-select
v-model:value="newGameScenarioId"
:options="catalogOptions"
placeholder="Выберите дело из каталога"
/>
<p v-if="isCatalogLoading" class="create-hint">Загружаем сценарии...</p>
<p v-else-if="catalogOptions.length == 0" class="create-hint">
{{
catalogError
? 'Не удалось загрузить список сценариев'
: 'В каталоге пока нет опубликованных сценариев'
}}
</p>
<p>Дата и время проведения</p>
<n-date-picker v-model:value="newGameStartAt" type="datetime" />
</n-space>
<template #footer>
<n-flex justify="end">
<n-button
@click="createGame()"
:loading="isCreating"
:disabled="
newGameName.trim().length == 0 ||
newGameScenarioId == null ||
newGameStartAt == null ||
isCreating
"
>
Создать
</n-button>
</n-flex>
</template>
</n-card>
</n-modal>
<HeaderMenu active="games" />
</template>
<style scoped>
.game-block {
background-color: #222;
border: 1px solid #222;
border-radius: 10px;
margin: 10px 0;
padding: 20px;
}
.game-block-hover:hover {
border: 1px solid #63e2b7;
color: #63e2b7;
cursor: pointer;
}
.game-create-content {
align-items: center;
display: flex;
gap: 12px;
justify-content: center;
}
.game-create-plus {
font-size: 30px;
}
.game-block:hover {
color: #63e2b7;
cursor: pointer;
}
.game-title {
font-size: 24px;
margin-bottom: 10px;
}
.game-actions {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.game-date,
.game-description {
color: #aaa;
margin-bottom: 10px;
}
.create-hint {
color: #aaa;
}
</style>