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 | 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