From afeeed281e60466b576fbe74d339634cc5d07b82 Mon Sep 17 00:00:00 2001 From: Codex Assistant <codex@example.com> Date: 星期三, 08 十月 2025 08:56:42 +0800 Subject: [PATCH] 修复评审功能和用户认证问题 --- web/src/views/review-detail.vue | 273 +++++++++++++++++++++++++++++++++++++++++------------- 1 files changed, 208 insertions(+), 65 deletions(-) diff --git a/web/src/views/review-detail.vue b/web/src/views/review-detail.vue index 2dcace8..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"> @@ -165,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> @@ -183,7 +190,7 @@ import { useRoute, useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' import { Document, UserFilled } from '@element-plus/icons-vue' -import { getProjectDetail, getRatingStats, submitRating, getCurrentJudgeRating } from '@/api/projectReview' +import { getProjectDetail, getRatingStats, submitRating, getCurrentJudgeRating, getActiveActivities } from '@/api/projectReview' import { userApi } from '@/api/user' import { getUserInfo } from '@/utils/auth' @@ -195,10 +202,14 @@ 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) @@ -211,7 +222,25 @@ // 璁$畻灞炴�� const projectId = computed(() => route.params.id) -const stageId = computed(() => route.query.stageId) +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 () => { @@ -225,48 +254,64 @@ return false } - // 妫�鏌ユ槸鍚︽湁employee韬唤 - if (userInfo.employee) { - isEmployee.value = true + // 璁剧疆鍛樺伐韬唤鏍囪瘑 + 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 } - // 濡傛灉娌℃湁employee韬唤锛屾鏌udge韬唤鍜屾潈闄� - const judgeInfo = await userApi.getCurrentJudgeInfo() + // 鏃㈡病鏈夎瘎濮旇韩浠戒篃娌℃湁鍛樺伐韬唤 + hasJudgePermission.value = false + ElMessage.error('鎮ㄦ病鏈夎瘎濮旀潈闄愶紝鏃犳硶杩涜璇勫') + router.push('/project-review') + return false - if (!judgeInfo) { - hasJudgePermission.value = false - ElMessage.error('鎮ㄦ病鏈夎瘎濮旀潈闄愶紝鏃犳硶杩涜璇勫') - router.push('/project-review') - return false - } - - 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 = false - ElMessage.error('鎮ㄤ笉鏄綋鍓嶆瘮璧涚殑璇勫锛屾棤娉曡繘琛岃瘎瀹�') - router.push('/project-review') - return false - } - - isJudgeInActivity.value = true - canModifyRating.value = true // judge鏈夋潈闄愪慨鏀硅瘎鍒� - } - - permissionChecked.value = true - return true } catch (error) { console.error('鏉冮檺楠岃瘉澶辫触:', error) hasJudgePermission.value = false @@ -278,8 +323,8 @@ // 鍔犺浇褰撳墠璇勫宸叉湁鐨勮瘎瀹℃暟鎹� const loadExistingRating = async () => { - // employee鐢ㄦ埛涓嶉渶瑕佸姞杞借瘎濮旂殑璇勫垎鏁版嵁 - if (isEmployee.value || !hasJudgePermission.value) return + // 鍙湁鍏锋湁璇勫鏉冮檺涓斿彲浠ヤ慨鏀硅瘎鍒嗙殑鐢ㄦ埛鎵嶉渶瑕佸姞杞借瘎濮旂殑璇勫垎鏁版嵁 + if (!hasJudgePermission.value || !canModifyRating.value) return try { const rating = await getCurrentJudgeRating(parseInt(projectId.value)) @@ -315,6 +360,7 @@ try { const data = await getProjectDetail(projectId.value) projectDetail.value = data + await loadStageMeta() // 鍒濆鍖栬瘎鍒嗛」 if (data.ratingForm && data.ratingForm.items) { @@ -357,12 +403,13 @@ return } - // 楠岃瘉stageId - if (!stageId.value) { + // 缁熶竴鑾峰彇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) { @@ -381,7 +428,7 @@ const ratingData = { activityPlayerId: parseInt(projectId.value), - stageId: parseInt(stageId.value), + stageId: sid, ratings: ratingItems.value.map(item => ({ itemId: item.id, score: item.score @@ -405,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 }) } } @@ -502,6 +616,7 @@ .description-content { line-height: 1.6; color: #606266; + white-space: pre-wrap; } .attachments { @@ -657,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; @@ -669,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