generated from VLADIMIR/template_frontend
up
This commit is contained in:
@@ -49,6 +49,8 @@ import HeaderMenu from '@/components/HeaderMenu.vue'
|
||||
import TicketCheckModal from '@/components/TicketCheckModal.vue'
|
||||
import UiBlock from '@/components/UiBlock.vue'
|
||||
import { useSimplePolling } from '@/composables/useSimplePolling'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useUIStore } from '@/stores/ui'
|
||||
import { applicationFileType, applicationFileURL } from '@/utils/application'
|
||||
import { formatGameDateTime, parseDateWithoutTimezone } from '@/utils/datetime'
|
||||
|
||||
@@ -109,6 +111,14 @@ const game = ref<Game>({
|
||||
teams: undefined,
|
||||
export: undefined,
|
||||
})
|
||||
const authStore = useAuthStore()
|
||||
const uiStore = useUIStore()
|
||||
const canExport = computed(
|
||||
() => uiStore.environment === 'prod' && authStore.hasAnyRole(['admin', 'organizer', 'developer']),
|
||||
)
|
||||
const isStartingExport = ref(false)
|
||||
const isDownloadingPackage = ref(false)
|
||||
|
||||
const participationOptions = [
|
||||
{ label: 'Ожидает ответа', value: 'pending' },
|
||||
{ label: 'Подтверждено', value: 'confirmed' },
|
||||
@@ -466,6 +476,46 @@ async function toTeamStory(id: number, password: string, newTab: boolean = false
|
||||
router.push(uri)
|
||||
}
|
||||
|
||||
async function preparePackage() {
|
||||
if (
|
||||
!canExport.value ||
|
||||
isStartingExport.value ||
|
||||
game.value.export?.state === 'preparing' ||
|
||||
game.value.export?.state === 'ready' ||
|
||||
game.value.status !== 'draft'
|
||||
)
|
||||
return
|
||||
isStartingExport.value = true
|
||||
try {
|
||||
const res = await client.StartGameExport({ ID: game.value.ID })
|
||||
game.value.export = res.export
|
||||
} catch (err) {
|
||||
message.error(err instanceof Error ? err.message : 'Не удалось подготовить пакет')
|
||||
} finally {
|
||||
isStartingExport.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadPackage() {
|
||||
if (!canExport.value || isDownloadingPackage.value || game.value.export?.state !== 'ready') return
|
||||
isDownloadingPackage.value = true
|
||||
try {
|
||||
const { blob, filename } = await binaryClient.downloadGamePackage(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 {
|
||||
isDownloadingPackage.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadTeamQR() {
|
||||
if (isDownloadingQR.value || !game.value.teams?.length) return
|
||||
isDownloadingQR.value = true
|
||||
@@ -855,18 +905,54 @@ useSimplePolling(() => {
|
||||
<n-button @click="updateGame()">Сохранить изменения</n-button>
|
||||
</n-flex>
|
||||
</UiBlock>
|
||||
<UiBlock v-if="canExport">
|
||||
<p class="screens-title">Передача игры на площадку</p>
|
||||
<n-space vertical>
|
||||
<p>
|
||||
Пакет содержит снимок на момент подготовки. Последующие онлайн-изменения в него не
|
||||
попадут. Готовность пакета не подтверждает импорт на площадку.
|
||||
</p>
|
||||
<n-alert v-if="game.export?.state === 'preparing'" type="info" role="status"
|
||||
>Подготовка пакета…</n-alert
|
||||
>
|
||||
<n-alert v-else-if="game.export?.state === 'ready'" type="success" role="status"
|
||||
>Пакет готов</n-alert
|
||||
>
|
||||
<n-alert v-else-if="game.export?.state === 'failed'" type="error" role="alert">{{
|
||||
game.export.failure || 'Не удалось подготовить пакет'
|
||||
}}</n-alert>
|
||||
<n-button
|
||||
v-if="game.export?.state === 'ready'"
|
||||
:loading="isDownloadingPackage"
|
||||
:disabled="isDownloadingPackage"
|
||||
@click="downloadPackage"
|
||||
>Скачать пакет</n-button
|
||||
>
|
||||
<n-button
|
||||
v-else
|
||||
:loading="isStartingExport || game.export?.state === 'preparing'"
|
||||
:disabled="
|
||||
isStartingExport ||
|
||||
game.export?.state === 'preparing' ||
|
||||
game.status !== 'draft'
|
||||
"
|
||||
@click="preparePackage"
|
||||
>{{
|
||||
game.export?.state === 'failed' ? 'Повторить подготовку' : 'Подготовить пакет'
|
||||
}}</n-button
|
||||
>
|
||||
<p v-if="game.status && game.status !== 'draft'">
|
||||
Подготовка доступна до старта игры.
|
||||
</p>
|
||||
</n-space>
|
||||
</UiBlock>
|
||||
<UiBlock>
|
||||
<p class="screens-title">Выпуск билетов</p>
|
||||
<n-space vertical>
|
||||
<n-input-number v-model:value="ticketCount" :min="1" :max="100" />
|
||||
<div v-for="ticket in issuedTickets" :key="ticket.ID" class="issued-ticket">
|
||||
<div>Билет №{{ ticket.ID }}</div>
|
||||
<a
|
||||
:href="ticket.URL"
|
||||
class="payment-link"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<a :href="ticket.URL" class="payment-link" target="_blank" rel="noreferrer">
|
||||
Открыть страницу билета
|
||||
</a>
|
||||
<n-button size="small" @click="openQR(ticket.URL!, `Билет №${ticket.ID}`)">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
NAlert,
|
||||
NButton,
|
||||
NCard,
|
||||
NDatePicker,
|
||||
@@ -9,14 +10,19 @@ import {
|
||||
NSelect,
|
||||
NSpace,
|
||||
NTag,
|
||||
NUpload,
|
||||
type UploadFileInfo,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { getAuthClient } from '@/api/auth_client'
|
||||
import type { Game, Scenario } from '@/api/generated/crabs/evening_detective_server'
|
||||
import { getBinaryClient } from '@/api/binary_client'
|
||||
import type { Game, GameImportRsp, Scenario } from '@/api/generated/crabs/evening_detective_server'
|
||||
import HeaderMenu from '@/components/HeaderMenu.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useUIStore } from '@/stores/ui'
|
||||
import { formatGameDateTime } from '@/utils/datetime'
|
||||
|
||||
const client = getAuthClient()
|
||||
@@ -25,6 +31,80 @@ const router = useRouter()
|
||||
|
||||
const games = ref<Game[]>([])
|
||||
|
||||
const uiStore = useUIStore()
|
||||
const authStore = useAuthStore()
|
||||
const binaryClient = getBinaryClient()
|
||||
const canImport = computed(
|
||||
() =>
|
||||
uiStore.environment === 'venue' && authStore.hasAnyRole(['admin', 'organizer', 'developer']),
|
||||
)
|
||||
const showImportModal = ref(false)
|
||||
const importFiles = ref<UploadFileInfo[]>([])
|
||||
const importResult = ref<GameImportRsp | null>(null)
|
||||
const importError = ref('')
|
||||
const isImporting = ref(false)
|
||||
const isOpeningImport = ref(false)
|
||||
|
||||
function openImport() {
|
||||
importFiles.value = []
|
||||
importResult.value = null
|
||||
importError.value = ''
|
||||
showImportModal.value = true
|
||||
}
|
||||
|
||||
function changeImportFile({ fileList }: { fileList: UploadFileInfo[] }) {
|
||||
if (isImporting.value) return
|
||||
importFiles.value = fileList.slice(-1)
|
||||
importError.value = ''
|
||||
importResult.value = null
|
||||
}
|
||||
|
||||
async function importGame() {
|
||||
if (!canImport.value || isImporting.value) return
|
||||
const file = importFiles.value[0]?.file
|
||||
if (!file || file.size === 0) {
|
||||
importError.value = 'Выберите непустой ZIP-файл'
|
||||
return
|
||||
}
|
||||
if (!file.name.toLowerCase().endsWith('.zip')) {
|
||||
importError.value = 'Выберите файл с расширением .zip'
|
||||
return
|
||||
}
|
||||
if (file.size > 1024 ** 3) {
|
||||
importError.value = 'Размер пакета не должен превышать 1 ГиБ'
|
||||
return
|
||||
}
|
||||
isImporting.value = true
|
||||
importError.value = ''
|
||||
try {
|
||||
importResult.value = await binaryClient.uploadGamePackage(file)
|
||||
await getGames()
|
||||
} catch (err) {
|
||||
importError.value = err instanceof Error ? err.message : 'Не удалось импортировать игру'
|
||||
} finally {
|
||||
isImporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openImportedGame() {
|
||||
const id = importResult.value?.gameID
|
||||
if (!id || isOpeningImport.value) return
|
||||
isOpeningImport.value = true
|
||||
importError.value = ''
|
||||
try {
|
||||
const res = await client.GetGame({ ID: id })
|
||||
if (!res.game?.ID)
|
||||
throw new Error(
|
||||
'Импортированная игра недоступна или удалена. Повторный импорт пакета не восстанавливает удалённую игру.',
|
||||
)
|
||||
await router.push('/games/' + id)
|
||||
} catch (err) {
|
||||
importError.value = err instanceof Error ? err.message : 'Не удалось проверить доступность игры'
|
||||
} finally {
|
||||
isOpeningImport.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function getGames() {
|
||||
try {
|
||||
games.value = []
|
||||
@@ -121,6 +201,7 @@ async function createGame() {
|
||||
<template>
|
||||
<div class="center-block-custom">
|
||||
<div class="width1200">
|
||||
<n-button v-if="canImport" @click="openImport">Импорт игры</n-button>
|
||||
<div class="game-block game-block-hover" @click="openCreateGame()">
|
||||
<div class="game-create-content">
|
||||
<span class="game-create-plus">+</span>
|
||||
@@ -152,6 +233,64 @@ async function createGame() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n-modal
|
||||
v-model:show="showImportModal"
|
||||
:mask-closable="!isImporting"
|
||||
:close-on-esc="!isImporting"
|
||||
>
|
||||
<n-card
|
||||
style="width: min(94vw, 600px)"
|
||||
title="Импорт игры"
|
||||
:bordered="false"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<template #header-extra>
|
||||
<n-button
|
||||
:disabled="isImporting"
|
||||
aria-label="Закрыть импорт"
|
||||
@click="showImportModal = false"
|
||||
>Закрыть</n-button
|
||||
>
|
||||
</template>
|
||||
<n-space vertical>
|
||||
<p>Выберите один ZIP-пакет размером до 1 ГиБ. Содержимое проверит сервер.</p>
|
||||
<n-upload
|
||||
:file-list="importFiles"
|
||||
:default-upload="false"
|
||||
:max="1"
|
||||
accept=".zip"
|
||||
:disabled="isImporting"
|
||||
@change="changeImportFile"
|
||||
>
|
||||
<n-button :disabled="isImporting">Выбрать ZIP</n-button>
|
||||
</n-upload>
|
||||
<p v-if="importFiles[0]?.file">
|
||||
{{ importFiles[0].name }} — {{ importFiles[0].file.size.toLocaleString('ru-RU') }} байт
|
||||
</p>
|
||||
<n-alert v-if="isImporting" type="info" role="status">Импортируем пакет…</n-alert>
|
||||
<n-alert v-if="importError" type="error" role="alert">{{ importError }}</n-alert>
|
||||
<n-alert v-if="importResult" type="success" role="status">{{
|
||||
importResult.repeated ? 'Пакет уже импортирован' : 'Игра импортирована'
|
||||
}}</n-alert>
|
||||
<n-button
|
||||
v-if="importResult"
|
||||
:loading="isOpeningImport"
|
||||
:disabled="isOpeningImport"
|
||||
@click="openImportedGame"
|
||||
>Открыть игру</n-button
|
||||
>
|
||||
<n-button
|
||||
v-else
|
||||
:loading="isImporting"
|
||||
:disabled="isImporting || !importFiles.length || !canImport"
|
||||
@click="importGame"
|
||||
>Загрузить пакет</n-button
|
||||
>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
|
||||
<!-- Окно создания игры -->
|
||||
<n-modal v-model:show="showCreateGameModal">
|
||||
<n-card
|
||||
@@ -176,7 +315,10 @@ async function createGame() {
|
||||
<p>Формат игры</p>
|
||||
<n-select
|
||||
v-model:value="newGameFormat"
|
||||
:options="[{ label: 'Online', value: 'online' }, { label: 'Offline', value: 'offline' }]"
|
||||
:options="[
|
||||
{ label: 'Online', value: 'online' },
|
||||
{ label: 'Offline', value: 'offline' },
|
||||
]"
|
||||
aria-label="Формат игры"
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user