generated from VLADIMIR/template_frontend
up
This commit is contained in:
@@ -634,6 +634,30 @@ service EveningDetectiveServer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dedicated HTTP handlers stream ZIPs; gRPC retains its message size limit.
|
||||||
|
rpc DownloadGameResultsPackage(GameResultsPackageReq) returns (google.api.HttpBody) {
|
||||||
|
option (google.api.http) = {get: "/api/games/{ID}/results-package/download"};
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
tags: "Игры";
|
||||||
|
summary: "Скачать свежий ZIP результатов завершённой venue-игры";
|
||||||
|
produces: "application/zip";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
rpc ApplyGameResultsPackage(ApplyGameResultsPackageReq) returns (GameResultsPackageRsp) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
post: "/api/games/{ID}/results-package/apply"
|
||||||
|
body: "package"
|
||||||
|
};
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
tags: "Игры";
|
||||||
|
summary: "Применить ZIP результатов на исходной prod-игре";
|
||||||
|
description: "HTTP принимает сырой ZIP до 1 ГиБ, включая поток без Content-Length. Ошибки: 400 неверный пакет/игра, 413 размер, 409 конфликт, 503 временный отказ.";
|
||||||
|
consumes: "application/zip";
|
||||||
|
produces: "application/json";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
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"
|
||||||
@@ -2345,3 +2369,14 @@ message DeleteTaskReq {
|
|||||||
message TaskRsp { DeveloperTask task = 1; }
|
message TaskRsp { DeveloperTask task = 1; }
|
||||||
|
|
||||||
message DeleteTaskRsp {}
|
message DeleteTaskRsp {}
|
||||||
|
|
||||||
|
message GameResultsPackageReq { int32 ID = 1; }
|
||||||
|
message ApplyGameResultsPackageReq {
|
||||||
|
int32 ID = 1;
|
||||||
|
google.api.HttpBody package = 2;
|
||||||
|
}
|
||||||
|
message GameResultsPackageRsp {
|
||||||
|
int32 gameID = 1;
|
||||||
|
string packageID = 2;
|
||||||
|
string appliedAt = 3;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import type { GameImportRsp } from '@/api/generated/crabs/evening_detective_server'
|
import type {
|
||||||
|
GameImportRsp,
|
||||||
|
GameResultsPackageRsp,
|
||||||
|
} from '@/api/generated/crabs/evening_detective_server'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
import { retryWithAuth } from './authRetry'
|
import { retryWithAuth } from './authRetry'
|
||||||
@@ -62,6 +65,32 @@ function buildBinaryClient(
|
|||||||
return download(`api/games/${id}/export/download`, `game-${id}.zip`)
|
return download(`api/games/${id}/export/download`, `game-${id}.zip`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function downloadGameResultsPackage(id: number): Promise<DownloadFileResult> {
|
||||||
|
return download(`api/games/${id}/results-package/download`, `game-results-${id}.zip`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyGameResultsPackage(id: number, file: File): Promise<GameResultsPackageRsp> {
|
||||||
|
const response = await request(`api/games/${id}/results-package/apply`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/zip' },
|
||||||
|
body: file,
|
||||||
|
})
|
||||||
|
if (!response.ok) throw new Error(await gatewayErrorMessage(response))
|
||||||
|
const data: unknown = await response.json()
|
||||||
|
if (
|
||||||
|
!data ||
|
||||||
|
typeof data !== 'object' ||
|
||||||
|
!('gameID' in data) ||
|
||||||
|
data.gameID !== id ||
|
||||||
|
!('packageID' in data) ||
|
||||||
|
typeof data.packageID !== 'string' ||
|
||||||
|
!('appliedAt' in data) ||
|
||||||
|
typeof data.appliedAt !== 'string'
|
||||||
|
)
|
||||||
|
throw new Error('Некорректный ответ применения результатов')
|
||||||
|
return { gameID: data.gameID, packageID: data.packageID, appliedAt: data.appliedAt }
|
||||||
|
}
|
||||||
|
|
||||||
async function uploadGamePackage(file: File): Promise<GameImportRsp> {
|
async function uploadGamePackage(file: File): Promise<GameImportRsp> {
|
||||||
const response = await request('api/games/import', {
|
const response = await request('api/games/import', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -106,6 +135,8 @@ function buildBinaryClient(
|
|||||||
uploadScenarioArchive,
|
uploadScenarioArchive,
|
||||||
downloadGamePackage,
|
downloadGamePackage,
|
||||||
uploadGamePackage,
|
uploadGamePackage,
|
||||||
|
downloadGameResultsPackage,
|
||||||
|
applyGameResultsPackage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1216,6 +1216,71 @@ export type TaskRsp = {
|
|||||||
export type DeleteTaskRsp = {
|
export type DeleteTaskRsp = {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GameResultsPackageReq = {
|
||||||
|
ID: number | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApplyGameResultsPackageReq = {
|
||||||
|
ID: number | undefined;
|
||||||
|
package: googleapi_HttpBody | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Message that represents an arbitrary HTTP body. It should only be used for
|
||||||
|
// payload formats that can't be represented as JSON, such as raw binary or
|
||||||
|
// an HTML page.
|
||||||
|
// This message can be used both in streaming and non-streaming API methods in
|
||||||
|
// the request as well as the response.
|
||||||
|
// It can be used as a top-level request field, which is convenient if one
|
||||||
|
// wants to extract parameters from either the URL or HTTP template into the
|
||||||
|
// request fields and also want access to the raw HTTP body.
|
||||||
|
// Example:
|
||||||
|
// message GetResourceRequest {
|
||||||
|
// // A unique request id.
|
||||||
|
// string request_id = 1;
|
||||||
|
// // The raw HTTP body is bound to this field.
|
||||||
|
// google.api.HttpBody http_body = 2;
|
||||||
|
// }
|
||||||
|
// service ResourceService {
|
||||||
|
// rpc GetResource(GetResourceRequest)
|
||||||
|
// returns (google.api.HttpBody);
|
||||||
|
// rpc UpdateResource(google.api.HttpBody)
|
||||||
|
// returns (google.protobuf.Empty);
|
||||||
|
// }
|
||||||
|
// Example with streaming methods:
|
||||||
|
// service CaldavService {
|
||||||
|
// rpc GetCalendar(stream google.api.HttpBody)
|
||||||
|
// returns (stream google.api.HttpBody);
|
||||||
|
// rpc UpdateCalendar(stream google.api.HttpBody)
|
||||||
|
// returns (stream google.api.HttpBody);
|
||||||
|
// }
|
||||||
|
// Use of this type only changes how the request and response bodies are
|
||||||
|
// handled, all other features will continue to work unchanged.
|
||||||
|
export type googleapi_HttpBody = {
|
||||||
|
// The HTTP Content-Type header value specifying the content type of the body.
|
||||||
|
contentType: string | undefined;
|
||||||
|
// The HTTP request/response body as raw binary.
|
||||||
|
data: string | undefined;
|
||||||
|
// Application specific response metadata. Must be set in the first response
|
||||||
|
// for streaming APIs.
|
||||||
|
extensions: wellKnownAny[] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the Any contains a value that has a special JSON mapping,
|
||||||
|
// it will be converted as follows:
|
||||||
|
// {"@type": xxx, "value": yyy}.
|
||||||
|
// Otherwise, the value will be converted into a JSON object,
|
||||||
|
// and the "@type" field will be inserted to indicate the actual data type.
|
||||||
|
interface wellKnownAny {
|
||||||
|
"@type": string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GameResultsPackageRsp = {
|
||||||
|
gameID: number | undefined;
|
||||||
|
packageID: string | undefined;
|
||||||
|
appliedAt: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export interface EveningDetectiveServer {
|
export interface EveningDetectiveServer {
|
||||||
GetTasks(request: GetTasksReq): Promise<GetTasksRsp>;
|
GetTasks(request: GetTasksReq): Promise<GetTasksRsp>;
|
||||||
GetTaskAssignees(request: GetTaskAssigneesReq): Promise<GetTaskAssigneesRsp>;
|
GetTaskAssignees(request: GetTaskAssigneesReq): Promise<GetTaskAssigneesRsp>;
|
||||||
@@ -1280,6 +1345,9 @@ export interface EveningDetectiveServer {
|
|||||||
GetGame(request: GetGameReq): Promise<GetGameRsp>;
|
GetGame(request: GetGameReq): Promise<GetGameRsp>;
|
||||||
// HTTP uploads are streamed by a dedicated handler, bypassing gRPC message limits.
|
// HTTP uploads are streamed by a dedicated handler, bypassing gRPC message limits.
|
||||||
ImportGame(request: googleapi_HttpBody): Promise<GameImportRsp>;
|
ImportGame(request: googleapi_HttpBody): Promise<GameImportRsp>;
|
||||||
|
// Dedicated HTTP handlers stream ZIPs; gRPC retains its message size limit.
|
||||||
|
DownloadGameResultsPackage(request: GameResultsPackageReq): Promise<googleapi_HttpBody>;
|
||||||
|
ApplyGameResultsPackage(request: ApplyGameResultsPackageReq): Promise<GameResultsPackageRsp>;
|
||||||
StartGameExport(request: StartGameExportReq): Promise<GameExportRsp>;
|
StartGameExport(request: StartGameExportReq): Promise<GameExportRsp>;
|
||||||
GetGameExport(request: GetGameExportReq): Promise<GameExportRsp>;
|
GetGameExport(request: GetGameExportReq): Promise<GameExportRsp>;
|
||||||
IssueGameTickets(request: IssueGameTicketsReq): Promise<IssueGameTicketsRsp>;
|
IssueGameTickets(request: IssueGameTicketsReq): Promise<IssueGameTicketsRsp>;
|
||||||
@@ -2443,6 +2511,46 @@ export function createEveningDetectiveServerClient(
|
|||||||
method: "ImportGame",
|
method: "ImportGame",
|
||||||
}) as Promise<GameImportRsp>;
|
}) as Promise<GameImportRsp>;
|
||||||
},
|
},
|
||||||
|
DownloadGameResultsPackage(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}/results-package/download`; // 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: "DownloadGameResultsPackage",
|
||||||
|
}) as Promise<googleapi_HttpBody>;
|
||||||
|
},
|
||||||
|
ApplyGameResultsPackage(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}/results-package/apply`; // eslint-disable-line quotes
|
||||||
|
const body = JSON.stringify(request?.package ?? {});
|
||||||
|
const queryParams: string[] = [];
|
||||||
|
let uri = path;
|
||||||
|
if (queryParams.length > 0) {
|
||||||
|
uri += `?${queryParams.join("&")}`
|
||||||
|
}
|
||||||
|
return handler({
|
||||||
|
path: uri,
|
||||||
|
method: "POST",
|
||||||
|
body,
|
||||||
|
}, {
|
||||||
|
service: "EveningDetectiveServer",
|
||||||
|
method: "ApplyGameResultsPackage",
|
||||||
|
}) as Promise<GameResultsPackageRsp>;
|
||||||
|
},
|
||||||
StartGameExport(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
StartGameExport(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");
|
||||||
@@ -3422,55 +3530,5 @@ export function createEveningDetectiveServerClient(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// Message that represents an arbitrary HTTP body. It should only be used for
|
|
||||||
// payload formats that can't be represented as JSON, such as raw binary or
|
|
||||||
// an HTML page.
|
|
||||||
// This message can be used both in streaming and non-streaming API methods in
|
|
||||||
// the request as well as the response.
|
|
||||||
// It can be used as a top-level request field, which is convenient if one
|
|
||||||
// wants to extract parameters from either the URL or HTTP template into the
|
|
||||||
// request fields and also want access to the raw HTTP body.
|
|
||||||
// Example:
|
|
||||||
// message GetResourceRequest {
|
|
||||||
// // A unique request id.
|
|
||||||
// string request_id = 1;
|
|
||||||
// // The raw HTTP body is bound to this field.
|
|
||||||
// google.api.HttpBody http_body = 2;
|
|
||||||
// }
|
|
||||||
// service ResourceService {
|
|
||||||
// rpc GetResource(GetResourceRequest)
|
|
||||||
// returns (google.api.HttpBody);
|
|
||||||
// rpc UpdateResource(google.api.HttpBody)
|
|
||||||
// returns (google.protobuf.Empty);
|
|
||||||
// }
|
|
||||||
// Example with streaming methods:
|
|
||||||
// service CaldavService {
|
|
||||||
// rpc GetCalendar(stream google.api.HttpBody)
|
|
||||||
// returns (stream google.api.HttpBody);
|
|
||||||
// rpc UpdateCalendar(stream google.api.HttpBody)
|
|
||||||
// returns (stream google.api.HttpBody);
|
|
||||||
// }
|
|
||||||
// Use of this type only changes how the request and response bodies are
|
|
||||||
// handled, all other features will continue to work unchanged.
|
|
||||||
export type googleapi_HttpBody = {
|
|
||||||
// The HTTP Content-Type header value specifying the content type of the body.
|
|
||||||
contentType: string | undefined;
|
|
||||||
// The HTTP request/response body as raw binary.
|
|
||||||
data: string | undefined;
|
|
||||||
// Application specific response metadata. Must be set in the first response
|
|
||||||
// for streaming APIs.
|
|
||||||
extensions: wellKnownAny[] | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
// If the Any contains a value that has a special JSON mapping,
|
|
||||||
// it will be converted as follows:
|
|
||||||
// {"@type": xxx, "value": yyy}.
|
|
||||||
// Otherwise, the value will be converted into a JSON object,
|
|
||||||
// and the "@type" field will be inserted to indicate the actual data type.
|
|
||||||
interface wellKnownAny {
|
|
||||||
"@type": string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// @@protoc_insertion_point(typescript-http-eof)
|
// @@protoc_insertion_point(typescript-http-eof)
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
NTabPane,
|
NTabPane,
|
||||||
NTabs,
|
NTabs,
|
||||||
NText,
|
NText,
|
||||||
|
NUpload,
|
||||||
|
type UploadFileInfo,
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import { useMessage } from 'naive-ui'
|
import { useMessage } from 'naive-ui'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
@@ -54,6 +56,8 @@ import { useUIStore } from '@/stores/ui'
|
|||||||
import { applicationFileType, applicationFileURL } from '@/utils/application'
|
import { applicationFileType, applicationFileURL } from '@/utils/application'
|
||||||
import { formatGameDateTime, parseDateWithoutTimezone } from '@/utils/datetime'
|
import { formatGameDateTime, parseDateWithoutTimezone } from '@/utils/datetime'
|
||||||
|
|
||||||
|
const emit = defineEmits<{ resultsApplied: [] }>()
|
||||||
|
|
||||||
const client = getAuthClient()
|
const client = getAuthClient()
|
||||||
const binaryClient = getBinaryClient()
|
const binaryClient = getBinaryClient()
|
||||||
const isDownloadingQR = ref(false)
|
const isDownloadingQR = ref(false)
|
||||||
@@ -118,6 +122,16 @@ const canExport = computed(
|
|||||||
)
|
)
|
||||||
const isStartingExport = ref(false)
|
const isStartingExport = ref(false)
|
||||||
const isDownloadingPackage = ref(false)
|
const isDownloadingPackage = ref(false)
|
||||||
|
const canDownloadResults = computed(
|
||||||
|
() =>
|
||||||
|
uiStore.environment === 'venue' &&
|
||||||
|
game.value.status === 'finish' &&
|
||||||
|
authStore.hasAnyRole(['admin', 'organizer', 'developer']),
|
||||||
|
)
|
||||||
|
const isDownloadingResults = ref(false)
|
||||||
|
const isApplyingResults = ref(false)
|
||||||
|
const resultsFiles = ref<UploadFileInfo[]>([])
|
||||||
|
const resultsError = ref('')
|
||||||
|
|
||||||
const participationOptions = [
|
const participationOptions = [
|
||||||
{ label: 'Ожидает ответа', value: 'pending' },
|
{ label: 'Ожидает ответа', value: 'pending' },
|
||||||
@@ -516,6 +530,60 @@ async function downloadPackage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changeResultsFile({ fileList }: { fileList: UploadFileInfo[] }) {
|
||||||
|
if (isApplyingResults.value) return
|
||||||
|
resultsFiles.value = fileList.slice(-1)
|
||||||
|
resultsError.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadResults() {
|
||||||
|
if (!canDownloadResults.value || isDownloadingResults.value) return
|
||||||
|
isDownloadingResults.value = true
|
||||||
|
try {
|
||||||
|
const { blob, filename } = await binaryClient.downloadGameResultsPackage(game.value.ID!)
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = filename
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
link.remove()
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
||||||
|
} catch (err) {
|
||||||
|
message.error(err instanceof Error ? err.message : 'Не удалось скачать результаты')
|
||||||
|
} finally {
|
||||||
|
isDownloadingResults.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyResults() {
|
||||||
|
if (!canExport.value || isApplyingResults.value) return
|
||||||
|
const file = resultsFiles.value[0]?.file
|
||||||
|
if (!file || file.size === 0) {
|
||||||
|
resultsError.value = 'Выберите непустой ZIP-файл'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!file.name.toLowerCase().endsWith('.zip')) {
|
||||||
|
resultsError.value = 'Выберите файл с расширением .zip'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (file.size > 1024 ** 3) {
|
||||||
|
resultsError.value = 'Размер пакета не должен превышать 1 ГиБ'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isApplyingResults.value = true
|
||||||
|
resultsError.value = ''
|
||||||
|
try {
|
||||||
|
await binaryClient.applyGameResultsPackage(game.value.ID!, file)
|
||||||
|
message.success('Результаты приняты')
|
||||||
|
emit('resultsApplied')
|
||||||
|
} catch (err) {
|
||||||
|
resultsError.value = err instanceof Error ? err.message : 'Не удалось применить результаты'
|
||||||
|
} finally {
|
||||||
|
isApplyingResults.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function downloadTeamQR() {
|
async function downloadTeamQR() {
|
||||||
if (isDownloadingQR.value || !game.value.teams?.length) return
|
if (isDownloadingQR.value || !game.value.teams?.length) return
|
||||||
isDownloadingQR.value = true
|
isDownloadingQR.value = true
|
||||||
@@ -679,6 +747,7 @@ async function issueTickets() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useSimplePolling(() => {
|
useSimplePolling(() => {
|
||||||
|
if (isApplyingResults.value) return
|
||||||
getGame(Number(gameId))
|
getGame(Number(gameId))
|
||||||
getGameScreens()
|
getGameScreens()
|
||||||
getGameMusic()
|
getGameMusic()
|
||||||
@@ -905,6 +974,42 @@ useSimplePolling(() => {
|
|||||||
<n-button @click="updateGame()">Сохранить изменения</n-button>
|
<n-button @click="updateGame()">Сохранить изменения</n-button>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</UiBlock>
|
</UiBlock>
|
||||||
|
<UiBlock v-if="canDownloadResults">
|
||||||
|
<n-button
|
||||||
|
:loading="isDownloadingResults"
|
||||||
|
:disabled="isDownloadingResults"
|
||||||
|
@click="downloadResults"
|
||||||
|
>
|
||||||
|
Скачать результаты
|
||||||
|
</n-button>
|
||||||
|
</UiBlock>
|
||||||
|
<UiBlock v-if="canExport">
|
||||||
|
<p class="screens-title">Возврат результатов с площадки</p>
|
||||||
|
<n-space vertical>
|
||||||
|
<n-alert type="warning">
|
||||||
|
Команды, билеты, результаты и игровой сценарий будут заменены данными пакета.
|
||||||
|
Онлайн-правки этих данных потеряются. Оформление и музыка сохраняются.
|
||||||
|
</n-alert>
|
||||||
|
<n-upload
|
||||||
|
:file-list="resultsFiles"
|
||||||
|
:default-upload="false"
|
||||||
|
:max="1"
|
||||||
|
accept=".zip"
|
||||||
|
:disabled="isApplyingResults"
|
||||||
|
@change="changeResultsFile"
|
||||||
|
>
|
||||||
|
<n-button :disabled="isApplyingResults">Выбрать ZIP результатов</n-button>
|
||||||
|
</n-upload>
|
||||||
|
<n-alert v-if="resultsError" type="error" role="alert">{{ resultsError }}</n-alert>
|
||||||
|
<n-button
|
||||||
|
:loading="isApplyingResults"
|
||||||
|
:disabled="isApplyingResults || !resultsFiles.length"
|
||||||
|
@click="applyResults"
|
||||||
|
>
|
||||||
|
Применить ZIP и перезаписать результаты
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</UiBlock>
|
||||||
<UiBlock v-if="canExport">
|
<UiBlock v-if="canExport">
|
||||||
<p class="screens-title">Передача игры на площадку</p>
|
<p class="screens-title">Передача игры на площадку</p>
|
||||||
<n-space vertical>
|
<n-space vertical>
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
import GamePage from '@/components/GamePage.vue'
|
import GamePage from '@/components/GamePage.vue'
|
||||||
|
|
||||||
|
const pageVersion = ref(0)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<GamePage />
|
<GamePage :key="pageVersion" @results-applied="pageVersion++" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user