From 8337c34fcc761d07acaad796d10f3e12e9bbe2d1 Mon Sep 17 00:00:00 2001
From: lrj <owen.stl@gmail.com>
Date: 星期日, 05 十月 2025 08:56:04 +0800
Subject: [PATCH] feat: 微信项目详情支持阶段评分时间轴

---
 backend/src/main/java/com/rongyichuang/player/service/ProjectStageRatingService.java             |  260 +++++++++++
 backend/src/main/java/com/rongyichuang/player/dto/response/ProjectStageTimelineResponse.java     |   38 +
 wx/pages/project/detail.wxss                                                                     |  275 +++++++++++
 backend/src/main/java/com/rongyichuang/player/dto/response/StageJudgeRatingItemResponse.java     |   53 ++
 wx/pages/project/detail.wxml                                                                     |  132 +++-
 backend/src/main/java/com/rongyichuang/player/repository/ActivityPlayerRepository.java           |    7 
 backend/src/main/java/com/rongyichuang/player/dto/response/StageJudgeRatingDetailResponse.java   |   74 +++
 backend/src/main/resources/graphql/player.graphqls                                               |   43 +
 backend/src/main/java/com/rongyichuang/player/dto/response/ProjectStageTimelineItemResponse.java |   98 ++++
 wx/pages/project/detail.js                                                                       |  330 ++++++++++----
 backend/src/main/java/com/rongyichuang/player/api/PlayerGraphqlApi.java                          |   22 
 11 files changed, 1,187 insertions(+), 145 deletions(-)

diff --git a/backend/src/main/java/com/rongyichuang/player/api/PlayerGraphqlApi.java b/backend/src/main/java/com/rongyichuang/player/api/PlayerGraphqlApi.java
index fdef59a..6f1be89 100644
--- a/backend/src/main/java/com/rongyichuang/player/api/PlayerGraphqlApi.java
+++ b/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
@@ -96,6 +102,18 @@
     @QueryMapping
     public ActivityPlayerDetailResponse activityPlayerDetail(@Argument Long id) {
         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("鑾峰彇闃舵璇勫垎璇︽儏锛宎ctivityPlayerId: {}", activityPlayerId);
+        return projectStageRatingService.getStageJudgeRatings(activityPlayerId);
     }
 
     /**
@@ -325,4 +343,4 @@
             return PromotionResult.failure("鏅嬬骇鎿嶄綔澶辫触: " + e.getMessage());
         }
     }
-}
\ No newline at end of file
+}
diff --git a/backend/src/main/java/com/rongyichuang/player/dto/response/ProjectStageTimelineItemResponse.java b/backend/src/main/java/com/rongyichuang/player/dto/response/ProjectStageTimelineItemResponse.java
new file mode 100644
index 0000000..b3677de
--- /dev/null
+++ b/backend/src/main/java/com/rongyichuang/player/dto/response/ProjectStageTimelineItemResponse.java
@@ -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;
+    }
+}
diff --git a/backend/src/main/java/com/rongyichuang/player/dto/response/ProjectStageTimelineResponse.java b/backend/src/main/java/com/rongyichuang/player/dto/response/ProjectStageTimelineResponse.java
new file mode 100644
index 0000000..8f5d82e
--- /dev/null
+++ b/backend/src/main/java/com/rongyichuang/player/dto/response/ProjectStageTimelineResponse.java
@@ -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;
+    }
+}
diff --git a/backend/src/main/java/com/rongyichuang/player/dto/response/StageJudgeRatingDetailResponse.java b/backend/src/main/java/com/rongyichuang/player/dto/response/StageJudgeRatingDetailResponse.java
new file mode 100644
index 0000000..5a827e2
--- /dev/null
+++ b/backend/src/main/java/com/rongyichuang/player/dto/response/StageJudgeRatingDetailResponse.java
@@ -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;
+    }
+}
diff --git a/backend/src/main/java/com/rongyichuang/player/dto/response/StageJudgeRatingItemResponse.java b/backend/src/main/java/com/rongyichuang/player/dto/response/StageJudgeRatingItemResponse.java
new file mode 100644
index 0000000..be54f03
--- /dev/null
+++ b/backend/src/main/java/com/rongyichuang/player/dto/response/StageJudgeRatingItemResponse.java
@@ -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;
+    }
+}
diff --git a/backend/src/main/java/com/rongyichuang/player/repository/ActivityPlayerRepository.java b/backend/src/main/java/com/rongyichuang/player/repository/ActivityPlayerRepository.java
index 014519b..82e151e 100644
--- a/backend/src/main/java/com/rongyichuang/player/repository/ActivityPlayerRepository.java
+++ b/backend/src/main/java/com/rongyichuang/player/repository/ActivityPlayerRepository.java
@@ -124,4 +124,9 @@
      * 妫�鏌ラ�夋墜鏄惁宸插湪鎸囧畾闃舵鎶ュ悕
      */
     boolean existsByStageIdAndPlayerId(Long stageId, Long playerId);
