// pages/judge/review.js const app = getApp() const { graphqlRequest, formatDate: formatDateUtil } = require('../../lib/utils') Page({ data: { loading: false, submitting: false, // 提交作品信息 submission: null, activityPlayerId: '', stageId: null, submissionId: null, // 活动信息 activity: null, // 评审标准 criteria: [], // 评分数据 scores: {}, // 评审意见 comment: '', // 总分 totalScore: 0, maxScore: 0, // 评审状态 reviewStatus: 'PENDING', // PENDING, COMPLETED // 已有评审记录 existingReview: null }, onLoad(options) { if (options.id) { this.setData({ activityPlayerId: options.id }) this.loadSubmissionDetail() } }, onShow() { // 页面显示时检查评审状态 if (this.data.submissionId) { this.checkReviewStatus() } }, transformMediaFile(file) { const url = file.fullUrl || file.url const thumbUrl = file.fullThumbUrl || url const ext = (file.fileExt || '').toLowerCase() let mediaType = 'file' if (file.mediaType === 1 || ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'heic'].includes(ext)) { mediaType = 'image' } else if (file.mediaType === 2 || ['mp4', 'mov', 'avi', 'wmv', 'mkv', 'webm', 'flv'].includes(ext)) { mediaType = 'video' } else if (ext === 'pdf') { mediaType = 'pdf' } else if (['doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'wps', 'txt', 'rtf'].includes(ext)) { mediaType = 'word' } return { id: file.id, name: file.name, url, thumbUrl, mediaType, size: file.fileSize || 0 } }, // 加载提交作品详情 async loadSubmissionDetail() { try { this.setData({ loading: true }) const query = ` query GetActivityPlayerDetail($id: ID!) { activityPlayerDetail(id: $id) { id projectName description activityName stageId state playerInfo { id name phone gender birthday education introduction userInfo { userId name phone avatarUrl } } regionInfo { id name fullPath } submissionFiles { id name fullUrl fullThumbUrl fileExt fileSize mediaType } ratingForm { schemeId schemeName totalMaxScore items { id name maxScore orderNo } } } } ` const result = await graphqlRequest(query, { id: this.data.activityPlayerId }) if (result && result.activityPlayerDetail) { const detail = result.activityPlayerDetail // 构建submission对象以兼容现有的WXML模板 const submission = { id: detail.id, title: detail.projectName, description: detail.description, submittedAt: detail.submitTime || null, team: detail.team || null, participant: { id: detail.playerInfo?.id, name: detail.playerInfo?.name, phone: detail.playerInfo?.phone || detail.playerInfo?.userInfo?.phone || '', avatar: detail.playerInfo?.userInfo?.avatarUrl || '/images/default-avatar.svg', gender: this.getGenderLabel(detail.playerInfo?.gender), birthday: this.getBirthdayText(detail.playerInfo?.birthday), region: detail.regionInfo?.fullPath || detail.regionInfo?.name || '', education: detail.playerInfo?.education || '', school: detail.regionInfo ? detail.regionInfo.name : '', major: detail.playerInfo?.education || '' }, status: detail.state === 1 ? 'APPROVED' : detail.state === 2 ? 'REJECTED' : 'PENDING', mediaList: (detail.submissionFiles || []).map(file => this.transformMediaFile(file)) } const criteria = (detail.ratingForm?.items || []).map(item => { const maxScore = item.maxScore || 0 return { id: item.id, name: item.name, maxScore, description: item.description || '暂无描述', step: maxScore > 20 ? 1 : 0.5, currentScore: 0 } }) const scores = {} criteria.forEach(criterion => { scores[criterion.id] = 0 }) const maxScore = detail.ratingForm?.totalMaxScore || criteria.reduce((sum, item) => sum + (item.maxScore || 0), 0) this.setData({ submission, activity: { id: detail.id, stageId: detail.stageId, ratingSchemeId: detail.ratingForm?.schemeId || null, totalMaxScore: maxScore }, stageId: detail.stageId || null, submissionId: detail.id, criteria, scores, maxScore, totalScore: 0, existingReview: null, reviewStatus: 'PENDING', comment: '' }) this.calculateTotalScore() // 检查是否已有评分 this.checkReviewStatus() } } catch (error) { console.error('加载作品详情失败:', error) wx.showToast({ title: '加载失败', icon: 'error' }) } finally { this.setData({ loading: false }) } }, // 检查评审状态 async checkReviewStatus() { try { const query = ` query GetCurrentJudgeRating($activityPlayerId: ID!) { currentJudgeRating(activityPlayerId: $activityPlayerId) { id totalScore remark status ratedAt items { ratingItemId ratingItemName score maxScore } } } ` const result = await graphqlRequest(query, { activityPlayerId: this.data.activityPlayerId }) if (result && result.currentJudgeRating) { const rating = result.currentJudgeRating // 如果已有评分,填充数据 const scores = {} let totalScore = 0 if (rating.items) { rating.items.forEach(item => { const numericScore = item.score !== undefined && item.score !== null ? Number(item.score) : 0 scores[item.ratingItemId] = numericScore totalScore += numericScore }) } const updatedCriteria = this.data.criteria.map(criterion => { const value = scores[criterion.id] !== undefined ? scores[criterion.id] : 0 return { ...criterion, currentScore: value } }) const normalizedTotal = Number(totalScore.toFixed(2)) this.setData({ scores, criteria: updatedCriteria, totalScore: normalizedTotal, comment: rating.remark || rating.comment || '', existingReview: { ...rating, totalScore: rating.totalScore ? Number(rating.totalScore) : normalizedTotal, reviewedAt: rating.ratedAt || rating.reviewedAt || rating.updateTime || null }, reviewStatus: 'COMPLETED' }) console.log('已加载现有评分:', rating) } else { console.log('当前评委尚未评分') } this.calculateTotalScore() } catch (error) { console.error('检查评审状态失败:', error) } }, normalizeScore(value, criterion) { const maxScore = Number(criterion.maxScore || 0) const step = Number(criterion.step || (maxScore > 20 ? 1 : 0.5)) if (Number.isNaN(value)) { value = 0 } let normalized = Math.round(value / step) * step if (normalized < 0) normalized = 0 if (normalized > maxScore) normalized = maxScore return Number(normalized.toFixed(2)) }, updateCriterionScore(criterionId, index, value) { const criterion = this.data.criteria[index] if (!criterion) return const normalized = this.normalizeScore(value, criterion) this.setData({ [`scores.${criterionId}`]: normalized, [`criteria[${index}].currentScore`]: normalized }) this.calculateTotalScore() }, // 评分改变 onScoreChange(e) { const { criterionId, index } = e.currentTarget.dataset const criterion = this.data.criteria[index] if (!criterion) return const inputValue = Number(e.detail.value) const newScore = this.normalizeScore(inputValue, criterion) this.updateCriterionScore(criterionId, index, newScore) }, increaseScore(e) { const { criterionId, index } = e.currentTarget.dataset const criterion = this.data.criteria[index] if (!criterion) return const current = Number(this.data.scores[criterionId] || criterion.currentScore || 0) const step = criterion.step || (criterion.maxScore > 20 ? 1 : 0.5) this.updateCriterionScore(criterionId, index, current + step) }, decreaseScore(e) { const { criterionId, index } = e.currentTarget.dataset const criterion = this.data.criteria[index] if (!criterion) return const current = Number(this.data.scores[criterionId] || criterion.currentScore || 0) const step = criterion.step || (criterion.maxScore > 20 ? 1 : 0.5) this.updateCriterionScore(criterionId, index, current - step) }, // 计算总分 calculateTotalScore() { const { scores, criteria } = this.data let totalScore = 0 criteria.forEach(criterion => { const score = Number(scores[criterion.id] || 0) totalScore += score }) this.setData({ totalScore: Number(totalScore.toFixed(2)) }) }, // 评审意见输入 onCommentInput(e) { this.setData({ comment: e.detail.value }) }, // 媒体点击 onMediaTap(e) { const index = Number(e.currentTarget.dataset.index) const mediaList = this.data.submission?.mediaList || [] const media = mediaList[index] if (!media) return if (media.mediaType === 'image') { const imageUrls = mediaList .filter(item => item.mediaType === 'image') .map(item => item.url) wx.previewImage({ current: media.url, urls: imageUrls }) } else if (media.mediaType === 'video') { wx.navigateTo({ url: `/pages/video/video?url=${encodeURIComponent(media.url)}&title=${encodeURIComponent(media.name)}` }) } else { this.openDocumentMedia(media) } }, async openDocumentMedia(media) { try { wx.showLoading({ title: '打开中...' }) const downloadRes = await new Promise((resolve, reject) => { wx.downloadFile({ url: media.url, success: resolve, fail: reject }) }) if (downloadRes.statusCode !== 200) { throw new Error('文件下载失败') } await new Promise((resolve, reject) => { wx.openDocument({ filePath: downloadRes.tempFilePath, showMenu: true, success: resolve, fail: reject }) }) } catch (error) { console.error('打开文件失败:', error) wx.showToast({ title: '无法打开文件', icon: 'error' }) } finally { wx.hideLoading() } }, // 验证评审数据 validateReview() { const { scores, criteria, comment } = this.data const commentText = (comment || '').trim() // 检查是否所有标准都已评分 for (let criterion of criteria) { if (!scores[criterion.id] || scores[criterion.id] === 0) { wx.showToast({ title: `请为"${criterion.name}"评分`, icon: 'error' }) return false } } // 检查评审意见 if (!commentText) { wx.showToast({ title: '请填写评审意见', icon: 'error' }) return false } if (commentText.length < 10) { wx.showToast({ title: '评审意见至少10个字符', icon: 'error' }) return false } return true }, // 提交评审 async onSubmitReview() { if (!this.validateReview()) { return } wx.showModal({ title: '确认提交', content: '评审提交后将无法修改,确定要提交吗?', success: async (res) => { if (res.confirm) { await this.submitReview() } } }) }, // 执行提交评审 async submitReview() { try { this.setData({ submitting: true }) wx.showLoading({ title: '提交中...' }) const { activityPlayerId, scores, comment, criteria, stageId } = this.data const commentText = (comment || '').trim() if (!stageId) { wx.showToast({ title: '缺少阶段信息,无法提交', icon: 'none' }) this.setData({ submitting: false }) wx.hideLoading() return } // 构建评分项数组 const ratings = criteria.map(criterion => ({ itemId: criterion.id, score: Number(scores[criterion.id] || 0) })) const mutation = ` mutation SaveActivityPlayerRating($input: ActivityPlayerRatingInput!) { saveActivityPlayerRating(input: $input) } ` const input = { activityPlayerId, stageId, ratings, comment: commentText } const result = await graphqlRequest(mutation, { input }) if (result && result.saveActivityPlayerRating) { wx.showToast({ title: '评分提交成功', icon: 'success' }) // 更新状态 this.setData({ reviewStatus: 'COMPLETED' }) // 重新加载评分状态 await this.checkReviewStatus() // 延迟返回上一页 setTimeout(() => { wx.navigateBack() }, 1500) } } catch (error) { console.error('提交评分失败:', error) wx.showToast({ title: '提交失败', icon: 'error' }) } finally { this.setData({ submitting: false }) wx.hideLoading() } }, // 查看其他评审 onViewOtherReviews() { wx.navigateTo({ url: `/pages/judge/reviews?activityPlayerId=${this.data.activityPlayerId}` }) }, // 联系参赛者 onContactParticipant() { const { submission } = this.data const phone = submission?.participant?.phone if (phone) { wx.makePhoneCall({ phoneNumber: phone }) } else { wx.showToast({ title: '暂无联系方式', icon: 'none' }) } }, // 获取文件大小文本 getFileSizeText(size) { if (size < 1024) { return `${size}B` } else if (size < 1024 * 1024) { return `${(size / 1024).toFixed(1)}KB` } else { return `${(size / (1024 * 1024)).toFixed(1)}MB` } }, // 统一处理性别显示文本 getGenderLabel(gender) { if (gender === null || gender === undefined || gender === '') { return '未填写' } const normalized = String(gender).trim().toLowerCase() if (normalized === '') { return '未填写' } if (normalized === 'male' || normalized === 'm') { return '男' } if (normalized === 'female' || normalized === 'f') { return '女' } if (/^-?\d+$/.test(normalized)) { const numeric = Number(normalized) if (numeric === 1) return '男' if (numeric === 0) return '女' if (numeric === 2) return '女' } return gender === undefined || gender === null ? '未填写' : String(gender) }, // 统一处理出生日期显示文本 getBirthdayText(dateString) { if (!dateString) { return '未填写' } const formatted = formatDateUtil(dateString, 'YYYY-MM-DD') return formatted || '未填写' }, // 格式化日期 formatDate(dateString) { return formatDateUtil(dateString, 'YYYY-MM-DD HH:mm') }, // 分享页面 onShareAppMessage() { return { title: '蓉易创 - 评审作品', path: '/pages/index/index' } } })