From 58d9f460b2f8c34430285115e2557d18333c5cab Mon Sep 17 00:00:00 2001
From: Codex Assistant <codex@example.com>
Date: 星期三, 08 十月 2025 14:16:55 +0800
Subject: [PATCH] feat: 修复Player实体phone字段数据冗余问题并优化小程序报名逻辑

---
 web/src/views/review-detail.vue |  337 +++++++++++++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 311 insertions(+), 26 deletions(-)

diff --git a/web/src/views/review-detail.vue b/web/src/views/review-detail.vue
index f59bbe1..fd0a3be 100644
--- a/web/src/views/review-detail.vue
+++ b/web/src/views/review-detail.vue
@@ -14,12 +14,15 @@
           <div class="project-section">
             <!-- 椤圭洰鍩烘湰淇℃伅 -->
             <h4>椤圭洰淇℃伅</h4>
-            <el-descriptions :column="2" border>
-              <el-descriptions-item label="椤圭洰鍚嶇О">
-                {{ projectDetail.projectName || '鏈~鍐�' }}
+            <el-descriptions :column="2" border class="project-info">
+              <el-descriptions-item label="姣旇禌鍚嶇О" :span="2">
+                {{ competitionName || '鏈~鍐�' }}
               </el-descriptions-item>
-              <el-descriptions-item label="姣旇禌鍚嶇О">
-                {{ projectDetail.activityName }}
+              <el-descriptions-item label="姣旇禌闃舵" :span="2">
+                {{ stageName || projectDetail.activityName || '鏈~鍐�' }}
+              </el-descriptions-item>
+              <el-descriptions-item label="鍙傝禌椤圭洰鍚嶇О" :span="2">
+                {{ projectDetail.projectName || '鏈~鍐�' }}
               </el-descriptions-item>
               <el-descriptions-item label="椤圭洰鎻忚堪" :span="2">
                 <div class="description-content">
@@ -118,6 +121,7 @@
                   :step="0.5"
                   size="small"
                   style="width: 100%; margin-top: 8px;"
+                  :disabled="!canModifyRating"
                 />
               </div>
 
@@ -128,17 +132,29 @@
                   v-model="ratingComment"
                   type="textarea"
                   :rows="4"
-                  placeholder="璇疯緭鍏ヨ瘎璇紙鍙�夛級"
+                  :placeholder="canModifyRating ? '璇疯緭鍏ヨ瘎璇紙鍙�夛級' : '璇勮锛堝彧璇伙級'"
                   maxlength="500"
                   show-word-limit
+                  :disabled="!canModifyRating"
                 />
               </div>
 
               <!-- 鎻愪氦鎸夐挳 -->
-              <div class="submit-section">
+              <div class="submit-section" v-if="canModifyRating">
                 <el-button type="primary" @click="handleSubmitRating" :loading="submitting" style="width: 100%;">
                   鎻愪氦璇勫垎
                 </el-button>
+              </div>
+              
+              <!-- Employee鐢ㄦ埛鎻愮ず -->
+              <div class="readonly-notice" v-if="isEmployee && !canModifyRating">
+                <el-alert
+                  title="鍙妯″紡"
+                  description="鎮ㄤ互鍛樺伐韬唤鏌ョ湅姝よ瘎瀹¤鎯咃紝鍙兘鏌ョ湅涓嶈兘淇敼璇勫垎"
+                  type="info"
+                  :closable="false"
+                  show-icon
+                />
               </div>
             </div>
             <div v-else class="no-template">
@@ -152,13 +168,17 @@
     <!-- 鏂囦欢棰勮瀵硅瘽妗� -->
     <el-dialog v-model="previewVisible" title="鏂囦欢棰勮" width="80%" center>
       <div class="preview-content">