-}
\ No newline at end of file
+
+    /**
+     * 鏍规嵁闃舵鍜岄�夋墜鏌ヨ鎶ュ悕璁板綍
+     */
+    Optional<ActivityPlayer> findByStageIdAndPlayerId(Long stageId, Long playerId);
+}
diff --git a/backend/src/main/java/com/rongyichuang/player/service/ProjectStageRatingService.java b/backend/src/main/java/com/rongyichuang/player/service/ProjectStageRatingService.java
new file mode 100644
index 0000000..b95075e
--- /dev/null
+++ b/backend/src/main/java/com/rongyichuang/player/service/ProjectStageRatingService.java
@@ -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);
+    }
+}
diff --git a/backend/src/main/resources/graphql/player.graphqls b/backend/src/main/resources/graphql/player.graphqls
index d72f9d7..2d165f0 100644
--- a/backend/src/main/resources/graphql/player.graphqls
+++ b/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
     # 鑾峰彇姣旇禌鏅嬬骇鍒楄〃
@@ -101,6 +105,43 @@
     ratingItemName: String!
     score: Float
     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 {
@@ -301,4 +342,4 @@
     success: Boolean!
     message: String
     promotedCount: Int
-}
\ No newline at end of file
+}
diff --git a/wx/pages/project/detail.js b/wx/pages/project/detail.js
index 9e95a97..6ce3629 100644
--- a/wx/pages/project/detail.js
+++ b/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) {
@@ -30,51 +36,38 @@
   // 鍔犺浇椤圭洰璇︽儏
   async loadProjectDetail() {
     try {
-      this.setData({ 
-        loading: true, 
-        error: '' 
+      this.setData({
+        loading: true,
+        error: ''
       })
 
-      // 璋冪敤API鑾峰彇椤圭洰璇︽儏
       const projectDetail = await this.getProjectDetailFromAPI(this.data.projectId)
-      
-      if (projectDetail) {
-        // 澶勭悊鏂囦欢澶у皬鏄剧ず
-        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
-        })
-      } else {
+      if (!projectDetail) {
         throw new Error('椤圭洰璇︽儏鑾峰彇澶辫触')
       }
+
+      if (projectDetail.submissionFiles) {
+        projectDetail.submissionFiles.forEach(file => {
+          file.fileSizeText = this.formatFileSize(file.fileSize)
+        })
+      }
+
+      this.setData({
+        projectDetail,
+        statusText: this.getStatusText(projectDetail.state),
+        genderText: this.getGenderText(projectDetail.playerInfo?.gender),
+        educationText: this.getEducationText(projectDetail.playerInfo?.education)
+      })
+
+      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
+          playerInfo {
+            id
+            name
+            phone
+            gender
+            birthday
+            education
+            introduction
+            description
+            avatarUrl
+            avatar {
+              id
+              fullUrl
+              fullThumbUrl
+              name
+              fileSize
+              fileExt
+            }
+            userInfo {
+              userId
+              name
+              phone
+              avatarUrl
+            }
+          }
+          regionInfo {
+            id
+            name
+            fullPath
+          }
+          activityName
           projectName
-          projectDescription
-          projectCategory
-          projectTags
-          projectFiles {
-            id
-            fileName
-            fileUrl
-            fileSize
-            fileType
-            uploadTime
-          }
-          submitTime
-          reviewTime
-          reviewerId
-          reviewerName
-          score
-          rating {
-            id
-            judgeId
-            judgeName
-            score
-            feedback
-            ratingTime
-          }
-          state
+          description
           feedback
+          state
+          stageId
+          submissionFiles {
+            id
+            fullUrl
+            fullThumbUrl
+            name
+            fileSize
+            fileExt
+            mediaType
+          }
+          ratingForm {
+            schemeId
+            schemeName
+            items {
+              id
+              name
+              maxScore
+              orderNo
+            }
+            totalMaxScore
+          }
         }
       }
     `
@@ -133,45 +146,174 @@
     }
   },
 
-  // 鑾峰彇璇勫垎缁熻
-  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) {
-          averageScore
-          totalRatings
-          scoreDistribution {
-            score
-            count
+      query ProjectStageTimeline($activityPlayerId: ID!) {
+        projectStageTimeline(activityPlayerId: $activityPlayerId) {
+          activityId
+          activityName
+          stages {
+            stageId
+            stageName
+            matchTime
+            sortOrder
+            participated
+            activityPlayerId
+            averageScore
+            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: ''
+    })
   },
 
   // 棰勮鏂囦欢
@@ -455,4 +597,4 @@
       path: `/pages/project/detail?id=${this.data.projectId}`
     }
   }
-})
\ No newline at end of file
+})
diff --git a/wx/pages/project/detail.wxml b/wx/pages/project/detail.wxml
index 56425fa..9c2f49d 100644
--- a/wx/pages/project/detail.wxml
+++ b/wx/pages/project/detail.wxml
@@ -72,6 +72,57 @@
           <text class="action-btn">{{item.mediaType === 'IMAGE' ? '棰勮' : item.mediaType === 'VIDEO' ? '鎾斁' : '鏌ョ湅'}}</text>
         </view>
       </view>
+  </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>
 
@@ -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>
 
 <!-- 鍔犺浇鐘舵�� -->
@@ -184,4 +192,40 @@
   <text class="error-icon">鈿狅笍</text>
   <text class="error-text">{{error}}</text>
   <button class="retry-btn" bindtap="loadProjectDetail">閲嶈瘯</button>
-</view>
\ No newline at end of file
+</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>
diff --git a/wx/pages/project/detail.wxss b/wx/pages/project/detail.wxss
index 899ab52..6322b42 100644
--- a/wx/pages/project/detail.wxss
+++ b/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;
@@ -536,4 +805,4 @@
   .card-title {
     font-size: 32rpx;
   }
-}
\ No newline at end of file
+}

--
Gitblit v1.8.0