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