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
+4 -12
View File
@@ -19,6 +19,7 @@ import { getURL } from '@/api/generated/crabs/evening_detective_server/client'
import HeaderMenu from '@/components/HeaderMenu.vue'
import { useSimplePolling } from '@/composables/useSimplePolling'
import { applicationFileType, applicationFileURL } from '@/utils/application'
import { formatGameDateTime, parseDateWithoutTimezone } from '@/utils/datetime'
const client = getAuthClient()
const router = useRouter()
@@ -345,18 +346,6 @@ async function saveAllScores() {
await loadGameAnswers()
}
function parseDateWithoutTimezone(dateStr: string): Date {
// Разбираем ISO строку: "2024-01-01T12:30:00"
const [datePart, timePart] = dateStr.split('T')
const [year, month, day] = datePart.split('-').map(Number)
const correctTimePart = timePart.split('.')[0]
const [hours, minutes, seconds] = (correctTimePart || '00:00:00').split(':').map(Number)
// Создаем дату в UTC, чтобы избежать сдвига
return new Date(Date.UTC(year, month - 1, day, hours, minutes, seconds || 0))
}
function getCurrentUTC(): Date {
const now = new Date()
return new Date(now.getTime() - now.getTimezoneOffset() * 60000)
@@ -407,6 +396,9 @@ useSimplePolling(() => {
<n-tag type="info">
{{ timeString }}
</n-tag>
<n-tag v-if="formatGameDateTime(game.startAt)" type="info">
Начало: {{ formatGameDateTime(game.startAt) }}
</n-tag>
</n-space>
<n-flex justify="end">
+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>
+32
View File
@@ -0,0 +1,32 @@
// Серверные даты-время — строки RFC 3339 (google.protobuf.Timestamp), в UTC: dev-поток
// создания игры шлёт new Date().toISOString(), таймер в GamePage считает разницу в UTC.
// Парсим часы строки как UTC и локализуем при показе, чтобы вывод не зависел от
// таймзоны зрителя.
// Разбирает строку вида "YYYY-MM-DDTHH:mm:ss[.доли][Z]" как UTC-часы.
// Поведение совпадает с прежним parseDateWithoutTimezone из GamePage.
export function parseDateWithoutTimezone(dateStr: string): Date {
const [datePart, timePart = ''] = dateStr.split('T')
const [year, month, day] = datePart.split('-').map(Number)
const [hours, minutes, seconds] = (timePart.split('.')[0] || '00:00:00').split(':').map(Number)
return new Date(Date.UTC(year, month - 1, day, hours, minutes, seconds || 0))
}
// "2026-09-07T08:00:00.000Z" (UTC, RFC 3339) → локальное стен-время зрителя,
// например "07.09.2026, 11:00".
export function formatGameDateTime(value: string | undefined): string {
if (!value) {
return ''
}
const date = parseDateWithoutTimezone(value)
if (Number.isNaN(date.getTime())) {
return ''
}
return date.toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
}