generated from VLADIMIR/template_frontend
add buttons
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
import { getURL } from './generated/crabs/evening_detective_server/client'
|
||||
|
||||
export type DownloadArchiveResult = {
|
||||
blob: Blob
|
||||
filename: string
|
||||
}
|
||||
|
||||
export type UploadArchiveResult = {
|
||||
id: number | null
|
||||
error: string | null
|
||||
}
|
||||
|
||||
// Клиент для бинарных операций со сценарием (ZIP-архив). Сгенерированный
|
||||
// HTTP-хендлер всегда парсит ответ как JSON, поэтому для архивов используем
|
||||
// сырой fetch: скачивание отдаёт байты архива, загрузка принимает их напрямую.
|
||||
export function getArchiveClient() {
|
||||
const authStore = useAuthStore()
|
||||
return buildArchiveClient(authStore.refreshTokenAction, authStore.getToken)
|
||||
}
|
||||
|
||||
function buildArchiveClient(
|
||||
refreshTokens: () => Promise<boolean>,
|
||||
getToken: () => string | null,
|
||||
) {
|
||||
async function request(path: string, init: RequestInit): Promise<Response> {
|
||||
const headers: Record<string, string> = {}
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
let response = await fetch(`${getURL()}/${path}`, {
|
||||
...init,
|
||||
headers: { ...headers, ...(init.headers as Record<string, string>) },
|
||||
})
|
||||
|
||||
// Токен протух — обновляем и повторяем запрос один раз.
|
||||
if (response.status == 401) {
|
||||
await refreshTokens()
|
||||
const newToken = getToken()
|
||||
if (newToken) {
|
||||
headers['Authorization'] = `Bearer ${newToken}`
|
||||
}
|
||||
response = await fetch(`${getURL()}/${path}`, {
|
||||
...init,
|
||||
headers: { ...headers, ...(init.headers as Record<string, string>) },
|
||||
})
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
async function downloadScenarioArchive(id: number): Promise<DownloadArchiveResult> {
|
||||
const response = await request(`api/scenarios/${id}/archive`, { 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`
|
||||
return { blob, filename }
|
||||
}
|
||||
|
||||
async function uploadScenarioArchive(file: File): Promise<UploadArchiveResult> {
|
||||
const contentType = file.type || 'application/zip'
|
||||
const response = await request('api/scenarios/archive', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': contentType },
|
||||
body: file,
|
||||
})
|
||||
|
||||
const data = await response.json().catch(() => null)
|
||||
if (!response.ok) {
|
||||
return { id: null, error: data?.message || `Ошибка сервера (${response.status})` }
|
||||
}
|
||||
|
||||
const error = typeof data?.error === 'string' && data.error !== '' ? data.error : null
|
||||
return { id: typeof data?.id === 'number' ? data.id : null, error }
|
||||
}
|
||||
|
||||
return { downloadScenarioArchive, uploadScenarioArchive }
|
||||
}
|
||||
|
||||
// Извлекает имя файла из Content-Disposition: attachment; filename="name.zip"
|
||||
function parseContentDispositionFilename(value: string | null): string {
|
||||
if (!value) {
|
||||
return ''
|
||||
}
|
||||
const match = value.match(/filename="?([^";]+)"?/i)
|
||||
const filename = match ? match[1] : ''
|
||||
return filename.replace(/[\\/:*?"<>|]/g, '_')
|
||||
}
|
||||
|
||||
// Ошибки grpc-gateway приходят JSON-объектом {"code": ..., "message": ...}
|
||||
async function gatewayErrorMessage(response: Response): Promise<string> {
|
||||
const data = await response.json().catch(() => null)
|
||||
if (data && typeof data.message === 'string' && data.message !== '') {
|
||||
return data.message
|
||||
}
|
||||
return `Ошибка сервера (${response.status})`
|
||||
}
|
||||
Reference in New Issue
Block a user