| | |
| | | * 返回权限错误响应 |
| | | */ |
| | | private void sendUnauthorizedResponse(HttpServletResponse response) throws IOException { |
| | | response.setStatus(HttpServletResponse.SC_FORBIDDEN); |
| | | response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); |
| | | response.setContentType("application/json;charset=UTF-8"); |
| | | response.getWriter().write("{\"errors\":[{\"message\":\"没有权限访问,请先登录\",\"extensions\":{\"code\":\"UNAUTHORIZED\"}}]}"); |
| | | } |
| | |
| | | private BigDecimal totalScore; |
| | | private Integer status; |
| | | private String remark; |
| | | private String ratedAt; |
| | | private List<CurrentJudgeRatingItemResponse> items; |
| | | |
| | | public CurrentJudgeRatingResponse() {} |
| | |
| | | this.remark = remark; |
| | | } |
| | | |
| | | public String getRatedAt() { |
| | | return ratedAt; |
| | | } |
| | | |
| | | public void setRatedAt(String ratedAt) { |
| | | this.ratedAt = ratedAt; |
| | | } |
| | | |
| | | public List<CurrentJudgeRatingItemResponse> getItems() { |
| | | return items; |
| | | } |
| | |
| | | private String ratingItemName; |
| | | private BigDecimal score; |
| | | private BigDecimal weightedScore; |
| | | private BigDecimal maxScore; |
| | | |
| | | public CurrentJudgeRatingItemResponse() {} |
| | | |
| | | public CurrentJudgeRatingItemResponse(Long ratingItemId, String ratingItemName, |
| | | BigDecimal score, BigDecimal weightedScore) { |
| | | BigDecimal score, BigDecimal weightedScore, |
| | | BigDecimal maxScore) { |
| | | this.ratingItemId = ratingItemId; |
| | | this.ratingItemName = ratingItemName; |
| | | this.score = score; |
| | | this.weightedScore = weightedScore; |
| | | this.maxScore = maxScore; |
| | | } |
| | | |
| | | public Long getRatingItemId() { |
| | |
| | | public void setWeightedScore(BigDecimal weightedScore) { |
| | | this.weightedScore = weightedScore; |
| | | } |
| | | |
| | | public BigDecimal getMaxScore() { |
| | | return maxScore; |
| | | } |
| | | |
| | | public void setMaxScore(BigDecimal maxScore) { |
| | | this.maxScore = maxScore; |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | // 获取评分项 |
| | | List<ActivityPlayerRatingItem> items = activityPlayerRatingItemRepository |
| | | .findByActivityPlayerRatingId(rating.getId()); |
| | | |
| | | List<CurrentJudgeRatingResponse.CurrentJudgeRatingItemResponse> itemResponses = items.stream() |
| | | .map(item -> new CurrentJudgeRatingResponse.CurrentJudgeRatingItemResponse( |
| | | item.getRatingItemId(), |
| | | "", // 评分项名称暂时为空 |
| | | item.getScore(), |
| | | item.getScore() // 使用得分作为加权得分 |
| | | )) |
| | | |
| | | List<Long> ratingItemIds = items.stream() |
| | | .map(ActivityPlayerRatingItem::getRatingItemId) |
| | | .filter(java.util.Objects::nonNull) |
| | | .distinct() |
| | | .collect(java.util.stream.Collectors.toList()); |
| | | |
| | | |
| | | java.util.Map<Long, RatingItem> ratingItemMap = ratingItemIds.isEmpty() |
| | | ? java.util.Collections.emptyMap() |
| | | : ratingItemRepository.findAllById(ratingItemIds).stream() |
| | | .collect(java.util.stream.Collectors.toMap(RatingItem::getId, java.util.function.Function.identity())); |
| | | |
| | | List<CurrentJudgeRatingResponse.CurrentJudgeRatingItemResponse> itemResponses = items.stream() |
| | | .map(item -> { |
| | | RatingItem ratingItem = ratingItemMap.get(item.getRatingItemId()); |
| | | String name = ratingItem != null ? ratingItem.getName() : ""; |
| | | BigDecimal maxScore = ratingItem != null && ratingItem.getMaxScore() != null |
| | | ? BigDecimal.valueOf(ratingItem.getMaxScore()) : null; |
| | | return new CurrentJudgeRatingResponse.CurrentJudgeRatingItemResponse( |
| | | item.getRatingItemId(), |
| | | name, |
| | | item.getScore(), |
| | | item.getScore(), |
| | | maxScore |
| | | ); |
| | | }) |
| | | .collect(java.util.stream.Collectors.toList()); |
| | | |
| | | response.setRatedAt(rating.getUpdateTime() != null ? rating.getUpdateTime().toString() : null); |
| | | response.setItems(itemResponses); |
| | | return response; |
| | | } |
| | |
| | | ((Number) row.get("rating_item_id")).longValue(), |
| | | (String) row.get("rating_item_name"), |
| | | (BigDecimal) row.get("score"), |
| | | (BigDecimal) row.get("score") // weightedScore 暂时使用相同值 |
| | | (BigDecimal) row.get("score"), |
| | | row.get("max_score") instanceof BigDecimal |
| | | ? (BigDecimal) row.get("max_score") |
| | | : row.get("max_score") instanceof Number |
| | | ? BigDecimal.valueOf(((Number) row.get("max_score")).doubleValue()) |
| | | : null |
| | | )) |
| | | .collect(java.util.stream.Collectors.toList()); |
| | | |
| | |
| | | response.setTotalScore(rating.getTotalScore()); |
| | | response.setStatus(rating.getState()); |
| | | response.setRemark(rating.getFeedback()); |
| | | response.setRatedAt(rating.getUpdateTime() != null ? rating.getUpdateTime().toString() : null); |
| | | response.setItems(items); |
| | | |
| | | return response; |
| | |
| | | return null; |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | */ |
| | | @QueryMapping |
| | | public ReviewProjectPageResponse unReviewedProjects( |
| | | @Argument String searchKeyword, |
| | | @Argument int page, |
| | | @Argument int pageSize) { |
| | | log.info("查询我未评审的项目列表,searchKeyword: {}, page: {}, pageSize: {}", searchKeyword, page, pageSize); |
| | | @Argument int pageSize, |
| | | @Argument String searchKeyword) { |
| | | log.info("查询我未评审的项目列表,page: {}, pageSize: {}, searchKeyword: {}", page, pageSize, searchKeyword); |
| | | |
| | | Long currentJudgeId = userContextUtil.getCurrentJudgeId(); |
| | | if (currentJudgeId == null) { |
| | |
| | | */ |
| | | @QueryMapping |
| | | public ReviewProjectPageResponse reviewedProjects( |
| | | @Argument String searchKeyword, |
| | | @Argument int page, |
| | | @Argument int pageSize) { |
| | | log.info("查询我已评审的项目列表,searchKeyword: {}, page: {}, pageSize: {}", searchKeyword, page, pageSize); |
| | | @Argument int pageSize, |
| | | @Argument String searchKeyword) { |
| | | log.info("查询我已评审的项目列表,page: {}, pageSize: {}, searchKeyword: {}", page, pageSize, searchKeyword); |
| | | |
| | | Long currentJudgeId = userContextUtil.getCurrentJudgeId(); |
| | | if (currentJudgeId == null) { |
| | |
| | | */ |
| | | @QueryMapping |
| | | public ReviewProjectPageResponse studentUnReviewedProjects( |
| | | @Argument String searchKeyword, |
| | | @Argument int page, |
| | | @Argument int pageSize) { |
| | | log.info("查询学员未评审的项目列表,searchKeyword: {}, page: {}, pageSize: {}", searchKeyword, page, pageSize); |
| | | @Argument int pageSize, |
| | | @Argument String searchKeyword) { |
| | | log.info("查询学员未评审的项目列表,page: {}, pageSize: {}, searchKeyword: {}", page, pageSize, searchKeyword); |
| | | |
| | | Long currentJudgeId = userContextUtil.getCurrentJudgeId(); |
| | | if (currentJudgeId == null) { |
| | |
| | | base-url: https://rych.9village.cn |
| | | jwt: |
| | | secret: ryc-jwt-secret-key-2024-secure-256bit-hmac-sha-algorithm-compatible |
| | | expiration: 86400000 # 24小时 |
| | | expiration: 7200000 # 2小时 |
| | | media-url: https://ryc-1379367838.cos.ap-chengdu.myqcloud.com |
| | | |
| | | # 微信小程序配置 |
| | |
| | | totalScore: Float |
| | | status: Int |
| | | remark: String |
| | | ratedAt: String |
| | | items: [CurrentJudgeRatingItemResponse!]! |
| | | } |
| | | |
| | |
| | | ratingItemName: String! |
| | | score: Float |
| | | weightedScore: Float |
| | | maxScore: Float |
| | | } |
| | | |
| | | type ProjectStageTimelineResponse { |
| New file |
| | |
| | | // 清理无效token的脚本 |
| | | // 这个脚本需要在小程序开发者工具的控制台中运行 |
| | | |
| | | console.log('🧹 开始清理无效的JWT token...'); |
| | | |
| | | // 检查当前存储的token |
| | | const currentToken = wx.getStorageSync('token'); |
| | | console.log('当前token:', currentToken ? `${currentToken.substring(0, 20)}...` : '无'); |
| | | |
| | | // 检查token格式是否有效 |
| | | function isValidJWTFormat(token) { |
| | | if (!token || typeof token !== 'string') { |
| | | return false; |
| | | } |
| | | |
| | | // JWT应该有3个部分,用.分隔 |
| | | const parts = token.split('.'); |
| | | if (parts.length !== 3) { |
| | | console.log('❌ Token格式无效:不是3个部分'); |
| | | return false; |
| | | } |
| | | |
| | | // 检查是否包含测试用的无效签名 |
| | | if (token.includes('invalid_token')) { |
| | | console.log('❌ 检测到测试用的无效token'); |
| | | return false; |
| | | } |
| | | |
| | | try { |
| | | // 尝试解码header和payload |
| | | const header = JSON.parse(atob(parts[0])); |
| | | const payload = JSON.parse(atob(parts[1])); |
| | | |
| | | console.log('Token header:', header); |
| | | console.log('Token payload:', payload); |
| | | |
| | | // 检查是否过期 |
| | | const now = Math.floor(Date.now() / 1000); |
| | | if (payload.exp && payload.exp < now) { |
| | | console.log('❌ Token已过期'); |
| | | return false; |
| | | } |
| | | |
| | | console.log('✅ Token格式有效'); |
| | | return true; |
| | | } catch (e) { |
| | | console.log('❌ Token解码失败:', e.message); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | // 清理无效token |
| | | if (currentToken) { |
| | | if (!isValidJWTFormat(currentToken)) { |
| | | console.log('🗑️ 清理无效token...'); |
| | | |
| | | // 清除存储的认证信息 |
| | | wx.removeStorageSync('token'); |
| | | wx.removeStorageSync('userInfo'); |
| | | wx.removeStorageSync('sessionKey'); |
| | | |
| | | // 清除globalData中的认证信息 |
| | | const app = getApp(); |
| | | if (app) { |
| | | app.globalData.token = null; |
| | | app.globalData.userInfo = null; |
| | | app.globalData.sessionKey = null; |
| | | } |
| | | |
| | | console.log('✅ 无效token已清理'); |
| | | console.log('💡 建议重新启动小程序以获取新的有效token'); |
| | | } else { |
| | | console.log('✅ 当前token有效,无需清理'); |
| | | } |
| | | } else { |
| | | console.log('ℹ️ 当前没有存储token'); |
| | | } |
| | | |
| | | console.log('🎉 清理完成!'); |
| | | |
| | | // 使用说明 |
| | | console.log('\n📋 使用说明:'); |
| | | console.log('1. 在小程序开发者工具中打开控制台'); |
| | | console.log('2. 复制并粘贴这段代码'); |
| | | console.log('3. 按回车执行'); |
| | | console.log('4. 如果清理了无效token,请重新启动小程序'); |
| | |
| | | userInfo: null, |
| | | token: null, |
| | | sessionKey: null, // 微信会话密钥,用于解密手机号等敏感数据 |
| | | baseUrl: 'http://localhost:8080/graphql', // 后台GraphQL接口地址 |
| | | baseUrl: 'http://localhost:8080/api/graphql', // 后台GraphQL接口地址 |
| | | hasPhoneAuth: false, // 是否已授权手机号 |
| | | rejectPhone: false, // 是否拒绝过手机号授权 |
| | | cos: { |
| | |
| | | // GraphQL请求封装 |
| | | graphqlRequest(query, variables = {}) { |
| | | return new Promise((resolve, reject) => { |
| | | // 确保token的一致性:优先使用globalData中的token,如果没有则从storage获取 |
| | | let token = this.globalData.token |
| | | if (!token) { |
| | | token = wx.getStorageSync('token') |
| | | if (token) { |
| | | this.globalData.token = token // 同步到globalData |
| | | } |
| | | } |
| | | this._makeGraphQLRequest(query, variables, resolve, reject, false) |
| | | }) |
| | | }, |
| | | |
| | | wx.request({ |
| | | url: this.globalData.baseUrl, |
| | | method: 'POST', |
| | | header: { |
| | | 'Content-Type': 'application/json', |
| | | 'Authorization': token ? `Bearer ${token}` : '' |
| | | }, |
| | | data: { |
| | | query: query, |
| | | variables: variables |
| | | }, |
| | | success: (res) => { |
| | | console.log('GraphQL响应:', res.data) |
| | | |
| | | // 检查HTTP状态码 |
| | | if (res.statusCode !== 200) { |
| | | // 内部GraphQL请求方法,支持重试机制 |
| | | _makeGraphQLRequest(query, variables, resolve, reject, isRetry = false) { |
| | | // 确保token的一致性:优先使用globalData中的token,如果没有则从storage获取 |
| | | let token = this.globalData.token |
| | | if (!token) { |
| | | token = wx.getStorageSync('token') |
| | | if (token) { |
| | | this.globalData.token = token // 同步到globalData |
| | | } |
| | | } |
| | | |
| | | wx.request({ |
| | | url: this.globalData.baseUrl, |
| | | method: 'POST', |
| | | header: { |
| | | 'Content-Type': 'application/json', |
| | | 'Authorization': token ? `Bearer ${token}` : '' |
| | | }, |
| | | data: { |
| | | query: query, |
| | | variables: variables |
| | | }, |
| | | success: (res) => { |
| | | console.log('GraphQL响应:', res.data) |
| | | |
| | | // 检查HTTP状态码 |
| | | if (res.statusCode !== 200) { |
| | | // 对于401状态码,可能是认证错误,需要检查响应内容 |
| | | if (res.statusCode === 401 && res.data && res.data.errors) { |
| | | console.log('收到401状态码,检查是否为认证错误') |
| | | // 继续处理,让下面的GraphQL错误检查逻辑处理认证错误 |
| | | } else { |
| | | console.error('GraphQL HTTP错误:', res.statusCode) |
| | | reject(new Error(`HTTP错误: ${res.statusCode}`)) |
| | | return |
| | | } |
| | | } |
| | | |
| | | // 检查GraphQL错误 |
| | | if (res.data.errors) { |
| | | console.error('GraphQL错误:', res.data.errors) |
| | | reject(new Error(res.data.errors[0]?.message || 'GraphQL请求错误')) |
| | | // 检查GraphQL错误 |
| | | if (res.data && res.data.errors) { |
| | | console.error('GraphQL错误:', res.data.errors) |
| | | |
| | | // 检查是否是认证错误(token过期或无效) |
| | | const authErrors = res.data.errors.filter(error => |
| | | error.message && ( |
| | | error.message.includes('没有权限访问') || |
| | | error.message.includes('请先登录') || |
| | | error.message.includes('UNAUTHORIZED') || |
| | | error.extensions?.code === 'UNAUTHORIZED' |
| | | ) |
| | | ) |
| | | |
| | | if (authErrors.length > 0 && !isRetry) { |
| | | console.log('🔄 检测到认证错误,尝试重新登录...') |
| | | // 清除过期的认证信息 |
| | | this.globalData.token = null |
| | | this.globalData.userInfo = null |
| | | this.globalData.sessionKey = null |
| | | wx.removeStorageSync('token') |
| | | wx.removeStorageSync('userInfo') |
| | | wx.removeStorageSync('sessionKey') |
| | | |
| | | // 重新登录 |
| | | wx.login({ |
| | | success: (loginRes) => { |
| | | if (loginRes.code) { |
| | | console.log('🔄 重新获取微信code成功,调用后端登录...') |
| | | this._retryAfterLogin(loginRes.code, query, variables, resolve, reject) |
| | | } else { |
| | | console.error('❌ 重新获取微信code失败') |
| | | reject(new Error('重新登录失败')) |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | console.error('❌ 重新登录失败:', err) |
| | | reject(new Error('重新登录失败')) |
| | | } |
| | | }) |
| | | return |
| | | } |
| | | |
| | | // 检查数据 |
| | | if (res.data.data !== undefined) { |
| | | resolve(res.data.data) |
| | | } else { |
| | | console.error('GraphQL响应异常:', res.data) |
| | | reject(new Error('GraphQL响应数据异常')) |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | console.error('GraphQL网络请求失败:', err) |
| | | reject(new Error('网络请求失败')) |
| | | |
| | | reject(new Error(res.data.errors[0]?.message || 'GraphQL请求错误')) |
| | | return |
| | | } |
| | | }) |
| | | |
| | | // 检查数据 |
| | | if (res.data.data !== undefined) { |
| | | resolve(res.data.data) |
| | | } else { |
| | | console.error('GraphQL响应异常:', res.data) |
| | | reject(new Error('GraphQL响应数据异常')) |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | console.error('GraphQL网络请求失败:', err) |
| | | reject(new Error('网络请求失败')) |
| | | } |
| | | }) |
| | | }, |
| | | |
| | | // 重新登录后重试GraphQL请求 |
| | | _retryAfterLogin(code, query, variables, resolve, reject) { |
| | | const that = this |
| | | const deviceInfo = this.getDeviceInfo() |
| | | const requestData = { |
| | | code: code, |
| | | loginIp: '127.0.0.1', // 小程序无法获取真实IP,使用默认值 |
| | | deviceInfo: deviceInfo |
| | | } |
| | | |
| | | wx.request({ |
| | | url: 'http://localhost:8080/api/auth/wx-login', |
| | | method: 'POST', |
| | | header: { |
| | | 'Content-Type': 'application/json' |
| | | }, |
| | | data: requestData, |
| | | success: (res) => { |
| | | console.log('🔄 重新登录响应:', res.data) |
| | | |
| | | if (res.statusCode !== 200 || res.data.error) { |
| | | console.error('❌ 重新登录失败:', res.data.error || res.data.message) |
| | | reject(new Error('重新登录失败')) |
| | | return |
| | | } |
| | | |
| | | // 检查响应数据格式 |
| | | let loginResult = null |
| | | if (res.data.token && res.data.userInfo) { |
| | | loginResult = res.data |
| | | } else if (res.data.success && res.data.data) { |
| | | loginResult = res.data.data |
| | | } |
| | | |
| | | if (loginResult && loginResult.token) { |
| | | console.log('✅ 重新登录成功,更新token') |
| | | |
| | | // 保存新的登录信息 |
| | | try { |
| | | wx.setStorageSync('token', loginResult.token) |
| | | wx.setStorageSync('userInfo', loginResult.userInfo) |
| | | if (loginResult.sessionKey) { |
| | | wx.setStorageSync('sessionKey', loginResult.sessionKey) |
| | | } |
| | | } catch (storageErr) { |
| | | console.error('❌ 保存重新登录信息失败:', storageErr) |
| | | } |
| | | |
| | | that.globalData.token = loginResult.token |
| | | that.globalData.userInfo = loginResult.userInfo |
| | | that.globalData.sessionKey = loginResult.sessionKey |
| | | |
| | | // 使用新token重试原始请求 |
| | | console.log('🔄 使用新token重试GraphQL请求...') |
| | | that._makeGraphQLRequest(query, variables, resolve, reject, true) |
| | | } else { |
| | | console.error('❌ 重新登录响应格式错误') |
| | | reject(new Error('重新登录响应格式错误')) |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | console.error('❌ 重新登录网络请求失败:', err) |
| | | reject(new Error('重新登录网络请求失败')) |
| | | } |
| | | }) |
| | | } |
| | | }) |
| | |
| | | // pages/judge/review.js |
| | | const app = getApp() |
| | | const { graphqlRequest, formatDate } = require('../../lib/utils') |
| | | const { graphqlRequest, formatDate: formatDateUtil } = require('../../lib/utils') |
| | | |
| | | Page({ |
| | | data: { |
| | |
| | | // 提交作品信息 |
| | | submission: null, |
| | | activityPlayerId: '', |
| | | |
| | | stageId: null, |
| | | submissionId: null, |
| | | |
| | | // 活动信息 |
| | | activity: null, |
| | | |
| | | |
| | | // 评审标准 |
| | | criteria: [], |
| | | |
| | | |
| | | // 评分数据 |
| | | scores: {}, |
| | | |
| | |
| | | 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 (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 |
| | | } |
| | | }, |
| | | |
| | |
| | | submissionFiles { |
| | | id |
| | | name |
| | | url |
| | | fullUrl |
| | | fullThumbUrl |
| | | fileExt |
| | | fileSize |
| | | mediaType |
| | | thumbUrl |
| | | fullThumbUrl |
| | | } |
| | | ratingForm { |
| | | schemeId |
| | |
| | | items { |
| | | id |
| | | name |
| | | description |
| | | maxScore |
| | | weight |
| | | sortOrder |
| | | orderNo |
| | | } |
| | | } |
| | | } |
| | |
| | | id: detail.id, |
| | | title: detail.projectName, |
| | | description: detail.description, |
| | | files: detail.submissionFiles ? detail.submissionFiles.map(file => ({ |
| | | id: file.id, |
| | | name: file.name, |
| | | url: file.fullUrl || file.url, |
| | | type: file.fileExt, |
| | | size: file.fileSize, |
| | | isDownloading: this.data.downloadingFiles.indexOf(file.id) > -1 |
| | | })) : [], |
| | | images: detail.submissionFiles ? detail.submissionFiles |
| | | .filter(file => file.mediaType === 1) |
| | | .map(file => file.fullUrl || file.url) : [], |
| | | videos: detail.submissionFiles ? detail.submissionFiles |
| | | .filter(file => file.mediaType === 2) |
| | | .map(file => file.fullUrl || file.url) : [], |
| | | submittedAt: detail.submitTime || null, |
| | | team: detail.team || null, |
| | | participant: { |
| | | id: detail.playerInfo.id, |
| | | name: detail.playerInfo.name, |
| | | 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 || '', |
| | | avatar: detail.playerInfo.userInfo?.avatarUrl || '/images/default-avatar.svg' |
| | | major: detail.playerInfo?.education || '' |
| | | }, |
| | | status: detail.state === 1 ? 'APPROVED' : detail.state === 2 ? 'REJECTED' : 'PENDING' |
| | | status: detail.state === 1 ? 'APPROVED' : detail.state === 2 ? 'REJECTED' : 'PENDING', |
| | | mediaList: (detail.submissionFiles || []).map(file => this.transformMediaFile(file)) |
| | | } |
| | | |
| | | // 构建activity对象 |
| | | const activity = { |
| | | id: detail.stageId, |
| | | title: detail.activityName, |
| | | description: detail.description, |
| | | judgeCriteria: detail.ratingForm ? detail.ratingForm.items || [] : [] |
| | | } |
| | | |
| | | // 初始化评分数据 |
| | | |
| | | 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 (activity.judgeCriteria) { |
| | | activity.judgeCriteria.forEach(criterion => { |
| | | 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, |
| | | criteria: 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: null, // 暂时设为null,后续需要查询已有评分 |
| | | totalScore: 0, |
| | | existingReview: null, |
| | | reviewStatus: 'PENDING', |
| | | comment: '' |
| | | }) |
| | | |
| | | |
| | | this.calculateTotalScore() |
| | | |
| | | |
| | | // 检查是否已有评分 |
| | | this.checkReviewStatus() |
| | | } |
| | |
| | | currentJudgeRating(activityPlayerId: $activityPlayerId) { |
| | | id |
| | | totalScore |
| | | comment |
| | | remark |
| | | status |
| | | ratedAt |
| | | items { |
| | |
| | | |
| | | if (rating.items) { |
| | | rating.items.forEach(item => { |
| | | scores[item.ratingItemId] = item.score |
| | | totalScore += item.score |
| | | 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, |
| | | totalScore, |
| | | comment: rating.comment || '', |
| | | existingReview: rating, |
| | | reviewStatus: rating.status || 'COMPLETED' |
| | | 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) |
| | | }, |
| | | |
| | | // 计算总分 |
| | |
| | | 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)) }) |
| | | }, |
| | | |
| | | // 评审意见输入 |
| | |
| | | |
| | | // 媒体点击 |
| | | 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() |
| | | } |
| | | }, |
| | |
| | | // 验证评审数据 |
| | | validateReview() { |
| | | const { scores, criteria, comment } = this.data |
| | | const commentText = (comment || '').trim() |
| | | |
| | | // 检查是否所有标准都已评分 |
| | | for (let criterion of criteria) { |
| | |
| | | } |
| | | |
| | | // 检查评审意见 |
| | | if (!comment.trim()) { |
| | | if (!commentText) { |
| | | wx.showToast({ |
| | | title: '请填写评审意见', |
| | | icon: 'error' |
| | |
| | | return false |
| | | } |
| | | |
| | | if (comment.trim().length < 10) { |
| | | if (commentText.length < 10) { |
| | | wx.showToast({ |
| | | title: '评审意见至少10个字符', |
| | | icon: 'error' |
| | |
| | | } |
| | | |
| | | return true |
| | | }, |
| | | |
| | | // 保存草稿 |
| | | async onSaveDraft() { |
| | | try { |
| | | wx.showLoading({ title: '保存中...' }) |
| | | |
| | | const { activityPlayerId, scores, comment, criteria, activity } = this.data |
| | | |
| | | // 构建评分项数组 |
| | | const ratings = criteria.map(criterion => ({ |
| | | itemId: criterion.id, |
| | | score: scores[criterion.id] || 0 |
| | | })) |
| | | |
| | | const mutation = ` |
| | | mutation SaveActivityPlayerRating($input: ActivityPlayerRatingInput!) { |
| | | saveActivityPlayerRating(input: $input) |
| | | } |
| | | ` |
| | | |
| | | const input = { |
| | | activityPlayerId, |
| | | stageId: activity.stageId, |
| | | ratings, |
| | | comment: comment.trim() |
| | | } |
| | | |
| | | const result = await graphqlRequest(mutation, { input }) |
| | | |
| | | if (result && result.saveActivityPlayerRating) { |
| | | wx.showToast({ |
| | | title: '草稿已保存', |
| | | icon: 'success' |
| | | }) |
| | | |
| | | // 重新加载评分状态 |
| | | await this.checkReviewStatus() |
| | | } |
| | | } catch (error) { |
| | | console.error('保存草稿失败:', error) |
| | | wx.showToast({ |
| | | title: '保存失败', |
| | | icon: 'error' |
| | | }) |
| | | } finally { |
| | | wx.hideLoading() |
| | | } |
| | | }, |
| | | |
| | | // 提交评审 |
| | |
| | | this.setData({ submitting: true }) |
| | | wx.showLoading({ title: '提交中...' }) |
| | | |
| | | const { activityPlayerId, scores, comment, criteria, activity } = 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: scores[criterion.id] || 0 |
| | | score: Number(scores[criterion.id] || 0) |
| | | })) |
| | | |
| | | const mutation = ` |
| | |
| | | |
| | | const input = { |
| | | activityPlayerId, |
| | | stageId: activity.stageId, |
| | | stageId, |
| | | ratings, |
| | | comment: comment.trim() |
| | | comment: commentText |
| | | } |
| | | |
| | | const result = await graphqlRequest(mutation, { input }) |
| | |
| | | // 联系参赛者 |
| | | 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}分` |
| | | }, |
| | | |
| | | // 获取文件大小文本 |
| | |
| | | } |
| | | }, |
| | | |
| | | // 统一处理性别显示文本 |
| | | 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') |
| | | }, |
| | | |
| | | // 分享页面 |
| | |
| | | path: '/pages/index/index' |
| | | } |
| | | } |
| | | }) |
| | | }) |
| | |
| | | {{reviewStatus === 'COMPLETED' ? '已评审' : '待评审'}} |
| | | </view> |
| | | </view> |
| | | |
| | | |
| | | <view class="submission-detail"> |
| | | <text class="submission-title">{{submission.title}}</text> |
| | | <text class="submission-desc">{{submission.description}}</text> |
| | | |
| | | <text class="submission-desc">{{submission.description || '暂无项目描述'}}</text> |
| | | |
| | | <!-- 参赛者信息 --> |
| | | <view class="participant-info"> |
| | | <view class="participant-header"> |
| | | <text class="participant-label">参赛者:</text> |
| | | <view class="contact-btn" bindtap="onContactParticipant"> |
| | | <text class="contact-icon">💬</text> |
| | | <text class="contact-text">联系</text> |
| | | <text class="contact-icon">📞</text> |
| | | <text class="contact-text">拨打电话</text> |
| | | </view> |
| | | </view> |
| | | |
| | | |
| | | <view class="participant-detail"> |
| | | <image class="participant-avatar" src="{{submission.participant.avatar}}" mode="aspectFill"></image> |
| | | <view class="participant-text"> |
| | | <text class="participant-name">{{submission.participant.name}}</text> |
| | | <text class="participant-school">{{submission.participant.school}} - {{submission.participant.major}}</text> |
| | | <text class="participant-name">{{submission.participant.name || '匿名'}}</text> |
| | | <text class="participant-meta">性别:{{submission.participant.gender || '未填写'}},出生日期:{{submission.participant.birthday || '未填写'}}</text> |
| | | <text class="participant-field">所属区域:{{submission.participant.region || '未填写'}}</text> |
| | | <text class="participant-field">学历:{{submission.participant.education || '未填写'}}</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 团队信息 --> |
| | | <view wx:if="{{submission.team}}" class="team-info"> |
| | | <text class="team-label">团队:{{submission.team.name}}</text> |
| | | |
| | | <view wx:if="{{submission.team && submission.team.members}}" class="team-info"> |
| | | <text class="team-label">团队:{{submission.team.name || '未命名团队'}}</text> |
| | | <view class="team-members"> |
| | | <view wx:for="{{submission.team.members}}" wx:key="id" class="team-member"> |
| | | <text class="member-name">{{item.name}}</text> |
| | |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <text class="submit-time">提交时间:{{formatDate(submission.submittedAt)}}</text> |
| | | |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 作品媒体 --> |
| | | <view wx:if="{{submission.images.length > 0 || submission.videos.length > 0}}" class="media-section"> |
| | | <text class="section-title">作品展示</text> |
| | | |
| | | <!-- 图片 --> |
| | | <view wx:if="{{submission.images.length > 0}}" class="media-grid"> |
| | | <view |
| | | wx:for="{{submission.images}}" |
| | | wx:key="*this" |
| | | class="media-item image-item" |
| | | data-url="{{item}}" |
| | | data-type="image" |
| | | bindtap="onMediaTap" |
| | | > |
| | | <image class="media-image" src="{{item}}" mode="aspectFill"></image> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 视频 --> |
| | | <view wx:if="{{submission.videos.length > 0}}" class="media-grid"> |
| | | <view |
| | | wx:for="{{submission.videos}}" |
| | | wx:key="*this" |
| | | class="media-item video-item" |
| | | data-url="{{item}}" |
| | | data-type="video" |
| | | bindtap="onMediaTap" |
| | | > |
| | | <video class="media-video" src="{{item}}" poster="{{item}}" controls></video> |
| | | <view class="play-overlay"> |
| | | <text class="play-icon">▶</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 作品文件 --> |
| | | <view wx:if="{{submission.files.length > 0}}" class="files-section"> |
| | | <text class="section-title">作品文件</text> |
| | | |
| | | <view class="file-list"> |
| | | <view |
| | | wx:for="{{submission.files}}" |
| | | <!-- 作品素材 --> |
| | | <view wx:if="{{submission.mediaList && submission.mediaList.length > 0}}" class="media-section"> |
| | | <text class="section-title">参赛作品</text> |
| | | <view class="media-list"> |
| | | <view |
| | | wx:for="{{submission.mediaList}}" |
| | | wx:key="id" |
| | | class="file-item" |
| | | class="media-item" |
| | | bindtap="onMediaTap" |
| | | data-index="{{index}}" |
| | | > |
| | | <view class="file-info"> |
| | | <text class="file-icon">📄</text> |
| | | <view class="file-detail"> |
| | | <text class="file-name">{{item.name}}</text> |
| | | <text class="file-size">{{getFileSizeText(item.size)}}</text> |
| | | </view> |
| | | <view class="media-thumb-wrapper"> |
| | | <image |
| | | wx:if="{{item.mediaType === 'image' || item.mediaType === 'video'}}" |
| | | class="media-thumb" |
| | | src="{{item.thumbUrl}}" |
| | | mode="aspectFill" |
| | | /> |
| | | <view wx:elif="{{item.mediaType === 'pdf'}}" class="media-icon pdf">PDF</view> |
| | | <view wx:elif="{{item.mediaType === 'word'}}" class="media-icon doc">DOC</view> |
| | | <view wx:else class="media-icon file">FILE</view> |
| | | <view wx:if="{{item.mediaType === 'video'}}" class="media-play">▶</view> |
| | | </view> |
| | | |
| | | <view |
| | | class="download-btn {{item.isDownloading ? 'downloading' : ''}}" |
| | | data-file-id="{{item.id}}" |
| | | data-file-name="{{item.name}}" |
| | | data-file-url="{{item.url}}" |
| | | bindtap="onDownloadFile" |
| | | > |
| | | <text class="download-icon">{{item.isDownloading ? '⏳' : '⬇'}}</text> |
| | | <text class="download-text">{{item.isDownloading ? '下载中' : '下载'}}</text> |
| | | <view class="media-info"> |
| | | <text class="media-name">{{item.name}}</text> |
| | | <text class="media-size">{{getFileSizeText(item.size)}}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | |
| | | <!-- 评审标准 --> |
| | | <view class="criteria-section"> |
| | | <text class="section-title">评审标准</text> |
| | | |
| | | |
| | | <view class="criteria-list"> |
| | | <view wx:for="{{criteria}}" wx:key="id" class="criterion-item"> |
| | | <view class="criterion-header"> |
| | | <text class="criterion-name">{{item.name}}</text> |
| | | <text class="criterion-score">{{scores[item.id] || 0}}/{{item.maxScore}}分</text> |
| | | <text class="criterion-score">{{scores[item.id] || 0}} / {{item.maxScore}} 分</text> |
| | | </view> |
| | | |
| | | <text class="criterion-desc">{{item.description}}</text> |
| | | |
| | | <!-- 评分选择器 --> |
| | | <view class="score-selector"> |
| | | <picker |
| | | range="{{scoreOptions}}" |
| | | range-key="label" |
| | | value="{{(scores[item.id] || 1) - 1}}" |
| | | |
| | | <text class="criterion-desc">{{item.description || '暂无评分说明'}}</text> |
| | | |
| | | <view class="score-control"> |
| | | <view class="score-btn" data-criterion-id="{{item.id}}" data-index="{{index}}" bindtap="decreaseScore">-</view> |
| | | <input |
| | | class="score-input" |
| | | type="digit" |
| | | value="{{scores[item.id] || 0}}" |
| | | data-criterion-id="{{item.id}}" |
| | | bindchange="onScoreChange" |
| | | disabled="{{reviewStatus === 'COMPLETED'}}" |
| | | > |
| | | <view class="score-picker {{reviewStatus === 'COMPLETED' ? 'disabled' : ''}}"> |
| | | <text class="score-text">{{getScoreLabel(scores[item.id] || 0)}}</text> |
| | | <text class="picker-arrow">{{reviewStatus === 'COMPLETED' ? '' : '▼'}}</text> |
| | | </view> |
| | | </picker> |
| | | data-index="{{index}}" |
| | | bindinput="onScoreChange" |
| | | placeholder="0" |
| | | /> |
| | | <view class="score-btn" data-criterion-id="{{item.id}}" data-index="{{index}}" bindtap="increaseScore">+</view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 总分显示 --> |
| | | |
| | | <view class="total-score"> |
| | | <text class="total-label">总分:</text> |
| | | <text class="total-value">{{totalScore}}/{{maxScore}}分</text> |
| | | <text class="total-value">{{totalScore}} / {{maxScore}} 分</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 评审意见 --> |
| | | <view class="comment-section"> |
| | | <text class="section-title">评审意见</text> |
| | | |
| | | <textarea |
| | | class="comment-input {{reviewStatus === 'COMPLETED' ? 'disabled' : ''}}" |
| | | placeholder="请填写详细的评审意见,包括作品的优点、不足和改进建议..." |
| | | <textarea |
| | | class="comment-input" |
| | | placeholder="请填写详细评审意见,包括作品优点、不足和改进建议..." |
| | | value="{{comment}}" |
| | | maxlength="1000" |
| | | show-confirm-bar="{{false}}" |
| | | disabled="{{reviewStatus === 'COMPLETED'}}" |
| | | bindinput="onCommentInput" |
| | | ></textarea> |
| | | |
| | | <view class="comment-counter"> |
| | | <text class="counter-text">{{comment.length}}/1000</text> |
| | | <text class="counter-text">{{comment.length}} / 1000</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 已有评审记录 --> |
| | | <!-- 既有评审 --> |
| | | <view wx:if="{{existingReview}}" class="existing-review"> |
| | | <text class="section-title">评审记录</text> |
| | | |
| | | <text class="section-title">历史评审</text> |
| | | <view class="review-info"> |
| | | <text class="review-time">评审时间:{{formatDate(existingReview.reviewedAt)}}</text> |
| | | <text class="review-total">总分:{{existingReview.totalScore}}/{{maxScore}}分</text> |
| | | <text class="review-time">评审时间:{{formatDate(existingReview.reviewedAt) || '未知'}}</text> |
| | | <text class="review-total">总分:{{existingReview.totalScore}} / {{maxScore}} 分</text> |
| | | </view> |
| | | |
| | | <view class="other-reviews-btn" bindtap="onViewOtherReviews"> |
| | | <text class="other-reviews-icon">👥</text> |
| | | <text class="other-reviews-text">查看其他评审</text> |
| | | <text class="other-reviews-text">查看其他评委评分</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 底部操作栏 --> |
| | | <view wx:if="{{reviewStatus !== 'COMPLETED'}}" class="bottom-actions"> |
| | | <view class="action-btn draft-btn" bindtap="onSaveDraft"> |
| | | <text class="btn-icon">💾</text> |
| | | <text class="btn-text">保存草稿</text> |
| | | </view> |
| | | |
| | | <view |
| | | <!-- 底部操作 --> |
| | | <view class="bottom-actions"> |
| | | <view |
| | | class="action-btn submit-btn {{submitting ? 'submitting' : ''}}" |
| | | bindtap="onSubmitReview" |
| | | > |
| | |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 视频预览模态框 --> |
| | | <view wx:if="{{showMediaPreview}}" class="media-preview-modal"> |
| | | <view class="modal-overlay" bindtap="onCloseMediaPreview"></view> |
| | | <view class="modal-content"> |
| | | <view class="modal-header"> |
| | | <text class="modal-title">视频预览</text> |
| | | <view class="close-btn" bindtap="onCloseMediaPreview"> |
| | | <text class="close-icon">✕</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="modal-body"> |
| | | <video |
| | | wx:if="{{mediaType === 'video'}}" |
| | | class="preview-video" |
| | | src="{{currentMedia}}" |
| | | controls |
| | | autoplay |
| | | ></video> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | |
| | | padding-bottom: 120rpx; |
| | | } |
| | | |
| | | /* 加载状态 */ |
| | | /* Loading */ |
| | | .loading-container { |
| | | display: flex; |
| | | flex-direction: column; |
| | |
| | | 100% { transform: rotate(360deg); } |
| | | } |
| | | |
| | | /* 评审内容 */ |
| | | .review-content { |
| | | padding: 30rpx; |
| | | } |
| | | |
| | | /* 通用样式 */ |
| | | .section-title { |
| | | font-size: 32rpx; |
| | | font-weight: 600; |
| | |
| | | display: block; |
| | | } |
| | | |
| | | /* 作品信息 */ |
| | | .submission-info { |
| | | .submission-info, |
| | | .media-section, |
| | | .criteria-section, |
| | | .comment-section, |
| | | .existing-review { |
| | | background-color: #fff; |
| | | border-radius: 16rpx; |
| | | padding: 30rpx; |
| | |
| | | color: #155724; |
| | | } |
| | | |
| | | .submission-detail { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 20rpx; |
| | | } |
| | | |
| | | .submission-title { |
| | | font-size: 30rpx; |
| | | font-weight: 600; |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8rpx; |
| | | padding: 8rpx 16rpx; |
| | | background-color: #007aff; |
| | | padding: 10rpx 24rpx; |
| | | background: linear-gradient(135deg, #34c759 0%, #2ea043 100%); |
| | | color: #fff; |
| | | border-radius: 16rpx; |
| | | font-size: 22rpx; |
| | | } |
| | | |
| | | .contact-icon { |
| | | font-size: 20rpx; |
| | | border-radius: 24rpx; |
| | | font-size: 24rpx; |
| | | box-shadow: 0 6rpx 16rpx rgba(46, 160, 67, 0.25); |
| | | } |
| | | |
| | | .participant-detail { |
| | |
| | | } |
| | | |
| | | .participant-avatar { |
| | | width: 80rpx; |
| | | height: 80rpx; |
| | | border-radius: 40rpx; |
| | | width: 96rpx; |
| | | height: 96rpx; |
| | | border-radius: 48rpx; |
| | | background-color: #f0f0f0; |
| | | } |
| | | |
| | | .participant-text { |
| | | flex: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8rpx; |
| | |
| | | |
| | | .participant-name { |
| | | font-size: 28rpx; |
| | | font-weight: 500; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | |
| | | .participant-school { |
| | | .participant-meta { |
| | | font-size: 24rpx; |
| | | color: #555; |
| | | line-height: 1.4; |
| | | } |
| | | |
| | | .participant-field { |
| | | font-size: 24rpx; |
| | | color: #666; |
| | | line-height: 1.4; |
| | | } |
| | | |
| | | .team-info { |
| | | margin-top: 20rpx; |
| | | padding-top: 20rpx; |
| | | border-top: 1rpx solid #f0f0f0; |
| | | background-color: #f8f9fa; |
| | | border-radius: 12rpx; |
| | | padding: 20rpx; |
| | | } |
| | | |
| | | .team-label { |
| | | font-size: 26rpx; |
| | | color: #666; |
| | | font-weight: 500; |
| | | margin-bottom: 16rpx; |
| | | display: block; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | |
| | | .team-members { |
| | | margin-top: 12rpx; |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 16rpx; |
| | | flex-direction: column; |
| | | gap: 8rpx; |
| | | } |
| | | |
| | | .team-member { |
| | | font-size: 24rpx; |
| | | color: #555; |
| | | } |
| | | |
| | | .media-list { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | padding: 12rpx 20rpx; |
| | | background-color: #f8f9fa; |
| | | border-radius: 12rpx; |
| | | } |
| | | |
| | | .member-name { |
| | | font-size: 24rpx; |
| | | color: #333; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .member-role { |
| | | font-size: 20rpx; |
| | | color: #666; |
| | | margin-top: 4rpx; |
| | | } |
| | | |
| | | .submit-time { |
| | | font-size: 24rpx; |
| | | color: #999; |
| | | margin-top: 10rpx; |
| | | } |
| | | |
| | | /* 媒体展示 */ |
| | | .media-section { |
| | | background-color: #fff; |
| | | border-radius: 16rpx; |
| | | padding: 30rpx; |
| | | margin-bottom: 30rpx; |
| | | box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .media-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(2, 1fr); |
| | | gap: 20rpx; |
| | | } |
| | | |
| | | .media-item { |
| | | position: relative; |
| | | border-radius: 12rpx; |
| | | overflow: hidden; |
| | | aspect-ratio: 1; |
| | | } |
| | | |
| | | .media-image, |
| | | .media-video { |
| | | width: 100%; |
| | | height: 100%; |
| | | object-fit: cover; |
| | | } |
| | | |
| | | .play-overlay { |
| | | position: absolute; |
| | | top: 50%; |
| | | left: 50%; |
| | | transform: translate(-50%, -50%); |
| | | width: 60rpx; |
| | | height: 60rpx; |
| | | background-color: rgba(0, 0, 0, 0.6); |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .play-icon { |
| | | color: #fff; |
| | | font-size: 24rpx; |
| | | margin-left: 4rpx; |
| | | } |
| | | |
| | | /* 文件列表 */ |
| | | .files-section { |
| | | background-color: #fff; |
| | | border-radius: 16rpx; |
| | | padding: 30rpx; |
| | | margin-bottom: 30rpx; |
| | | box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .file-list { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 20rpx; |
| | | } |
| | | |
| | | .file-item { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | gap: 24rpx; |
| | | padding: 20rpx; |
| | | background-color: #f8f9fa; |
| | | border-radius: 12rpx; |
| | | } |
| | | |
| | | .file-info { |
| | | .media-thumb-wrapper { |
| | | width: 120rpx; |
| | | height: 120rpx; |
| | | border-radius: 12rpx; |
| | | overflow: hidden; |
| | | position: relative; |
| | | background: #eaeaea; |
| | | } |
| | | |
| | | .media-thumb { |
| | | width: 100%; |
| | | height: 100%; |
| | | object-fit: cover; |
| | | } |
| | | |
| | | .media-icon { |
| | | width: 100%; |
| | | height: 100%; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 16rpx; |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .file-icon { |
| | | justify-content: center; |
| | | font-size: 32rpx; |
| | | font-weight: 600; |
| | | color: #fff; |
| | | } |
| | | |
| | | .file-detail { |
| | | .media-icon.pdf { background: #e74c3c; } |
| | | .media-icon.doc { background: #1e88e5; } |
| | | .media-icon.file { background: #6c757d; } |
| | | |
| | | .media-play { |
| | | position: absolute; |
| | | bottom: 8rpx; |
| | | right: 8rpx; |
| | | width: 44rpx; |
| | | height: 44rpx; |
| | | background: rgba(0, 0, 0, 0.6); |
| | | border-radius: 50%; |
| | | color: #fff; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 24rpx; |
| | | } |
| | | |
| | | .media-info { |
| | | flex: 1; |
| | | min-width: 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 4rpx; |
| | | gap: 6rpx; |
| | | } |
| | | |
| | | .file-name { |
| | | font-size: 26rpx; |
| | | .media-name { |
| | | font-size: 28rpx; |
| | | font-weight: 600; |
| | | color: #333; |
| | | font-weight: 500; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .file-size { |
| | | .media-size { |
| | | font-size: 22rpx; |
| | | color: #666; |
| | | } |
| | | |
| | | .download-btn { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8rpx; |
| | | padding: 12rpx 20rpx; |
| | | background-color: #007aff; |
| | | color: #fff; |
| | | border-radius: 20rpx; |
| | | font-size: 22rpx; |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .download-btn.downloading { |
| | | background-color: #6c757d; |
| | | } |
| | | |
| | | .download-icon { |
| | | font-size: 20rpx; |
| | | } |
| | | |
| | | /* 评审标准 */ |
| | | .criteria-section { |
| | | background-color: #fff; |
| | | border-radius: 16rpx; |
| | | padding: 30rpx; |
| | | margin-bottom: 30rpx; |
| | | box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .criteria-list { |
| | |
| | | display: block; |
| | | } |
| | | |
| | | .score-selector { |
| | | width: 100%; |
| | | } |
| | | |
| | | .score-picker { |
| | | .score-control { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 16rpx 20rpx; |
| | | background-color: #fff; |
| | | border: 2rpx solid #e0e0e0; |
| | | border-radius: 8rpx; |
| | | font-size: 26rpx; |
| | | gap: 20rpx; |
| | | margin-top: 16rpx; |
| | | } |
| | | |
| | | .score-picker.disabled { |
| | | background-color: #f5f5f5; |
| | | color: #999; |
| | | .score-btn { |
| | | width: 60rpx; |
| | | height: 60rpx; |
| | | border-radius: 12rpx; |
| | | background: #eef2ff; |
| | | color: #4c60ff; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 36rpx; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .score-text { |
| | | color: #333; |
| | | } |
| | | |
| | | .picker-arrow { |
| | | color: #999; |
| | | font-size: 20rpx; |
| | | .score-input { |
| | | flex: 1; |
| | | height: 60rpx; |
| | | border: 2rpx solid #dcdfe6; |
| | | border-radius: 12rpx; |
| | | text-align: center; |
| | | font-size: 28rpx; |
| | | padding: 0 12rpx; |
| | | background: #fff; |
| | | } |
| | | |
| | | .total-score { |
| | |
| | | font-weight: 700; |
| | | } |
| | | |
| | | /* 评审意见 */ |
| | | .comment-section { |
| | | background-color: #fff; |
| | | border-radius: 16rpx; |
| | | padding: 30rpx; |
| | | margin-bottom: 30rpx; |
| | | box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .comment-input { |
| | | width: 100%; |
| | | min-height: 200rpx; |
| | |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .comment-input.disabled { |
| | | background-color: #f5f5f5; |
| | | color: #999; |
| | | } |
| | | |
| | | .comment-counter { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | text-align: right; |
| | | margin-top: 12rpx; |
| | | } |
| | | |
| | | .counter-text { |
| | | font-size: 22rpx; |
| | | color: #999; |
| | | } |
| | | |
| | | /* 已有评审记录 */ |
| | | .existing-review { |
| | | background-color: #fff; |
| | | border-radius: 16rpx; |
| | | padding: 30rpx; |
| | | margin-bottom: 30rpx; |
| | | box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1); |
| | | background-color: #fff7e6; |
| | | border: 2rpx solid #ffb347; |
| | | } |
| | | |
| | | .review-info { |
| | |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20rpx; |
| | | padding: 20rpx; |
| | | background-color: #f8f9fa; |
| | | border-radius: 12rpx; |
| | | } |
| | | |
| | | .review-time, |
| | | .review-total { |
| | | font-size: 24rpx; |
| | | font-size: 26rpx; |
| | | color: #666; |
| | | } |
| | | |
| | | .other-reviews-btn { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 12rpx; |
| | | padding: 16rpx; |
| | | background-color: #007aff; |
| | | gap: 8rpx; |
| | | padding: 14rpx 24rpx; |
| | | border-radius: 24rpx; |
| | | background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); |
| | | color: #fff; |
| | | border-radius: 12rpx; |
| | | font-size: 26rpx; |
| | | } |
| | | |
| | | .other-reviews-icon { |
| | | font-size: 24rpx; |
| | | } |
| | | |
| | | /* 底部操作栏 */ |
| | | .bottom-actions { |
| | | position: fixed; |
| | | bottom: 0; |
| | | left: 0; |
| | | right: 0; |
| | | display: flex; |
| | | gap: 20rpx; |
| | | bottom: 0; |
| | | padding: 20rpx 30rpx; |
| | | background-color: #fff; |
| | | border-top: 1rpx solid #e0e0e0; |
| | | box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.1); |
| | | background-color: rgba(255, 255, 255, 0.95); |
| | | box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08); |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: stretch; |
| | | } |
| | | |
| | | .action-btn { |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | |
| | | font-size: 28rpx; |
| | | font-weight: 500; |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .draft-btn { |
| | | background-color: #6c757d; |
| | | color: #fff; |
| | | flex: 1; |
| | | width: 100%; |
| | | } |
| | | |
| | | .submit-btn { |
| | |
| | | font-size: 24rpx; |
| | | } |
| | | |
| | | /* 视频预览模态框 */ |
| | | .media-preview-modal { |
| | | position: fixed; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | z-index: 1000; |
| | | } |
| | | |
| | | .modal-overlay { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background-color: rgba(0, 0, 0, 0.8); |
| | | } |
| | | |
| | | .modal-content { |
| | | position: absolute; |
| | | top: 50%; |
| | | left: 50%; |
| | | transform: translate(-50%, -50%); |
| | | width: 90%; |
| | | max-width: 600rpx; |
| | | background-color: #fff; |
| | | border-radius: 16rpx; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .modal-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 20rpx 30rpx; |
| | | border-bottom: 1rpx solid #e0e0e0; |
| | | } |
| | | |
| | | .modal-title { |
| | | font-size: 30rpx; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | |
| | | .close-btn { |
| | | width: 60rpx; |
| | | height: 60rpx; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-radius: 50%; |
| | | background-color: #f8f9fa; |
| | | } |
| | | |
| | | .close-icon { |
| | | font-size: 24rpx; |
| | | color: #666; |
| | | } |
| | | |
| | | .modal-body { |
| | | padding: 30rpx; |
| | | } |
| | | |
| | | .preview-video { |
| | | width: 100%; |
| | | height: 400rpx; |
| | | border-radius: 12rpx; |
| | | } |
| | | |
| | | /* 响应式适配 */ |
| | | @media (max-width: 375px) { |
| | | .container { |
| | | padding-bottom: 100rpx; |
| | | } |
| | | |
| | | |
| | | .review-content { |
| | | padding: 20rpx; |
| | | } |
| | | |
| | | |
| | | .submission-info, |
| | | .media-section, |
| | | .files-section, |
| | | .criteria-section, |
| | | .comment-section, |
| | | .existing-review { |
| | | padding: 20rpx; |
| | | margin-bottom: 20rpx; |
| | | } |
| | | |
| | | .media-grid { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | |
| | | |
| | | .bottom-actions { |
| | | padding: 15rpx 20rpx; |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | // 加载消息列表 |
| | | loadMessages() { |
| | | // 检查用户是否已登录 |
| | | const userInfo = app.globalData.userInfo |
| | | // 检查用户是否已登录,如果globalData中没有,尝试从本地存储恢复 |
| | | let userInfo = app.globalData.userInfo |
| | | if (!userInfo || !userInfo.userId) { |
| | | console.log('globalData中没有userInfo,尝试从本地存储恢复') |
| | | try { |
| | | const storedUserInfo = wx.getStorageSync('userInfo') |
| | | if (storedUserInfo && storedUserInfo.userId) { |
| | | console.log('从本地存储恢复userInfo成功') |
| | | app.globalData.userInfo = storedUserInfo |
| | | userInfo = storedUserInfo |
| | | } |
| | | } catch (error) { |
| | | console.error('从本地存储恢复userInfo失败:', error) |
| | | } |
| | | } |
| | | |
| | | if (!userInfo || !userInfo.userId) { |
| | | console.error('用户未登录或userId不存在') |
| | | wx.showToast({ |
| | |
| | | path: `/pages/project/detail?id=${this.data.projectId}` |
| | | } |
| | | } |
| | | }) |
| | | }) |
| | |
| | | const app = getApp() |
| | | const { graphqlRequest, formatDate } = require('../../lib/utils') |
| | | |
| | | const GET_RATING_STATS_QUERY = ` |
| | | query GetRatingStats($activityPlayerId: ID!) { |
| | | judgeRatingsForPlayer(activityPlayerId: $activityPlayerId) { |
| | | hasRated |
| | | } |
| | | } |
| | | ` |
| | | |
| | | Page({ |
| | | data: { |
| | | loading: false, |
| | |
| | | |
| | | // 切换选项卡 |
| | | switchTab(e) { |
| | | const index = e.currentTarget.dataset.index |
| | | const index = parseInt(e.currentTarget.dataset.index) || 0 |
| | | if (index === this.data.currentTab) return |
| | | |
| | | this.setData({ |
| | |
| | | ]) |
| | | } catch (error) { |
| | | console.error('加载数据失败:', error) |
| | | wx.showToast({ |
| | | title: '加载失败', |
| | | icon: 'none' |
| | | }) |
| | | |
| | | // 检查是否是认证相关错误 |
| | | if (error.message && ( |
| | | error.message.includes('没有权限访问') || |
| | | error.message.includes('请先登录') || |
| | | error.message.includes('重新登录') |
| | | )) { |
| | | wx.showToast({ |
| | | title: '正在重新登录...', |
| | | icon: 'loading', |
| | | duration: 2000 |
| | | }) |
| | | |
| | | // 等待一段时间后重试 |
| | | setTimeout(() => { |
| | | this.loadData() |
| | | }, 2000) |
| | | } else { |
| | | wx.showToast({ |
| | | title: '加载失败,请重试', |
| | | icon: 'none' |
| | | }) |
| | | } |
| | | } finally { |
| | | this.setData({ loading: false }) |
| | | wx.stopPullDownRefresh() |
| | |
| | | const projects = data.items.map(item => ({ |
| | | ...item, |
| | | submitTime: item.submitTime ? formatDate(item.submitTime) : '', |
| | | reviewTime: item.reviewTime ? formatDate(item.reviewTime) : '', |
| | | statusText: this.getStatusText(item.status), |
| | | statusType: this.getStatusType(item.status) |
| | | reviewTime: item.reviewTime ? formatDate(item.reviewTime) : '' |
| | | })) |
| | | |
| | | const projectsWithRatingCount = await this.enrichProjectsWithRatingCounts(projects) |
| | | |
| | | this.setData({ |
| | | projectList: isLoadMore ? [...this.data.projectList, ...projects] : projects, |
| | | projectList: isLoadMore ? [...this.data.projectList, ...projectsWithRatingCount] : projectsWithRatingCount, |
| | | hasMore: data.hasMore || false, |
| | | currentPage: variables.page |
| | | }) |
| | |
| | | } |
| | | }, |
| | | |
| | | // 获取状态文本 |
| | | getStatusText(status) { |
| | | const statusMap = { |
| | | 'SUBMITTED': '已提交', |
| | | 'UNDER_REVIEW': '评审中', |
| | | 'REVIEWED': '已评审', |
| | | 'REJECTED': '已拒绝' |
| | | async enrichProjectsWithRatingCounts(projects) { |
| | | if (!Array.isArray(projects) || projects.length === 0) { |
| | | return projects || [] |
| | | } |
| | | return statusMap[status] || status |
| | | |
| | | try { |
| | | const counts = await Promise.all(projects.map(project => this.getProjectRatingCount(project.id))) |
| | | return projects.map((project, index) => ({ |
| | | ...project, |
| | | ratingCount: counts[index] |
| | | })) |
| | | } catch (error) { |
| | | console.error('批量获取评审次数失败:', error) |
| | | return projects.map(project => ({ |
| | | ...project, |
| | | ratingCount: typeof project.ratingCount === 'number' ? project.ratingCount : 0 |
| | | })) |
| | | } |
| | | }, |
| | | |
| | | // 获取状态类型 |
| | | getStatusType(status) { |
| | | const typeMap = { |
| | | 'SUBMITTED': 'info', |
| | | 'UNDER_REVIEW': 'warning', |
| | | 'REVIEWED': 'success', |
| | | 'REJECTED': 'danger' |
| | | async getProjectRatingCount(activityPlayerId) { |
| | | if (!activityPlayerId) { |
| | | return 0 |
| | | } |
| | | return typeMap[status] || 'info' |
| | | |
| | | try { |
| | | const result = await graphqlRequest(GET_RATING_STATS_QUERY, { activityPlayerId }) |
| | | const ratings = result?.judgeRatingsForPlayer || [] |
| | | return ratings.filter(item => item?.hasRated).length |
| | | } catch (error) { |
| | | console.error(`获取项目 ${activityPlayerId} 的评审次数失败:`, error) |
| | | return 0 |
| | | } |
| | | }, |
| | | |
| | | // 获取空状态文本 |
| | |
| | | ] |
| | | return emptyDescs[currentTab] || '' |
| | | } |
| | | }) |
| | | }) |
| | |
| | | <view class="project-info"> |
| | | <view class="project-header"> |
| | | <text class="project-name">{{project.projectName || '未命名项目'}}</text> |
| | | <view class="project-status" wx:if="{{project.status}}"> |
| | | <text class="status-text status-{{project.statusType}}">{{project.statusText}}</text> |
| | | <view class="review-count" wx:if="{{project.ratingCount !== undefined}}"> |
| | | <text class="review-count-label">评审次数:</text> |
| | | <text class="review-count-value">{{project.ratingCount}}</text> |
| | | </view> |
| | | </view> |
| | | |
| | |
| | | </view> |
| | | |
| | | <view class="project-actions"> |
| | | <view |
| | | class="review-btn" |
| | | bindtap="goToReviewDetail" |
| | | data-project="{{project}}" |
| | | > |
| | | <text class="btn-text">评审</text> |
| | | </view> |
| | | <view |
| | | class="review-btn" |
| | | bindtap="goToReviewDetail" |
| | | data-activity-player-id="{{project.activityPlayerId || project.id}}" |
| | | > |
| | | <text class="btn-text">评审</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | |
| | | <text class="load-more-text" wx:else>加载中...</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | |
| | | margin-right: 20rpx; |
| | | } |
| | | |
| | | .project-status { |
| | | .review-count { |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .status-text { |
| | | font-size: 24rpx; |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 6rpx 12rpx; |
| | | background: #f1f5ff; |
| | | border-radius: 12rpx; |
| | | font-weight: 500; |
| | | gap: 6rpx; |
| | | } |
| | | |
| | | .status-text.status-warning { |
| | | background: #fff3e0; |
| | | color: #f57c00; |
| | | .review-count-label { |
| | | font-size: 24rpx; |
| | | color: #5a6c90; |
| | | } |
| | | |
| | | .status-text.status-success { |
| | | background: #e8f5e8; |
| | | color: #388e3c; |
| | | } |
| | | |
| | | .status-text.status-info { |
| | | background: #e3f2fd; |
| | | color: #1976d2; |
| | | .review-count-value { |
| | | font-size: 28rpx; |
| | | color: #1a3a7b; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .project-details { |
| | |
| | | .load-more-text { |
| | | font-size: 26rpx; |
| | | color: #999; |
| | | } |
| | | } |