add statuses game

This commit is contained in:
2026-08-04 02:28:16 +07:00
parent 2241c33902
commit 33cde1d28b
12 changed files with 537 additions and 143 deletions
@@ -301,6 +301,8 @@ export type Game = {
status: string | undefined;
scenario: Scenario | undefined;
teams: Team[] | undefined;
startedAt?: wellKnownTimestamp;
endedAt?: wellKnownTimestamp;
};
export type Team = {
@@ -333,6 +335,46 @@ export type UpdateGameRsp = {
error: string | undefined;
};
export type StartGameReq = {
id: number | undefined;
};
export type StartGameRsp = {
error: string | undefined;
};
export type PauseGameReq = {
id: number | undefined;
};
export type PauseGameRsp = {
error: string | undefined;
};
export type PlayGameReq = {
id: number | undefined;
};
export type PlayGameRsp = {
error: string | undefined;
};
export type FinishGameReq = {
id: number | undefined;
};
export type FinishGameRsp = {
error: string | undefined;
};
export type ResetGameReq = {
id: number | undefined;
};
export type ResetGameRsp = {
error: string | undefined;
};
export type DeleteGameReq = {
id: number | undefined;
};
@@ -438,6 +480,11 @@ export interface EveningDetectiveServer {
GetGames(request: GetGamesReq): Promise<GetGamesRsp>;
GetGame(request: GetGameReq): Promise<GetGameRsp>;
UpdateGame(request: UpdateGameReq): Promise<UpdateGameRsp>;
StartGame(request: StartGameReq): Promise<StartGameRsp>;
PauseGame(request: PauseGameReq): Promise<PauseGameRsp>;
PlayGame(request: PlayGameReq): Promise<PlayGameRsp>;
FinishGame(request: FinishGameReq): Promise<FinishGameRsp>;
ResetGame(request: ResetGameReq): Promise<ResetGameRsp>;
DeleteGame(request: DeleteGameReq): Promise<DeleteGameRsp>;
AddTeam(request: AddTeamReq): Promise<AddTeamRsp>;
UpdateTeam(request: UpdateTeamReq): Promise<UpdateTeamRsp>;
@@ -1024,6 +1071,106 @@ export function createEveningDetectiveServerClient(
method: "UpdateGame",
}) as Promise<UpdateGameRsp>;
},
StartGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/games/${request.id}/start`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
if (queryParams.length > 0) {
uri += `?${queryParams.join("&")}`
}
return handler({
path: uri,
method: "PUT",
body,
}, {
service: "EveningDetectiveServer",
method: "StartGame",
}) as Promise<StartGameRsp>;
},
PauseGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/games/${request.id}/pause`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
if (queryParams.length > 0) {
uri += `?${queryParams.join("&")}`
}
return handler({
path: uri,
method: "PUT",
body,
}, {
service: "EveningDetectiveServer",
method: "PauseGame",
}) as Promise<PauseGameRsp>;
},
PlayGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/games/${request.id}/play`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
if (queryParams.length > 0) {
uri += `?${queryParams.join("&")}`
}
return handler({
path: uri,
method: "PUT",
body,
}, {
service: "EveningDetectiveServer",
method: "PlayGame",
}) as Promise<PlayGameRsp>;
},
FinishGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/games/${request.id}/finish`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
if (queryParams.length > 0) {
uri += `?${queryParams.join("&")}`
}
return handler({
path: uri,
method: "PUT",
body,
}, {
service: "EveningDetectiveServer",
method: "FinishGame",
}) as Promise<FinishGameRsp>;
},
ResetGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/games/${request.id}/reset`; // eslint-disable-line quotes
const body = JSON.stringify(request);
const queryParams: string[] = [];
let uri = path;
if (queryParams.length > 0) {
uri += `?${queryParams.join("&")}`
}
return handler({
path: uri,
method: "PUT",
body,
}, {
service: "EveningDetectiveServer",
method: "ResetGame",
}) as Promise<ResetGameRsp>;
},
DeleteGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
+3 -3
View File
@@ -42,7 +42,7 @@ getScenarios()
<p class="scenario-label">
<n-tag :bordered="false" type="success" class="status-block">Автор: {{
scenario.author?.username
}}</n-tag>
}}</n-tag>
</p>
</div>
</div>
@@ -62,9 +62,9 @@ getScenarios()
border-radius: 12px;
/* background-color: #553333; */
background-color: #333;
background-color: #222;
border: 2px solid #333;
border: 2px solid #222;
}
.scenario-content-block {
+180 -31
View File
@@ -1,9 +1,9 @@
<script setup lang="ts">
import { Settings } from '@vicons/carbon'
import { Settings, Play, Pause, Reset, Flag } from '@vicons/carbon'
import { getAuthClient } from '@/api/auth_client';
import { Icon } from '@vicons/utils'
import { NButton } from 'naive-ui'
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'
@@ -179,6 +179,113 @@ async function openQR(url: string) {
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>
@@ -186,13 +293,53 @@ useSimplePolling(() => {
<div class="width1200">
<div class="settings-block">
Команд: {{ game.teams?.length }}
<n-button quaternary @click="showSettingsModal = true" class="settings-button">
<Icon class="settings-icon">
<Settings />
</Icon>
Настройки
</n-button>
<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">
@@ -201,25 +348,29 @@ useSimplePolling(() => {
<div v-for="team in game.teams" class="team-block">
<div class="team-content-block">
<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 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"
@@ -345,7 +496,7 @@ useSimplePolling(() => {
.team-block {
margin: 10px 0;
border-radius: 10px;
border: 1px solid #444;
background-color: #222;
}
.team-block-hover:hover {
@@ -375,9 +526,7 @@ useSimplePolling(() => {
width: 20px;
}
.title-block {
margin-bottom: 20px;
}
.title-block {}
.link-button {
margin-top: 20px;
+1 -1
View File
@@ -50,7 +50,7 @@ async function toGame(id: number) {
margin: 10px 0;
padding: 20px;
border-radius: 10px;
border: 1px solid #444;
background-color: #222;
}
.game-block:hover {
+2 -1
View File
@@ -284,7 +284,8 @@ function onCreateKey(): Key {
margin: 10px 0;
padding: 20px;
border-radius: 10px;
border: 1px solid #444;
border: 1px solid #222;
background-color: #222;
}
.place-block-hover:hover {
+2 -2
View File
@@ -99,9 +99,9 @@ getScenarios()
border-radius: 12px;
/* background-color: #553333; */
background-color: #333;
background-color: #222;
border: 2px solid #333;
border: 2px solid #222;
}
.scenario-content-block {
+1 -1
View File
@@ -77,7 +77,7 @@ async function deleteRole(userId: number, role: string) {
<style scoped>
.user-block {
border: 1px solid #444;
background-color: #222;
border-radius: 10px;
margin: 0 0 20px;
padding: 20px;