feat: harden platform workflows and UI governance
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Download, RotateCcw } from 'lucide-react';
|
||||
import type { SystemLogExportResult } from '@/api/adminApi';
|
||||
import { Button } from './Button';
|
||||
|
||||
type Filters = { keyword?: string; level?: string; module?: string; range?: string };
|
||||
|
||||
export function SystemLogExport({
|
||||
portal,
|
||||
filters,
|
||||
exportLogs,
|
||||
}: {
|
||||
portal: 'admin' | 'client';
|
||||
filters: Filters;
|
||||
exportLogs: (filters: Filters) => Promise<SystemLogExportResult>;
|
||||
}) {
|
||||
const storageKey = `cmpp:${portal}:system-log-export-recovery`;
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [result, setResult] = useState<SystemLogExportResult | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
const [retryFilters, setRetryFilters] = useState<Filters | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const saved = sessionStorage.getItem(storageKey);
|
||||
if (saved) {
|
||||
setRetryFilters(JSON.parse(saved) as Filters);
|
||||
setError('上次导出未完成,筛选条件已保留,可直接重试。');
|
||||
}
|
||||
} catch {
|
||||
sessionStorage.removeItem(storageKey);
|
||||
}
|
||||
}, [storageKey]);
|
||||
|
||||
async function run(nextFilters: Filters) {
|
||||
setExporting(true);
|
||||
setError('');
|
||||
setResult(null);
|
||||
setRetryFilters(nextFilters);
|
||||
sessionStorage.setItem(storageKey, JSON.stringify(nextFilters));
|
||||
try {
|
||||
const exported = await exportLogs(nextFilters);
|
||||
setResult(exported);
|
||||
setRetryFilters(null);
|
||||
sessionStorage.removeItem(storageKey);
|
||||
} catch (reason) {
|
||||
setError(reason instanceof Error ? reason.message : '日志导出失败,请重试');
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
}
|
||||
|
||||
function download() {
|
||||
if (!result) return;
|
||||
const url = URL.createObjectURL(new Blob([`\uFEFF${result.content}`], { type: 'text/csv;charset=utf-8' }));
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = result.fileName;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="system-log-export">
|
||||
<Button disabled={exporting} icon={<Download size={17} />} onClick={() => run(filters)} variant="secondary">
|
||||
{exporting ? '导出中…' : '导出日志'}
|
||||
</Button>
|
||||
{result ? (
|
||||
<div className="system-log-export__result" role="status">
|
||||
<span>导出完成,共 {result.recordCount} 条{result.truncated ? '(已截取前 10000 条)' : ''},操作单号 {result.operationId}</span>
|
||||
<Button icon={<Download size={15} />} onClick={download} size="sm" variant="ghost">下载文件</Button>
|
||||
</div>
|
||||
) : null}
|
||||
{error ? (
|
||||
<div className="system-log-export__error" role="alert">
|
||||
<span>{error}</span>
|
||||
<Button disabled={exporting} icon={<RotateCcw size={15} />} onClick={() => run(retryFilters ?? filters)} size="sm" variant="ghost">重试</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user