diff --git a/lib/__tests__/performance-survey-dashboard-export.test.ts b/lib/__tests__/performance-survey-dashboard-export.test.ts index 099c174..fae69f5 100644 --- a/lib/__tests__/performance-survey-dashboard-export.test.ts +++ b/lib/__tests__/performance-survey-dashboard-export.test.ts @@ -2,7 +2,9 @@ import ExcelJS from 'exceljs' import { buildPerformanceSurveyDashboardWorkbook, buildPerformanceSurveyExportTable, + choosePerformanceSurveyExportDestination, sanitizePerformanceSurveyExportFileName, + savePerformanceSurveyDashboardWorkbook, type PerformanceSurveyDashboardExportData, } from '@/lib/performance-survey-dashboard-export' @@ -85,4 +87,44 @@ describe('performance survey dashboard xlsx export', () => { expect(sanitizePerformanceSurveyExportFileName('考核:品牌/设计*主管')) .toBe('考核_品牌_设计_主管.xlsx') }) + + test('keeps the browser context when opening the native save picker', async () => { + const handle = { createWritable: jest.fn() } + const picker = jest.fn(function (this: unknown) { + expect(this).toBe(window) + return Promise.resolve(handle) + }) + Object.defineProperty(window, 'showSaveFilePicker', { + configurable: true, + value: picker, + }) + + await expect(choosePerformanceSurveyExportDestination('考核调研.xlsx')) + .resolves.toEqual({ kind: 'file-system', handle }) + expect(picker).toHaveBeenCalledWith(expect.objectContaining({ + suggestedName: '考核调研.xlsx', + })) + + delete (window as typeof window & { showSaveFilePicker?: unknown }).showSaveFilePicker + }) + + test('uses a native blob download when the save picker is unavailable', async () => { + const createObjectURL = jest.fn(() => 'blob:performance-survey-export') + const revokeObjectURL = jest.fn() + Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: createObjectURL }) + Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: revokeObjectURL }) + const click = jest.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}) + + await savePerformanceSurveyDashboardWorkbook( + exportData, + '考核调研_品牌创意主管_2026-07.xlsx', + { kind: 'download' }, + ) + + expect(createObjectURL).toHaveBeenCalledWith(expect.any(Blob)) + expect(click).toHaveBeenCalledTimes(1) + await new Promise((resolve) => window.setTimeout(resolve, 0)) + expect(revokeObjectURL).toHaveBeenCalledWith('blob:performance-survey-export') + click.mockRestore() + }) }) diff --git a/lib/performance-survey-dashboard-export.ts b/lib/performance-survey-dashboard-export.ts index ede4afa..b5ffdc9 100644 --- a/lib/performance-survey-dashboard-export.ts +++ b/lib/performance-survey-dashboard-export.ts @@ -211,7 +211,7 @@ export async function choosePerformanceSurveyExportDestination( if (!picker) return { kind: 'download' } try { - const handle = await picker({ + const handle = await picker.call(window, { suggestedName, types: [{ description: 'WPS/Excel 工作簿', @@ -243,6 +243,26 @@ export async function savePerformanceSurveyDashboardWorkbook( return } - const { saveAs } = await import('file-saver') - saveAs(blob, fileName) + if ( + typeof document === 'undefined' + || typeof URL === 'undefined' + || typeof URL.createObjectURL !== 'function' + ) { + throw new Error('当前浏览器无法创建下载文件,请更换 Chrome 或 Edge 后重试。') + } + + const objectUrl = URL.createObjectURL(blob) + const anchor = document.createElement('a') + anchor.href = objectUrl + anchor.download = fileName + anchor.rel = 'noopener' + anchor.style.display = 'none' + document.body.appendChild(anchor) + + try { + anchor.click() + } finally { + anchor.remove() + window.setTimeout(() => URL.revokeObjectURL(objectUrl), 0) + } }