-        <iframe 
-          v-if="previewUrl" 
-          :src="previewUrl" 
-          style="width: 100%; height: 500px; border: none;"
-        ></iframe>
+        <!-- 鍥剧墖棰勮 -->
+        <img v-if="previewType === 'image' && previewUrl" :src="previewUrl" style="max-width: 100%; max-height: 70vh; object-fit: contain;" />
+        <!-- 瑙嗛棰勮 -->
+        <video v-else-if="previewType === 'video' && previewUrl" :src="previewUrl" controls style="width: 100%; max-height: 70vh;"></video>
+        <!-- PDF 棰勮 -->
+        <iframe v-else-if="previewType === 'pdf' && previewUrl" :src="previewUrl" style="width: 100%; height: 70vh; border: none;"></iframe>
+        <!-- DOCX 棰勮 -->
+        <div v-else-if="previewType === 'docx'" ref="docxContainer" class="docx-preview"></div>
+        <!-- 鍏跺畠涓嶆敮鎸� -->
         <div v-else class="preview-error">
-          <el-empty description="鏃犳硶棰勮姝ゆ枃浠剁被鍨�" />
+          <el-empty description="鏃犳硶棰勮姝ゆ枃浠剁被鍨嬶紝璇蜂笅杞芥煡鐪�" />
         </div>
       </div>
     </el-dialog>
@@ -170,7 +190,9 @@
 import { useRoute, useRouter } from 'vue-router'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { Document, UserFilled } from '@element-plus/icons-vue'
-import { getProjectDetail, getRatingStats, submitRating } from '@/api/projectReview'
+import { getProjectDetail, getRatingStats, submitRating, getCurrentJudgeRating, getActiveActivities } from '@/api/projectReview'
+import { userApi } from '@/api/user'
+import { getUserInfo } from '@/utils/auth'
 
 const route = useRoute()
 const router = useRouter()
@@ -180,13 +202,157 @@
 const submitting = ref(false)
 const projectDetail = ref(null)
 const ratingStats = ref({ ratingCount: 0, averageScore: 0 })
+const competitionName = ref('')
+const stageName = ref('')
 const ratingItems = ref([])
 const ratingComment = ref('')
 const previewVisible = ref(false)
 const previewUrl = ref('')
+const previewType = ref('') // image | video | pdf | docx | unknown
+const docxContainer = ref(null)
+
+// 鏉冮檺楠岃瘉鐩稿叧
+const currentJudge = ref(null)
+const hasJudgePermission = ref(false)
+const isJudgeInActivity = ref(false)
+const permissionChecked = ref(false)
+const existingRating = ref(null)
+const isEmployee = ref(false)
+const canModifyRating = ref(false)
 
 // 璁$畻灞炴��
 const projectId = computed(() => route.params.id)
