fix(export): harden downloads and CSV encoding

This commit is contained in:
Supra4E8C
2026-02-22 12:42:32 +08:00
Unverified
parent a47c990cf4
commit c75aeecec5
5 changed files with 48 additions and 47 deletions
@@ -12,6 +12,7 @@ import {
collectUsageDetails,
extractTotalTokens
} from '@/utils/usage';
import { downloadBlob } from '@/utils/download';
import styles from '@/pages/UsagePage.module.scss';
const ALL_FILTER = '__all__';
@@ -53,17 +54,9 @@ const toNumber = (value: unknown): number => {
const encodeCsv = (value: string | number): string => {
const text = String(value ?? '');
return `"${text.replace(/"/g, '""')}"`;
};
const downloadFile = (filename: string, content: string, mimeType: string) => {
const blob = new Blob([content], { type: mimeType });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
window.URL.revokeObjectURL(url);
const trimmedLeft = text.replace(/^\s+/, '');
const safeText = trimmedLeft && /^[=+\-@]/.test(trimmedLeft) ? `'${text}` : text;
return `"${safeText.replace(/"/g, '""')}"`;
};
type CredentialInfo = {
@@ -370,7 +363,10 @@ export function RequestEventsDetailsCard({
const content = [csvHeader.join(','), ...csvRows].join('\n');
const fileTime = new Date().toISOString().replace(/[:.]/g, '-');
downloadFile(`usage-events-${fileTime}.csv`, content, 'text/csv;charset=utf-8');
downloadBlob({
filename: `usage-events-${fileTime}.csv`,
blob: new Blob([content], { type: 'text/csv;charset=utf-8' })
});
};
const handleExportJson = () => {
@@ -394,7 +390,10 @@ export function RequestEventsDetailsCard({
const content = JSON.stringify(payload, null, 2);
const fileTime = new Date().toISOString().replace(/[:.]/g, '-');
downloadFile(`usage-events-${fileTime}.json`, content, 'application/json;charset=utf-8');
downloadBlob({
filename: `usage-events-${fileTime}.json`,
blob: new Blob([content], { type: 'application/json;charset=utf-8' })
});
};
return (
+5 -7
View File
@@ -2,6 +2,7 @@ import { useEffect, useState, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { USAGE_STATS_STALE_TIME_MS, useNotificationStore, useUsageStatsStore } from '@/stores';
import { usageApi } from '@/services/api/usage';
import { downloadBlob } from '@/utils/download';
import { loadModelPrices, saveModelPrices, type ModelPrice } from '@/utils/usage';
export interface UsagePayload {
@@ -62,13 +63,10 @@ export function useUsageData(): UseUsageDataReturn {
? new Date().toISOString()
: exportedAt.toISOString();
const filename = `usage-export-${safeTimestamp.replace(/[:.]/g, '-')}.json`;
const blob = new Blob([JSON.stringify(data ?? {}, null, 2)], { type: 'application/json' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
window.URL.revokeObjectURL(url);
downloadBlob({
filename,
blob: new Blob([JSON.stringify(data ?? {}, null, 2)], { type: 'application/json' })
});
showNotification(t('usage_stats.export_success'), 'success');
} catch (err: unknown) {
const message = err instanceof Error ? err.message : '';
@@ -6,6 +6,7 @@ import { useNotificationStore } from '@/stores';
import type { AuthFileItem } from '@/types';
import { formatFileSize } from '@/utils/format';
import { MAX_AUTH_FILE_SIZE } from '@/utils/constants';
import { downloadBlob } from '@/utils/download';
import { getTypeLabel, isRuntimeOnlyAuthFile } from '@/features/authFiles/constants';
type DeleteAllOptions = {
@@ -316,12 +317,7 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles
{ responseType: 'blob' }
);
const blob = new Blob([response.data]);
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = name;
a.click();
window.URL.revokeObjectURL(url);
downloadBlob({ filename: name, blob });
showNotification(t('auth_files.download_success'), 'success');
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : '';
+7 -21
View File
@@ -24,6 +24,7 @@ import { logsApi } from '@/services/api/logs';
import { usageApi } from '@/services/api/usage';
import type { AuthFileItem } from '@/types';
import { copyToClipboard } from '@/utils/clipboard';
import { downloadBlob } from '@/utils/download';
import { MANAGEMENT_API_PREFIX } from '@/utils/constants';
import { formatUnixTimestamp } from '@/utils/format';
import {
@@ -710,13 +711,7 @@ export function LogsPage() {
const downloadLogs = () => {
const text = logState.buffer.join('\n');
const blob = new Blob([text], { type: 'text/plain' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'logs.txt';
a.click();
window.URL.revokeObjectURL(url);
downloadBlob({ filename: 'logs.txt', blob: new Blob([text], { type: 'text/plain' }) });
showNotification(t('logs.download_success'), 'success');
};
@@ -747,13 +742,7 @@ export function LogsPage() {
const downloadErrorLog = async (name: string) => {
try {
const response = await logsApi.downloadErrorLog(name);
const blob = new Blob([response.data], { type: 'text/plain' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = name;
a.click();
window.URL.revokeObjectURL(url);
downloadBlob({ filename: name, blob: new Blob([response.data], { type: 'text/plain' }) });
showNotification(t('logs.error_log_download_success'), 'success');
} catch (err: unknown) {
const message = getErrorMessage(err);
@@ -1187,13 +1176,10 @@ export function LogsPage() {
setRequestLogDownloading(true);
try {
const response = await logsApi.downloadRequestLogById(id);
const blob = new Blob([response.data], { type: 'text/plain' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `request-${id}.log`;
a.click();
window.URL.revokeObjectURL(url);
downloadBlob({
filename: `request-${id}.log`,
blob: new Blob([response.data], { type: 'text/plain' })
});
showNotification(t('logs.request_log_download_success'), 'success');
setRequestLogId(null);
} catch (err: unknown) {
+22
View File
@@ -0,0 +1,22 @@
export type DownloadBlobOptions = {
filename: string;
blob: Blob;
revokeDelayMs?: number;
};
export function downloadBlob({ filename, blob, revokeDelayMs = 1000 }: DownloadBlobOptions) {
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.rel = 'noopener';
link.style.display = 'none';
document.body.appendChild(link);
link.click();
window.setTimeout(() => {
window.URL.revokeObjectURL(url);
link.remove();
}, revokeDelayMs);
}