修改班级报告导出方式

This commit is contained in:
2026-07-17 12:51:00 +08:00
parent e999f8b606
commit 07ead3ee73
7 changed files with 48 additions and 46 deletions
+36 -27
View File
@@ -129,6 +129,22 @@ function getReportDownloadFileName(report: ReportEntity | ReportItem): string {
return `${sanitizeFileName(`${report.className}${report.studentName}`)}${report.fileExtension}` return `${sanitizeFileName(`${report.className}${report.studentName}`)}${report.fileExtension}`
} }
async function createUniqueExportFolder(parentPath: string, folderName: string): Promise<string> {
for (let suffix = 1; ; suffix += 1) {
const candidateName = suffix === 1 ? folderName : `${folderName} (${suffix})`
const candidatePath = join(parentPath, candidateName)
try {
await mkdir(candidatePath)
return candidatePath
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {
throw error
}
}
}
}
function sendReportProgress(progress: ReportGenerationProgress): void { function sendReportProgress(progress: ReportGenerationProgress): void {
for (const window of BrowserWindow.getAllWindows()) { for (const window of BrowserWindow.getAllWindows()) {
window.webContents.send('reports:generation-progress', progress) window.webContents.send('reports:generation-progress', progress)
@@ -1133,18 +1149,24 @@ export async function downloadClassReports(
} }
const className = reports[0]?.className || '班级报告' const className = reports[0]?.className || '班级报告'
const result = await dialog.showSaveDialog({ const result = await dialog.showOpenDialog({
title: '下载班级报告', title: '选择班级报告导出目录',
defaultPath: `${sanitizeFileName(className)}成长报告.zip`, defaultPath: app.getPath('downloads'),
filters: [{ name: 'ZIP 压缩包', extensions: ['zip'] }] buttonLabel: '选择目录',
properties: ['openDirectory', 'createDirectory']
}) })
if (result.canceled || !result.filePath) { const parentPath = result.filePaths[0]
if (result.canceled || !parentPath) {
return { filePath: '', reportCount: reports.length, canceled: true } return { filePath: '', reportCount: reports.length, canceled: true }
} }
const batchId = randomUUID() const batchId = randomUUID()
const zip = new JSZip() const exportPath = await createUniqueExportFolder(
parentPath,
`${sanitizeFileName(className)}成长报告`
)
const usedFileNames = new Map<string, number>() const usedFileNames = new Map<string, number>()
let completedCount = 0 let completedCount = 0
@@ -1164,7 +1186,7 @@ export async function downloadClassReports(
status: 'started', status: 'started',
current: 0, current: 0,
total: reports.length, total: reports.length,
filePath: result.filePath filePath: exportPath
}) })
for (const report of reports) { for (const report of reports) {
@@ -1174,7 +1196,7 @@ export async function downloadClassReports(
reportTitle: report.title, reportTitle: report.title,
current: completedCount, current: completedCount,
total: reports.length, total: reports.length,
filePath: result.filePath filePath: exportPath
}) })
await access(report.filePath) await access(report.filePath)
@@ -1186,7 +1208,7 @@ export async function downloadClassReports(
? baseFileName ? baseFileName
: `${baseFileName.replace(report.fileExtension, '')}-${usedCount + 1}${report.fileExtension}` : `${baseFileName.replace(report.fileExtension, '')}-${usedCount + 1}${report.fileExtension}`
zip.file(fileName, await readFile(report.filePath)) await copyFile(report.filePath, join(exportPath, fileName))
completedCount += 1 completedCount += 1
publishProgress({ publishProgress({
@@ -1195,41 +1217,28 @@ export async function downloadClassReports(
reportTitle: report.title, reportTitle: report.title,
current: completedCount, current: completedCount,
total: reports.length, total: reports.length,
filePath: result.filePath filePath: exportPath
}) })
} }
publishProgress({
status: 'writing',
current: reports.length,
total: reports.length,
filePath: result.filePath
})
const zipBuffer = await zip.generateAsync({
type: 'nodebuffer',
compression: 'DEFLATE'
})
await writeFile(result.filePath, zipBuffer)
publishProgress({ publishProgress({
status: 'finished', status: 'finished',
current: reports.length, current: reports.length,
total: reports.length, total: reports.length,
filePath: result.filePath filePath: exportPath
}) })
} catch (error) { } catch (error) {
publishProgress({ publishProgress({
status: 'failed', status: 'failed',
current: completedCount, current: completedCount,
total: reports.length, total: reports.length,
filePath: result.filePath, filePath: exportPath,
error: error instanceof Error ? error.message : '下载班级报告失败' error: error instanceof Error ? error.message : '导出班级报告失败'
}) })
throw error throw error
} }
return { filePath: result.filePath, reportCount: reports.length } return { filePath: exportPath, reportCount: reports.length }
} }
export async function deleteReport(id: string): Promise<void> { export async function deleteReport(id: string): Promise<void> {
+1 -1
View File
@@ -44,7 +44,7 @@ export type ReportGenerationProgress = {
export type ReportDownloadProgress = { export type ReportDownloadProgress = {
batchId: string batchId: string
status: 'started' | 'file-started' | 'file-finished' | 'writing' | 'finished' | 'failed' status: 'started' | 'file-started' | 'file-finished' | 'finished' | 'failed'
classId: string classId: string
className: string className: string
reportId?: string reportId?: string
+1 -1
View File
@@ -108,7 +108,7 @@ type ReportGenerationProgress = {
type ReportDownloadProgress = { type ReportDownloadProgress = {
batchId: string batchId: string
status: 'started' | 'file-started' | 'file-finished' | 'writing' | 'finished' | 'failed' status: 'started' | 'file-started' | 'file-finished' | 'finished' | 'failed'
classId: string classId: string
className: string className: string
reportId?: string reportId?: string
+1 -1
View File
@@ -210,7 +210,7 @@ const api = {
onReportDownloadProgress: ( onReportDownloadProgress: (
callback: (progress: { callback: (progress: {
batchId: string batchId: string
status: 'started' | 'file-started' | 'file-finished' | 'writing' | 'finished' | 'failed' status: 'started' | 'file-started' | 'file-finished' | 'finished' | 'failed'
classId: string classId: string
className: string className: string
reportId?: string reportId?: string
@@ -160,28 +160,21 @@ export function GenerationProgressToasts(): null {
const toastId = getToastId('report-downloads', progress.batchId) const toastId = getToastId('report-downloads', progress.batchId)
const isFinished = progress.status === 'finished' const isFinished = progress.status === 'finished'
const isFailed = progress.status === 'failed' const isFailed = progress.status === 'failed'
const isWriting = progress.status === 'writing'
const activeReportTitle = progress.reportTitle const activeReportTitle = progress.reportTitle
? `正在打包 ${progress.reportTitle}` ? `正在导出 ${progress.reportTitle}`
: '正在打包班级报告' : '正在导出班级报告'
showProgressToast({ showProgressToast({
id: toastId, id: toastId,
icon: Download, icon: Download,
title: isFinished title: isFinished ? '班级报告导出完成' : isFailed ? '班级报告导出失败' : activeReportTitle,
? '班级报告导出完成'
: isFailed
? '班级报告导出失败'
: isWriting
? '正在写入压缩包'
: activeReportTitle,
description: isFinished description: isFinished
? progress.filePath ? progress.filePath
: `${progress.className} · ${progress.current}/${progress.total}`, : `${progress.className} · ${progress.current}/${progress.total}`,
current: progress.current, current: progress.current,
total: progress.total, total: progress.total,
status: isFinished ? 'success' : isFailed ? 'error' : 'running', status: isFinished ? 'success' : isFailed ? 'error' : 'running',
statusLabel: isFinished ? '已完成' : isFailed ? '失败' : isWriting ? '写入中' : '打包中', statusLabel: isFinished ? '已完成' : isFailed ? '失败' : '导出中',
error: progress.error, error: progress.error,
duration: isFinished || isFailed ? 5000 : undefined duration: isFinished || isFailed ? 5000 : undefined
}) })
+4 -4
View File
@@ -601,13 +601,13 @@ export function ClassPage(): React.JSX.Element {
if (!response.ok) { if (!response.ok) {
if (!response.canceled) { if (!response.canceled) {
showError('下载班级报告失败', response.message) showError('导出班级报告失败', response.message)
} }
return return
} }
showSuccess( showSuccess(
`下载${classItem.name}」报告`, `导出${classItem.name}」报告`,
`${response.filePath},共 ${response.reportCount}` `${response.filePath},共 ${response.reportCount}`
) )
} finally { } finally {
@@ -895,8 +895,8 @@ export function ClassPage(): React.JSX.Element {
> >
<Download /> <Download />
{downloadingReportClassId === classItem.id {downloadingReportClassId === classItem.id
? '报告打包中' ? '报告导出中'
: '一键下载班级报告'} : '一键导出班级报告'}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
+1 -1
View File
@@ -128,7 +128,7 @@ export type ReportGenerationProgress = {
export type ReportDownloadProgress = { export type ReportDownloadProgress = {
batchId: string batchId: string
status: 'started' | 'file-started' | 'file-finished' | 'writing' | 'finished' | 'failed' status: 'started' | 'file-started' | 'file-finished' | 'finished' | 'failed'
classId: string classId: string
className: string className: string
reportId?: string reportId?: string