+const stageId = computed(() => route.query.stageId || (projectDetail.value ? projectDetail.value.stageId : null))
+
+const loadStageMeta = async () => {
+  try {
+    if (!projectDetail.value || !projectDetail.value.stageId) return
+    const stages = await getActiveActivities()
+    const stage = (stages || []).find(s => String(s.id) === String(projectDetail.value.stageId))
+    if (stage) {
+      stageName.value = stage.name || ''
+      competitionName.value = stage.parent?.name || ''
+    } else {
+      stageName.value = projectDetail.value.activityName || ''
+      competitionName.value = ''
+    }
+  } catch (e) {
+    stageName.value = projectDetail.value?.activityName || ''
+    competitionName.value = ''
+  }
+}
+
+// 鏉冮檺楠岃瘉鏂规硶
+const checkPermissions = async () => {
+  try {
+    // 鑾峰彇褰撳墠鐢ㄦ埛淇℃伅
+    const userInfo = getUserInfo()
+    
+    if (!userInfo) {
+      ElMessage.error('鐢ㄦ埛淇℃伅鑾峰彇澶辫触锛岃閲嶆柊鐧诲綍')
+      router.push('/project-review')
+      return false
+    }
+    
+    // 璁剧疆鍛樺伐韬唤鏍囪瘑
+    isEmployee.value = !!userInfo.employee
+    
+    // 浼樺厛妫�鏌ヨ瘎濮旇韩浠藉拰鏉冮檺锛堝嵆浣跨敤鎴峰悓鏃舵湁鍛樺伐韬唤锛�
+    const judgeInfo = await userApi.getCurrentJudgeInfo()
+    
+    if (judgeInfo) {
+      currentJudge.value = judgeInfo
+      hasJudgePermission.value = true
+      
+      // 妫�鏌ユ槸鍚﹀湪褰撳墠姣旇禌闃舵鐨勮瘎濮斿垪琛ㄤ腑
+      if (projectDetail.value && projectDetail.value.stageId) {
+        const isInActivity = await userApi.checkJudgeInActivity(
+          projectDetail.value.stageId, 
+          judgeInfo.judgeId
+        )
+        
+        if (isInActivity) {
+          isJudgeInActivity.value = true
+          canModifyRating.value = true // 鏈夎瘎濮旀潈闄愶紝鍙互淇敼璇勫垎
+          permissionChecked.value = true
+          
+          if (isEmployee.value) {
+            ElMessage.success('鎮ㄥ悓鏃舵嫢鏈夊憳宸ュ拰璇勫韬唤锛屽綋鍓嶄互璇勫韬唤杩涜璇勫')
+          }
+          return true
+        } else {
+          isJudgeInActivity.value = false
+          // 濡傛灉娌℃湁褰撳墠姣旇禌鐨勮瘎濮旀潈闄愶紝浣嗘湁鍛樺伐韬唤锛屽垯浠ュ憳宸ヨ韩浠芥煡鐪�
+          if (isEmployee.value) {
+            canModifyRating.value = false
+            permissionChecked.value = true
+            ElMessage.info('鎮ㄦ病鏈夊綋鍓嶆瘮璧涚殑璇勫鏉冮檺锛屼互鍛樺伐韬唤鏌ョ湅璇勫璇︽儏')
+            return true
+          } else {
+            ElMessage.error('鎮ㄤ笉鏄綋鍓嶆瘮璧涚殑璇勫锛屾棤娉曡繘琛岃瘎瀹�')
+            router.push('/project-review')
+            return false
+          }
+        }
+      }
+    }
+    
+    // 濡傛灉娌℃湁璇勫韬唤锛屼絾鏈夊憳宸ヨ韩浠斤紝鍒欎互鍛樺伐韬唤鏌ョ湅
+    if (isEmployee.value) {
+      hasJudgePermission.value = false
+      canModifyRating.value = false // employee鍙兘鏌ョ湅锛屼笉鑳戒慨鏀�
+      permissionChecked.value = true
+      ElMessage.info('鎮ㄤ互鍛樺伐韬唤鏌ョ湅璇勫璇︽儏锛屽彧鑳芥煡鐪嬩笉鑳戒慨鏀硅瘎鍒�')
+      return true
+    }
+    
+    // 鏃㈡病鏈夎瘎濮旇韩浠戒篃娌℃湁鍛樺伐韬唤
+    hasJudgePermission.value = false
+    ElMessage.error('鎮ㄦ病鏈夎瘎濮旀潈闄愶紝鏃犳硶杩涜璇勫')
+    router.push('/project-review')
+    return false
+    
+  } catch (error) {
+    console.error('鏉冮檺楠岃瘉澶辫触:', error)
+    hasJudgePermission.value = false
+    ElMessage.error('鏉冮檺楠岃瘉澶辫触锛岃閲嶆柊鐧诲綍')
+    router.push('/project-review')
+    return false
+  }
+}
+
+// 鍔犺浇褰撳墠璇勫宸叉湁鐨勮瘎瀹℃暟鎹�
+const loadExistingRating = async () => {
+  // 鍙湁鍏锋湁璇勫鏉冮檺涓斿彲浠ヤ慨鏀硅瘎鍒嗙殑鐢ㄦ埛鎵嶉渶瑕佸姞杞借瘎濮旂殑璇勫垎鏁版嵁
+  if (!hasJudgePermission.value || !canModifyRating.value) return
+  
+  try {
+    const rating = await getCurrentJudgeRating(parseInt(projectId.value))
+    if (rating) {
+      existingRating.value = rating
+      
+      // 濡傛灉鏈夊凡鏈夎瘎鍒嗭紝濉厖鍒拌〃鍗曚腑
+      if (rating.items && rating.items.length > 0) {
+        ratingItems.value = rating.items.map(item => ({
+          id: item.ratingItemId,
+          name: item.ratingItemName,
+          score: item.score,
+          maxScore: item.maxScore || 100
+        }))
+      }
+      
+      // 濉厖璇勮
+      if (rating.remark) {
+        ratingComment.value = rating.remark
+      }
+      
+      ElMessage.success('宸插姞杞芥偍涔嬪墠鐨勮瘎鍒嗘暟鎹紝鍙互缁х画缂栬緫')
+    }
+  } catch (error) {
+    console.error('鍔犺浇宸叉湁璇勫垎澶辫触:', error)
+    // 涓嶆樉绀洪敊璇秷鎭紝鍥犱负鍙兘鏄涓�娆¤瘎鍒�
+  }
+}
 
 // 鍔犺浇椤圭洰璇︽儏
 const loadProjectDetail = async () => {
@@ -194,6 +360,7 @@
   try {
     const data = await getProjectDetail(projectId.value)
     projectDetail.value = data
+    await loadStageMeta()
     
     // 鍒濆鍖栬瘎鍒嗛」
     if (data.ratingForm && data.ratingForm.items) {
@@ -202,6 +369,14 @@
         score: 0
       }))
     }
