diff --git a/src/api/admin/applications.ts b/src/api/admin/applications.ts index 97cc0eb..5594030 100644 --- a/src/api/admin/applications.ts +++ b/src/api/admin/applications.ts @@ -62,3 +62,34 @@ export async function downloadAdminApplicationFile( a.remove() URL.revokeObjectURL(blobUrl) } + +export async function exportAdminApplications( + competitionId: number, + params: AdminApplicationListParams, +): Promise { + const res = await adminHttp.get(`/competitions/${competitionId}/applications/export`, { + params, + responseType: 'blob', + }) + + 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] + } + } + + 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) +} diff --git a/src/api/admin/types.ts b/src/api/admin/types.ts index 26154d0..bf8d2ed 100644 --- a/src/api/admin/types.ts +++ b/src/api/admin/types.ts @@ -101,10 +101,12 @@ export interface AdminApplicationListParams { status?: string track?: string keyword?: string + signup_channel_id?: number } export interface AdminApplicationRow { id: number + project_code: string status: string project_name: string player_name: string @@ -113,6 +115,8 @@ export interface AdminApplicationRow { entry_group: string track_code: string track_title: string + signup_channel: { id: number; channel_code: string; channel_name: string } | null + signup_channel_code: string | null submitted_at: string | null updated_at: string | null files_count: number diff --git a/src/views/admin/competition/CompetitionFormView.vue b/src/views/admin/competition/CompetitionFormView.vue index 658e47e..7953d02 100644 --- a/src/views/admin/competition/CompetitionFormView.vue +++ b/src/views/admin/competition/CompetitionFormView.vue @@ -22,11 +22,13 @@ import { } from '../../../api/admin/signupChannels' import { downloadAdminApplicationFile, + exportAdminApplications, getAdminApplication, listAdminApplications, } from '../../../api/admin/applications' import type { AdminApplicationDetail, + AdminApplicationFileRow, AdminApplicationRow, CompetitionPayload, CompetitionSuccessNoticeSettings, @@ -98,6 +100,7 @@ const channels = ref([]) const channelsLoading = ref(false) const applications = ref([]) const applicationsLoading = ref(false) +const applicationsExporting = ref(false) const applicationDetailLoading = ref(false) const applicationDetailVisible = ref(false) const applicationDetail = ref(null) @@ -110,6 +113,7 @@ const applicationFilters = ref({ keyword: '', status: 'submitted', track: '', + signup_channel_id: undefined as number | undefined, }) const trackDialogVisible = ref(false) @@ -280,7 +284,10 @@ watch(tabKey, (t) => { const cid = competitionId.value if (!cid) return if (t === 'channels') void refreshChannels() - if (t === 'applications') void refreshApplications() + if (t === 'applications') { + void refreshChannels() + void refreshApplications() + } if (t === 'signupForm') void refreshSignupSchemas() if (t === 'review') { void refreshReviewSchemas() @@ -661,6 +668,33 @@ function formatFileSize(size: number | null | undefined): string { return `${(size / 1024 / 1024).toFixed(1)}MB` } +const APPLICATION_FILE_KIND_ORDER = ['plan', 'supporting'] as const + +function applicationFileKindLabel(kind: string | null | undefined): string { + if (kind === 'plan') return '商业计划书' + if (kind === 'supporting') return '佐证材料' + return kind?.trim() || '其他附件' +} + +function applicationFileKindRank(kind: string | null | undefined): number { + const index = APPLICATION_FILE_KIND_ORDER.indexOf(kind as (typeof APPLICATION_FILE_KIND_ORDER)[number]) + return index === -1 ? APPLICATION_FILE_KIND_ORDER.length : index +} + +function sortApplicationDetailFiles(files: AdminApplicationFileRow[]): AdminApplicationFileRow[] { + return [...files].sort((a, b) => { + const kindDiff = applicationFileKindRank(a.kind) - applicationFileKindRank(b.kind) + if (kindDiff !== 0) return kindDiff + return (a.original_name ?? '').localeCompare(b.original_name ?? '', 'zh-CN') + }) +} + +const applicationDetailDisplayFiles = computed(() => { + const files = applicationDetail.value?.files + if (!files?.length) return [] + return sortApplicationDetailFiles(files) +}) + async function refreshApplications(page = applicationPager.value.page) { const cid = competitionId.value if (!cid) { @@ -676,6 +710,7 @@ async function refreshApplications(page = applicationPager.value.page) { keyword: applicationFilters.value.keyword.trim() || undefined, status: applicationFilters.value.status || undefined, track: applicationFilters.value.track || undefined, + signup_channel_id: applicationFilters.value.signup_channel_id || undefined, }) applications.value = res.data applicationPager.value = { @@ -692,10 +727,29 @@ async function refreshApplications(page = applicationPager.value.page) { } function resetApplicationFilters() { - applicationFilters.value = { keyword: '', status: 'submitted', track: '' } + applicationFilters.value = { keyword: '', status: 'submitted', track: '', signup_channel_id: undefined } void refreshApplications(1) } +async function exportApplications() { + const cid = competitionId.value + if (!cid) return + applicationsExporting.value = true + try { + await exportAdminApplications(cid, { + keyword: applicationFilters.value.keyword.trim() || undefined, + status: applicationFilters.value.status || undefined, + track: applicationFilters.value.track || undefined, + signup_channel_id: applicationFilters.value.signup_channel_id || undefined, + }) + ElMessage.success('导出已开始下载') + } catch (e) { + ElMessage.error(e instanceof Error ? e.message : '导出失败') + } finally { + applicationsExporting.value = false + } +} + async function openApplicationDetail(row: AdminApplicationRow) { const cid = competitionId.value if (!cid) return @@ -1313,7 +1367,7 @@ onMounted(() => {
-
+
{ class="application-filter-keyword" @keyup.enter="refreshApplications(1)" /> - + - + { :value="track.track_code" /> + + +
+ 导出 ZIP 重置 查询
+ + + + @@ -1619,15 +1701,16 @@ onMounted(() => {