refactor(admin): 用已安装的 file-saver/fast-deep-equal 替代手写下载与 JSON 比较
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import React, { lazy, Suspense, useEffect, useMemo, useState } from 'react';
|
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 { XCard, registerCatalog, type XAgentCommand_v0_9 } from '@ant-design/x-card';
|
||||||
import { Button, Spin, Tag, Tooltip, Typography } from 'antd';
|
import { Button, Spin, Tag, Tooltip, Typography } from 'antd';
|
||||||
import { DownloadOutlined } from '@ant-design/icons';
|
import { DownloadOutlined } from '@ant-design/icons';
|
||||||
@@ -232,12 +233,7 @@ const ChartPreview: React.FC<ChartPreviewProps> = ({ chart }) => {
|
|||||||
pixelRatio: 2,
|
pixelRatio: 2,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
});
|
});
|
||||||
const link = document.createElement('a');
|
saveAs(url, `${chart.title || '图表'}.png`);
|
||||||
link.href = url;
|
|
||||||
link.download = `${chart.title || '图表'}.png`;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
link.remove();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
importErrorReportUrl,
|
importErrorReportUrl,
|
||||||
previewImportStep,
|
previewImportStep,
|
||||||
} from '../../api/imports';
|
} from '../../api/imports';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
import {
|
import {
|
||||||
STEP_FIELDS,
|
STEP_FIELDS,
|
||||||
type ImportPreviewResult,
|
type ImportPreviewResult,
|
||||||
@@ -101,12 +102,7 @@ async function downloadErrorReport(runId: string, stepKey?: ImportStepKey): Prom
|
|||||||
});
|
});
|
||||||
if (!response.ok) throw new Error('错误报告下载失败');
|
if (!response.ok) throw new Error('错误报告下载失败');
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
const url = URL.createObjectURL(blob);
|
saveAs(blob, `导入错误报告-${runId.slice(0, 8)}.csv`);
|
||||||
const anchor = document.createElement('a');
|
|
||||||
anchor.href = url;
|
|
||||||
anchor.download = `导入错误报告-${runId.slice(0, 8)}.csv`;
|
|
||||||
anchor.click();
|
|
||||||
window.setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ImportWizardModal: React.FC<ImportWizardModalProps> = ({
|
export const ImportWizardModal: React.FC<ImportWizardModalProps> = ({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import { App } from 'antd';
|
import { App } from 'antd';
|
||||||
import type { FormInstance } 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) {
|
export function useDirtyGuard(form: FormInstance) {
|
||||||
const { modal } = App.useApp();
|
const { modal } = App.useApp();
|
||||||
const pristineRef = useRef<string>('');
|
const pristineRef = useRef<unknown>(null);
|
||||||
|
|
||||||
/** 记录当前表单值为「未修改」基准;打开弹窗/回填后调用 */
|
/** 记录当前表单值为「未修改」基准;打开弹窗/回填后调用 */
|
||||||
const snapshot = useCallback(() => {
|
const snapshot = useCallback(() => {
|
||||||
pristineRef.current = JSON.stringify(form.getFieldsValue());
|
pristineRef.current = form.getFieldsValue();
|
||||||
}, [form]);
|
}, [form]);
|
||||||
|
|
||||||
/** 表单是否有未保存修改(与 snapshot 时对比) */
|
/** 表单是否有未保存修改(与 snapshot 时对比) */
|
||||||
const isDirty = useCallback(() => {
|
const isDirty = useCallback(() => {
|
||||||
return JSON.stringify(form.getFieldsValue()) !== pristineRef.current;
|
return !equal(form.getFieldsValue(), pristineRef.current);
|
||||||
}, [form]);
|
}, [form]);
|
||||||
|
|
||||||
const confirmClose = useCallback(
|
const confirmClose = useCallback(
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
type DingTalkSyncStatus,
|
type DingTalkSyncStatus,
|
||||||
type HistoryScheduleOption,
|
type HistoryScheduleOption,
|
||||||
} from './AttendanceAdmin.helpers';
|
} from './AttendanceAdmin.helpers';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
|
||||||
export const AdminAttendanceArchive: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
export const AdminAttendanceArchive: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
||||||
const { modal } = App.useApp();
|
const { modal } = App.useApp();
|
||||||
@@ -349,14 +350,7 @@ export const AdminAttendanceArchive: React.FC<{ canEdit: boolean }> = ({ canEdit
|
|||||||
if (!response.ok) throw new Error('导出失败');
|
if (!response.ok) throw new Error('导出失败');
|
||||||
return response.blob();
|
return response.blob();
|
||||||
})
|
})
|
||||||
.then((blob) => {
|
.then((blob) => saveAs(blob, `学生考勤-${dayjs().format('YYYYMMDD')}.xlsx`))
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const anchor = document.createElement('a');
|
|
||||||
anchor.href = url;
|
|
||||||
anchor.download = `学生考勤-${dayjs().format('YYYYMMDD')}.xlsx`;
|
|
||||||
anchor.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
})
|
|
||||||
.catch(() => message.error('导出失败'));
|
.catch(() => message.error('导出失败'));
|
||||||
}, [buildParams]);
|
}, [buildParams]);
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { QueryEmpty } from '../../components/QueryState';
|
|||||||
import { useSubmitShortcut } from '../../hooks/useSubmitShortcut';
|
import { useSubmitShortcut } from '../../hooks/useSubmitShortcut';
|
||||||
import { message } from '../../ui/app-message';
|
import { message } from '../../ui/app-message';
|
||||||
import { buildTeacherCandidateOptions, type TeacherCandidateUser } from './teacher-candidate';
|
import { buildTeacherCandidateOptions, type TeacherCandidateUser } from './teacher-candidate';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
|
||||||
export interface ClassStudent {
|
export interface ClassStudent {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -312,12 +313,7 @@ export const ClassStudentsTab: React.FC<{
|
|||||||
});
|
});
|
||||||
if (!res.ok) throw new Error('导出失败');
|
if (!res.ok) throw new Error('导出失败');
|
||||||
const blob = await res.blob();
|
const blob = await res.blob();
|
||||||
const url = URL.createObjectURL(blob);
|
saveAs(blob, `班级花名册-${detail?.name || id}.xlsx`);
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = `班级花名册-${detail?.name || id}.xlsx`;
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
message.success('花名册导出成功');
|
message.success('花名册导出成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('花名册导出失败', error);
|
console.error('花名册导出失败', error);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { usePermission } from '../../hooks/usePermission';
|
|||||||
import { useUserStore } from '../../store/user/userStore';
|
import { useUserStore } from '../../store/user/userStore';
|
||||||
import { useVisibleRefetch } from '../../hooks/usePageVisible';
|
import { useVisibleRefetch } from '../../hooks/usePageVisible';
|
||||||
import { useDirtyGuard } from '../../hooks/useDirtyGuard';
|
import { useDirtyGuard } from '../../hooks/useDirtyGuard';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
|
||||||
const statusMap: Record<string, { text: string; color: string }> = {
|
const statusMap: Record<string, { text: string; color: string }> = {
|
||||||
available: { text: '可用', color: 'green' },
|
available: { text: '可用', color: 'green' },
|
||||||
@@ -221,14 +222,7 @@ const ClassroomsPage: React.FC = () => {
|
|||||||
const token = useUserStore.getState().token;
|
const token = useUserStore.getState().token;
|
||||||
fetch(`${baseURL}/classrooms/template`, { headers: { Authorization: `Bearer ${token}` } })
|
fetch(`${baseURL}/classrooms/template`, { headers: { Authorization: `Bearer ${token}` } })
|
||||||
.then((res) => res.blob())
|
.then((res) => res.blob())
|
||||||
.then((blob) => {
|
.then((blob) => saveAs(blob, '教室导入模板.xlsx'))
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = '教室导入模板.xlsx';
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
})
|
|
||||||
.catch(() => message.error('下载失败'));
|
.catch(() => message.error('下载失败'));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -486,12 +480,8 @@ const ClassroomsPage: React.FC = () => {
|
|||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
})
|
})
|
||||||
.then((r) => r.blob())
|
.then((r) => r.blob())
|
||||||
.then((b) => {
|
.then((b) => saveAs(b, '教室使用报表.xlsx'))
|
||||||
const a = document.createElement('a');
|
.catch(() => message.error('导出失败'));
|
||||||
a.href = URL.createObjectURL(b);
|
|
||||||
a.download = '教室使用报表.xlsx';
|
|
||||||
a.click();
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
导出报表
|
导出报表
|
||||||
|
|||||||
Reference in New Issue
Block a user