backend/src/main/java/com/rongyichuang/player/api/PlayerGraphqlApi.java
@@ -6,12 +6,14 @@ import com.rongyichuang.player.dto.response.ActivityPlayerApplicationResponse; import com.rongyichuang.player.dto.response.ActivityPlayerDetailResponse; import com.rongyichuang.player.dto.response.ProjectReviewApplicationPageResponse; import com.rongyichuang.player.dto.response.ProjectStageTimelineResponse; import com.rongyichuang.player.dto.response.PlayerApplicationPageResponse; import com.rongyichuang.player.dto.ActivityRegistrationResponse; import com.rongyichuang.player.dto.response.JudgeRatingStatusResponse; import com.rongyichuang.player.dto.response.CurrentJudgeRatingResponse; import com.rongyichuang.player.dto.response.CurrentJudgeInfoResponse; import com.rongyichuang.player.dto.response.PlayerRegistrationResponse; import com.rongyichuang.player.dto.response.StageJudgeRatingDetailResponse; import com.rongyichuang.player.dto.PromotionCompetitionResponse; import com.rongyichuang.player.dto.CompetitionParticipantResponse; import com.rongyichuang.player.dto.PromotionInput; @@ -22,6 +24,7 @@ import com.rongyichuang.player.service.ActivityPlayerDetailService; import com.rongyichuang.player.service.ActivityPlayerRatingService; import com.rongyichuang.player.service.ActivityPlayerService; import com.rongyichuang.player.service.ProjectStageRatingService; import com.rongyichuang.player.service.PromotionService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,17 +46,20 @@ private final ActivityPlayerRatingService ratingService; private final ActivityPlayerService activityPlayerService; private final PromotionService promotionService; private final ProjectStageRatingService projectStageRatingService; public PlayerGraphqlApi(PlayerApplicationService service, ActivityPlayerDetailService detailService, ActivityPlayerRatingService ratingService, ActivityPlayerService activityPlayerService, PromotionService promotionService) { PromotionService promotionService, ProjectStageRatingService projectStageRatingService) { this.service = service; this.detailService = detailService; this.ratingService = ratingService; this.activityPlayerService = activityPlayerService; this.promotionService = promotionService; this.projectStageRatingService = projectStageRatingService; } @QueryMapping @@ -98,6 +104,18 @@ return detailService.getDetailForRating(id); } @QueryMapping public ProjectStageTimelineResponse projectStageTimeline(@Argument Long activityPlayerId) { log.info("获取参赛项目阶段时间轴,activityPlayerId: {}", activityPlayerId); return projectStageRatingService.getProjectStageTimeline(activityPlayerId); } @QueryMapping public StageJudgeRatingDetailResponse stageJudgeRatings(@Argument Long activityPlayerId) { log.info("获取阶段评分详情,activityPlayerId: {}", activityPlayerId); return projectStageRatingService.getStageJudgeRatings(activityPlayerId); } /** * 查询玩家在指定活动中的报名状态 */ backend/src/main/java/com/rongyichuang/player/dto/response/ProjectStageTimelineItemResponse.java
New file @@ -0,0 +1,98 @@ package com.rongyichuang.player.dto.response; /** * 项目阶段时间轴条目响应 */ public class ProjectStageTimelineItemResponse { private Long stageId; private String stageName; private String matchTime; private Integer sortOrder; private boolean participated; private Long activityPlayerId; private Double averageScore; private Integer ratingCount = 0; private boolean hasRating; private String latestRatingTime; public Long getStageId() { return stageId; } public void setStageId(Long stageId) { this.stageId = stageId; } public String getStageName() { return stageName; } public void setStageName(String stageName) { this.stageName = stageName; } public String getMatchTime() { return matchTime; } public void setMatchTime(String matchTime) { this.matchTime = matchTime; } public Integer getSortOrder() { return sortOrder; } public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; } public boolean isParticipated() { return participated; } public void setParticipated(boolean participated) { this.participated = participated; } public Long getActivityPlayerId() { return activityPlayerId; } public void setActivityPlayerId(Long activityPlayerId) { this.activityPlayerId = activityPlayerId; } public Double getAverageScore() { return averageScore; } public void setAverageScore(Double averageScore) { this.averageScore = averageScore; } public Integer getRatingCount() { return ratingCount; } public void setRatingCount(Integer ratingCount) { this.ratingCount = ratingCount; } public boolean isHasRating() { return hasRating; } public void setHasRating(boolean hasRating) { this.hasRating = hasRating; } public String getLatestRatingTime() { return latestRatingTime; } public void setLatestRatingTime(String latestRatingTime) { this.latestRatingTime = latestRatingTime; } } backend/src/main/java/com/rongyichuang/player/dto/response/ProjectStageTimelineResponse.java
New file @@ -0,0 +1,38 @@ package com.rongyichuang.player.dto.response; import java.util.ArrayList; import java.util.List; /** * 项目阶段时间轴响应 */ public class ProjectStageTimelineResponse { private Long activityId; private String activityName; private List<ProjectStageTimelineItemResponse> stages = new ArrayList<>(); public Long getActivityId() { return activityId; } public void setActivityId(Long activityId) { this.activityId = activityId; } public String getActivityName() { return activityName; } public void setActivityName(String activityName) { this.activityName = activityName; } public List<ProjectStageTimelineItemResponse> getStages() { return stages; } public void setStages(List<ProjectStageTimelineItemResponse> stages) { this.stages = stages; } } backend/src/main/java/com/rongyichuang/player/dto/response/StageJudgeRatingDetailResponse.java
New file @@ -0,0 +1,74 @@ package com.rongyichuang.player.dto.response; import java.util.ArrayList; import java.util.List; /** * 项目阶段评分详情响应 */ public class StageJudgeRatingDetailResponse { private Long activityPlayerId; private Long stageId; private String stageName; private String matchTime; private Integer ratingCount = 0; private Double averageScore; private List<StageJudgeRatingItemResponse> judgeRatings = new ArrayList<>(); public Long getActivityPlayerId() { return activityPlayerId; } public void setActivityPlayerId(Long activityPlayerId) { this.activityPlayerId = activityPlayerId; } public Long getStageId() { return stageId; } public void setStageId(Long stageId) { this.stageId = stageId; } public String getStageName() { return stageName; } public void setStageName(String stageName) { this.stageName = stageName; } public String getMatchTime() { return matchTime; } public void setMatchTime(String matchTime) { this.matchTime = matchTime; } public Integer getRatingCount() { return ratingCount; } public void setRatingCount(Integer ratingCount) { this.ratingCount = ratingCount; } public Double getAverageScore() { return averageScore; } public void setAverageScore(Double averageScore) { this.averageScore = averageScore; } public List<StageJudgeRatingItemResponse> getJudgeRatings() { return judgeRatings; } public void setJudgeRatings(List<StageJudgeRatingItemResponse> judgeRatings) { this.judgeRatings = judgeRatings; } } backend/src/main/java/com/rongyichuang/player/dto/response/StageJudgeRatingItemResponse.java
New file @@ -0,0 +1,53 @@ package com.rongyichuang.player.dto.response; /** * 阶段评委评分明细响应 */ public class StageJudgeRatingItemResponse { private Long judgeId; private String judgeName; private Double totalScore; private String feedback; private String ratingTime; public Long getJudgeId() { return judgeId; } public void setJudgeId(Long judgeId) { this.judgeId = judgeId; } public String getJudgeName() { return judgeName; } public void setJudgeName(String judgeName) { this.judgeName = judgeName; } public Double getTotalScore() { return totalScore; } public void setTotalScore(Double totalScore) { this.totalScore = totalScore; } public String getFeedback() { return feedback; } public void setFeedback(String feedback) { this.feedback = feedback; } public String getRatingTime() { return ratingTime; } public void setRatingTime(String ratingTime) { this.ratingTime = ratingTime; } } backend/src/main/java/com/rongyichuang/player/repository/ActivityPlayerRepository.java
@@ -124,4 +124,9 @@ * 检查选手是否已在指定阶段报名 */ boolean existsByStageIdAndPlayerId(Long stageId, Long playerId); /** * 根据阶段和选手查询报名记录 */ Optional<ActivityPlayer> findByStageIdAndPlayerId(Long stageId, Long playerId); } backend/src/main/java/com/rongyichuang/player/service/ProjectStageRatingService.java
New file @@ -0,0 +1,260 @@ package com.rongyichuang.player.service; import com.rongyichuang.activity.entity.Activity; import com.rongyichuang.activity.entity.ActivityPlayerRating; import com.rongyichuang.activity.repository.ActivityPlayerRatingRepository; import com.rongyichuang.activity.repository.ActivityRepository; import com.rongyichuang.judge.entity.Judge; import com.rongyichuang.judge.repository.JudgeRepository; import com.rongyichuang.player.dto.response.ProjectStageTimelineItemResponse; import com.rongyichuang.player.dto.response.ProjectStageTimelineResponse; import com.rongyichuang.player.dto.response.StageJudgeRatingDetailResponse; import com.rongyichuang.player.dto.response.StageJudgeRatingItemResponse; import com.rongyichuang.player.entity.ActivityPlayer; import com.rongyichuang.player.repository.ActivityPlayerRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; /** * 项目阶段评分相关服务 */ @Service public class ProjectStageRatingService { private static final Logger log = LoggerFactory.getLogger(ProjectStageRatingService.class); private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); private final ActivityPlayerRepository activityPlayerRepository; private final ActivityRepository activityRepository; private final ActivityPlayerRatingRepository activityPlayerRatingRepository; private final JudgeRepository judgeRepository; public ProjectStageRatingService(ActivityPlayerRepository activityPlayerRepository, ActivityRepository activityRepository, ActivityPlayerRatingRepository activityPlayerRatingRepository, JudgeRepository judgeRepository) { this.activityPlayerRepository = activityPlayerRepository; this.activityRepository = activityRepository; this.activityPlayerRatingRepository = activityPlayerRatingRepository; this.judgeRepository = judgeRepository; } /** * 获取项目在各阶段的时间轴与评分概况 */ public ProjectStageTimelineResponse getProjectStageTimeline(Long activityPlayerId) { ActivityPlayer current = activityPlayerRepository.findById(activityPlayerId) .orElseThrow(() -> new IllegalArgumentException("未找到参赛记录,ID: " + activityPlayerId)); Long activityId = current.getActivityId(); Long playerId = current.getPlayerId(); ProjectStageTimelineResponse response = new ProjectStageTimelineResponse(); response.setActivityId(activityId); Activity activity = activityRepository.findActivityById(activityId); if (activity != null) { response.setActivityName(activity.getName()); } List<Activity> stages = new ArrayList<>(activityRepository.findByPidAndStateOrderBySortOrderAsc(activityId, 1)); Map<Long, ActivityPlayer> stagePlayerMap = activityPlayerRepository .findByActivityIdAndPlayerIdOrderByCreateTimeDesc(activityId, playerId) .stream() .filter(ap -> ap.getStageId() != null) .collect(Collectors.toMap(ActivityPlayer::getStageId, ap -> ap, (existing, replacement) -> existing)); // 确保当前阶段包含在时间轴中 if (current.getStageId() != null && !stagePlayerMap.containsKey(current.getStageId())) { stagePlayerMap.put(current.getStageId(), current); } // 如果阶段列表中缺少已参赛阶段,补全它们 Set<Long> stageIdsInList = stages.stream().map(Activity::getId).collect(Collectors.toSet()); for (Long stageId : stagePlayerMap.keySet()) { if (!stageIdsInList.contains(stageId)) { Activity stage = activityRepository.findActivityById(stageId); if (stage != null) { stages.add(stage); stageIdsInList.add(stageId); } } } // 按 sortOrder 及创建时间排序,确保展示顺序 stages.sort(Comparator .comparing((Activity stage) -> Optional.ofNullable(stage.getSortOrder()).orElse(Integer.MAX_VALUE)) .thenComparing(Activity::getId)); List<ProjectStageTimelineItemResponse> items = new ArrayList<>(); for (Activity stage : stages) { ProjectStageTimelineItemResponse item = new ProjectStageTimelineItemResponse(); item.setStageId(stage.getId()); item.setStageName(stage.getName()); item.setMatchTime(formatDateTime(stage.getMatchTime())); item.setSortOrder(stage.getSortOrder()); ActivityPlayer stagePlayer = stagePlayerMap.get(stage.getId()); boolean participated = stagePlayer != null; item.setParticipated(participated); if (participated) { item.setActivityPlayerId(stagePlayer.getId()); applyRatingMetrics(item, stagePlayer.getId()); } else { item.setHasRating(false); item.setRatingCount(0); } items.add(item); } response.setStages(items); return response; } /** * 获取指定阶段的评委评分详情 */ public StageJudgeRatingDetailResponse getStageJudgeRatings(Long activityPlayerId) { ActivityPlayer stagePlayer = activityPlayerRepository.findById(activityPlayerId) .orElseThrow(() -> new IllegalArgumentException("未找到参赛记录,ID: " + activityPlayerId)); StageJudgeRatingDetailResponse response = new StageJudgeRatingDetailResponse(); response.setActivityPlayerId(activityPlayerId); Activity stage = null; if (stagePlayer.getStageId() != null) { stage = activityRepository.findActivityById(stagePlayer.getStageId()); } if (stage != null) { response.setStageId(stage.getId()); response.setStageName(stage.getName()); response.setMatchTime(formatDateTime(stage.getMatchTime())); } else { response.setStageId(stagePlayer.getStageId()); response.setStageName("阶段信息缺失"); } List<ActivityPlayerRating> ratings = activityPlayerRatingRepository.findByActivityPlayerId(activityPlayerId); Map<Long, ActivityPlayerRating> latestRatingByJudge = new HashMap<>(); for (ActivityPlayerRating rating : ratings) { Long judgeId = rating.getJudgeId(); if (judgeId == null) { continue; } ActivityPlayerRating existing = latestRatingByJudge.get(judgeId); if (existing == null) { latestRatingByJudge.put(judgeId, rating); } else { LocalDateTime existingTime = existing.getUpdateTime(); LocalDateTime currentTime = rating.getUpdateTime(); if (existingTime == null || (currentTime != null && currentTime.isAfter(existingTime))) { latestRatingByJudge.put(judgeId, rating); } } } Collection<ActivityPlayerRating> effectiveRatings = latestRatingByJudge.values(); List<BigDecimal> validScores = effectiveRatings.stream() .map(ActivityPlayerRating::getTotalScore) .filter(Objects::nonNull) .collect(Collectors.toList()); int ratingCount = validScores.size(); response.setRatingCount(ratingCount); if (ratingCount > 0) { BigDecimal sum = validScores.stream().reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal avg = sum.divide(BigDecimal.valueOf(ratingCount), 2, RoundingMode.HALF_UP); response.setAverageScore(avg.doubleValue()); } if (!effectiveRatings.isEmpty()) { Set<Long> judgeIds = effectiveRatings.stream() .map(ActivityPlayerRating::getJudgeId) .filter(Objects::nonNull) .collect(Collectors.toSet()); Map<Long, String> judgeNameMap = new HashMap<>(); if (!judgeIds.isEmpty()) { List<Judge> judges = judgeRepository.findAllById(judgeIds); for (Judge judge : judges) { judgeNameMap.put(judge.getId(), judge.getName()); } } List<StageJudgeRatingItemResponse> judgeItems = effectiveRatings.stream() .sorted(Comparator.comparing(ActivityPlayerRating::getUpdateTime, Comparator.nullsLast(LocalDateTime::compareTo)).reversed()) .map(rating -> { StageJudgeRatingItemResponse item = new StageJudgeRatingItemResponse(); Long judgeId = rating.getJudgeId(); item.setJudgeId(judgeId); item.setJudgeName(judgeNameMap.getOrDefault(judgeId, "未知评委")); if (rating.getTotalScore() != null) { item.setTotalScore(rating.getTotalScore().doubleValue()); } item.setFeedback(rating.getFeedback()); item.setRatingTime(formatDateTime(rating.getUpdateTime())); return item; }) .collect(Collectors.toList()); response.setJudgeRatings(judgeItems); } return response; } private void applyRatingMetrics(ProjectStageTimelineItemResponse item, Long stageActivityPlayerId) { List<ActivityPlayerRating> ratings = activityPlayerRatingRepository .findCompletedRatingsByActivityPlayerId(stageActivityPlayerId); List<BigDecimal> validScores = ratings.stream() .map(ActivityPlayerRating::getTotalScore) .filter(Objects::nonNull) .collect(Collectors.toList()); int ratingCount = validScores.size(); item.setRatingCount(ratingCount); item.setHasRating(ratingCount > 0); if (ratingCount > 0) { BigDecimal sum = validScores.stream().reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal avg = sum.divide(BigDecimal.valueOf(ratingCount), 2, RoundingMode.HALF_UP); item.setAverageScore(avg.doubleValue()); } ratings.stream() .map(ActivityPlayerRating::getUpdateTime) .filter(Objects::nonNull) .max(LocalDateTime::compareTo) .ifPresent(time -> item.setLatestRatingTime(formatDateTime(time))); } private String formatDateTime(LocalDateTime time) { if (time == null) { return null; } return DATE_TIME_FORMATTER.format(time); } } backend/src/main/resources/graphql/player.graphqls
@@ -17,6 +17,10 @@ # 获取当前评委对选手的评分 currentJudgeRating(activityPlayerId: ID!): CurrentJudgeRatingResponse activityPlayerDetail(id: ID!): ActivityPlayerDetailResponse # 获取参赛项目阶段时间轴及评分概况 projectStageTimeline(activityPlayerId: ID!): ProjectStageTimelineResponse # 获取指定阶段的评委评分详情 stageJudgeRatings(activityPlayerId: ID!): StageJudgeRatingDetailResponse # 微信端获取选手报名状态 getPlayerRegistrationState(activityId: ID!): PlayerRegistrationResponse # 获取比赛晋级列表 @@ -103,6 +107,43 @@ weightedScore: Float } type ProjectStageTimelineResponse { activityId: ID! activityName: String stages: [ProjectStageTimelineItemResponse!]! } type ProjectStageTimelineItemResponse { stageId: ID! stageName: String! matchTime: String sortOrder: Int participated: Boolean! activityPlayerId: ID averageScore: Float ratingCount: Int! hasRating: Boolean! latestRatingTime: String } type StageJudgeRatingDetailResponse { activityPlayerId: ID! stageId: ID stageName: String matchTime: String ratingCount: Int! averageScore: Float judgeRatings: [StageJudgeRatingItemResponse!]! } type StageJudgeRatingItemResponse { judgeId: ID! judgeName: String! totalScore: Float feedback: String ratingTime: String } type ActivityPlayerApplicationResponse { id: ID playerName: String wx/pages/project/detail.js
@@ -5,12 +5,18 @@ data: { projectId: '', projectDetail: null, ratingStats: null, timeline: [], loading: true, error: '', statusText: '', genderText: '', educationText: '' educationText: '', timelineLoading: false, timelineError: '', showRatingDetail: false, ratingDetail: null, ratingDetailLoading: false, ratingDetailError: '' }, onLoad(options) { @@ -35,46 +41,33 @@ error: '' }) // 调用API获取项目详情 const projectDetail = await this.getProjectDetailFromAPI(this.data.projectId) if (projectDetail) { // 处理文件大小显示 if (!projectDetail) { throw new Error('项目详情获取失败') } if (projectDetail.submissionFiles) { projectDetail.submissionFiles.forEach(file => { file.fileSizeText = this.formatFileSize(file.fileSize) }) } // 获取评分统计 const ratingStats = await this.getRatingStatsFromAPI(this.data.projectId) // 处理评分时间显示 if (ratingStats && ratingStats.judgeRatings) { ratingStats.judgeRatings.forEach(rating => { if (rating.ratingTime) { rating.ratingTimeText = this.formatDateTime(rating.ratingTime) } }) } this.setData({ projectDetail, ratingStats, statusText: this.getStatusText(projectDetail.state), genderText: this.getGenderText(projectDetail.playerInfo?.gender), educationText: this.getEducationText(projectDetail.playerInfo?.education), loading: false educationText: this.getEducationText(projectDetail.playerInfo?.education) }) } else { throw new Error('项目详情获取失败') } await this.loadProjectTimeline(this.data.projectId) } catch (error) { console.error('加载项目详情失败:', error) this.setData({ error: error.message || '加载失败,请重试', loading: false error: error.message || '加载失败,请重试' }) } finally { this.setData({ loading: false }) } }, @@ -85,42 +78,62 @@ query GetProjectDetail($id: ID!) { activityPlayerDetail(id: $id) { id activityId playerId playerName playerGender playerPhone playerEducation playerBirthDate playerIdCard playerAddress projectName projectDescription projectCategory projectTags projectFiles { playerInfo { id fileName fileUrl name phone gender birthday education introduction description avatarUrl avatar { id fullUrl fullThumbUrl name fileSize fileType uploadTime fileExt } submitTime reviewTime reviewerId reviewerName score rating { userInfo { userId name phone avatarUrl } } regionInfo { id judgeId judgeName score feedback ratingTime name fullPath } state activityName projectName description feedback state stageId submissionFiles { id fullUrl fullThumbUrl name fileSize fileExt mediaType } ratingForm { schemeId schemeName items { id name maxScore orderNo } totalMaxScore } } } ` @@ -133,47 +146,176 @@ } }, // 获取评分统计 async getRatingStatsFromAPI(projectId) { async loadProjectTimeline(activityPlayerId) { if (!activityPlayerId) { return } const idNumber = Number(activityPlayerId) const variables = { activityPlayerId: Number.isNaN(idNumber) ? activityPlayerId : idNumber } this.setData({ timelineLoading: true, timelineError: '' }) const query = ` query GetRatingStats($activityPlayerId: ID!) { ratingStats(activityPlayerId: $activityPlayerId) { query ProjectStageTimeline($activityPlayerId: ID!) { projectStageTimeline(activityPlayerId: $activityPlayerId) { activityId activityName stages { stageId stageName matchTime sortOrder participated activityPlayerId averageScore totalRatings scoreDistribution { score count ratingCount hasRating latestRatingTime } } } ` try { const result = await app.graphqlRequest(query, { activityPlayerId: projectId }) return result.ratingStats const result = await app.graphqlRequest(query, variables) const projectStageTimeline = result && result.projectStageTimeline ? result.projectStageTimeline : null const stages = projectStageTimeline && projectStageTimeline.stages ? projectStageTimeline.stages : [] const timeline = stages.map(stage => { const hasScore = stage.hasRating && stage.averageScore !== null && stage.averageScore !== undefined let scoreText = '未参赛' if (stage.participated) { scoreText = hasScore ? `平均分:${Number(stage.averageScore).toFixed(2)}` : '未评分' } return { ...stage, matchTimeText: stage.matchTime ? this.formatDateTime(stage.matchTime) : '', scoreText, displayAverageScore: hasScore ? Number(stage.averageScore).toFixed(2) : null, isClickable: stage.participated && hasScore && !!stage.activityPlayerId } }) this.setData({ timeline, timelineLoading: false }) } catch (error) { throw error console.error('加载阶段时间轴失败:', error) this.setData({ timelineError: error.message || '时间轴加载失败', timelineLoading: false }) } }, // 获取评委评分详情 async getJudgeRatingDetail(activityPlayerId, judgeId) { async fetchStageRatingDetail(activityPlayerId) { const idNumber = Number(activityPlayerId) const variables = { activityPlayerId: Number.isNaN(idNumber) ? activityPlayerId : idNumber } const query = ` query GetJudgeRatingDetail($activityPlayerId: ID!, $judgeId: ID!) { judgeRatingDetail(activityPlayerId: $activityPlayerId, judgeId: $judgeId) { remark query StageJudgeRatings($activityPlayerId: ID!) { stageJudgeRatings(activityPlayerId: $activityPlayerId) { activityPlayerId stageId stageName matchTime ratingCount averageScore judgeRatings { judgeId judgeName totalScore feedback ratingTime } } } ` try { const result = await app.graphqlRequest(query, { activityPlayerId, judgeId }) return result.judgeRatingDetail } catch (error) { throw error const result = await app.graphqlRequest(query, variables) const detail = result && result.stageJudgeRatings ? result.stageJudgeRatings : null const sourceJudgeRatings = detail && detail.judgeRatings ? detail.judgeRatings : [] const judgeRatings = sourceJudgeRatings.map(item => ({ ...item, totalScoreText: item.totalScore !== null && item.totalScore !== undefined ? `${Number(item.totalScore).toFixed(2)}分` : '未评分', ratingTimeText: item.ratingTime ? this.formatDateTime(item.ratingTime) : '' })) const averageScoreValue = detail && detail.averageScore !== undefined && detail.averageScore !== null ? detail.averageScore : null return { activityPlayerId: detail && detail.activityPlayerId ? detail.activityPlayerId : variables.activityPlayerId, stageId: detail && detail.stageId ? detail.stageId : null, stageName: detail && detail.stageName ? detail.stageName : '阶段信息', matchTime: detail && detail.matchTime ? detail.matchTime : null, matchTimeText: detail && detail.matchTime ? this.formatDateTime(detail.matchTime) : '', ratingCount: detail && detail.ratingCount ? detail.ratingCount : 0, averageScore: averageScoreValue, averageScoreText: averageScoreValue !== null ? Number(averageScoreValue).toFixed(2) : '暂无评分', judgeRatings } }, async openStageDetail(e) { const { playerId } = e.currentTarget.dataset const clickable = e.currentTarget.dataset.clickable === true || e.currentTarget.dataset.clickable === 'true' const participated = e.currentTarget.dataset.participated === true || e.currentTarget.dataset.participated === 'true' if (!playerId || !participated) { return } if (!clickable) { wx.showToast({ title: '暂无评分', icon: 'none' }) return } this.setData({ showRatingDetail: true, ratingDetailLoading: true, ratingDetailError: '', ratingDetail: { judgeRatings: [] } }) try { const detail = await this.fetchStageRatingDetail(playerId) this.setData({ ratingDetail: detail, ratingDetailLoading: false }) } catch (error) { console.error('加载阶段评分详情失败:', error) this.setData({ ratingDetailError: error.message || '加载失败', ratingDetailLoading: false }) } }, closeStageDetail() { this.setData({ showRatingDetail: false, ratingDetail: null, ratingDetailError: '' }) }, // 预览文件 previewFile(e) { const file = e.currentTarget.dataset.file wx/pages/project/detail.wxml
@@ -75,6 +75,57 @@ </view> </view> <!-- 比赛阶段时间轴 --> <view class="timeline-card"> <view class="card-header"> <text class="card-title">比赛阶段</text> </view> <view class="timeline-loading" wx:if="{{timelineLoading}}"> <view class="loading-spinner small"></view> <text class="loading-text">时间轴加载中...</text> </view> <view class="timeline-error" wx:elif="{{timelineError}}"> {{timelineError}} </view> <view class="timeline-wrapper" wx:elif="{{timeline.length > 0}}"> <view class="timeline-item {{item.participated ? 'timeline-item-active' : 'timeline-item-inactive'}} {{item.isClickable ? 'timeline-item-clickable' : ''}}" wx:for="{{timeline}}" wx:key="stageId" bindtap="openStageDetail" data-player-id="{{item.activityPlayerId}}" data-clickable="{{item.isClickable}}" data-participated="{{item.participated}}" > <view class="timeline-axis"> <view class="timeline-dot"></view> <view class="timeline-line" wx:if="{{index < timeline.length - 1}}"></view> </view> <view class="timeline-body"> <view class="timeline-title"> <text class="stage-name">{{item.stageName}}</text> <text class="stage-score">{{item.scoreText}}</text> </view> <view class="timeline-sub"> <text class="stage-time">{{item.matchTimeText || '时间待定'}}</text> <text class="stage-status" wx:if="{{item.participated}}">已参赛</text> <text class="stage-status stage-status-inactive" wx:else>未参赛</text> </view> <view class="timeline-actions" wx:if="{{item.hasRating && item.activityPlayerId}}"> <text class="detail-link">【详情】</text> </view> </view> </view> </view> <view class="timeline-empty" wx:else> 暂无阶段信息 </view> </view> <!-- 参赛人信息 - 已隐藏 --> <!-- <view class="player-card" wx:if="{{projectDetail.playerInfo}}"> @@ -128,49 +179,6 @@ </view> </view> <!-- 评审信息 --> <view class="rating-card" wx:if="{{ratingStats}}"> <view class="card-header"> <text class="card-title">评审信息</text> </view> <view class="rating-info"> <!-- 总平均分显示 --> <view class="average-score-section"> <view class="average-score-container"> <text class="average-score-label">总平均分</text> <text class="average-score-value">{{ratingStats.averageScore || '暂无评分'}}</text> <text class="rating-count-text">({{ratingStats.ratingCount || 0}}位评委已评分)</text> </view> </view> <!-- 各评委评分详情 --> <view class="judge-ratings-section" wx:if="{{ratingStats.judgeRatings && ratingStats.judgeRatings.length > 0}}"> <text class="section-title">评委评分详情</text> <view class="judge-rating" wx:for="{{ratingStats.judgeRatings}}" wx:key="judgeId" wx:if="{{item.hasRated}}" > <view class="rating-header"> <view class="judge-info"> <text class="judge-label">评委 {{index + 1}}</text> <text class="rating-status rated">已评分</text> </view> <view class="score-info"> <text class="score">{{item.totalScore}}分</text> <text class="rating-time">{{item.ratingTimeText}}</text> </view> </view> <view class="rating-comment" wx:if="{{item.remark}}"> <text class="comment-label">点评:</text> <text class="comment-text">{{item.remark}}</text> </view> </view> </view> </view> </view> </view> <!-- 加载状态 --> @@ -185,3 +193,39 @@ <text class="error-text">{{error}}</text> <button class="retry-btn" bindtap="loadProjectDetail">重试</button> </view> <!-- 阶段评分详情弹窗 --> <view class="rating-detail-overlay" wx:if="{{showRatingDetail}}"> <view class="overlay-mask" bindtap="closeStageDetail"></view> <view class="overlay-panel"> <view class="overlay-header"> <text class="overlay-title">{{ratingDetail ? ratingDetail.stageName : '评分详情'}}</text> <view class="overlay-close" bindtap="closeStageDetail">×</view> </view> <text class="overlay-subtitle">{{ratingDetail && ratingDetail.matchTimeText ? ratingDetail.matchTimeText : '时间待定'}}</text> <view class="overlay-summary"> <text class="summary-score">平均分:{{ratingDetail ? ratingDetail.averageScoreText : '暂无评分'}}</text> <text class="summary-count">评委数:{{ratingDetail ? ratingDetail.ratingCount : 0}}</text> </view> <view class="overlay-body"> <view class="panel-loading" wx:if="{{ratingDetailLoading}}"> <view class="loading-spinner small"></view> <text class="loading-text">加载评分详情...</text> </view> <view class="panel-error" wx:elif="{{ratingDetailError}}">{{ratingDetailError}}</view> <block wx:else> <view class="judge-item" wx:for="{{(ratingDetail && ratingDetail.judgeRatings) ? ratingDetail.judgeRatings : []}}" wx:key="judgeId"> <view class="judge-header"> <text class="judge-name">{{item.judgeName}}</text> <text class="judge-score">{{item.totalScoreText}}</text> </view> <text class="judge-time" wx:if="{{item.ratingTimeText}}">{{item.ratingTimeText}}</text> <view class="judge-feedback" wx:if="{{item.feedback}}">{{item.feedback}}</view> </view> <view class="panel-empty" wx:if="{{ratingDetail && ratingDetail.judgeRatings && ratingDetail.judgeRatings.length === 0}}"> 暂无评分详情 </view> </block> </view> </view> </view> wx/pages/project/detail.wxss
@@ -9,8 +9,8 @@ .project-card, .attachments-card, .player-card, .rating-card, .feedback-card { .feedback-card, .timeline-card { background-color: #ffffff; border-radius: 16rpx; margin-bottom: 20rpx; @@ -456,6 +456,275 @@ border-left: 6rpx solid #409eff; } /* 时间轴 */ .timeline-loading, .timeline-error, .timeline-empty { padding: 40rpx 10rpx; text-align: center; color: #666666; font-size: 28rpx; } .timeline-wrapper { margin-top: 10rpx; } .timeline-item { display: flex; position: relative; padding-left: 80rpx; padding-bottom: 40rpx; } .timeline-item:last-child { padding-bottom: 0; } .timeline-item.timeline-item-clickable .timeline-body { border-color: rgba(22, 119, 255, 0.45); box-shadow: 0 6rpx 16rpx rgba(22, 119, 255, 0.08); } .timeline-axis { position: absolute; left: 32rpx; top: 0; bottom: 0; display: flex; align-items: flex-start; } .timeline-dot { width: 24rpx; height: 24rpx; border-radius: 50%; background-color: #d0d3d8; margin-top: 8rpx; } .timeline-item-active .timeline-dot { background-color: #1677ff; box-shadow: 0 0 0 8rpx rgba(22, 119, 255, 0.12); } .timeline-line { width: 4rpx; background-color: #e5e9ef; flex: 1; margin: 0 auto; margin-top: 8rpx; } .timeline-item-active .timeline-line { background-color: rgba(22, 119, 255, 0.2); } .timeline-body { flex: 1; background-color: #f8f9fa; border-radius: 16rpx; padding: 24rpx; border: 2rpx solid #e0e4eb; } .timeline-item-active .timeline-body { border-color: rgba(22, 119, 255, 0.35); background-color: rgba(22, 119, 255, 0.05); } .timeline-title { display: flex; justify-content: space-between; align-items: center; gap: 20rpx; } .stage-name { font-size: 32rpx; font-weight: 600; color: #333333; } .stage-score { font-size: 28rpx; color: #1677ff; font-weight: 600; } .timeline-item-inactive .stage-score { color: #999999; } .timeline-sub { margin-top: 12rpx; display: flex; align-items: center; gap: 16rpx; font-size: 26rpx; color: #666666; } .stage-status { padding: 4rpx 16rpx; border-radius: 16rpx; background-color: rgba(22, 119, 255, 0.1); color: #1677ff; font-size: 24rpx; } .stage-status-inactive { background-color: #f4f5f8; color: #909399; } .timeline-actions { margin-top: 12rpx; } .detail-link { color: #1677ff; font-size: 26rpx; } .detail-link:active { opacity: 0.7; } /* 评分详情弹窗 */ .rating-detail-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 999; } .overlay-mask { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.45); } .overlay-panel { position: absolute; left: 0; right: 0; bottom: 0; background-color: #ffffff; border-top-left-radius: 24rpx; border-top-right-radius: 24rpx; padding: 40rpx 32rpx 60rpx; max-height: 80vh; overflow-y: auto; } .overlay-header { display: flex; justify-content: space-between; align-items: center; } .overlay-title { font-size: 34rpx; font-weight: 600; color: #333333; } .overlay-close { font-size: 44rpx; color: #999999; padding: 0 10rpx; } .overlay-subtitle { font-size: 26rpx; color: #666666; margin-top: 12rpx; } .overlay-summary { margin-top: 24rpx; padding: 24rpx; background-color: #f8f9fa; border-radius: 16rpx; display: flex; justify-content: space-between; align-items: center; font-size: 26rpx; color: #555555; } .summary-score { font-weight: 600; color: #1677ff; } .overlay-body { margin-top: 30rpx; } .panel-loading, .panel-error, .panel-empty { text-align: center; font-size: 28rpx; color: #666666; padding: 40rpx 0; } .judge-item { border: 2rpx solid #e4e7ed; border-radius: 16rpx; padding: 24rpx; margin-bottom: 24rpx; background-color: #ffffff; } .judge-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8rpx; } .judge-name { font-size: 30rpx; font-weight: 600; color: #333333; } .judge-score { font-size: 30rpx; font-weight: 600; color: #1677ff; } .judge-time { font-size: 24rpx; color: #999999; } .judge-feedback { margin-top: 16rpx; font-size: 28rpx; color: #333333; line-height: 1.5; background-color: #f8f9fa; padding: 16rpx; border-radius: 12rpx; } .loading-spinner.small { width: 40rpx; height: 40rpx; border-width: 5rpx; } /* 加载状态 */ .loading-container { display: flex;