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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Вечерний детектив</title>
<script type="module" crossorigin src="/assets/index-DaRs3nRs.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-x53yzfRc.css">
<script type="module" crossorigin src="/assets/index-nOa26MN3.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BreLDq9T.css">
</head>
<body>
<div id="app"></div>
+104 -7
View File
@@ -366,6 +366,61 @@ service EveningDetectiveServer {
};
}
rpc StartGame(StartGameReq) returns (StartGameRsp) {
option (google.api.http) = {
put : "/api/games/{id}/start"
body: "*"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags : "Игры";
summary: "Начать игру";
};
}
rpc PauseGame(PauseGameReq) returns (PauseGameRsp) {
option (google.api.http) = {
put : "/api/games/{id}/pause"
body: "*"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags : "Игры";
summary: "Остановить игру";
};
}
rpc PlayGame(PlayGameReq) returns (PlayGameRsp) {
option (google.api.http) = {
put : "/api/games/{id}/play"
body: "*"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags : "Игры";
summary: "Продолжить игру";
};
}
rpc FinishGame(FinishGameReq) returns (FinishGameRsp) {
option (google.api.http) = {
put : "/api/games/{id}/finish"
body: "*"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags : "Игры";
summary: "Закончить игру";
};
}
rpc ResetGame(ResetGameReq) returns (ResetGameRsp) {
option (google.api.http) = {
put : "/api/games/{id}/reset"
body: "*"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags : "Игры";
summary: "Сбросить игру";
};
}
rpc DeleteGame(DeleteGameReq) returns (DeleteGameRsp) {
option (google.api.http) = {
delete: "/api/games/{id}"
@@ -736,13 +791,15 @@ message GetGamesRsp {
}
message Game {
int32 id = 1;
string name = 2;
string description = 3;
google.protobuf.Timestamp start_at = 4;
string status = 5;
Scenario scenario = 6;
repeated Team teams = 7;
int32 id = 1;
string name = 2;
string description = 3;
google.protobuf.Timestamp start_at = 4;
string status = 5;
Scenario scenario = 6;
repeated Team teams = 7;
optional google.protobuf.Timestamp started_at = 8;
optional google.protobuf.Timestamp ended_at = 9;
}
message Team {
@@ -775,6 +832,46 @@ message UpdateGameRsp {
string error = 1;
}
message StartGameReq {
int32 id = 1;
}
message StartGameRsp {
string error = 1;
}
message PauseGameReq {
int32 id = 1;
}
message PauseGameRsp {
string error = 1;
}
message PlayGameReq {
int32 id = 1;
}
message PlayGameRsp {
string error = 1;
}
message FinishGameReq {
int32 id = 1;
}
message FinishGameRsp {
string error = 1;
}
message ResetGameReq {
int32 id = 1;
}
message ResetGameRsp {
string error = 1;
}
message DeleteGameReq {
int32 id = 1;
}
@@ -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;