From 858f515995fd1dca7cf825069ce38c32703298d0 Mon Sep 17 00:00:00 2001
From: peng <peng.com>
Date: 星期五, 07 十一月 2025 14:14:50 +0800
Subject: [PATCH] 报名人员导出
---
web/src/views/ActivityDetail.vue | 260 ++++++++++++++++++++++++++++++++++++++++++++++++---
1 files changed, 241 insertions(+), 19 deletions(-)
diff --git a/web/src/views/ActivityDetail.vue b/web/src/views/ActivityDetail.vue
index 7b33dda..61575e3 100644
--- a/web/src/views/ActivityDetail.vue
+++ b/web/src/views/ActivityDetail.vue
@@ -5,6 +5,7 @@
<div class="card-header">
<span>姣旇禌璇︽儏</span>
<div>
+ <el-button v-if="canExport" type="primary" :loading="exportingActivity" @click="handleExportActivity">瀵煎嚭璇勫ZIP</el-button>
<el-button @click="handleEdit">缂栬緫姣旇禌</el-button>
<el-button @click="goBack">杩斿洖</el-button>
</div>
@@ -21,10 +22,20 @@
<el-descriptions-item label="鎶ュ悕鎴鏃堕棿">{{ formatDateTime(activity.signupDeadline) }}</el-descriptions-item>
<el-descriptions-item label="姣旇禌寮�濮嬫椂闂�">{{ formatDateTime(activity.matchTime) }}</el-descriptions-item>
<el-descriptions-item label="姣旇禌鍦板潃">{{ activity.address || '-' }}</el-descriptions-item>
- <el-descriptions-item label="浜烘暟">{{ activity.playerMax || '-' }}</el-descriptions-item>
- <el-descriptions-item label="褰撳墠鎶ュ悕浜烘暟">{{ activity.playerCount || 0 }}</el-descriptions-item>
<el-descriptions-item label="璇勫垎妯℃澘">
{{ activity.ratingScheme ? activity.ratingScheme.name : '-' }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鏈�杩戣瘎瀹″鍑�" :span="2">
+ <template v-if="activity.reviewExportUrl">
+ <a :href="activity.reviewExportUrl" target="_blank">鐐瑰嚮涓嬭浇</a>
+ </template>
+ <template v-else>
+ 鏆傛棤
+ </template>
+ <span v-if="exportJobActivity && exportingActivity" class="export-status">
+ 瀵煎嚭浠诲姟杩涜涓�
+ <span v-if="exportJobActivity.progress !== null && exportJobActivity.progress !== undefined">锛坽{ exportJobActivity.progress }}%锛�</span>
+ </span>
</el-descriptions-item>
<el-descriptions-item label="鍒涘缓鏃堕棿" :span="2">{{ formatDateTime(activity.createTime) }}</el-descriptions-item>
</el-descriptions>
@@ -38,26 +49,17 @@
<!-- 姣旇禌闃舵 -->
<div v-if="activity.stages && activity.stages.length > 0" class="stages-section">
<h3>姣旇禌闃舵</h3>
- <el-table :data="activity.stages" style="width: 100%">
- <el-table-column prop="name" label="闃舵鍚嶇О" min-width="150" />
+ <el-table :data="sortedStages" style="width: 100%" table-layout="auto">
+ <el-table-column prop="sortOrder" label="椤哄簭" width="80" align="center" />
+ <el-table-column prop="name" label="闃舵鍚嶇О" width="200" show-overflow-tooltip />
<el-table-column prop="matchTime" label="寮�濮嬫椂闂�" width="180">
<template #default="{ row }">
{{ formatDateTime(row.matchTime) }}
</template>
</el-table-column>
- <el-table-column prop="address" label="鍦板潃" min-width="150">
+ <el-table-column prop="address" label="鍦板潃" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
{{ row.address || '-' }}
- </template>
- </el-table-column>
- <el-table-column prop="playerMax" label="鏈�澶т汉鏁�" width="100">
- <template #default="{ row }">
- {{ row.playerMax || '-' }}
- </template>
- </el-table-column>
- <el-table-column prop="playerCount" label="瀹為檯浜烘暟" width="100">
- <template #default="{ row }">
- {{ row.playerCount || 0 }}
</template>
</el-table-column>
<el-table-column prop="stateName" label="鐘舵��" width="100">
@@ -65,11 +67,57 @@
<el-tag :type="getStateType(row.state)">{{ row.stateName }}</el-tag>
</template>
</el-table-column>
- <el-table-column label="鎿嶄綔" width="200">
+ <el-table-column label="鏈�杩戝鍑�" width="200">
+ <template #default="{ row }">
+ <template v-if="row.reviewExportUrl">
+ <a :href="row.reviewExportUrl" target="_blank">涓嬭浇ZIP</a>
+ </template>
+ <template v-else>
+ <span style="color: #909399;">鏆傛棤</span>
+ </template>
+ <div v-if="exportJobStageMap[row.id] && exportingStageId === row.id" class="export-status">
+ 瀵煎嚭浠诲姟杩涜涓�
+ <span v-if="exportJobStageMap[row.id].progress !== null && exportJobStageMap[row.id].progress !== undefined">锛坽{ exportJobStageMap[row.id].progress }}%锛�</span>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="320" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="viewStageDetail(row)">鏌ョ湅璇︽儏</el-button>
<el-button size="small" type="warning" @click="closeStage(row)" v-if="row.state === 1">鍏抽棴</el-button>
<el-button size="small" type="danger" @click="deleteStage(row)">鍒犻櫎</el-button>
+ <el-button
+ v-if="canExport"
+ size="small"
+ type="primary"
+ :loading="exportingStageId === row.id"
+ @click="handleExportStage(row)"
+ >瀵煎嚭闃舵璇勫</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+
+ <!-- 璇勫淇℃伅 -->
+ <div v-if="activity.judges && activity.judges.length > 0" class="judges-section">
+ <h3>璇勫淇℃伅</h3>
+ <el-table :data="activity.judges" style="width: 100%">
+ <el-table-column prop="name" label="璇勫濮撳悕" min-width="120" />
+ <el-table-column prop="phone" label="鑱旂郴鐢佃瘽" width="150" />
+ <el-table-column prop="description" label="绠�浠�" min-width="200" show-overflow-tooltip />
+ <el-table-column label="璐熻矗闃舵" min-width="200">
+ <template #default="{ row }">
+ <el-tag
+ v-for="stageId in row.stageIds"
+ :key="stageId"
+ size="small"
+ style="margin-right: 5px;"
+ >
+ {{ getStageName(stageId) }}
+ </el-tag>
+ <el-tag v-if="!row.stageIds || row.stageIds.length === 0" type="info" size="small">
+ 鍏ㄩ儴闃舵
+ </el-tag>
</template>
</el-table-column>
</el-table>
@@ -96,7 +144,6 @@
</el-descriptions-item>
<el-descriptions-item label="寮�濮嬫椂闂�">{{ formatDateTime(selectedStage.matchTime) }}</el-descriptions-item>
<el-descriptions-item label="鍦板潃">{{ selectedStage.address || '-' }}</el-descriptions-item>
- <el-descriptions-item label="浜烘暟">{{ selectedStage.playerMax || '-' }}</el-descriptions-item>
<el-descriptions-item label="璇勫垎妯℃澘">
{{ selectedStage.ratingScheme ? selectedStage.ratingScheme.name : '缁ф壙姣旇禌妯℃澘' }}
</el-descriptions-item>
@@ -112,10 +159,12 @@
</template>
<script setup>
-import { ref, onMounted } from 'vue'
+import { ref, onMounted, onUnmounted, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getActivity } from '@/api/activity'
+import { exportReviewZip, startReviewExportJob, getReviewExportJobStatus } from '@/api/reviewExport'
+import { isEmployee } from '@/utils/auth'
const router = useRouter()
const route = useRoute()
@@ -125,6 +174,23 @@
const activity = ref(null)
const stageDialogVisible = ref(false)
const selectedStage = ref(null)
+const exportingActivity = ref(false)
+const exportingStageId = ref(null)
+// 寮傛瀵煎嚭浠诲姟鐘舵��
+const exportJobActivity = ref(null) // { jobId, status, progress, message }
+const exportJobStageMap = ref({}) // { [stageId]: { jobId, status, progress, message } }
+let activityPollTimer = null
+const stagePollTimers = {}
+
+// 璁$畻灞炴��
+const sortedStages = computed(() => {
+ if (!activity.value || !activity.value.stages) return []
+ return [...activity.value.stages].sort((a, b) => {
+ const orderA = a.sortOrder || 999
+ const orderB = b.sortOrder || 999
+ return orderA - orderB
+ })
+})
// 鍔犺浇姣旇禌璇︽儏
const loadActivity = async () => {
@@ -142,7 +208,7 @@
// 鎿嶄綔澶勭悊
const handleEdit = () => {
- router.push(`/activity/form/${route.params.id}`)
+ router.push(`/activity/edit/${route.params.id}`)
}
const goBack = () => {
@@ -207,10 +273,157 @@
}
}
+const getStageName = (stageId) => {
+ if (!activity.value) return '鏈煡闃舵'
+
+ // 鍙鏌ユ瘮璧涢樁娈�
+ if (activity.value.stages) {
+ const stage = activity.value.stages.find(s => s.id === stageId)
+ if (stage) {
+ return stage.name
+ }
+ }
+
+ return '鏈煡闃舵'
+}
+
// 鐢熷懡鍛ㄦ湡
onMounted(() => {
loadActivity()
})
+
+onUnmounted(() => {
+ if (activityPollTimer) {
+ clearInterval(activityPollTimer)
+ activityPollTimer = null
+ }
+ Object.keys(stagePollTimers).forEach((sid) => {
+ if (stagePollTimers[sid]) {
+ clearInterval(stagePollTimers[sid])
+ delete stagePollTimers[sid]
+ }
+ })
+})
+
+// 鏄惁鍙墽琛屽鍑猴紙浠呭憳宸ワ級
+const canExport = computed(() => isEmployee())
+
+// 瀵煎嚭涓绘椿鍔ㄨ瘎瀹IP
+const handleExportActivity = async () => {
+ try {
+ await ElMessageBox.confirm('灏嗗鍑鸿姣旇禌涓嬫墍鏈夐樁娈电殑宸茶瘎鍒嗘暟鎹负ZIP锛岀‘璁ょ户缁紵', '纭瀵煎嚭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ exportingActivity.value = true
+ // 浼樺厛浣跨敤寮傛瀵煎嚭浠诲姟
+ const jobId = await startReviewExportJob(route.params.id)
+ if (jobId) {
+ exportJobActivity.value = { jobId, status: 'PENDING', progress: 0 }
+ // 杞浠诲姟鐘舵��
+ activityPollTimer = setInterval(async () => {
+ try {
+ const status = await getReviewExportJobStatus(jobId)
+ if (status) {
+ exportJobActivity.value = status
+ if (status.status === 'SUCCEEDED') {
+ clearInterval(activityPollTimer)
+ activityPollTimer = null
+ ElMessage.success('瀵煎嚭鎴愬姛')
+ await loadActivity()
+ exportingActivity.value = false
+ } else if (status.status === 'FAILED') {
+ clearInterval(activityPollTimer)
+ activityPollTimer = null
+ ElMessage.error(status.message || '瀵煎嚭澶辫触')
+ exportingActivity.value = false
+ }
+ }
+ } catch (e) {
+ console.warn('鏌ヨ瀵煎嚭浠诲姟鐘舵�佸け璐ワ細', e?.message || e)
+ }
+ }, 2000)
+ } else {
+ // 鍥為��锛氫娇鐢ㄥ悓姝ュ鍑�
+ const res = await exportReviewZip(route.params.id)
+ if (res?.success) {
+ ElMessage.success('瀵煎嚭鎴愬姛')
+ await loadActivity()
+ } else {
+ ElMessage.error(res?.message || '瀵煎嚭澶辫触')
+ }
+ exportingActivity.value = false
+ }
+ } catch (e) {
+ // 鐢ㄦ埛鍙栨秷鎴栧紓甯�
+ if (e && e.message) {
+ console.warn('瀵煎嚭鎿嶄綔鍙栨秷鎴栧け璐ワ細', e.message)
+ }
+ } finally {
+ // 寮傛瀵煎嚭鏃剁敱杞鍥炶皟璐熻矗鍏抽棴 loading锛涙澶勪粎鍦ㄦ湭鍚姩浠诲姟鏃堕噸缃�
+ if (!activityPollTimer) {
+ exportingActivity.value = false
+ }
+ }
+}
+
+// 瀵煎嚭鎸囧畾闃舵璇勫ZIP
+const handleExportStage = async (stage) => {
+ try {
+ await ElMessageBox.confirm(`灏嗗鍑恒��${stage.name}銆戦樁娈电殑宸茶瘎鍒嗘暟鎹负ZIP锛岀‘璁ょ户缁紵`, '纭瀵煎嚭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ exportingStageId.value = stage.id
+ const jobId = await startReviewExportJob(route.params.id, [stage.id])
+ if (jobId) {
+ exportJobStageMap.value[stage.id] = { jobId, status: 'PENDING', progress: 0 }
+ // 杞闃舵瀵煎嚭鐘舵��
+ stagePollTimers[stage.id] = setInterval(async () => {
+ try {
+ const status = await getReviewExportJobStatus(jobId)
+ if (status) {
+ exportJobStageMap.value[stage.id] = status
+ if (status.status === 'SUCCEEDED') {
+ clearInterval(stagePollTimers[stage.id])
+ delete stagePollTimers[stage.id]
+ ElMessage.success('瀵煎嚭鎴愬姛')
+ await loadActivity()
+ exportingStageId.value = null
+ } else if (status.status === 'FAILED') {
+ clearInterval(stagePollTimers[stage.id])
+ delete stagePollTimers[stage.id]
+ ElMessage.error(status.message || '瀵煎嚭澶辫触')
+ exportingStageId.value = null
+ }
+ }
+ } catch (e) {
+ console.warn('鏌ヨ闃舵瀵煎嚭浠诲姟鐘舵�佸け璐ワ細', e?.message || e)
+ }
+ }, 2000)
+ } else {
+ // 鍥為��锛氬悓姝ュ鍑�
+ const res = await exportReviewZip(route.params.id, [stage.id])
+ if (res?.success) {
+ ElMessage.success('瀵煎嚭鎴愬姛')
+ await loadActivity()
+ } else {
+ ElMessage.error(res?.message || '瀵煎嚭澶辫触')
+ }
+ exportingStageId.value = null
+ }
+ } catch (e) {
+ if (e && e.message) {
+ console.warn('瀵煎嚭闃舵鎿嶄綔鍙栨秷鎴栧け璐ワ細', e.message)
+ }
+ } finally {
+ if (!stagePollTimers[stage.id]) {
+ exportingStageId.value = null
+ }
+ }
+}
</script>
<style scoped>
@@ -253,6 +466,15 @@
color: #303133;
}
+.judges-section {
+ margin-top: 30px;
+}
+
+.judges-section h3 {
+ margin-bottom: 15px;
+ color: #303133;
+}
+
.players-section {
margin-top: 30px;
}
--
Gitblit v1.8.0