diff --git a/apps/admin/src/components/AiChat/DynamicChart.tsx b/apps/admin/src/components/AiChat/DynamicChart.tsx index 58ea6d7..06e0d0d 100644 --- a/apps/admin/src/components/AiChat/DynamicChart.tsx +++ b/apps/admin/src/components/AiChat/DynamicChart.tsx @@ -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 = ({ 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 ( diff --git a/apps/admin/src/components/ImportWizard/ImportWizardModal.tsx b/apps/admin/src/components/ImportWizard/ImportWizardModal.tsx index eb6b2e3..0515bf7 100644 --- a/apps/admin/src/components/ImportWizard/ImportWizardModal.tsx +++ b/apps/admin/src/components/ImportWizard/ImportWizardModal.tsx @@ -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 = ({ diff --git a/apps/admin/src/hooks/useDirtyGuard.ts b/apps/admin/src/hooks/useDirtyGuard.ts index 2222aae..bd22989 100644 --- a/apps/admin/src/hooks/useDirtyGuard.ts +++ b/apps/admin/src/hooks/useDirtyGuard.ts @@ -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(''); + const pristineRef = useRef(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( diff --git a/apps/admin/src/pages/Attendance/admin.tsx b/apps/admin/src/pages/Attendance/admin.tsx index f0895b3..67fbf47 100644 --- a/apps/admin/src/pages/Attendance/admin.tsx +++ b/apps/admin/src/pages/Attendance/admin.tsx @@ -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]); diff --git a/apps/admin/src/pages/Classes/ClassDetailTabs.tsx b/apps/admin/src/pages/Classes/ClassDetailTabs.tsx index 5e38327..74d77b4 100644 --- a/apps/admin/src/pages/Classes/ClassDetailTabs.tsx +++ b/apps/admin/src/pages/Classes/ClassDetailTabs.tsx @@ -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); diff --git a/apps/admin/src/pages/Classrooms/index.tsx b/apps/admin/src/pages/Classrooms/index.tsx index 1059860..e6bfe48 100644 --- a/apps/admin/src/pages/Classrooms/index.tsx +++ b/apps/admin/src/pages/Classrooms/index.tsx @@ -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 = { 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('导出失败')); }} > 导出报表