refactor(admin): 用已安装的 file-saver/fast-deep-equal 替代手写下载与 JSON 比较

This commit is contained in:
2026-08-08 17:11:10 +08:00
parent 01cceea237
commit dab28f85e8
6 changed files with 16 additions and 43 deletions

View File

@@ -1,4 +1,5 @@
import React, { lazy, Suspense, useEffect, useMemo, useState } from 'react';
import { saveAs } from 'file-saver';
import { XCard, registerCatalog, type XAgentCommand_v0_9 } from '@ant-design/x-card';
import { Button, Spin, Tag, Tooltip, Typography } from 'antd';
import { DownloadOutlined } from '@ant-design/icons';
@@ -232,12 +233,7 @@ const ChartPreview: React.FC<ChartPreviewProps> = ({ chart }) => {
pixelRatio: 2,
backgroundColor: '#fff',
});
const link = document.createElement('a');
link.href = url;
link.download = `${chart.title || '图表'}.png`;
document.body.appendChild(link);
link.click();
link.remove();
saveAs(url, `${chart.title || '图表'}.png`);
};
return (

View File

@@ -35,6 +35,7 @@ import {
importErrorReportUrl,
previewImportStep,
} from '../../api/imports';
import { saveAs } from 'file-saver';
import {
STEP_FIELDS,
type ImportPreviewResult,
@@ -101,12 +102,7 @@ async function downloadErrorReport(runId: string, stepKey?: ImportStepKey): Prom
});
if (!response.ok) throw new Error('错误报告下载失败');
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = `导入错误报告-${runId.slice(0, 8)}.csv`;
anchor.click();
window.setTimeout(() => URL.revokeObjectURL(url), 60_000);
saveAs(blob, `导入错误报告-${runId.slice(0, 8)}.csv`);
}
export const ImportWizardModal: React.FC<ImportWizardModalProps> = ({

View File

@@ -1,6 +1,7 @@
import { useCallback, useRef } from 'react';
import { App } from 'antd';
import type { FormInstance } from 'antd';
import equal from 'fast-deep-equal';
/**
* 弹窗「未保存内容」保护:关闭弹窗时若表单值已被修改,先确认再关闭,
@@ -15,16 +16,16 @@ import type { FormInstance } from 'antd';
*/
export function useDirtyGuard(form: FormInstance) {
const { modal } = App.useApp();
const pristineRef = useRef<string>('');
const pristineRef = useRef<unknown>(null);
/** 记录当前表单值为「未修改」基准;打开弹窗/回填后调用 */
const snapshot = useCallback(() => {
pristineRef.current = JSON.stringify(form.getFieldsValue());
pristineRef.current = form.getFieldsValue();
}, [form]);
/** 表单是否有未保存修改(与 snapshot 时对比) */
const isDirty = useCallback(() => {
return JSON.stringify(form.getFieldsValue()) !== pristineRef.current;
return !equal(form.getFieldsValue(), pristineRef.current);
}, [form]);
const confirmClose = useCallback(

View File

@@ -39,6 +39,7 @@ import {
type DingTalkSyncStatus,
type HistoryScheduleOption,
} from './AttendanceAdmin.helpers';
import { saveAs } from 'file-saver';
export const AdminAttendanceArchive: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
const { modal } = App.useApp();
@@ -349,14 +350,7 @@ export const AdminAttendanceArchive: React.FC<{ canEdit: boolean }> = ({ canEdit
if (!response.ok) throw new Error('导出失败');
return response.blob();
})
.then((blob) => {
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = `学生考勤-${dayjs().format('YYYYMMDD')}.xlsx`;
anchor.click();
URL.revokeObjectURL(url);
})
.then((blob) => saveAs(blob, `学生考勤-${dayjs().format('YYYYMMDD')}.xlsx`))
.catch(() => message.error('导出失败'));
}, [buildParams]);

View File

@@ -26,6 +26,7 @@ import { QueryEmpty } from '../../components/QueryState';
import { useSubmitShortcut } from '../../hooks/useSubmitShortcut';
import { message } from '../../ui/app-message';
import { buildTeacherCandidateOptions, type TeacherCandidateUser } from './teacher-candidate';
import { saveAs } from 'file-saver';
export interface ClassStudent {
id: number;
@@ -312,12 +313,7 @@ export const ClassStudentsTab: React.FC<{
});
if (!res.ok) throw new Error('导出失败');
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `班级花名册-${detail?.name || id}.xlsx`;
a.click();
URL.revokeObjectURL(url);
saveAs(blob, `班级花名册-${detail?.name || id}.xlsx`);
message.success('花名册导出成功');
} catch (error) {
console.error('花名册导出失败', error);

View File

@@ -36,6 +36,7 @@ import { usePermission } from '../../hooks/usePermission';
import { useUserStore } from '../../store/user/userStore';
import { useVisibleRefetch } from '../../hooks/usePageVisible';
import { useDirtyGuard } from '../../hooks/useDirtyGuard';
import { saveAs } from 'file-saver';
const statusMap: Record<string, { text: string; color: string }> = {
available: { text: '可用', color: 'green' },
@@ -221,14 +222,7 @@ const ClassroomsPage: React.FC = () => {
const token = useUserStore.getState().token;
fetch(`${baseURL}/classrooms/template`, { headers: { Authorization: `Bearer ${token}` } })
.then((res) => res.blob())
.then((blob) => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = '教室导入模板.xlsx';
a.click();
URL.revokeObjectURL(url);
})
.then((blob) => saveAs(blob, '教室导入模板.xlsx'))
.catch(() => message.error('下载失败'));
};
@@ -486,12 +480,8 @@ const ClassroomsPage: React.FC = () => {
headers: { Authorization: `Bearer ${token}` },
})
.then((r) => r.blob())
.then((b) => {
const a = document.createElement('a');
a.href = URL.createObjectURL(b);
a.download = '教室使用报表.xlsx';
a.click();
});
.then((b) => saveAs(b, '教室使用报表.xlsx'))
.catch(() => message.error('导出失败'));
}}
>