|
|
|
|
@ -1,6 +1,8 @@
|
|
|
|
|
import axios from 'axios'
|
|
|
|
|
import { adminHttp } from './http'
|
|
|
|
|
import type {
|
|
|
|
|
AdminApplicationDetail,
|
|
|
|
|
AdminApplicationExportMeta,
|
|
|
|
|
AdminApplicationListParams,
|
|
|
|
|
AdminApplicationRow,
|
|
|
|
|
Paginated,
|
|
|
|
|
@ -53,43 +55,111 @@ export async function downloadAdminApplicationFile(
|
|
|
|
|
`/competitions/${competitionId}/applications/${applicationId}/files/${fileId}/download`,
|
|
|
|
|
{ responseType: 'blob' },
|
|
|
|
|
)
|
|
|
|
|
const blobUrl = URL.createObjectURL(res.data)
|
|
|
|
|
triggerBlobDownload(res.data, fallbackName || '附件')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function triggerBlobDownload(blob: Blob, filename: string): void {
|
|
|
|
|
const blobUrl = URL.createObjectURL(blob)
|
|
|
|
|
const a = document.createElement('a')
|
|
|
|
|
a.href = blobUrl
|
|
|
|
|
a.download = fallbackName || '附件'
|
|
|
|
|
a.download = filename
|
|
|
|
|
document.body.appendChild(a)
|
|
|
|
|
a.click()
|
|
|
|
|
a.remove()
|
|
|
|
|
URL.revokeObjectURL(blobUrl)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function exportAdminApplications(
|
|
|
|
|
function parseDownloadFilename(disposition: string | undefined, fallback: string): string {
|
|
|
|
|
if (!disposition) return fallback
|
|
|
|
|
const utf8Match = /filename\*=UTF-8''([^;]+)/i.exec(disposition)
|
|
|
|
|
if (utf8Match?.[1]) {
|
|
|
|
|
return decodeURIComponent(utf8Match[1])
|
|
|
|
|
}
|
|
|
|
|
const plainMatch = /filename="?([^";]+)"?/i.exec(disposition)
|
|
|
|
|
if (plainMatch?.[1]) return plainMatch[1]
|
|
|
|
|
return fallback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function readExportError(error: unknown): Promise<Error> {
|
|
|
|
|
if (axios.isAxiosError(error)) {
|
|
|
|
|
const data = error.response?.data
|
|
|
|
|
if (data instanceof Blob) {
|
|
|
|
|
const text = await data.text()
|
|
|
|
|
try {
|
|
|
|
|
const payload = JSON.parse(text) as { message?: string; errors?: Record<string, string[]> }
|
|
|
|
|
return new Error(payload.errors?.export?.[0] || payload.errors?.part?.[0] || payload.message || '导出失败')
|
|
|
|
|
} catch {
|
|
|
|
|
// ignore non-json blob errors
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (data && typeof data === 'object') {
|
|
|
|
|
const payload = data as { message?: string; errors?: Record<string, string[]> }
|
|
|
|
|
return new Error(payload.errors?.export?.[0] || payload.errors?.part?.[0] || payload.message || '导出失败')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return error instanceof Error ? error : new Error('导出失败')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sleep(ms: number): Promise<void> {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
window.setTimeout(resolve, ms)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getAdminApplicationExportMeta(
|
|
|
|
|
competitionId: number,
|
|
|
|
|
params: AdminApplicationListParams,
|
|
|
|
|
): Promise<AdminApplicationExportMeta> {
|
|
|
|
|
try {
|
|
|
|
|
const { data } = await adminHttp.get<AdminApplicationExportMeta>(
|
|
|
|
|
`/competitions/${competitionId}/applications/export/meta`,
|
|
|
|
|
{ params },
|
|
|
|
|
)
|
|
|
|
|
if (!data || typeof data !== 'object') throw new Error('导出信息无效')
|
|
|
|
|
return data
|
|
|
|
|
} catch (error) {
|
|
|
|
|
throw await readExportError(error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function downloadAdminApplicationExportPart(
|
|
|
|
|
competitionId: number,
|
|
|
|
|
params: AdminApplicationListParams,
|
|
|
|
|
part: number,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const res = await adminHttp.get<Blob>(`/competitions/${competitionId}/applications/export`, {
|
|
|
|
|
params,
|
|
|
|
|
responseType: 'blob',
|
|
|
|
|
})
|
|
|
|
|
let res
|
|
|
|
|
try {
|
|
|
|
|
res = await adminHttp.get<Blob>(`/competitions/${competitionId}/applications/export`, {
|
|
|
|
|
params: { ...params, part },
|
|
|
|
|
responseType: 'blob',
|
|
|
|
|
timeout: 0,
|
|
|
|
|
})
|
|
|
|
|
} catch (error) {
|
|
|
|
|
throw await readExportError(error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const filename = parseDownloadFilename(
|
|
|
|
|
res.headers['content-disposition'] as string | undefined,
|
|
|
|
|
`报名信息导出_第${part}部分.zip`,
|
|
|
|
|
)
|
|
|
|
|
triggerBlobDownload(res.data, filename)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function exportAdminApplications(
|
|
|
|
|
competitionId: number,
|
|
|
|
|
params: AdminApplicationListParams,
|
|
|
|
|
onProgress?: (current: number, total: number) => void,
|
|
|
|
|
): Promise<AdminApplicationExportMeta> {
|
|
|
|
|
const meta = await getAdminApplicationExportMeta(competitionId, params)
|
|
|
|
|
|
|
|
|
|
const disposition = res.headers['content-disposition'] as string | undefined
|
|
|
|
|
let filename = '报名信息导出.zip'
|
|
|
|
|
if (disposition) {
|
|
|
|
|
const utf8Match = /filename\*=UTF-8''([^;]+)/i.exec(disposition)
|
|
|
|
|
if (utf8Match?.[1]) {
|
|
|
|
|
filename = decodeURIComponent(utf8Match[1])
|
|
|
|
|
} else {
|
|
|
|
|
const plainMatch = /filename="?([^";]+)"?/i.exec(disposition)
|
|
|
|
|
if (plainMatch?.[1]) filename = plainMatch[1]
|
|
|
|
|
for (let part = 1; part <= meta.part_count; part += 1) {
|
|
|
|
|
onProgress?.(part, meta.part_count)
|
|
|
|
|
await downloadAdminApplicationExportPart(competitionId, params, part)
|
|
|
|
|
if (part < meta.part_count) {
|
|
|
|
|
await sleep(500)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const blobUrl = URL.createObjectURL(res.data)
|
|
|
|
|
const a = document.createElement('a')
|
|
|
|
|
a.href = blobUrl
|
|
|
|
|
a.download = filename
|
|
|
|
|
document.body.appendChild(a)
|
|
|
|
|
a.click()
|
|
|
|
|
a.remove()
|
|
|
|
|
URL.revokeObjectURL(blobUrl)
|
|
|
|
|
return meta
|
|
|
|
|
}
|
|
|
|
|
|