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