add create game

This commit is contained in:
2026-09-03 01:28:39 +07:00
parent 8b8fb33d45
commit aec2e550eb
57 changed files with 2734 additions and 1953 deletions
+188 -4
View File
@@ -1,12 +1,22 @@
<script setup lang="ts">
import { NButton } from 'naive-ui'
import { useMessage } from 'naive-ui'
import { ref } from 'vue'
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 } from '@/api/generated/crabs/evening_detective_server'
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()
@@ -32,11 +42,94 @@ async function toGame(id: number) {
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 }}
@@ -44,6 +137,9 @@ function toResults(id: number) {
<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>
@@ -51,17 +147,100 @@ function toResults(id: number) {
</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;
@@ -78,8 +257,13 @@ function toResults(id: number) {
margin-top: 10px;
}
.game-date,
.game-description {
color: #aaa;
margin-bottom: 10px;
}
.create-hint {
color: #aaa;
}
</style>