修改班级报告导出方式

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}`
}
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 {
for (const window of BrowserWindow.getAllWindows()) {
window.webContents.send('reports:generation-progress', progress)
@@ -1133,18 +1149,24 @@ export async function downloadClassReports(
}
const className = reports[0]?.className || '班级报告'
const result = await dialog.showSaveDialog({
title: '下载班级报告',
defaultPath: `${sanitizeFileName(className)}成长报告.zip`,
filters: [{ name: 'ZIP 压缩包', extensions: ['zip'] }]
const result = await dialog.showOpenDialog({
title: '选择班级报告导出目录',
defaultPath: app.getPath('downloads'),
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 }
}
const batchId = randomUUID()
const zip = new JSZip()
const exportPath = await createUniqueExportFolder(
parentPath,
`${sanitizeFileName(className)}成长报告`
)
const usedFileNames = new Map<string, number>()
let completedCount = 0
@@ -1164,7 +1186,7 @@ export async function downloadClassReports(
status: 'started',
current: 0,
total: reports.length,
filePath: result.filePath
filePath: exportPath
})
for (const report of reports) {
@@ -1174,7 +1196,7 @@ export async function downloadClassReports(
reportTitle: report.title,
current: completedCount,
total: reports.length,
filePath: result.filePath
filePath: exportPath
})
await access(report.filePath)
@@ -1186,7 +1208,7 @@ export async function downloadClassReports(
? baseFileName
: `${baseFileName.replace(report.fileExtension, '')}-${usedCount + 1}${report.fileExtension}`
zip.file(fileName, await readFile(report.filePath))
await copyFile(report.filePath, join(exportPath, fileName))
completedCount += 1
publishProgress({
@@ -1195,41 +1217,28 @@ export async function downloadClassReports(
reportTitle: report.title,
current: completedCount,
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({
status: 'finished',
current: reports.length,
total: reports.length,
filePath: result.filePath
filePath: exportPath
})
} catch (error) {
publishProgress({
status: 'failed',
current: completedCount,
total: reports.length,
filePath: result.filePath,
error: error instanceof Error ? error.message : '下载班级报告失败'
filePath: exportPath,
error: error instanceof Error ? error.message : '导出班级报告失败'
})
throw error
}
return { filePath: result.filePath, reportCount: reports.length }
return { filePath: exportPath, reportCount: reports.length }
}
export async function deleteReport(id: string): Promise<void> {
+1 -1
View File
@@ -44,7 +44,7 @@ export type ReportGenerationProgress = {
export type ReportDownloadProgress = {
batchId: string
status: 'started' | 'file-started' | 'file-finished' | 'writing' | 'finished' | 'failed'
status: 'started' | 'file-started' | 'file-finished' | 'finished' | 'failed'
classId: string
className: string
reportId?: string
+1 -1
View File
@@ -108,7 +108,7 @@ type ReportGenerationProgress = {
type ReportDownloadProgress = {
batchId: string
status: 'started' | 'file-started' | 'file-finished' | 'writing' | 'finished' | 'failed'
status: 'started' | 'file-started' | 'file-finished' | 'finished' | 'failed'
classId: string
className: string
reportId?: string
+1 -1
View File
@@ -210,7 +210,7 @@ const api = {
onReportDownloadProgress: (
callback: (progress: {
batchId: string
status: 'started' | 'file-started' | 'file-finished' | 'writing' | 'finished' | 'failed'
status: 'started' | 'file-started' | 'file-finished' | 'finished' | 'failed'
classId: string
className: string
reportId?: string
@@ -160,28 +160,21 @@ export function GenerationProgressToasts(): null {
const toastId = getToastId('report-downloads', progress.batchId)
const isFinished = progress.status === 'finished'
const isFailed = progress.status === 'failed'
const isWriting = progress.status === 'writing'
const activeReportTitle = progress.reportTitle
? `正在打包 ${progress.reportTitle}`
: '正在打包班级报告'
? `正在导出 ${progress.reportTitle}`
: '正在导出班级报告'
showProgressToast({
id: toastId,
icon: Download,
title: isFinished
? '班级报告导出完成'
: isFailed
? '班级报告导出失败'
: isWriting
? '正在写入压缩包'
: activeReportTitle,
title: isFinished ? '班级报告导出完成' : isFailed ? '班级报告导出失败' : activeReportTitle,
description: isFinished
? progress.filePath
: `${progress.className} · ${progress.current}/${progress.total}`,
current: progress.current,
total: progress.total,
status: isFinished ? 'success' : isFailed ? 'error' : 'running',
statusLabel: isFinished ? '已完成' : isFailed ? '失败' : isWriting ? '写入中' : '打包中',
statusLabel: isFinished ? '已完成' : isFailed ? '失败' : '导出中',
error: progress.error,
duration: isFinished || isFailed ? 5000 : undefined
})
+4 -4
View File
@@ -601,13 +601,13 @@ export function ClassPage(): React.JSX.Element {
if (!response.ok) {
if (!response.canceled) {
showError('下载班级报告失败', response.message)
showError('导出班级报告失败', response.message)
}
return
}
showSuccess(
`下载${classItem.name}」报告`,
`导出${classItem.name}」报告`,
`${response.filePath},共 ${response.reportCount}`
)
} finally {
@@ -895,8 +895,8 @@ export function ClassPage(): React.JSX.Element {
>
<Download />
{downloadingReportClassId === classItem.id
? '报告打包中'
: '一键下载班级报告'}
? '报告导出中'
: '一键导出班级报告'}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
+1 -1
View File
@@ -128,7 +128,7 @@ export type ReportGenerationProgress = {
export type ReportDownloadProgress = {
batchId: string
status: 'started' | 'file-started' | 'file-finished' | 'writing' | 'finished' | 'failed'
status: 'started' | 'file-started' | 'file-finished' | 'finished' | 'failed'
classId: string
className: string
reportId?: string