This commit is contained in:
2026-10-11 01:45:53 +07:00
parent 927aa81dda
commit 535d2c3649
5 changed files with 115 additions and 1 deletions
+27
View File
@@ -626,6 +626,17 @@ service EveningDetectiveServer {
}; };
} }
rpc StartGameExport(StartGameExportReq) returns (GameExportRsp) {
option (google.api.http) = {
post: "/api/games/{ID}/export"
body: "*"
};
}
rpc GetGameExport(GetGameExportReq) returns (GameExportRsp) {
option (google.api.http) = {get: "/api/games/{ID}/export"};
}
rpc IssueGameTickets(IssueGameTicketsReq) returns (IssueGameTicketsRsp) { rpc IssueGameTickets(IssueGameTicketsReq) returns (IssueGameTicketsRsp) {
option (google.api.http) = { option (google.api.http) = {
post: "/api/games/{gameID}/tickets" post: "/api/games/{gameID}/tickets"
@@ -1628,8 +1639,24 @@ message Game {
optional google.protobuf.Timestamp ended_at = 9; optional google.protobuf.Timestamp ended_at = 9;
string format = 10; string format = 10;
string location = 11; string location = 11;
GameExport export = 12;
} }
// Download ready ZIP through the authenticated streaming HTTP route:
// GET /api/games/{ID}/export/download (not an HttpBody RPC).
message GameExport {
string ID = 1;
string state = 2; // preparing, ready or failed; preparing/ready lock game writes.
string packageID = 3;
int64 size = 4;
string sha256 = 5;
string failure = 6;
}
message StartGameExportReq { int32 ID = 1; }
message GetGameExportReq { int32 ID = 1; }
message GameExportRsp { GameExport export = 1; }
enum ScreenMode { enum ScreenMode {
SCREEN_MODE_UNSPECIFIED = 0; SCREEN_MODE_UNSPECIFIED = 0;
SCREEN_MODE_COVER = 1; SCREEN_MODE_COVER = 1;
@@ -560,6 +560,30 @@ export type Game = {
endedAt?: wellKnownTimestamp; endedAt?: wellKnownTimestamp;
format: string | undefined; format: string | undefined;
location: string | undefined; location: string | undefined;
export: GameExport | undefined;
};
// Download ready ZIP through the authenticated streaming HTTP route:
// GET /api/games/{ID}/export/download (not an HttpBody RPC).
export type GameExport = {
ID: string | undefined;
state: string | undefined;
packageID: string | undefined;
size: number | undefined;
sha256: string | undefined;
failure: string | undefined;
};
export type StartGameExportReq = {
ID: number | undefined;
};
export type GetGameExportReq = {
ID: number | undefined;
};
export type GameExportRsp = {
export: GameExport | undefined;
}; };
export type ScreenState = { export type ScreenState = {
@@ -1248,6 +1272,8 @@ export interface EveningDetectiveServer {
AddGame(request: AddGameReq): Promise<AddGameRsp>; AddGame(request: AddGameReq): Promise<AddGameRsp>;
GetGames(request: GetGamesReq): Promise<GetGamesRsp>; GetGames(request: GetGamesReq): Promise<GetGamesRsp>;
GetGame(request: GetGameReq): Promise<GetGameRsp>; GetGame(request: GetGameReq): Promise<GetGameRsp>;
StartGameExport(request: StartGameExportReq): Promise<GameExportRsp>;
GetGameExport(request: GetGameExportReq): Promise<GameExportRsp>;
IssueGameTickets(request: IssueGameTicketsReq): Promise<IssueGameTicketsRsp>; IssueGameTickets(request: IssueGameTicketsReq): Promise<IssueGameTicketsRsp>;
GetTicket(request: GetTicketReq): Promise<GetTicketRsp>; GetTicket(request: GetTicketReq): Promise<GetTicketRsp>;
AcceptTicket(request: AcceptTicketReq): Promise<AcceptTicketRsp>; AcceptTicket(request: AcceptTicketReq): Promise<AcceptTicketRsp>;
@@ -2392,6 +2418,46 @@ export function createEveningDetectiveServerClient(
method: "GetGame", method: "GetGame",
}) as Promise<GetGameRsp>; }) as Promise<GetGameRsp>;
}, },
StartGameExport(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}/export`; // 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: "POST",
body,
}, {
service: "EveningDetectiveServer",
method: "StartGameExport",
}) as Promise<GameExportRsp>;
},
GetGameExport(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}/export`; // eslint-disable-line quotes
const body = null;
const queryParams: string[] = [];
let uri = path;
if (queryParams.length > 0) {
uri += `?${queryParams.join("&")}`
}
return handler({
path: uri,
method: "GET",
body,
}, {
service: "EveningDetectiveServer",
method: "GetGameExport",
}) as Promise<GameExportRsp>;
},
IssueGameTickets(request) { // eslint-disable-line @typescript-eslint/no-unused-vars IssueGameTickets(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.gameID) { if (!request.gameID) {
throw new Error("missing required field request.gameID"); throw new Error("missing required field request.gameID");
+14 -1
View File
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Flag, Pause, Play, Reset, Trophy } from '@vicons/carbon' import { Flag, Pause, Play, Reset, Trophy } from '@vicons/carbon'
import { Icon } from '@vicons/utils' import { Icon } from '@vicons/utils'
import { NButton, NTag } from 'naive-ui' import { NAlert, NButton, NTag } from 'naive-ui'
import { import {
NCard, NCard,
NFlex, NFlex,
@@ -107,6 +107,7 @@ const game = ref<Game>({
location: undefined, location: undefined,
scenario: undefined, scenario: undefined,
teams: undefined, teams: undefined,
export: undefined,
}) })
const participationOptions = [ const participationOptions = [
{ label: 'Ожидает ответа', value: 'pending' }, { label: 'Ожидает ответа', value: 'pending' },
@@ -782,6 +783,18 @@ useSimplePolling(() => {
<template> <template>
<div class="center-block-custom"> <div class="center-block-custom">
<div class="width1200"> <div class="width1200">
<UiBlock v-if="game.export?.state === 'preparing' || game.export?.state === 'ready'">
<n-alert
type="info"
:title="game.export.state === 'ready' ? 'Игра в офлайн-режиме' : 'Подготовка офлайн-пакета'"
>
{{
game.export.state === 'ready'
? 'Изменения на онлайн-сервере не синхронизируются с офлайн-площадкой.'
: 'Создаётся пакет для офлайн-площадки.'
}}
</n-alert>
</UiBlock>
<n-tabs :value="activeTab" type="line" animated @update:value="setActiveTab"> <n-tabs :value="activeTab" type="line" animated @update:value="setActiveTab">
<n-tab-pane name="settings" tab="Настройки"> <n-tab-pane name="settings" tab="Настройки">
<div class="game-tab-content"> <div class="game-tab-content">
+7
View File
@@ -8,6 +8,7 @@ import {
NModal, NModal,
NSelect, NSelect,
NSpace, NSpace,
NTag,
useMessage, useMessage,
} from 'naive-ui' } from 'naive-ui'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
@@ -131,6 +132,12 @@ async function createGame() {
<div class="game-title"> <div class="game-title">
{{ game.name }} — {{ game.format === 'offline' ? 'Offline' : 'Online' }} {{ game.name }} — {{ game.format === 'offline' ? 'Offline' : 'Online' }}
</div> </div>
<n-tag v-if="game.export?.state === 'preparing'" type="warning" size="small">
Подготовка офлайн-пакета
</n-tag>
<n-tag v-else-if="game.export?.state === 'ready'" type="info" size="small">
Игра в офлайн-режиме
</n-tag>
<div class="game-description"> <div class="game-description">
{{ game.description }} {{ game.description }}
</div> </div>
+1
View File
@@ -49,6 +49,7 @@ const game = ref<Game | undefined>({
location: undefined, location: undefined,
scenario: undefined, scenario: undefined,
teams: undefined, teams: undefined,
export: undefined,
}) })
const code = ref('') const code = ref('')