Выгрузка маршрутов: в имени файла сокращённые типы заявок (до 7) и дата
Имя формирует бэкенд и отдаёт в Content-Disposition (он добавлен в экспонируемые заголовки CORS), фронт сохраняет файл под этим именем. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,7 +44,7 @@ export const RoutesExportScreen: React.FC<IRoutesExportScreenProps> = ({ api })
|
||||
setError(undefined);
|
||||
setIsBusy(true);
|
||||
api.reports.routes(selected)
|
||||
.then(blob => download(blob, texts.routesExport.fileName))
|
||||
.then(file => download(file.blob, file.fileName ?? texts.routesExport.fileName))
|
||||
.catch((e: Error) => setError(texts.routesExport.failed(reportError(e, 'RoutesExportScreen: export'))))
|
||||
.then(() => setIsBusy(false), () => setIsBusy(false));
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IHttpClient } from './httpClient';
|
||||
import { IDownloadedFile, IHttpClient } from './httpClient';
|
||||
import {
|
||||
CreateRequestTypeAccessInput,
|
||||
CreateRequestTypeInput,
|
||||
@@ -175,7 +175,7 @@ export class ApprovalRouteApi {
|
||||
|
||||
// Выгрузки файлами (ReportsController) — в ответ приходит xlsx, не JSON.
|
||||
public readonly reports = {
|
||||
routes: (requestTypeIds: number[]): Promise<Blob> =>
|
||||
routes: (requestTypeIds: number[]): Promise<IDownloadedFile> =>
|
||||
this.http.postForBlob('/api/reports/routes', { requestTypeIds })
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,6 +18,28 @@ export class ApiClientError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/** Скачанный файл; fileName пуст, если сервер не прислал Content-Disposition. */
|
||||
export interface IDownloadedFile {
|
||||
blob: Blob;
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
// filename*=UTF-8''... (RFC 5987, так отдаёт ASP.NET для кириллицы) или обычный filename=...
|
||||
// eslint-disable-next-line @rushstack/no-new-null -- header — результат Headers.get() (DOM API)
|
||||
function fileNameFromDisposition(header: string | null | undefined): string | undefined {
|
||||
if (!header) return undefined;
|
||||
const encoded = /filename\*=UTF-8''([^;]+)/i.exec(header);
|
||||
if (encoded) {
|
||||
try {
|
||||
return decodeURIComponent(encoded[1]);
|
||||
} catch {
|
||||
// Битая кодировка — падаем на обычный filename ниже.
|
||||
}
|
||||
}
|
||||
const plain = /filename="?([^";]+)"?/i.exec(header);
|
||||
return plain ? plain[1] : undefined;
|
||||
}
|
||||
|
||||
export type QueryParams = Record<string, string | number | boolean | undefined>;
|
||||
|
||||
export interface IHttpClient {
|
||||
@@ -27,7 +49,7 @@ export interface IHttpClient {
|
||||
patch<T>(path: string, body?: unknown): Promise<T>;
|
||||
delete(path: string): Promise<void>;
|
||||
/** POST, на который сервер отвечает файлом (xlsx), а не JSON-конвертом. */
|
||||
postForBlob(path: string, body: unknown): Promise<Blob>;
|
||||
postForBlob(path: string, body: unknown): Promise<IDownloadedFile>;
|
||||
}
|
||||
|
||||
function buildUrl(baseUrl: string, path: string, query?: QueryParams): string {
|
||||
@@ -107,7 +129,7 @@ export class AadApiHttpClient implements IHttpClient {
|
||||
await this.send<void>('DELETE', path);
|
||||
}
|
||||
|
||||
public async postForBlob(path: string, body: unknown): Promise<Blob> {
|
||||
public async postForBlob(path: string, body: unknown): Promise<IDownloadedFile> {
|
||||
const url = buildUrl(this.baseUrl, path);
|
||||
const response = await this.client.fetch(url, AadHttpClient.configurations.v1, {
|
||||
method: 'POST',
|
||||
@@ -119,7 +141,7 @@ export class AadApiHttpClient implements IHttpClient {
|
||||
await parseResponse<void>(response);
|
||||
throw new ApiClientError(response.status, [`HTTP ${response.status}`]);
|
||||
}
|
||||
return response.blob();
|
||||
return { blob: await response.blob(), fileName: fileNameFromDisposition(response.headers.get('Content-Disposition')) };
|
||||
}
|
||||
|
||||
private send<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||
|
||||
Reference in New Issue
Block a user