修改班级报告导出方式
This commit is contained in:
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user