Codex Assistant
昨天 0a48616045ddce1562584543a0e89e5144051fde
wx/pages/judge/review.js
@@ -1,6 +1,6 @@
// pages/judge/review.js
const app = getApp()
const { graphqlRequest, formatDate } = require('../../lib/utils')
const { graphqlRequest, formatDate: formatDateUtil } = require('../../lib/utils')
Page({
  data: {
@@ -9,14 +9,16 @@
    
    // 提交作品信息
    submission: null,
    submissionId: '',
    activityPlayerId: '',
    stageId: null,
    submissionId: null,
    // 活动信息
    activity: null,
    // 评审标准
    criteria: [],
    // 评分数据
    scores: {},
    
@@ -31,29 +33,12 @@
    reviewStatus: 'PENDING', // PENDING, COMPLETED
    
    // 已有评审记录
    existingReview: null,
    // 媒体预览
    showMediaPreview: false,
    currentMedia: null,
    mediaType: 'image',
    // 文件下载
    downloadingFiles: [],
    // 评分等级
    scoreOptions: [
      { value: 1, label: '1分 - 很差' },
      { value: 2, label: '2分 - 较差' },
      { value: 3, label: '3分 - 一般' },
      { value: 4, label: '4分 - 良好' },
      { value: 5, label: '5分 - 优秀' }
    ]
    existingReview: null
  },
  onLoad(options) {
    if (options.id) {
      this.setData({ submissionId: options.id })
      this.setData({ activityPlayerId: options.id })
      this.loadSubmissionDetail()
    }
  },
@@ -65,105 +50,160 @@
    }
  },
  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 GetSubmissionDetail($id: ID!) {
          submission(id: $id) {
        query GetActivityPlayerDetail($id: ID!) {
          activityPlayerDetail(id: $id) {
            id
            title
            projectName
            description
            files {
            activityName
            stageId
            state
            playerInfo {
              id
              name
              url
              type
              size
            }
            images
            videos
            submittedAt
            status
            participant {
              id
              name
              school
              major
              avatar
            }
            team {
              id
              name
              members {
                id
              phone
              gender
              birthday
              education
              introduction
              userInfo {
                userId
                name
                role
                phone
                avatarUrl
              }
            }
            activity {
            regionInfo {
              id
              title
              description
              judgeCriteria {
              name
              fullPath
            }
            submissionFiles {
              id
              name
              fullUrl
              fullThumbUrl
              fileExt
              fileSize
              mediaType
            }
            ratingForm {
              schemeId
              schemeName
              totalMaxScore
              items {
                id
                name
                description
                maxScore
                weight
                orderNo
              }
            }
            myReview {
              id
              scores
              comment
              totalScore
              status
              reviewedAt
            }
          }
        }
      `
      
      const result = await graphqlRequest(query, { id: this.data.submissionId })
      const result = await graphqlRequest(query, { id: this.data.activityPlayerId })
      
      if (result && result.submission) {
        const submission = result.submission
      if (result && result.activityPlayerDetail) {
        const detail = result.activityPlayerDetail
        
        // 为每个文件添加下载状态
        if (submission.files) {
          submission.files = submission.files.map(file => ({
            ...file,
            isDownloading: this.data.downloadingFiles.indexOf(file.id) > -1
          }))
        // 构建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 = {}
        let maxScore = 0
        if (submission.activity.judgeCriteria) {
          submission.activity.judgeCriteria.forEach(criterion => {
            scores[criterion.id] = submission.myReview ?
              submission.myReview.scores[criterion.id] || 0 : 0
            maxScore += criterion.maxScore
          })
        }
        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: submission.activity,
          criteria: submission.activity.judgeCriteria || [],
          activity: {
            id: detail.id,
            stageId: detail.stageId,
            ratingSchemeId: detail.ratingForm?.schemeId || null,
            totalMaxScore: maxScore
          },
          stageId: detail.stageId || null,
          submissionId: detail.id,
          criteria,
          scores,
          maxScore,
          existingReview: submission.myReview,
          reviewStatus: submission.myReview ? submission.myReview.status : 'PENDING',
          comment: submission.myReview ? submission.myReview.comment : ''
          totalScore: 0,
          existingReview: null,
          reviewStatus: 'PENDING',
          comment: ''
        })
        this.calculateTotalScore()
        // 检查是否已有评分
        this.checkReviewStatus()
      }
    } catch (error) {
      console.error('加载作品详情失败:', error)
@@ -180,46 +220,126 @@
  async checkReviewStatus() {
    try {
      const query = `
        query CheckReviewStatus($submissionId: ID!) {
          reviewStatus(submissionId: $submissionId) {
        query GetCurrentJudgeRating($activityPlayerId: ID!) {
          currentJudgeRating(activityPlayerId: $activityPlayerId) {
            id
            totalScore
            remark
            status
            canReview
            deadline
            ratedAt
            items {
              ratingItemId
              ratingItemName
              score
              maxScore
            }
          }
        }
      `
      
      const result = await graphqlRequest(query, { submissionId: this.data.submissionId })
      const result = await graphqlRequest(query, { activityPlayerId: this.data.activityPlayerId })
      
      if (result && result.reviewStatus) {
        const { status, canReview, deadline } = result.reviewStatus
      if (result && result.currentJudgeRating) {
        const rating = result.currentJudgeRating
        
        if (!canReview) {
          wx.showModal({
            title: '无法评审',
            content: deadline ? `评审已截止(截止时间:${formatDate(deadline)})` : '当前无法进行评审',
            showCancel: false,
            success: () => {
              wx.navigateBack()
            }
        // 如果已有评分,填充数据
        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 } = e.currentTarget.dataset
    const { value } = e.detail
    this.setData({
      [`scores.${criterionId}`]: parseInt(value)
    })
    this.calculateTotalScore()
    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)
  },
  // 计算总分
@@ -228,11 +348,11 @@
    let totalScore = 0
    
    criteria.forEach(criterion => {
      const score = scores[criterion.id] || 0
      totalScore += score * (criterion.weight || 1)
      const score = Number(scores[criterion.id] || 0)
      totalScore += score
    })
    
    this.setData({ totalScore })
    this.setData({ totalScore: Number(totalScore.toFixed(2)) })
  },
  // 评审意见输入
@@ -244,106 +364,58 @@
  // 媒体点击
  onMediaTap(e) {
    const { url, type } = e.currentTarget.dataset
    if (type === 'image') {
    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: url,
        urls: this.data.submission.images || []
        current: media.url,
        urls: imageUrls
      })
    } else if (type === 'video') {
      this.setData({
        showMediaPreview: true,
        currentMedia: url,
        mediaType: 'video'
    } else if (media.mediaType === 'video') {
      wx.navigateTo({
        url: `/pages/video/video?url=${encodeURIComponent(media.url)}&title=${encodeURIComponent(media.name)}`
      })
    } else {
      this.openDocumentMedia(media)
    }
  },
  // 关闭媒体预览
  onCloseMediaPreview() {
    this.setData({
      showMediaPreview: false,
      currentMedia: null
    })
  },
  // 下载文件
  async onDownloadFile(e) {
    const { fileId, fileName, fileUrl } = e.currentTarget.dataset
  async openDocumentMedia(media) {
    try {
      // 添加到下载中列表
      const downloadingFiles = [...this.data.downloadingFiles, fileId]
      // 同时更新文件的isDownloading字段
      const submission = { ...this.data.submission }
      if (submission.files) {
        submission.files = submission.files.map(file => ({
          ...file,
          isDownloading: file.id === fileId ? true : file.isDownloading
        }))
      }
      this.setData({
        downloadingFiles,
        submission
      wx.showLoading({ title: '打开中...' })
      const downloadRes = await new Promise((resolve, reject) => {
        wx.downloadFile({
          url: media.url,
          success: resolve,
          fail: reject
        })
      })
      wx.showLoading({ title: '下载中...' })
      const result = await wx.downloadFile({
        url: fileUrl,
        success: (res) => {
          if (res.statusCode === 200) {
            // 保存到相册或文件
            wx.saveFile({
              tempFilePath: res.tempFilePath,
              success: () => {
                wx.showToast({
                  title: '下载成功',
                  icon: 'success'
                })
              },
              fail: () => {
                wx.showToast({
                  title: '保存失败',
                  icon: 'error'
                })
              }
            })
          }
        },
        fail: () => {
          wx.showToast({
            title: '下载失败',
            icon: 'error'
          })
        }
      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)
      console.error('打开文件失败:', error)
      wx.showToast({
        title: '下载失败',
        title: '无法打开文件',
        icon: 'error'
      })
    } finally {
      // 从下载中列表移除
      const downloadingFiles = this.data.downloadingFiles.filter(id => id !== fileId)
      // 同时更新文件的isDownloading字段
      const submission = { ...this.data.submission }
      if (submission.files) {
        submission.files = submission.files.map(file => ({
          ...file,
          isDownloading: file.id === fileId ? false : file.isDownloading
        }))
      }
      this.setData({
        downloadingFiles,
        submission
      })
      wx.hideLoading()
    }
  },
@@ -351,6 +423,7 @@
  // 验证评审数据
  validateReview() {
    const { scores, criteria, comment } = this.data
    const commentText = (comment || '').trim()
    
    // 检查是否所有标准都已评分
    for (let criterion of criteria) {
@@ -364,7 +437,7 @@
    }
    
    // 检查评审意见
    if (!comment.trim()) {
    if (!commentText) {
      wx.showToast({
        title: '请填写评审意见',
        icon: 'error'
@@ -372,7 +445,7 @@
      return false
    }
    
    if (comment.trim().length < 10) {
    if (commentText.length < 10) {
      wx.showToast({
        title: '评审意见至少10个字符',
        icon: 'error'
@@ -381,51 +454,6 @@
    }
    
    return true
  },
  // 保存草稿
  async onSaveDraft() {
    try {
      wx.showLoading({ title: '保存中...' })
      const { submissionId, scores, comment, totalScore } = this.data
      const mutation = `
        mutation SaveReviewDraft($input: ReviewDraftInput!) {
          saveReviewDraft(input: $input) {
            success
            review {
              id
              status
            }
          }
        }
      `
      const input = {
        submissionId,
        scores,
        comment: comment.trim(),
        totalScore
      }
      const result = await graphqlRequest(mutation, { input })
      if (result && result.saveReviewDraft.success) {
        wx.showToast({
          title: '草稿已保存',
          icon: 'success'
        })
      }
    } catch (error) {
      console.error('保存草稿失败:', error)
      wx.showToast({
        title: '保存失败',
        icon: 'error'
      })
    } finally {
      wx.hideLoading()
    }
  },
  // 提交评审
@@ -451,41 +479,53 @@
      this.setData({ submitting: true })
      wx.showLoading({ title: '提交中...' })
      
      const { submissionId, scores, comment, totalScore } = this.data
      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 SubmitReview($input: ReviewSubmitInput!) {
          submitReview(input: $input) {
            success
            review {
              id
              status
              reviewedAt
            }
          }
        mutation SaveActivityPlayerRating($input: ActivityPlayerRatingInput!) {
          saveActivityPlayerRating(input: $input)
        }
      `
      
      const input = {
        submissionId,
        scores,
        comment: comment.trim(),
        totalScore
        activityPlayerId,
        stageId,
        ratings,
        comment: commentText
      }
      
      const result = await graphqlRequest(mutation, { input })
      
      if (result && result.submitReview.success) {
      if (result && result.saveActivityPlayerRating) {
        wx.showToast({
          title: '评审提交成功',
          title: '评分提交成功',
          icon: 'success'
        })
        
        // 更新状态
        this.setData({
          reviewStatus: 'COMPLETED',
          existingReview: result.submitReview.review
          reviewStatus: 'COMPLETED'
        })
        // 重新加载评分状态
        await this.checkReviewStatus()
        
        // 延迟返回上一页
        setTimeout(() => {
@@ -493,7 +533,7 @@
        }, 1500)
      }
    } catch (error) {
      console.error('提交评审失败:', error)
      console.error('提交评分失败:', error)
      wx.showToast({
        title: '提交失败',
        icon: 'error'
@@ -507,41 +547,24 @@
  // 查看其他评审
  onViewOtherReviews() {
    wx.navigateTo({
      url: `/pages/judge/reviews?submissionId=${this.data.submissionId}`
      url: `/pages/judge/reviews?activityPlayerId=${this.data.activityPlayerId}`
    })
  },
  // 联系参赛者
  onContactParticipant() {
    const { submission } = this.data
    if (submission.participant) {
      wx.showActionSheet({
        itemList: ['发送消息', '查看详情'],
        success: (res) => {
          switch (res.tapIndex) {
            case 0:
              // 发送消息功能
              wx.navigateTo({
                url: `/pages/chat/chat?userId=${submission.participant.id}`
              })
              break
            case 1:
              // 查看用户详情
              wx.navigateTo({
                url: `/pages/user/profile?userId=${submission.participant.id}`
              })
              break
          }
        }
    const phone = submission?.participant?.phone
    if (phone) {
      wx.makePhoneCall({
        phoneNumber: phone
      })
    } else {
      wx.showToast({
        title: '暂无联系方式',
        icon: 'none'
      })
    }
  },
  // 获取评分等级文本
  getScoreLabel(score) {
    const option = this.data.scoreOptions.find(opt => opt.value === score)
    return option ? option.label : `${score}分`
  },
  // 获取文件大小文本
@@ -555,9 +578,47 @@
    }
  },
  // 统一处理性别显示文本
  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 formatDate(dateString, 'YYYY-MM-DD HH:mm')
    return formatDateUtil(dateString, 'YYYY-MM-DD HH:mm')
  },
  // 分享页面
@@ -567,4 +628,4 @@
      path: '/pages/index/index'
    }
  }
})
})