generated from VLADIMIR/template_frontend
270 lines
7.0 KiB
Vue
270 lines
7.0 KiB
Vue
<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>
|