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