mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
fix(export): harden downloads and CSV encoding
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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 : '';
|
||||
|
||||
Reference in New Issue
Block a user