Files
evening_detective_frontend/src/components/GamePage.vue
T
2026-08-04 02:28:16 +07:00

555 lines
16 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 { Settings, Play, Pause, Reset, Flag } from '@vicons/carbon'
import { getAuthClient } from '@/api/auth_client';
import { Icon } from '@vicons/utils'
import { NButton, NTag } from 'naive-ui'
import type { Application, Game, Team } from '@/api/generated/crabs/evening_detective_server';
import { NCard, NInput, NModal, NSpace, NFlex, NText, NQrCode } from 'naive-ui'
import HeaderMenu from '@/components/HeaderMenu.vue'
import { useMessage } from 'naive-ui';
import { ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { getURL } from '@/api/generated/crabs/evening_detective_server/client';
import { useSimplePolling } from '@/composables/useSimplePolling';
const client = getAuthClient()
const router = useRouter()
const route = useRoute()
const gameId = route.params.id
const message = useMessage()
const showSettingsModal = ref(false)
const showGiveApplicationModal = ref(false)
const showAddTeamModal = ref(false)
const showDeleteTeamModal = ref(false)
const newTeamName = ref('')
const showQR = ref(false)
const urlQR = ref('')
const game = ref<Game>({
id: undefined,
name: undefined,
description: undefined,
startAt: undefined,
status: undefined,
scenario: undefined,
teams: undefined,
})
async function getGame(id: number) {
const res = await client.GetGame({ id: id })
if (res.error != '') {
message.error(res.error!)
return
}
game.value = res.game!
game.value.teams = game.value.teams?.sort((a, b) => {
let aLen = 0
if ((a.applications?.length || 0) > 0) {
aLen = 1
}
let bLen = 0
if ((b.applications?.length || 0) > 0) {
bLen = 1
}
if (aLen > bLen) {
return -1
}
if (aLen < bLen) {
return 1
}
const aName = a.name || ''
const bName = b.name || ''
if (aName > bName) {
return 1
}
if (aName < bName) {
return -1
}
return 0
})
}
getGame(Number(gameId))
async function toTeamStory(id: number, password: string) {
router.push('/team-story/' + id + '?password=' + password)
}
async function updateGame() {
const res = await client.UpdateGame({
id: game.value.id,
name: game.value.name,
description: game.value.description,
startAt: game.value.startAt,
scenarioId: game.value.scenario?.id,
})
if (res.error != '') {
message.error(res.error!)
return
}
await getGame(game.value.id!)
showSettingsModal.value = false
}
const giveApplicationTeam = ref<Team>({
id: 0,
name: '',
status: '',
password: '',
actionsCount: 0,
applications: []
})
const deletedTeamName = ref('')
const deletedTeam = ref<Team>({
id: 0,
name: '',
status: '',
password: '',
actionsCount: 0,
applications: []
})
const giveApplicationApplication = ref<Application>({
name: '',
image: ''
})
function giveApplication(team: Team, application: Application) {
giveApplicationTeam.value = team
giveApplicationApplication.value = application
showGiveApplicationModal.value = true
}
async function addTeam() {
const res = await client.AddTeam({
gameId: game.value.id,
name: newTeamName.value,
})
if (res.error != '') {
message.error(res.error!)
return
}
await getGame(game.value.id!)
showAddTeamModal.value = false
}
async function confirmGiveApplication() {
const res = await client.GiveTeamApplications({
id: giveApplicationTeam.value.id,
name: giveApplicationApplication.value.name,
})
if (res.error != '') {
message.error(res.error!)
return
}
await getGame(game.value.id!)
showGiveApplicationModal.value = false
}
async function deleteTeam(team: Team) {
deletedTeam.value = team
showDeleteTeamModal.value = true
}
async function confirmDeleteTeam() {
const res = await client.DeleteTeam({
id: deletedTeam.value.id,
})
if (res.error != '') {
message.error(res.error!)
return
}
await getGame(game.value.id!)
showDeleteTeamModal.value = false
}
function getTeamStoryURL(id: number, password: string): string {
return getURL() + '/team-story/' + id + '?password=' + password
}
async function openQR(url: string) {
urlQR.value = url
showQR.value = true
}
useSimplePolling(() => {
getGame(Number(gameId))
}, 2000);
async function resetGame() {
const res = await client.ResetGame({
id: game.value.id
})
if (res.error != '') {
message.error(res.error!)
return
}
await getGame(game.value.id!)
}
async function startGame() {
const res = await client.StartGame({
id: game.value.id
})
if (res.error != '') {
message.error(res.error!)
return
}
await getGame(game.value.id!)
}
async function pauseGame() {
const res = await client.PauseGame({
id: game.value.id
})
if (res.error != '') {
message.error(res.error!)
return
}
await getGame(game.value.id!)
}
async function playGame() {
const res = await client.PlayGame({
id: game.value.id
})
if (res.error != '') {
message.error(res.error!)
return
}
await getGame(game.value.id!)
}
async function finishGame() {
const res = await client.FinishGame({
id: game.value.id
})
if (res.error != '') {
message.error(res.error!)
return
}
await getGame(game.value.id!)
}
function parseDateWithoutTimezone(dateStr: string): Date {
// Разбираем ISO строку: "2024-01-01T12:30:00"
let [datePart, timePart] = dateStr.split('T');
const [year, month, day] = datePart.split('-').map(Number);
timePart = timePart.split('.')[0]
const [hours, minutes, seconds] = (timePart || '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);
}
function getTimeDiff(startStr: string, endStr: string): string {
if (startStr == '') {
return 'Игра ещё не началась'
}
const start = parseDateWithoutTimezone(startStr)
let endTime = getCurrentUTC();
if (endStr != '') {
endTime = parseDateWithoutTimezone(endStr);
}
const diffMs = endTime.getTime() - start.getTime();
if (diffMs < 0) return 'Игра ещё не началась';
const seconds = Math.floor(diffMs / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const months = Math.floor(days / 30);
const years = Math.floor(days / 365);
if (years > 0) return `${years}г ${days % 365}д`;
if (months > 0) return `${months}мес ${days % 30}д`;
if (days > 0) return `${days}д ${hours % 24}ч`;
if (hours > 0) return `${hours}ч ${minutes % 60}м`;
if (minutes > 0) return `${minutes}м ${seconds % 60}с`;
return `${seconds}с`;
}
const timeString = ref('')
useSimplePolling(() => {
timeString.value = getTimeDiff(game.value.startedAt || '', game.value.endedAt || '')
}, 1000);
</script>
<template>
<div class="center-block-custom">
<div class="width1200">
<div class="settings-block">
<n-flex justify="space-between">
<n-space>
<n-tag :bordered="false" type="info">
Команд: {{ game.teams?.length }}
</n-tag>
<n-tag type="info">
{{ timeString }}
</n-tag>
</n-space>
<div>
<n-button quaternary @click="showSettingsModal = true" class="settings-button">
<Icon class="settings-icon">
<Settings />
</Icon>
Настройки
</n-button>
<n-space>
<n-button type="info" v-if="game.status != 'draft'" dashed @click="resetGame()">
<Icon>
<Reset />
</Icon>
</n-button>
<n-button type="info" v-if="game.status == 'draft'" dashed @click="startGame()">
<Icon>
<Play />
</Icon>
</n-button>
<n-button type="info" v-if="game.status == 'pause'" dashed @click="playGame()">
<Icon>
<Play />
</Icon>
</n-button>
<n-button type="info" v-if="game.status == 'run'" dashed @click="pauseGame()">
<Icon>
<Pause />
</Icon>
</n-button>
<n-button type="info" v-if="game.status != 'finish'" dashed @click="finishGame()">
<Icon>
<Flag />
</Icon>
</n-button>
</n-space>
</div>
</n-flex>
</div>
<div class="team-block team-block-hover" @click="showAddTeamModal = true">
<p class="place-image-plus">+</p>
</div>
<div v-for="team in game.teams" class="team-block">
<div class="team-content-block">
<n-flex justify="space-between">
<div class="title-block">
<span class="team-name-block">
{{ team.name }}
</span>
<div>
Поездки: {{ team.actionsCount }}
</div>
</div>
<n-flex justify="end">
<n-button @click="openQR(getTeamStoryURL(team.id!, team.password!))">
QR
</n-button>
<n-button @click="toTeamStory(team.id!, team.password!)">
Подсмотреть игру
</n-button>
<n-button type="error" ghost @click="deleteTeam(team)">
Удалить
</n-button>
</n-flex>
</n-flex>
<n-button v-for="application in team.applications" :key="application.name" class="link-button"
@click="giveApplication(team, application)">
Выдать: {{ application.name }}
</n-button>
</div>
</div>
</div>
</div>
<!-- Окно настроек -->
<n-modal v-model:show="showSettingsModal">
<n-card style="width: 600px" title="Настройки игры" :bordered="false" size="huge" role="dialog"
aria-modal="true">
<template #header-extra>
<n-button @click="showSettingsModal = false"> x </n-button>
</template>
<n-space vertical>
<p>Название</p>
<n-input v-model:value="game.name" type="text" placeholder='Дело №0 "Следствие ведут овечки"' />
<p class="settings-header">Описание</p>
<n-input v-model:value="game.description" type="textarea" placeholder="Убийство пастуха..." />
</n-space>
<template #footer>
<n-flex justify="end">
<n-button @click="updateGame()" ghost> Сохранить изменения </n-button>
</n-flex>
</template>
</n-card>
</n-modal>
<!-- Окно выдачи улик -->
<n-modal v-model:show="showGiveApplicationModal">
<n-card style="width: 600px" title="Настройки игры" :bordered="false" size="huge" role="dialog"
aria-modal="true">
<template #header-extra>
<n-button @click="showGiveApplicationModal = false"> x </n-button>
</template>
<n-space vertical>
<p>Команда</p>
<h3>{{ giveApplicationTeam.name }}</h3>
<p>Улика</p>
<h3>{{ giveApplicationApplication.name }}</h3>
</n-space>
<template #footer>
<n-flex justify="end">
<n-button @click="confirmGiveApplication()" ghost> Выдать </n-button>
</n-flex>
</template>
</n-card>
</n-modal>
<!-- Окно создания команды -->
<n-modal v-model:show="showAddTeamModal">
<n-card style="width: 600px" title="Создание команды" :bordered="false" size="huge" role="dialog"
aria-modal="true">
<template #header-extra>
<n-button @click="showAddTeamModal = false"> x </n-button>
</template>
<n-input v-model:value="newTeamName" type="text" placeholder='Три дебила' />
<template #footer>
<n-flex justify="end">
<n-button @click="addTeam()" :disabled="newTeamName.length == 0">
Создать
</n-button>
</n-flex>
</template>
</n-card>
</n-modal>
<!-- Окно создания команды -->
<n-modal v-model:show="showDeleteTeamModal">
<n-card style="width: 600px" title="Удаление команды" :bordered="false" size="huge" role="dialog"
aria-modal="true">
<template #header-extra>
<n-button @click="showDeleteTeamModal = false"> x </n-button>
</template>
<n-space vertical>
<p>
Введите название команды
<n-text class="name-text" strong>{{ deletedTeam.name }}</n-text> чтобы удалить его
</p>
<n-input v-model:value="deletedTeamName" type="text" placeholder='Три дебила' />
</n-space>
<template #footer>
<n-flex justify="end">
<n-button @click="confirmDeleteTeam()" :disabled="deletedTeamName != deletedTeam.name">
Удалить
</n-button>
</n-flex>
</template>
</n-card>
</n-modal>
<!-- Окно qr -->
<n-modal v-model:show="showQR">
<n-card style="width: 600px" title="Подсмотреть игру" :bordered="false" size="huge" role="dialog"
aria-modal="true">
<template #header-extra>
<n-button @click="showQR = false"> x </n-button>
</template>
<n-space vertical>
<div class="qr-block">
<n-qr-code :value="urlQR" :size="300" :padding="0" />
</div>
</n-space>
<template #footer></template>
</n-card>
</n-modal>
<HeaderMenu active="games" />
</template>
<style scoped>
.team-block {
margin: 10px 0;
border-radius: 10px;
background-color: #222;
}
.team-block-hover:hover {
color: #63e2b7;
cursor: pointer;
border: 1px solid #63e2b7;
}
.team-content-block {
padding: 20px;
}
.team-name-block {
font-weight: 600;
font-size: 20px;
}
.settings-block {
height: 40px;
}
.settings-button {
float: right;
}
.settings-icon {
width: 20px;
}
.title-block {}
.link-button {
margin-top: 20px;
width: 100%;
}
.place-image-plus {
font-size: 30px;
display: flex;
justify-content: center;
align-items: center;
}
.name-text {
padding: 3px;
border-radius: 3px;
background-color: rgb(255 255 255 / 10%);
}
.qr-block {
display: inline-block;
background-color: white;
padding: 12px;
}
</style>