+    
+    // 椤圭洰璇︽儏鍔犺浇瀹屾垚鍚庯紝杩涜鏉冮檺楠岃瘉
+    const hasPermission = await checkPermissions()
+    if (hasPermission) {
+      // 鏉冮檺楠岃瘉閫氳繃鍚庯紝鍔犺浇宸叉湁璇勫垎鏁版嵁
+      await loadExistingRating()
+    }
+    
   } catch (error) {
     ElMessage.error('鍔犺浇椤圭洰璇︽儏澶辫触')
     console.error(error)
@@ -222,6 +397,19 @@
 
 // 鎻愪氦璇勫垎
 const handleSubmitRating = async () => {
+  // 鏉冮檺妫�鏌ワ細employee鐢ㄦ埛涓嶈兘鎻愪氦璇勫垎
+  if (!canModifyRating.value) {
+    ElMessage.error('鎮ㄦ病鏈夋潈闄愭彁浜よ瘎鍒�')
+    return
+  }
+  
+  // 缁熶竴鑾峰彇stageId锛堜紭鍏堣矾鐢卞弬鏁帮紝鍏舵璇︽儏閲岀殑stageId锛�
+  const sid = stageId.value ? parseInt(stageId.value) : (projectDetail.value?.stageId ? parseInt(projectDetail.value.stageId) : null)
+  if (!sid) {
+    ElMessage.error('缂哄皯姣旇禌闃舵淇℃伅锛岃閲嶆柊杩涘叆椤甸潰')
+    return
+  }
+
   // 楠岃瘉璇勫垎
   const hasEmptyScore = ratingItems.value.some(item => item.score === 0 || item.score === null)
   if (hasEmptyScore) {
@@ -239,7 +427,8 @@
     submitting.value = true
     
     const ratingData = {
-      activityPlayerId: projectId.value,
+      activityPlayerId: parseInt(projectId.value),
+      stageId: sid,
       ratings: ratingItems.value.map(item => ({
         itemId: item.id,
         score: item.score
@@ -263,17 +452,84 @@
   }
 }
 
-// 鏂囦欢棰勮
-const previewFile = (file) => {
-  // 鏍规嵁鏂囦欢绫诲瀷鍐冲畾棰勮鏂瑰紡
-  const fileExtension = file.name.split('.').pop().toLowerCase()
-  const previewableTypes = ['pdf', 'txt', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'mp4', 'avi', 'mov', 'wmv', 'flv', 'webm']
-  
-  if (previewableTypes.includes(fileExtension)) {
-    // 鍦ㄦ柊绐楀彛涓墦寮�棰勮
-    window.open(file.url, '_blank')
-  } else {
-    ElMessage.warning('姝ゆ枃浠剁被鍨嬩笉鏀寔棰勮锛岃涓嬭浇鏌ョ湅')
+/**
+ * 鏂囦欢棰勮锛氭寜鎵╁睍鍚嶅垎鍙戝埌鍥剧墖/瑙嗛/PDF/DOCX
+ */
+const previewFile = async (file) => {
+  const ext = (file.name?.split('.').pop() || '').toLowerCase()
+  previewVisible.value = true
+  previewUrl.value = ''
+  previewType.value = ''
+  // 缁熶竴鍙栧彲鐢ㄧ殑瀹屾暣 URL
+  const url = file.url || file.fullUrl || file.full_path || file.path
+
+  const imageExts = ['jpg','jpeg','png','gif','bmp','webp']
+  const videoExts = ['mp4','webm','ogg','avi','mov','wmv','flv','mkv']
+
+  if (imageExts.includes(ext)) {
+    previewType.value = 'image'
+    previewUrl.value = url
+    return
+  }
+  if (videoExts.includes(ext)) {
+    previewType.value = 'video'
+    previewUrl.value = url
+    return
+  }
+  if (ext === 'pdf') {
+    previewType.value = 'pdf'
+    previewUrl.value = url
+    return
+  }
+  if (ext === 'docx') {
+    previewType.value = 'docx'
+    try {
+      await renderDocx(url)
+    } catch (e) {
+      console.error('DOCX 棰勮澶辫触:', e)
+      ElMessage.warning('DOCX 棰勮澶辫触锛屽缓璁笅杞芥煡鐪�')
+      previewType.value = 'unknown'
+    }
+    return
+  }
+  if (ext === 'doc') {
+    ElMessage.info('鏆備笉鏀寔 .doc 棰勮锛岃涓嬭浇鏌ョ湅')
+    previewType.value = 'unknown'
+    return
+  }
+
+  ElMessage.warning('姝ゆ枃浠剁被鍨嬩笉鏀寔棰勮锛岃涓嬭浇鏌ョ湅')
+  previewType.value = 'unknown'
+}
+
+/**
+ * 鍔ㄦ�佸姞杞� docx-preview 骞舵覆鏌� DOCX
+ */
+const renderDocx = async (url) => {
+  const ensureScript = () => new Promise((resolve, reject) => {
+    if (window.docx && window.docx.renderAsync) return resolve(true)
+    const existed = document.querySelector('script[data-docx-preview]')
+    if (existed) {
+      existed.addEventListener('load', () => resolve(true))
+      existed.addEventListener('error', reject)
+      return
+    }
+    const s = document.createElement('script')
+    s.src = 'https://unpkg.com/docx-preview/dist/docx-preview.min.js'
+    s.async = true
+    s.setAttribute('data-docx-preview', '1')
+    s.onload = () => resolve(true)
+    s.onerror = reject
+    document.head.appendChild(s)
+  })
+
+  await ensureScript()
+  const res = await fetch(url, { credentials: 'include' })
+  if (!res.ok) throw new Error('鑾峰彇 DOCX 澶辫触: ' + res.status)
+  const blob = await res.blob()
+  if (docxContainer.value) {
+    docxContainer.value.innerHTML = ''
+    await window.docx.renderAsync(blob, docxContainer.value, null, { inWrapper: true })
   }
 }
 
@@ -360,6 +616,7 @@
 .description-content {
   line-height: 1.6;
   color: #606266;
+  white-space: pre-wrap;
 }
 
 .attachments {
@@ -515,6 +772,13 @@
 .preview-content {
   text-align: center;
 }
+.docx-preview {
+  text-align: left;
+  max-height: 70vh;
+  overflow: auto;
+  background: #fff;
+  padding: 12px;
+}
 
 .preview-error {
   padding: 40px 0;
@@ -527,4 +791,25 @@
 :deep(.el-card__body) {
   padding: 16px;
 }
+
+/* 浠呴拡瀵归」鐩俊鎭繖缁勬弿杩拌缃爣绛�/鍐呭瀹藉害姣斾緥 */
+.project-info :deep(.el-descriptions__label) {
+  width: 40% !important;
+  min-width: 40%;
+  box-sizing: border-box;
+}
+.project-info :deep(.el-descriptions__content) {
+  width: 60% !important;
+  min-width: 60%;
+  box-sizing: border-box;
+}
+
+/* 绐勫睆鑷�傚簲锛氬皬灞忔椂鍥為��涓轰笂涓嬬粨鏋� */
+@media (max-width: 768px) {
+  .project-info :deep(.el-descriptions__label),
+  .project-info :deep(.el-descriptions__content) {
+    width: 100% !important;
+    min-width: 100%;
+  }
+}
 </style>
\ No newline at end of file

--
Gitblit v1.8.0