generated from VLADIMIR/template_frontend
add qr-s
This commit is contained in:
@@ -2,7 +2,7 @@ import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
import { getURL } from './generated/crabs/evening_detective_server/client'
|
||||
|
||||
export type DownloadArchiveResult = {
|
||||
export type DownloadFileResult = {
|
||||
blob: Blob
|
||||
filename: string
|
||||
}
|
||||
@@ -12,15 +12,14 @@ export type UploadArchiveResult = {
|
||||
error: string | null
|
||||
}
|
||||
|
||||
// Клиент для бинарных операций со сценарием (ZIP-архив). Сгенерированный
|
||||
// HTTP-хендлер всегда парсит ответ как JSON, поэтому для архивов используем
|
||||
// сырой fetch: скачивание отдаёт байты архива, загрузка принимает их напрямую.
|
||||
export function getArchiveClient() {
|
||||
// Клиент бинарных файлов: ZIP сценариев и PDF команд. Сгенерированный
|
||||
// HTTP-хендлер парсит ответы как JSON, поэтому здесь используется fetch.
|
||||
export function getBinaryClient() {
|
||||
const authStore = useAuthStore()
|
||||
return buildArchiveClient(authStore.refreshTokenAction, authStore.getToken)
|
||||
return buildBinaryClient(authStore.refreshTokenAction, authStore.getToken)
|
||||
}
|
||||
|
||||
function buildArchiveClient(refreshTokens: () => Promise<boolean>, getToken: () => string | null) {
|
||||
function buildBinaryClient(refreshTokens: () => Promise<boolean>, getToken: () => string | null) {
|
||||
async function request(path: string, init: RequestInit): Promise<Response> {
|
||||
const headers: Record<string, string> = {}
|
||||
const token = getToken()
|
||||
@@ -49,18 +48,26 @@ function buildArchiveClient(refreshTokens: () => Promise<boolean>, getToken: ()
|
||||
return response
|
||||
}
|
||||
|
||||
async function downloadScenarioArchive(id: number): Promise<DownloadArchiveResult> {
|
||||
const response = await request(`api/scenarios/${id}/archive`, { method: 'GET' })
|
||||
async function download(path: string, fallbackName: string): Promise<DownloadFileResult> {
|
||||
const response = await request(path, { method: 'GET' })
|
||||
if (!response.ok) {
|
||||
throw new Error(await gatewayErrorMessage(response))
|
||||
}
|
||||
const blob = await response.blob()
|
||||
const filename =
|
||||
parseContentDispositionFilename(response.headers.get('Content-Disposition')) ||
|
||||
`scenario-${id}.zip`
|
||||
fallbackName
|
||||
return { blob, filename }
|
||||
}
|
||||
|
||||
function downloadScenarioArchive(id: number): Promise<DownloadFileResult> {
|
||||
return download(`api/scenarios/${id}/archive`, `scenario-${id}.zip`)
|
||||
}
|
||||
|
||||
function downloadTeamQR(id: number): Promise<DownloadFileResult> {
|
||||
return download(`api/games/${id}/teams/qr.pdf`, `game-${id}.pdf`)
|
||||
}
|
||||
|
||||
async function uploadScenarioArchive(file: File): Promise<UploadArchiveResult> {
|
||||
const contentType = file.type || 'application/zip'
|
||||
const response = await request('api/scenarios/archive', {
|
||||
@@ -78,7 +85,7 @@ function buildArchiveClient(refreshTokens: () => Promise<boolean>, getToken: ()
|
||||
return { id: typeof data?.id === 'number' ? data.id : null, error }
|
||||
}
|
||||
|
||||
return { downloadScenarioArchive, uploadScenarioArchive }
|
||||
return { downloadScenarioArchive, downloadTeamQR, uploadScenarioArchive }
|
||||
}
|
||||
|
||||
// Извлекает имя файла из Content-Disposition: attachment; filename="name.zip"
|
||||
@@ -86,6 +93,14 @@ function parseContentDispositionFilename(value: string | null): string {
|
||||
if (!value) {
|
||||
return ''
|
||||
}
|
||||
const utf8 = value.match(/filename\*=UTF-8''([^;]+)/i)
|
||||
if (utf8) {
|
||||
try {
|
||||
return decodeURIComponent(utf8[1]!.trim()).replace(/[\\/:*?"<>|]/g, '_')
|
||||
} catch {
|
||||
// Некорректный filename*: пробуем обычный filename ниже.
|
||||
}
|
||||
}
|
||||
const match = value.match(/filename="?([^";]+)"?/i)
|
||||
const filename = match ? match[1] : ''
|
||||
return filename.replace(/[\\/:*?"<>|]/g, '_')
|
||||
@@ -729,6 +729,7 @@ export interface EveningDetectiveServer {
|
||||
AddGame(request: AddGameReq): Promise<AddGameRsp>;
|
||||
GetGames(request: GetGamesReq): Promise<GetGamesRsp>;
|
||||
GetGame(request: GetGameReq): Promise<GetGameRsp>;
|
||||
DownloadTeamQR(request: GetGameReq): Promise<googleapi_HttpBody>;
|
||||
GetGameAnswers(request: GetGameAnswersReq): Promise<GetGameAnswersRsp>;
|
||||
GetGameResults(request: GetGameResultsReq): Promise<GetGameResultsRsp>;
|
||||
UpdateGame(request: UpdateGameReq): Promise<UpdateGameRsp>;
|
||||
@@ -1537,6 +1538,26 @@ export function createEveningDetectiveServerClient(
|
||||
method: "GetGame",
|
||||
}) as Promise<GetGameRsp>;
|
||||
},
|
||||
DownloadTeamQR(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}/teams/qr.pdf`; // 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: "DownloadTeamQR",
|
||||
}) as Promise<googleapi_HttpBody>;
|
||||
},
|
||||
GetGameAnswers(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
if (!request.gameId) {
|
||||
throw new Error("missing required field request.gameId");
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { getAuthClient } from '@/api/auth_client'
|
||||
import { getBinaryClient } from '@/api/binary_client'
|
||||
import type {
|
||||
Application,
|
||||
Game,
|
||||
@@ -22,6 +23,8 @@ import { applicationFileType, applicationFileURL } from '@/utils/application'
|
||||
import { formatGameDateTime, parseDateWithoutTimezone } from '@/utils/datetime'
|
||||
|
||||
const client = getAuthClient()
|
||||
const binaryClient = getBinaryClient()
|
||||
const isDownloadingQR = ref(false)
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const gameId = route.params.id
|
||||
@@ -95,6 +98,26 @@ async function toTeamStory(id: number, password: string, newTab: boolean = false
|
||||
router.push(uri)
|
||||
}
|
||||
|
||||
async function downloadTeamQR() {
|
||||
if (isDownloadingQR.value || !game.value.teams?.length) return
|
||||
isDownloadingQR.value = true
|
||||
try {
|
||||
const { blob, filename } = await binaryClient.downloadTeamQR(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 : 'Не удалось скачать QR команд')
|
||||
} finally {
|
||||
isDownloadingQR.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function toResults() {
|
||||
router.push('/games/' + gameId + '/results')
|
||||
}
|
||||
@@ -410,6 +433,9 @@ useSimplePolling(() => {
|
||||
</Icon>
|
||||
Итоги
|
||||
</n-button>
|
||||
<n-button v-if="game.teams?.length" quaternary :loading="isDownloadingQR" :disabled="isDownloadingQR" @click="downloadTeamQR()">
|
||||
QR команд
|
||||
</n-button>
|
||||
<n-button quaternary @click="openSettings()">
|
||||
<Icon class="settings-icon">
|
||||
<Settings />
|
||||
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { getArchiveClient } from '@/api/archive_client'
|
||||
import { getAuthClient } from '@/api/auth_client'
|
||||
import { getBinaryClient } from '@/api/binary_client'
|
||||
import type { Conclusion, Scenario } from '@/api/generated/crabs/evening_detective_server'
|
||||
import HeaderMenu from '@/components/HeaderMenu.vue'
|
||||
import IntroBlock from '@/components/IntroBlock.vue'
|
||||
@@ -36,7 +36,7 @@ import { applicationFileURL } from '@/utils/application'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const client = getAuthClient()
|
||||
const archiveClient = getArchiveClient()
|
||||
const archiveClient = getBinaryClient()
|
||||
const route = useRoute()
|
||||
const scenarioId = route.params.id
|
||||
|
||||
|
||||
@@ -17,13 +17,13 @@ import {
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { getArchiveClient } from '@/api/archive_client'
|
||||
import { getAuthClient } from '@/api/auth_client'
|
||||
import { getBinaryClient } from '@/api/binary_client'
|
||||
import type { Scenario } from '@/api/generated/crabs/evening_detective_server'
|
||||
import HeaderMenu from '@/components/HeaderMenu.vue'
|
||||
|
||||
const client = getAuthClient()
|
||||
const archiveClient = getArchiveClient()
|
||||
const archiveClient = getBinaryClient()
|
||||
|
||||
const scenarios = ref<Scenario[]>([])
|
||||
|
||||
@@ -218,8 +218,8 @@ getScenarios()
|
||||
<style scoped>
|
||||
.center-block-custom {
|
||||
height: calc(100dvh - 70px);
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overflow: hidden auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user