From 375c18a6d2713ff19b22093eec57315992d8333f Mon Sep 17 00:00:00 2001
From: Codex Assistant <codex@example.com>
Date: 星期四, 06 十一月 2025 13:33:52 +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