This commit is contained in:
2026-10-17 16:00:16 +07:00
parent 417d88cce4
commit 5c0852f975
3 changed files with 142 additions and 22 deletions
+17 -1
View File
@@ -675,6 +675,7 @@ service EveningDetectiveServer {
}; };
} }
// Each request creates a fresh setup package, replacing any unfinished preparation.
rpc StartGameExport(StartGameExportReq) returns (GameExportRsp) { rpc StartGameExport(StartGameExportReq) returns (GameExportRsp) {
option (google.api.http) = { option (google.api.http) = {
post: "/api/games/{ID}/export" post: "/api/games/{ID}/export"
@@ -682,6 +683,14 @@ service EveningDetectiveServer {
}; };
} }
// Release the transfer without discarding ready packages or their future results.
rpc ResetGameLocalMode(ResetGameLocalModeReq) returns (ResetGameLocalModeRsp) {
option (google.api.http) = {
post: "/api/games/{ID}/local-mode/reset"
body: "*"
};
}
rpc GetGameExport(GetGameExportReq) returns (GameExportRsp) { rpc GetGameExport(GetGameExportReq) returns (GameExportRsp) {
option (google.api.http) = {get: "/api/games/{ID}/export"}; option (google.api.http) = {get: "/api/games/{ID}/export"};
} }
@@ -1719,7 +1728,7 @@ message Game {
// GET /api/games/{ID}/export/download (not an HttpBody RPC). // GET /api/games/{ID}/export/download (not an HttpBody RPC).
message GameExport { message GameExport {
string ID = 1; string ID = 1;
string state = 2; // preparing, ready or failed; preparing/ready lock game writes. string state = 2; // preparing, ready or failed; ready indicates a completed package.
string packageID = 3; string packageID = 3;
int64 size = 4; int64 size = 4;
string sha256 = 5; string sha256 = 5;
@@ -1736,6 +1745,13 @@ message StartGameExportReq { int32 ID = 1; }
message GetGameExportReq { int32 ID = 1; } message GetGameExportReq { int32 ID = 1; }
message ResetGameLocalModeReq {
int32 ID = 1;
string exportID = 2; // Expected latest export, protecting against stale confirmations.
}
message ResetGameLocalModeRsp {}
message GameExportRsp { GameExport export = 1; } message GameExportRsp { GameExport export = 1; }
enum ScreenMode { enum ScreenMode {
@@ -588,6 +588,14 @@ export type GetGameExportReq = {
ID: number | undefined; ID: number | undefined;
}; };
export type ResetGameLocalModeReq = {
ID: number | undefined;
exportID: string | undefined;
};
export type ResetGameLocalModeRsp = {
};
export type GameExportRsp = { export type GameExportRsp = {
export: GameExport | undefined; export: GameExport | undefined;
}; };
@@ -1419,7 +1427,10 @@ export interface EveningDetectiveServer {
// Dedicated HTTP handlers stream ZIPs; gRPC retains its message size limit. // Dedicated HTTP handlers stream ZIPs; gRPC retains its message size limit.
DownloadGameResultsPackage(request: GameResultsPackageReq): Promise<googleapi_HttpBody>; DownloadGameResultsPackage(request: GameResultsPackageReq): Promise<googleapi_HttpBody>;
ApplyGameResultsPackage(request: ApplyGameResultsPackageReq): Promise<GameResultsPackageRsp>; ApplyGameResultsPackage(request: ApplyGameResultsPackageReq): Promise<GameResultsPackageRsp>;
// Each request creates a fresh setup package, replacing any unfinished preparation.
StartGameExport(request: StartGameExportReq): Promise<GameExportRsp>; StartGameExport(request: StartGameExportReq): Promise<GameExportRsp>;
// Release the transfer without discarding ready packages or their future results.
ResetGameLocalMode(request: ResetGameLocalModeReq): Promise<ResetGameLocalModeRsp>;
GetGameExport(request: GetGameExportReq): 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>;
@@ -2682,6 +2693,26 @@ export function createEveningDetectiveServerClient(
method: "StartGameExport", method: "StartGameExport",
}) as Promise<GameExportRsp>; }) as Promise<GameExportRsp>;
}, },
ResetGameLocalMode(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}/local-mode/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: "POST",
body,
}, {
service: "EveningDetectiveServer",
method: "ResetGameLocalMode",
}) as Promise<ResetGameLocalModeRsp>;
},
GetGameExport(request) { // eslint-disable-line @typescript-eslint/no-unused-vars GetGameExport(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.ID) { if (!request.ID) {
throw new Error("missing required field request.ID"); throw new Error("missing required field request.ID");
+94 -21
View File
@@ -202,6 +202,14 @@ const canExport = computed(
) )
const isStartingExport = ref(false) const isStartingExport = ref(false)
const isDownloadingPackage = ref(false) const isDownloadingPackage = ref(false)
const canResetLocalMode = computed(
() =>
canExport.value &&
(game.value.export?.state === 'preparing' || game.value.export?.state === 'ready'),
)
const isResettingLocalMode = ref(false)
const isResetLocalModeModalOpen = ref(false)
const resetLocalModeExportID = ref('')
const canDownloadResults = computed( const canDownloadResults = computed(
() => () =>
uiStore.environment === 'venue' && uiStore.environment === 'venue' &&
@@ -649,14 +657,7 @@ async function toTeamStory(id: string, password: string, newTab: boolean = false
} }
async function preparePackage() { async function preparePackage() {
if ( if (!canExport.value || isStartingExport.value || isResettingLocalMode.value) return
!canExport.value ||
isStartingExport.value ||
game.value.export?.state === 'preparing' ||
game.value.export?.state === 'ready' ||
game.value.status !== 'draft'
)
return
isStartingExport.value = true isStartingExport.value = true
try { try {
const res = await client.StartGameExport({ ID: game.value.ID }) const res = await client.StartGameExport({ ID: game.value.ID })
@@ -668,6 +669,30 @@ async function preparePackage() {
} }
} }
function openResetLocalMode() {
if (!canResetLocalMode.value || isStartingExport.value || isResettingLocalMode.value) return
resetLocalModeExportID.value = game.value.export?.ID || ''
isResetLocalModeModalOpen.value = true
}
async function resetLocalMode() {
if (!canExport.value || !resetLocalModeExportID.value || isResettingLocalMode.value) return
isResettingLocalMode.value = true
try {
await client.ResetGameLocalMode({
ID: game.value.ID,
exportID: resetLocalModeExportID.value,
})
isResetLocalModeModalOpen.value = false
await getGame(Number(game.value.ID))
message.success('Локальный режим сброшен')
} catch (err) {
message.error(err instanceof Error ? err.message : 'Не удалось сбросить локальный режим')
} finally {
isResettingLocalMode.value = false
}
}
async function downloadPackage() { async function downloadPackage() {
if (!canExport.value || isDownloadingPackage.value || game.value.export?.state !== 'ready') return if (!canExport.value || isDownloadingPackage.value || game.value.export?.state !== 'ready') return
isDownloadingPackage.value = true isDownloadingPackage.value = true
@@ -1264,7 +1289,9 @@ useSimplePolling(() => {
<n-space vertical> <n-space vertical>
<p> <p>
Пакет содержит снимок на момент подготовки. Последующие онлайн-изменения в него не Пакет содержит снимок на момент подготовки. Последующие онлайн-изменения в него не
попадут. Готовность пакета не подтверждает импорт на площадку. попадут. На площадке игра начнётся с нуля: ходы, ответы и баллы в пакет не входят.
Статус и прогресс игры на сервере сохраняются. Готовность пакета не подтверждает
импорт на площадку.
</p> </p>
<n-alert v-if="game.export?.state === 'preparing'" type="info" role="status" <n-alert v-if="game.export?.state === 'preparing'" type="info" role="status"
>Подготовка пакета…</n-alert >Подготовка пакета…</n-alert
@@ -1283,21 +1310,25 @@ useSimplePolling(() => {
>Скачать пакет</n-button >Скачать пакет</n-button
> >
<n-button <n-button
v-else :loading="isStartingExport"
:loading="isStartingExport || game.export?.state === 'preparing'" :disabled="isStartingExport || isResettingLocalMode"
:disabled="
isStartingExport ||
game.export?.state === 'preparing' ||
game.status !== 'draft'
"
@click="preparePackage" @click="preparePackage"
>{{
game.export?.state === 'failed' ? 'Повторить подготовку' : 'Подготовить пакет'
}}</n-button
> >
<p v-if="game.status && game.status !== 'draft'"> Подготовить новый пакет
Подготовка доступна до старта игры. </n-button>
<p>
Новый пакет заменяет текущую подготовку. Результаты ранее скачанных пакетов можно
загрузить позже.
</p> </p>
<n-button
v-if="canResetLocalMode"
type="warning"
:loading="isResettingLocalMode"
:disabled="isStartingExport || isResettingLocalMode"
@click="openResetLocalMode"
>
Сбросить локальный режим
</n-button>
</n-space> </n-space>
</UiBlock> </UiBlock>
<UiBlock> <UiBlock>
@@ -1559,6 +1590,43 @@ useSimplePolling(() => {
</div> </div>
</div> </div>
<n-modal
v-if="canExport"
v-model:show="isResetLocalModeModalOpen"
:mask-closable="!isResettingLocalMode"
:close-on-esc="!isResettingLocalMode"
>
<n-card
class="reset-local-mode-modal"
:bordered="false"
role="dialog"
aria-modal="true"
aria-labelledby="reset-local-mode-title"
aria-describedby="reset-local-mode-description"
>
<template #header><span id="reset-local-mode-title">Сбросить локальный режим?</span></template>
<p id="reset-local-mode-description">
Игра вернётся в обычный режим на сервере. Данные игры сохранятся. Локальная площадка продолжит
работать отдельно; её результаты можно будет загрузить позже.
</p>
<template #footer>
<n-flex justify="end">
<n-button :disabled="isResettingLocalMode" @click="isResetLocalModeModalOpen = false">
Отмена
</n-button>
<n-button
type="warning"
:loading="isResettingLocalMode"
:disabled="isResettingLocalMode"
@click="resetLocalMode"
>
Вернуть в обычный режим
</n-button>
</n-flex>
</template>
</n-card>
</n-modal>
<TicketCheckModal v-if="canManageGame" v-model:show="showTicketCheckModal" /> <TicketCheckModal v-if="canManageGame" v-model:show="showTicketCheckModal" />
<TeamReceiptModal <TeamReceiptModal
v-if="canManageGame && uiStore.environment === 'venue'" v-if="canManageGame && uiStore.environment === 'venue'"
@@ -1805,6 +1873,11 @@ useSimplePolling(() => {
</template> </template>
<style scoped> <style scoped>
.reset-local-mode-modal {
max-width: calc(100vw - 32px);
width: 560px;
}
.reviews-summary { .reviews-summary {
margin-bottom: 16px; margin-bottom: 16px;
} }