From 0a48616045ddce1562584543a0e89e5144051fde Mon Sep 17 00:00:00 2001
From: Codex Assistant <codex@example.com>
Date: 星期日, 05 十月 2025 14:52:44 +0800
Subject: [PATCH] 报名审核

---
 wx/pages/judge/review.js                                                                   |  522 +++++++++--------
 wx/app.js                                                                                  |  202 +++++-
 wx/pages/judge/review.wxss                                                                 |  444 ++++----------
 wx/pages/judge/review.wxml                                                                 |  210 ++----
 backend/src/main/java/com/rongyichuang/player/dto/response/CurrentJudgeRatingResponse.java |   24 
 backend/src/main/java/com/rongyichuang/review/resolver/ReviewResolver.java                 |   18 
 backend/src/main/java/com/rongyichuang/auth/filter/JwtAuthenticationFilter.java            |    2 
 clear-invalid-tokens.js                                                                    |   86 ++
 backend/src/main/java/com/rongyichuang/player/service/ActivityPlayerRatingService.java     |   47 +
 wx/pages/review/index.js                                                                   |   92 ++
 wx/pages/review/index.wxml                                                                 |   21 
 wx/pages/review/index.wxss                                                                 |   31 
 backend/src/main/resources/application.yml                                                 |    2 
 backend/src/main/resources/graphql/player.graphqls                                         |    2 
 wx/pages/project/detail.js                                                                 |    2 
 wx/pages/message/message.js                                                                |   18 
 16 files changed, 883 insertions(+), 840 deletions(-)

diff --git a/backend/src/main/java/com/rongyichuang/auth/filter/JwtAuthenticationFilter.java b/backend/src/main/java/com/rongyichuang/auth/filter/JwtAuthenticationFilter.java
index e7a03b1..c0f9452 100644
--- a/backend/src/main/java/com/rongyichuang/auth/filter/JwtAuthenticationFilter.java
+++ b/backend/src/main/java/com/rongyichuang/auth/filter/JwtAuthenticationFilter.java
@@ -169,7 +169,7 @@
      * 杩斿洖鏉冮檺閿欒鍝嶅簲
      */
     private void sendUnauthorizedResponse(HttpServletResponse response) throws IOException {
-        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
         response.setContentType("application/json;charset=UTF-8");
         response.getWriter().write("{\"errors\":[{\"message\":\"娌℃湁鏉冮檺璁块棶锛岃鍏堢櫥褰昞",\"extensions\":{\"code\":\"UNAUTHORIZED\"}}]}");
     }
diff --git a/backend/src/main/java/com/rongyichuang/player/dto/response/CurrentJudgeRatingResponse.java b/backend/src/main/java/com/rongyichuang/player/dto/response/CurrentJudgeRatingResponse.java
index f71bf5c..df38ac5 100644
--- a/backend/src/main/java/com/rongyichuang/player/dto/response/CurrentJudgeRatingResponse.java
+++ b/backend/src/main/java/com/rongyichuang/player/dto/response/CurrentJudgeRatingResponse.java
@@ -12,6 +12,7 @@
     private BigDecimal totalScore;
     private Integer status;
     private String remark;
+    private String ratedAt;
     private List<CurrentJudgeRatingItemResponse> items;
 
     public CurrentJudgeRatingResponse() {}
@@ -48,6 +49,14 @@
         this.remark = remark;
     }
 
+    public String getRatedAt() {
+        return ratedAt;
+    }
+
+    public void setRatedAt(String ratedAt) {
+        this.ratedAt = ratedAt;
+    }
+
     public List<CurrentJudgeRatingItemResponse> getItems() {
         return items;
     }
@@ -64,15 +73,18 @@
         private String ratingItemName;
         private BigDecimal score;
         private BigDecimal weightedScore;
+        private BigDecimal maxScore;
 
         public CurrentJudgeRatingItemResponse() {}
 
         public CurrentJudgeRatingItemResponse(Long ratingItemId, String ratingItemName, 
-                                            BigDecimal score, BigDecimal weightedScore) {
+                                            BigDecimal score, BigDecimal weightedScore,
+                                            BigDecimal maxScore) {
             this.ratingItemId = ratingItemId;
             this.ratingItemName = ratingItemName;
             this.score = score;
             this.weightedScore = weightedScore;
+            this.maxScore = maxScore;
         }
 
         public Long getRatingItemId() {
@@ -106,5 +118,13 @@
         public void setWeightedScore(BigDecimal weightedScore) {
             this.weightedScore = weightedScore;
         }
+
+        public BigDecimal getMaxScore() {
+            return maxScore;
+        }
+
+        public void setMaxScore(BigDecimal maxScore) {
+            this.maxScore = maxScore;
+        }
     }
-}
\ No newline at end of file
+}
diff --git a/backend/src/main/java/com/rongyichuang/player/service/ActivityPlayerRatingService.java b/backend/src/main/java/com/rongyichuang/player/service/ActivityPlayerRatingService.java
index e41faf5..94615e1 100644
--- a/backend/src/main/java/com/rongyichuang/player/service/ActivityPlayerRatingService.java
+++ b/backend/src/main/java/com/rongyichuang/player/service/ActivityPlayerRatingService.java
@@ -233,16 +233,35 @@
         // 鑾峰彇璇勫垎椤�
         List<ActivityPlayerRatingItem> items = activityPlayerRatingItemRepository
                 .findByActivityPlayerRatingId(rating.getId());
-        
-        List<CurrentJudgeRatingResponse.CurrentJudgeRatingItemResponse> itemResponses = items.stream()
-                .map(item -> new CurrentJudgeRatingResponse.CurrentJudgeRatingItemResponse(
-                        item.getRatingItemId(),
-                        "", // 璇勫垎椤瑰悕绉版殏鏃朵负绌�
-                        item.getScore(),
-                        item.getScore() // 浣跨敤寰楀垎浣滀负鍔犳潈寰楀垎
-                ))
+
+        List<Long> ratingItemIds = items.stream()
+                .map(ActivityPlayerRatingItem::getRatingItemId)
+                .filter(java.util.Objects::nonNull)
+                .distinct()
                 .collect(java.util.stream.Collectors.toList());
-        
+
+        java.util.Map<Long, RatingItem> ratingItemMap = ratingItemIds.isEmpty()
+                ? java.util.Collections.emptyMap()
+                : ratingItemRepository.findAllById(ratingItemIds).stream()
+                        .collect(java.util.stream.Collectors.toMap(RatingItem::getId, java.util.function.Function.identity()));
+
+        List<CurrentJudgeRatingResponse.CurrentJudgeRatingItemResponse> itemResponses = items.stream()
+                .map(item -> {
+                    RatingItem ratingItem = ratingItemMap.get(item.getRatingItemId());
+                    String name = ratingItem != null ? ratingItem.getName() : "";
+                    BigDecimal maxScore = ratingItem != null && ratingItem.getMaxScore() != null
+                            ? BigDecimal.valueOf(ratingItem.getMaxScore()) : null;
+                    return new CurrentJudgeRatingResponse.CurrentJudgeRatingItemResponse(
+                            item.getRatingItemId(),
+                            name,
+                            item.getScore(),
+                            item.getScore(),
+                            maxScore
+                    );
+                })
+                .collect(java.util.stream.Collectors.toList());
+
+        response.setRatedAt(rating.getUpdateTime() != null ? rating.getUpdateTime().toString() : null);
         response.setItems(itemResponses);
         return response;
     }
@@ -403,7 +422,12 @@
                             ((Number) row.get("rating_item_id")).longValue(),
                             (String) row.get("rating_item_name"),
                             (BigDecimal) row.get("score"),
-                            (BigDecimal) row.get("score") // weightedScore 鏆傛椂浣跨敤鐩稿悓鍊�
+                            (BigDecimal) row.get("score"),
+                            row.get("max_score") instanceof BigDecimal
+                                    ? (BigDecimal) row.get("max_score")
+                                    : row.get("max_score") instanceof Number
+                                        ? BigDecimal.valueOf(((Number) row.get("max_score")).doubleValue())
+                                        : null
                     ))
                     .collect(java.util.stream.Collectors.toList());
             
@@ -412,6 +436,7 @@
             response.setTotalScore(rating.getTotalScore());
             response.setStatus(rating.getState());
             response.setRemark(rating.getFeedback());
+            response.setRatedAt(rating.getUpdateTime() != null ? rating.getUpdateTime().toString() : null);
             response.setItems(items);
             
             return response;
@@ -421,4 +446,4 @@
             return null;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/backend/src/main/java/com/rongyichuang/review/resolver/ReviewResolver.java b/backend/src/main/java/com/rongyichuang/review/resolver/ReviewResolver.java
index e8c8503..0f513a3 100644
--- a/backend/src/main/java/com/rongyichuang/review/resolver/ReviewResolver.java
+++ b/backend/src/main/java/com/rongyichuang/review/resolver/ReviewResolver.java
@@ -31,10 +31,10 @@
      */
     @QueryMapping
     public ReviewProjectPageResponse unReviewedProjects(
-            @Argument String searchKeyword,
             @Argument int page,
-            @Argument int pageSize) {
-        log.info("鏌ヨ鎴戞湭璇勫鐨勯」鐩垪琛紝searchKeyword: {}, page: {}, pageSize: {}", searchKeyword, page, pageSize);
+            @Argument int pageSize,
+            @Argument String searchKeyword) {
+        log.info("鏌ヨ鎴戞湭璇勫鐨勯」鐩垪琛紝page: {}, pageSize: {}, searchKeyword: {}", page, pageSize, searchKeyword);
         
         Long currentJudgeId = userContextUtil.getCurrentJudgeId();
         if (currentJudgeId == null) {
@@ -49,10 +49,10 @@
      */
     @QueryMapping
     public ReviewProjectPageResponse reviewedProjects(
-            @Argument String searchKeyword,
             @Argument int page,
-            @Argument int pageSize) {
-        log.info("鏌ヨ鎴戝凡璇勫鐨勯」鐩垪琛紝searchKeyword: {}, page: {}, pageSize: {}", searchKeyword, page, pageSize);
+            @Argument int pageSize,
+            @Argument String searchKeyword) {
+        log.info("鏌ヨ鎴戝凡璇勫鐨勯」鐩垪琛紝page: {}, pageSize: {}, searchKeyword: {}", page, pageSize, searchKeyword);
         
         Long currentJudgeId = userContextUtil.getCurrentJudgeId();
         if (currentJudgeId == null) {
@@ -67,10 +67,10 @@
      */
     @QueryMapping
     public ReviewProjectPageResponse studentUnReviewedProjects(
-            @Argument String searchKeyword,
             @Argument int page,
-            @Argument int pageSize) {
-        log.info("鏌ヨ瀛﹀憳鏈瘎瀹$殑椤圭洰鍒楄〃锛宻earchKeyword: {}, page: {}, pageSize: {}", searchKeyword, page, pageSize);
+            @Argument int pageSize,
+            @Argument String searchKeyword) {
+        log.info("鏌ヨ瀛﹀憳鏈瘎瀹$殑椤圭洰鍒楄〃锛宲age: {}, pageSize: {}, searchKeyword: {}", page, pageSize, searchKeyword);
         
         Long currentJudgeId = userContextUtil.getCurrentJudgeId();
         if (currentJudgeId == null) {
diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml
index f4b9dda..9f5dff8 100644
--- a/backend/src/main/resources/application.yml
+++ b/backend/src/main/resources/application.yml
@@ -86,7 +86,7 @@
   base-url: https://rych.9village.cn
   jwt:
     secret: ryc-jwt-secret-key-2024-secure-256bit-hmac-sha-algorithm-compatible
-    expiration: 86400000 # 24灏忔椂
+    expiration: 7200000 # 2灏忔椂
   media-url:  https://ryc-1379367838.cos.ap-chengdu.myqcloud.com
 
 # 寰俊灏忕▼搴忛厤缃�
diff --git a/backend/src/main/resources/graphql/player.graphqls b/backend/src/main/resources/graphql/player.graphqls
index 2d165f0..e686a1d 100644
--- a/backend/src/main/resources/graphql/player.graphqls
+++ b/backend/src/main/resources/graphql/player.graphqls
@@ -96,6 +96,7 @@
     totalScore: Float
     status: Int
     remark: String
+    ratedAt: String
     items: [CurrentJudgeRatingItemResponse!]!
 }
 
@@ -105,6 +106,7 @@
     ratingItemName: String!
     score: Float
     weightedScore: Float
+    maxScore: Float
 }
 
 type ProjectStageTimelineResponse {
diff --git a/clear-invalid-tokens.js b/clear-invalid-tokens.js
new file mode 100644
index 0000000..f445506
--- /dev/null
+++ b/clear-invalid-tokens.js
@@ -0,0 +1,86 @@
+// 娓呯悊鏃犳晥token鐨勮剼鏈�
+// 杩欎釜鑴氭湰闇�瑕佸湪灏忕▼搴忓紑鍙戣�呭伐鍏风殑鎺у埗鍙颁腑杩愯
+
+console.log('馃Ч 寮�濮嬫竻鐞嗘棤鏁堢殑JWT token...');
+
+// 妫�鏌ュ綋鍓嶅瓨鍌ㄧ殑token
+const currentToken = wx.getStorageSync('token');
+console.log('褰撳墠token:', currentToken ? `${currentToken.substring(0, 20)}...` : '鏃�');
+
+// 妫�鏌oken鏍煎紡鏄惁鏈夋晥
+function isValidJWTFormat(token) {
+  if (!token || typeof token !== 'string') {
+    return false;
+  }
+  
+  // JWT搴旇鏈�3涓儴鍒嗭紝鐢�.鍒嗛殧
+  const parts = token.split('.');
+  if (parts.length !== 3) {
+    console.log('鉂� Token鏍煎紡鏃犳晥锛氫笉鏄�3涓儴鍒�');
+    return false;
+  }
+  
+  // 妫�鏌ユ槸鍚﹀寘鍚祴璇曠敤鐨勬棤鏁堢鍚�
+  if (token.includes('invalid_token')) {
+    console.log('鉂� 妫�娴嬪埌娴嬭瘯鐢ㄧ殑鏃犳晥token');
+    return false;
+  }
+  
+  try {
+    // 灏濊瘯瑙g爜header鍜宲ayload
+    const header = JSON.parse(atob(parts[0]));
+    const payload = JSON.parse(atob(parts[1]));
+    
+    console.log('Token header:', header);
+    console.log('Token payload:', payload);
+    
+    // 妫�鏌ユ槸鍚﹁繃鏈�
+    const now = Math.floor(Date.now() / 1000);
+    if (payload.exp && payload.exp < now) {
+      console.log('鉂� Token宸茶繃鏈�');
+      return false;
+    }
+    
+    console.log('鉁� Token鏍煎紡鏈夋晥');
+    return true;
+  } catch (e) {
+    console.log('鉂� Token瑙g爜澶辫触:', e.message);
+    return false;
+  }
+}
+
+// 娓呯悊鏃犳晥token
+if (currentToken) {
+  if (!isValidJWTFormat(currentToken)) {
+    console.log('馃棏锔� 娓呯悊鏃犳晥token...');
+    
+    // 娓呴櫎瀛樺偍鐨勮璇佷俊鎭�
+    wx.removeStorageSync('token');
+    wx.removeStorageSync('userInfo');
+    wx.removeStorageSync('sessionKey');
+    
+    // 娓呴櫎globalData涓殑璁よ瘉淇℃伅
+    const app = getApp();
+    if (app) {
+      app.globalData.token = null;
+      app.globalData.userInfo = null;
+      app.globalData.sessionKey = null;
+    }
+    
+    console.log('鉁� 鏃犳晥token宸叉竻鐞�');
+    console.log('馃挕 寤鸿閲嶆柊鍚姩灏忕▼搴忎互鑾峰彇鏂扮殑鏈夋晥token');
+  } else {
+    console.log('鉁� 褰撳墠token鏈夋晥锛屾棤闇�娓呯悊');
+  }
+} else {
+  console.log('鈩癸笍 褰撳墠娌℃湁瀛樺偍token');
+}
+
+console.log('馃帀 娓呯悊瀹屾垚锛�');
+
+// 浣跨敤璇存槑
+console.log('\n馃搵 浣跨敤璇存槑:');
+console.log('1. 鍦ㄥ皬绋嬪簭寮�鍙戣�呭伐鍏蜂腑鎵撳紑鎺у埗鍙�');
+console.log('2. 澶嶅埗骞剁矘璐磋繖娈典唬鐮�');
+console.log('3. 鎸夊洖杞︽墽琛�');
+console.log('4. 濡傛灉娓呯悊浜嗘棤鏁坱oken锛岃閲嶆柊鍚姩灏忕▼搴�');
\ No newline at end of file
diff --git a/wx/app.js b/wx/app.js
index 41ba5ea..57d5a4b 100644
--- a/wx/app.js
+++ b/wx/app.js
@@ -4,7 +4,7 @@
     userInfo: null,
     token: null,
     sessionKey: null, // 寰俊浼氳瘽瀵嗛挜锛岀敤浜庤В瀵嗘墜鏈哄彿绛夋晱鎰熸暟鎹�
-    baseUrl: 'http://localhost:8080/graphql', // 鍚庡彴GraphQL鎺ュ彛鍦板潃
+    baseUrl: 'http://localhost:8080/api/graphql', // 鍚庡彴GraphQL鎺ュ彛鍦板潃
     hasPhoneAuth: false, // 鏄惁宸叉巿鏉冩墜鏈哄彿
     rejectPhone: false, // 鏄惁鎷掔粷杩囨墜鏈哄彿鎺堟潈
     cos: {
@@ -276,56 +276,174 @@
   // GraphQL璇锋眰灏佽
   graphqlRequest(query, variables = {}) {
     return new Promise((resolve, reject) => {
-      // 纭繚token鐨勪竴鑷存�э細浼樺厛浣跨敤globalData涓殑token锛屽鏋滄病鏈夊垯浠巗torage鑾峰彇
-      let token = this.globalData.token
-      if (!token) {
-        token = wx.getStorageSync('token')
-        if (token) {
-          this.globalData.token = token // 鍚屾鍒癵lobalData
-        }
-      }
+      this._makeGraphQLRequest(query, variables, resolve, reject, false)
+    })
+  },
 
-      wx.request({
-        url: this.globalData.baseUrl,
-        method: 'POST',
-        header: {
-          'Content-Type': 'application/json',
-          'Authorization': token ? `Bearer ${token}` : ''
-        },
-        data: {
-          query: query,
-          variables: variables
-        },
-        success: (res) => {
-          console.log('GraphQL鍝嶅簲:', res.data)
-          
-          // 妫�鏌TTP鐘舵�佺爜
-          if (res.statusCode !== 200) {
+  // 鍐呴儴GraphQL璇锋眰鏂规硶锛屾敮鎸侀噸璇曟満鍒�
+  _makeGraphQLRequest(query, variables, resolve, reject, isRetry = false) {
+    // 纭繚token鐨勪竴鑷存�э細浼樺厛浣跨敤globalData涓殑token锛屽鏋滄病鏈夊垯浠巗torage鑾峰彇
+    let token = this.globalData.token
+    if (!token) {
+      token = wx.getStorageSync('token')
+      if (token) {
+        this.globalData.token = token // 鍚屾鍒癵lobalData
+      }
+    }
+
+    wx.request({
+      url: this.globalData.baseUrl,
+      method: 'POST',
+      header: {
+        'Content-Type': 'application/json',
+        'Authorization': token ? `Bearer ${token}` : ''
+      },
+      data: {
+        query: query,
+        variables: variables
+      },
+      success: (res) => {
+        console.log('GraphQL鍝嶅簲:', res.data)
+        
+        // 妫�鏌TTP鐘舵�佺爜
+        if (res.statusCode !== 200) {
+          // 瀵逛簬401鐘舵�佺爜锛屽彲鑳芥槸璁よ瘉閿欒锛岄渶瑕佹鏌ュ搷搴斿唴瀹�
+          if (res.statusCode === 401 && res.data && res.data.errors) {
+            console.log('鏀跺埌401鐘舵�佺爜锛屾鏌ユ槸鍚︿负璁よ瘉閿欒')
+            // 缁х画澶勭悊锛岃涓嬮潰鐨凣raphQL閿欒妫�鏌ラ�昏緫澶勭悊璁よ瘉閿欒
+          } else {
             console.error('GraphQL HTTP閿欒:', res.statusCode)
             reject(new Error(`HTTP閿欒: ${res.statusCode}`))
             return
           }
+        }
 
-          // 妫�鏌raphQL閿欒
-          if (res.data.errors) {
-            console.error('GraphQL閿欒:', res.data.errors)
-            reject(new Error(res.data.errors[0]?.message || 'GraphQL璇锋眰閿欒'))
+        // 妫�鏌raphQL閿欒
+        if (res.data && res.data.errors) {
+          console.error('GraphQL閿欒:', res.data.errors)
+          
+          // 妫�鏌ユ槸鍚︽槸璁よ瘉閿欒锛坱oken杩囨湡鎴栨棤鏁堬級
+          const authErrors = res.data.errors.filter(error => 
+            error.message && (
+              error.message.includes('娌℃湁鏉冮檺璁块棶') ||
+              error.message.includes('璇峰厛鐧诲綍') ||
+              error.message.includes('UNAUTHORIZED') ||
+              error.extensions?.code === 'UNAUTHORIZED'
+            )
+          )
+          
+          if (authErrors.length > 0 && !isRetry) {
+            console.log('馃攧 妫�娴嬪埌璁よ瘉閿欒锛屽皾璇曢噸鏂扮櫥褰�...')
+            // 娓呴櫎杩囨湡鐨勮璇佷俊鎭�
+            this.globalData.token = null
+            this.globalData.userInfo = null
+            this.globalData.sessionKey = null
+            wx.removeStorageSync('token')
+            wx.removeStorageSync('userInfo')
+            wx.removeStorageSync('sessionKey')
+            
+            // 閲嶆柊鐧诲綍
+            wx.login({
+              success: (loginRes) => {
+                if (loginRes.code) {
+                  console.log('馃攧 閲嶆柊鑾峰彇寰俊code鎴愬姛锛岃皟鐢ㄥ悗绔櫥褰�...')
+                  this._retryAfterLogin(loginRes.code, query, variables, resolve, reject)
+                } else {
+                  console.error('鉂� 閲嶆柊鑾峰彇寰俊code澶辫触')
+                  reject(new Error('閲嶆柊鐧诲綍澶辫触'))
+                }
+              },
+              fail: (err) => {
+                console.error('鉂� 閲嶆柊鐧诲綍澶辫触:', err)
+                reject(new Error('閲嶆柊鐧诲綍澶辫触'))
+              }
+            })
             return
           }
-
-          // 妫�鏌ユ暟鎹�
-          if (res.data.data !== undefined) {
-            resolve(res.data.data)
-          } else {
-            console.error('GraphQL鍝嶅簲寮傚父:', res.data)
-            reject(new Error('GraphQL鍝嶅簲鏁版嵁寮傚父'))
-          }
-        },
-        fail: (err) => {
-          console.error('GraphQL缃戠粶璇锋眰澶辫触:', err)
-          reject(new Error('缃戠粶璇锋眰澶辫触'))
+          
+          reject(new Error(res.data.errors[0]?.message || 'GraphQL璇锋眰閿欒'))
+          return
         }
-      })
+
+        // 妫�鏌ユ暟鎹�
+        if (res.data.data !== undefined) {
+          resolve(res.data.data)
+        } else {
+          console.error('GraphQL鍝嶅簲寮傚父:', res.data)
+          reject(new Error('GraphQL鍝嶅簲鏁版嵁寮傚父'))
+        }
+      },
+      fail: (err) => {
+        console.error('GraphQL缃戠粶璇锋眰澶辫触:', err)
+        reject(new Error('缃戠粶璇锋眰澶辫触'))
+      }
+    })
+  },
+
+  // 閲嶆柊鐧诲綍鍚庨噸璇旼raphQL璇锋眰
+  _retryAfterLogin(code, query, variables, resolve, reject) {
+    const that = this
+    const deviceInfo = this.getDeviceInfo()
+    const requestData = {
+      code: code,
+      loginIp: '127.0.0.1', // 灏忕▼搴忔棤娉曡幏鍙栫湡瀹濱P锛屼娇鐢ㄩ粯璁ゅ��
+      deviceInfo: deviceInfo
+    }
+    
+    wx.request({
+      url: 'http://localhost:8080/api/auth/wx-login',
+      method: 'POST',
+      header: {
+        'Content-Type': 'application/json'
+      },
+      data: requestData,
+      success: (res) => {
+        console.log('馃攧 閲嶆柊鐧诲綍鍝嶅簲:', res.data)
+        
+        if (res.statusCode !== 200 || res.data.error) {
+          console.error('鉂� 閲嶆柊鐧诲綍澶辫触:', res.data.error || res.data.message)
+          reject(new Error('閲嶆柊鐧诲綍澶辫触'))
+          return
+        }
+        
+        // 妫�鏌ュ搷搴旀暟鎹牸寮�
+        let loginResult = null
+        if (res.data.token && res.data.userInfo) {
+          loginResult = res.data
+        } else if (res.data.success && res.data.data) {
+          loginResult = res.data.data
+        }
+        
+        if (loginResult && loginResult.token) {
+          console.log('鉁� 閲嶆柊鐧诲綍鎴愬姛锛屾洿鏂皌oken')
+          
+          // 淇濆瓨鏂扮殑鐧诲綍淇℃伅
+          try {
+            wx.setStorageSync('token', loginResult.token)
+            wx.setStorageSync('userInfo', loginResult.userInfo)
+            if (loginResult.sessionKey) {
+              wx.setStorageSync('sessionKey', loginResult.sessionKey)
+            }
+          } catch (storageErr) {
+            console.error('鉂� 淇濆瓨閲嶆柊鐧诲綍淇℃伅澶辫触:', storageErr)
+          }
+          
+          that.globalData.token = loginResult.token
+          that.globalData.userInfo = loginResult.userInfo
+          that.globalData.sessionKey = loginResult.sessionKey
+          
+          // 浣跨敤鏂皌oken閲嶈瘯鍘熷璇锋眰
+          console.log('馃攧 浣跨敤鏂皌oken閲嶈瘯GraphQL璇锋眰...')
+          that._makeGraphQLRequest(query, variables, resolve, reject, true)
+        } else {
+          console.error('鉂� 閲嶆柊鐧诲綍鍝嶅簲鏍煎紡閿欒')
+          reject(new Error('閲嶆柊鐧诲綍鍝嶅簲鏍煎紡閿欒'))
+        }
+      },
+      fail: (err) => {
+        console.error('鉂� 閲嶆柊鐧诲綍缃戠粶璇锋眰澶辫触:', err)
+        reject(new Error('閲嶆柊鐧诲綍缃戠粶璇锋眰澶辫触'))
+      }
     })
   }
 })
\ No newline at end of file
diff --git a/wx/pages/judge/review.js b/wx/pages/judge/review.js
index 27afd37..e317235 100644
--- a/wx/pages/judge/review.js
+++ b/wx/pages/judge/review.js
@@ -1,6 +1,6 @@
 // pages/judge/review.js
 const app = getApp()
-const { graphqlRequest, formatDate } = require('../../lib/utils')
+const { graphqlRequest, formatDate: formatDateUtil } = require('../../lib/utils')
 
 Page({
   data: {
@@ -10,13 +10,15 @@
     // 鎻愪氦浣滃搧淇℃伅
     submission: null,
     activityPlayerId: '',
-    
+    stageId: null,
+    submissionId: null,
+
     // 娲诲姩淇℃伅
     activity: null,
-    
+
     // 璇勫鏍囧噯
     criteria: [],
-    
+
     // 璇勫垎鏁版嵁
     scores: {},
     
@@ -31,24 +33,7 @@
     reviewStatus: 'PENDING', // PENDING, COMPLETED
     
     // 宸叉湁璇勫璁板綍
-    existingReview: null,
-    
-    // 濯掍綋棰勮
-    showMediaPreview: false,
-    currentMedia: null,
-    mediaType: 'image',
-    
-    // 鏂囦欢涓嬭浇
-    downloadingFiles: [],
-    
-    // 璇勫垎绛夌骇
-    scoreOptions: [
-      { value: 1, label: '1鍒� - 寰堝樊' },
-      { value: 2, label: '2鍒� - 杈冨樊' },
-      { value: 3, label: '3鍒� - 涓�鑸�' },
-      { value: 4, label: '4鍒� - 鑹ソ' },
-      { value: 5, label: '5鍒� - 浼樼' }
-    ]
+    existingReview: null
   },
 
   onLoad(options) {
@@ -62,6 +47,32 @@
     // 椤甸潰鏄剧ず鏃舵鏌ヨ瘎瀹$姸鎬�
     if (this.data.submissionId) {
       this.checkReviewStatus()
+    }
+  },
+
+  transformMediaFile(file) {
+    const url = file.fullUrl || file.url
+    const thumbUrl = file.fullThumbUrl || url
+    const ext = (file.fileExt || '').toLowerCase()
+    let mediaType = 'file'
+
+    if (file.mediaType === 1 || ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'heic'].includes(ext)) {
+      mediaType = 'image'
+    } else if (file.mediaType === 2 || ['mp4', 'mov', 'avi', 'wmv', 'mkv', 'webm', 'flv'].includes(ext)) {
+      mediaType = 'video'
+    } else if (ext === 'pdf') {
+      mediaType = 'pdf'
+    } else if (['doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'wps', 'txt', 'rtf'].includes(ext)) {
+      mediaType = 'word'
+    }
+
+    return {
+      id: file.id,
+      name: file.name,
+      url,
+      thumbUrl,
+      mediaType,
+      size: file.fileSize || 0
     }
   },
 
@@ -102,13 +113,11 @@
             submissionFiles {
               id
               name
-              url
               fullUrl
+              fullThumbUrl
               fileExt
               fileSize
               mediaType
-              thumbUrl
-              fullThumbUrl
             }
             ratingForm {
               schemeId
@@ -117,10 +126,8 @@
               items {
                 id
                 name
-                description
                 maxScore
-                weight
-                sortOrder
+                orderNo
               }
             }
           }
@@ -137,62 +144,64 @@
           id: detail.id,
           title: detail.projectName,
           description: detail.description,
-          files: detail.submissionFiles ? detail.submissionFiles.map(file => ({
-            id: file.id,
-            name: file.name,
-            url: file.fullUrl || file.url,
-            type: file.fileExt,
-            size: file.fileSize,
-            isDownloading: this.data.downloadingFiles.indexOf(file.id) > -1
-          })) : [],
-          images: detail.submissionFiles ? detail.submissionFiles
-            .filter(file => file.mediaType === 1)
-            .map(file => file.fullUrl || file.url) : [],
-          videos: detail.submissionFiles ? detail.submissionFiles
-            .filter(file => file.mediaType === 2)
-            .map(file => file.fullUrl || file.url) : [],
+          submittedAt: detail.submitTime || null,
+          team: detail.team || null,
           participant: {
-            id: detail.playerInfo.id,
-            name: detail.playerInfo.name,
+            id: detail.playerInfo?.id,
+            name: detail.playerInfo?.name,
+            phone: detail.playerInfo?.phone || detail.playerInfo?.userInfo?.phone || '',
+            avatar: detail.playerInfo?.userInfo?.avatarUrl || '/images/default-avatar.svg',
+            gender: this.getGenderLabel(detail.playerInfo?.gender),
+            birthday: this.getBirthdayText(detail.playerInfo?.birthday),
+            region: detail.regionInfo?.fullPath || detail.regionInfo?.name || '',
+            education: detail.playerInfo?.education || '',
             school: detail.regionInfo ? detail.regionInfo.name : '',
-            major: detail.playerInfo.education || '',
-            avatar: detail.playerInfo.userInfo?.avatarUrl || '/images/default-avatar.svg'
+            major: detail.playerInfo?.education || ''
           },
-          status: detail.state === 1 ? 'APPROVED' : detail.state === 2 ? 'REJECTED' : 'PENDING'
+          status: detail.state === 1 ? 'APPROVED' : detail.state === 2 ? 'REJECTED' : 'PENDING',
+          mediaList: (detail.submissionFiles || []).map(file => this.transformMediaFile(file))
         }
-        
-        // 鏋勫缓activity瀵硅薄
-        const activity = {
-          id: detail.stageId,
-          title: detail.activityName,
-          description: detail.description,
-          judgeCriteria: detail.ratingForm ? detail.ratingForm.items || [] : []
-        }
-        
-        // 鍒濆鍖栬瘎鍒嗘暟鎹�
+
+        const criteria = (detail.ratingForm?.items || []).map(item => {
+          const maxScore = item.maxScore || 0
+          return {
+            id: item.id,
+            name: item.name,
+            maxScore,
+            description: item.description || '鏆傛棤鎻忚堪',
+            step: maxScore > 20 ? 1 : 0.5,
+            currentScore: 0
+          }
+        })
+
         const scores = {}
-        let maxScore = 0
-        
-        if (activity.judgeCriteria) {
-          activity.judgeCriteria.forEach(criterion => {
-            scores[criterion.id] = 0 // 鏆傛椂璁句负0锛屽悗缁渶瑕佹煡璇㈠凡鏈夎瘎鍒�
-            maxScore += criterion.maxScore
-          })
-        }
-        
+        criteria.forEach(criterion => {
+          scores[criterion.id] = 0
+        })
+
+        const maxScore = detail.ratingForm?.totalMaxScore || criteria.reduce((sum, item) => sum + (item.maxScore || 0), 0)
+
         this.setData({
           submission,
-          activity,
-          criteria: activity.judgeCriteria || [],
+          activity: {
+            id: detail.id,
+            stageId: detail.stageId,
+            ratingSchemeId: detail.ratingForm?.schemeId || null,
+            totalMaxScore: maxScore
+          },
+          stageId: detail.stageId || null,
+          submissionId: detail.id,
+          criteria,
           scores,
           maxScore,
-          existingReview: null, // 鏆傛椂璁句负null锛屽悗缁渶瑕佹煡璇㈠凡鏈夎瘎鍒�
+          totalScore: 0,
+          existingReview: null,
           reviewStatus: 'PENDING',
           comment: ''
         })
-        
+
         this.calculateTotalScore()
-        
+
         // 妫�鏌ユ槸鍚﹀凡鏈夎瘎鍒�
         this.checkReviewStatus()
       }
@@ -215,7 +224,7 @@
           currentJudgeRating(activityPlayerId: $activityPlayerId) {
             id
             totalScore
-            comment
+            remark
             status
             ratedAt
             items {
@@ -239,38 +248,98 @@
         
         if (rating.items) {
           rating.items.forEach(item => {
-            scores[item.ratingItemId] = item.score
-            totalScore += item.score
+            const numericScore = item.score !== undefined && item.score !== null ? Number(item.score) : 0
+            scores[item.ratingItemId] = numericScore
+            totalScore += numericScore
           })
         }
-        
+
+        const updatedCriteria = this.data.criteria.map(criterion => {
+          const value = scores[criterion.id] !== undefined ? scores[criterion.id] : 0
+          return {
+            ...criterion,
+            currentScore: value
+          }
+        })
+
+        const normalizedTotal = Number(totalScore.toFixed(2))
+
         this.setData({
           scores,
-          totalScore,
-          comment: rating.comment || '',
-          existingReview: rating,
-          reviewStatus: rating.status || 'COMPLETED'
+          criteria: updatedCriteria,
+          totalScore: normalizedTotal,
+          comment: rating.remark || rating.comment || '',
+          existingReview: {
+            ...rating,
+            totalScore: rating.totalScore ? Number(rating.totalScore) : normalizedTotal,
+            reviewedAt: rating.ratedAt || rating.reviewedAt || rating.updateTime || null
+          },
+          reviewStatus: 'COMPLETED'
         })
         
         console.log('宸插姞杞界幇鏈夎瘎鍒�:', rating)
       } else {
         console.log('褰撳墠璇勫灏氭湭璇勫垎')
       }
+
+      this.calculateTotalScore()
     } catch (error) {
       console.error('妫�鏌ヨ瘎瀹$姸鎬佸け璐�:', error)
     }
   },
 
+  normalizeScore(value, criterion) {
+    const maxScore = Number(criterion.maxScore || 0)
+    const step = Number(criterion.step || (maxScore > 20 ? 1 : 0.5))
+    if (Number.isNaN(value)) {
+      value = 0
+    }
+    let normalized = Math.round(value / step) * step
+    if (normalized < 0) normalized = 0
+    if (normalized > maxScore) normalized = maxScore
+    return Number(normalized.toFixed(2))
+  },
+
+  updateCriterionScore(criterionId, index, value) {
+    const criterion = this.data.criteria[index]
+    if (!criterion) return
+    const normalized = this.normalizeScore(value, criterion)
+
+    this.setData({
+      [`scores.${criterionId}`]: normalized,
+      [`criteria[${index}].currentScore`]: normalized
+    })
+
+    this.calculateTotalScore()
+  },
+
   // 璇勫垎鏀瑰彉
   onScoreChange(e) {
-    const { criterionId } = e.currentTarget.dataset
-    const { value } = e.detail
-    
-    this.setData({
-      [`scores.${criterionId}`]: parseInt(value)
-    })
-    
-    this.calculateTotalScore()
+    const { criterionId, index } = e.currentTarget.dataset
+    const criterion = this.data.criteria[index]
+    if (!criterion) return
+
+    const inputValue = Number(e.detail.value)
+    const newScore = this.normalizeScore(inputValue, criterion)
+    this.updateCriterionScore(criterionId, index, newScore)
+  },
+
+  increaseScore(e) {
+    const { criterionId, index } = e.currentTarget.dataset
+    const criterion = this.data.criteria[index]
+    if (!criterion) return
+    const current = Number(this.data.scores[criterionId] || criterion.currentScore || 0)
+    const step = criterion.step || (criterion.maxScore > 20 ? 1 : 0.5)
+    this.updateCriterionScore(criterionId, index, current + step)
+  },
+
+  decreaseScore(e) {
+    const { criterionId, index } = e.currentTarget.dataset
+    const criterion = this.data.criteria[index]
+    if (!criterion) return
+    const current = Number(this.data.scores[criterionId] || criterion.currentScore || 0)
+    const step = criterion.step || (criterion.maxScore > 20 ? 1 : 0.5)
+    this.updateCriterionScore(criterionId, index, current - step)
   },
 
   // 璁$畻鎬诲垎
@@ -279,11 +348,11 @@
     let totalScore = 0
     
     criteria.forEach(criterion => {
-      const score = scores[criterion.id] || 0
-      totalScore += score * (criterion.weight || 1)
+      const score = Number(scores[criterion.id] || 0)
+      totalScore += score
     })
     
-    this.setData({ totalScore })
+    this.setData({ totalScore: Number(totalScore.toFixed(2)) })
   },
 
   // 璇勫鎰忚杈撳叆
@@ -295,106 +364,58 @@
 
   // 濯掍綋鐐瑰嚮
   onMediaTap(e) {
-    const { url, type } = e.currentTarget.dataset
-    
-    if (type === 'image') {
+    const index = Number(e.currentTarget.dataset.index)
+    const mediaList = this.data.submission?.mediaList || []
+    const media = mediaList[index]
+    if (!media) return
+
+    if (media.mediaType === 'image') {
+      const imageUrls = mediaList
+        .filter(item => item.mediaType === 'image')
+        .map(item => item.url)
       wx.previewImage({
-        current: url,
-        urls: this.data.submission.images || []
+        current: media.url,
+        urls: imageUrls
       })
-    } else if (type === 'video') {
-      this.setData({
-        showMediaPreview: true,
-        currentMedia: url,
-        mediaType: 'video'
+    } else if (media.mediaType === 'video') {
+      wx.navigateTo({
+        url: `/pages/video/video?url=${encodeURIComponent(media.url)}&title=${encodeURIComponent(media.name)}`
       })
+    } else {
+      this.openDocumentMedia(media)
     }
   },
 
-  // 鍏抽棴濯掍綋棰勮
-  onCloseMediaPreview() {
-    this.setData({
-      showMediaPreview: false,
-      currentMedia: null
-    })
-  },
-
-  // 涓嬭浇鏂囦欢
-  async onDownloadFile(e) {
-    const { fileId, fileName, fileUrl } = e.currentTarget.dataset
-    
+  async openDocumentMedia(media) {
     try {
-      // 娣诲姞鍒颁笅杞戒腑鍒楄〃
-      const downloadingFiles = [...this.data.downloadingFiles, fileId]
-      
-      // 鍚屾椂鏇存柊鏂囦欢鐨刬sDownloading瀛楁
-      const submission = { ...this.data.submission }
-      if (submission.files) {
-        submission.files = submission.files.map(file => ({
-          ...file,
-          isDownloading: file.id === fileId ? true : file.isDownloading
-        }))
-      }
-      
-      this.setData({ 
-        downloadingFiles,
-        submission
+      wx.showLoading({ title: '鎵撳紑涓�...' })
+      const downloadRes = await new Promise((resolve, reject) => {
+        wx.downloadFile({
+          url: media.url,
+          success: resolve,
+          fail: reject
+        })
       })
-      
-      wx.showLoading({ title: '涓嬭浇涓�...' })
-      
-      const result = await wx.downloadFile({
-        url: fileUrl,
-        success: (res) => {
-          if (res.statusCode === 200) {
-            // 淇濆瓨鍒扮浉鍐屾垨鏂囦欢
-            wx.saveFile({
-              tempFilePath: res.tempFilePath,
-              success: () => {
-                wx.showToast({
-                  title: '涓嬭浇鎴愬姛',
-                  icon: 'success'
-                })
-              },
-              fail: () => {
-                wx.showToast({
-                  title: '淇濆瓨澶辫触',
-                  icon: 'error'
-                })
-              }
-            })
-          }
-        },
-        fail: () => {
-          wx.showToast({
-            title: '涓嬭浇澶辫触',
-            icon: 'error'
-          })
-        }
+
+      if (downloadRes.statusCode !== 200) {
+        throw new Error('鏂囦欢涓嬭浇澶辫触')
+      }
+
+      await new Promise((resolve, reject) => {
+        wx.openDocument({
+          filePath: downloadRes.tempFilePath,
+          showMenu: true,
+          success: resolve,
+          fail: reject
+        })
       })
     } catch (error) {
-      console.error('涓嬭浇鏂囦欢澶辫触:', error)
+      console.error('鎵撳紑鏂囦欢澶辫触:', error)
       wx.showToast({
-        title: '涓嬭浇澶辫触',
+        title: '鏃犳硶鎵撳紑鏂囦欢',
         icon: 'error'
       })
     } finally {
-      // 浠庝笅杞戒腑鍒楄〃绉婚櫎
-      const downloadingFiles = this.data.downloadingFiles.filter(id => id !== fileId)
-      
-      // 鍚屾椂鏇存柊鏂囦欢鐨刬sDownloading瀛楁
-      const submission = { ...this.data.submission }
-      if (submission.files) {
-        submission.files = submission.files.map(file => ({
-          ...file,
-          isDownloading: file.id === fileId ? false : file.isDownloading
-        }))
-      }
-      
-      this.setData({ 
-        downloadingFiles,
-        submission
-      })
       wx.hideLoading()
     }
   },
@@ -402,6 +423,7 @@
   // 楠岃瘉璇勫鏁版嵁
   validateReview() {
     const { scores, criteria, comment } = this.data
+    const commentText = (comment || '').trim()
     
     // 妫�鏌ユ槸鍚︽墍鏈夋爣鍑嗛兘宸茶瘎鍒�
     for (let criterion of criteria) {
@@ -415,7 +437,7 @@
     }
     
     // 妫�鏌ヨ瘎瀹℃剰瑙�
-    if (!comment.trim()) {
+    if (!commentText) {
       wx.showToast({
         title: '璇峰~鍐欒瘎瀹℃剰瑙�',
         icon: 'error'
@@ -423,7 +445,7 @@
       return false
     }
     
-    if (comment.trim().length < 10) {
+    if (commentText.length < 10) {
       wx.showToast({
         title: '璇勫鎰忚鑷冲皯10涓瓧绗�',
         icon: 'error'
@@ -432,54 +454,6 @@
     }
     
     return true
-  },
-
-  // 淇濆瓨鑽夌
-  async onSaveDraft() {
-    try {
-      wx.showLoading({ title: '淇濆瓨涓�...' })
-      
-      const { activityPlayerId, scores, comment, criteria, activity } = this.data
-      
-      // 鏋勫缓璇勫垎椤规暟缁�
-      const ratings = criteria.map(criterion => ({
-        itemId: criterion.id,
-        score: scores[criterion.id] || 0
-      }))
-      
-      const mutation = `
-        mutation SaveActivityPlayerRating($input: ActivityPlayerRatingInput!) {
-          saveActivityPlayerRating(input: $input)
-        }
-      `
-      
-      const input = {
-        activityPlayerId,
-        stageId: activity.stageId,
-        ratings,
-        comment: comment.trim()
-      }
-      
-      const result = await graphqlRequest(mutation, { input })
-      
-      if (result && result.saveActivityPlayerRating) {
-        wx.showToast({
-          title: '鑽夌宸蹭繚瀛�',
-          icon: 'success'
-        })
-        
-        // 閲嶆柊鍔犺浇璇勫垎鐘舵��
-        await this.checkReviewStatus()
-      }
-    } catch (error) {
-      console.error('淇濆瓨鑽夌澶辫触:', error)
-      wx.showToast({
-        title: '淇濆瓨澶辫触',
-        icon: 'error'
-      })
-    } finally {
-      wx.hideLoading()
-    }
   },
 
   // 鎻愪氦璇勫
@@ -505,12 +479,23 @@
       this.setData({ submitting: true })
       wx.showLoading({ title: '鎻愪氦涓�...' })
       
-      const { activityPlayerId, scores, comment, criteria, activity } = this.data
+      const { activityPlayerId, scores, comment, criteria, stageId } = this.data
+      const commentText = (comment || '').trim()
+
+      if (!stageId) {
+        wx.showToast({
+          title: '缂哄皯闃舵淇℃伅锛屾棤娉曟彁浜�',
+          icon: 'none'
+        })
+        this.setData({ submitting: false })
+        wx.hideLoading()
+        return
+      }
       
       // 鏋勫缓璇勫垎椤规暟缁�
       const ratings = criteria.map(criterion => ({
         itemId: criterion.id,
-        score: scores[criterion.id] || 0
+        score: Number(scores[criterion.id] || 0)
       }))
       
       const mutation = `
@@ -521,9 +506,9 @@
       
       const input = {
         activityPlayerId,
-        stageId: activity.stageId,
+        stageId,
         ratings,
-        comment: comment.trim()
+        comment: commentText
       }
       
       const result = await graphqlRequest(mutation, { input })
@@ -569,34 +554,17 @@
   // 鑱旂郴鍙傝禌鑰�
   onContactParticipant() {
     const { submission } = this.data
-    
-    if (submission.participant) {
-      wx.showActionSheet({
-        itemList: ['鍙戦�佹秷鎭�', '鏌ョ湅璇︽儏'],
-        success: (res) => {
-          switch (res.tapIndex) {
-            case 0:
-              // 鍙戦�佹秷鎭姛鑳�
-              wx.navigateTo({
-                url: `/pages/chat/chat?userId=${submission.participant.id}`
-              })
-              break
-            case 1:
-              // 鏌ョ湅鐢ㄦ埛璇︽儏
-              wx.navigateTo({
-                url: `/pages/user/profile?userId=${submission.participant.id}`
-              })
-              break
-          }
-        }
+    const phone = submission?.participant?.phone
+    if (phone) {
+      wx.makePhoneCall({
+        phoneNumber: phone
+      })
+    } else {
+      wx.showToast({
+        title: '鏆傛棤鑱旂郴鏂瑰紡',
+        icon: 'none'
       })
     }
-  },
-
-  // 鑾峰彇璇勫垎绛夌骇鏂囨湰
-  getScoreLabel(score) {
-    const option = this.data.scoreOptions.find(opt => opt.value === score)
-    return option ? option.label : `${score}鍒哷
   },
 
   // 鑾峰彇鏂囦欢澶у皬鏂囨湰
@@ -610,9 +578,47 @@
     }
   },
 
+  // 缁熶竴澶勭悊鎬у埆鏄剧ず鏂囨湰
+  getGenderLabel(gender) {
+    if (gender === null || gender === undefined || gender === '') {
+      return '鏈~鍐�'
+    }
+
+    const normalized = String(gender).trim().toLowerCase()
+
+    if (normalized === '') {
+      return '鏈~鍐�'
+    }
+
+    if (normalized === 'male' || normalized === 'm') {
+      return '鐢�'
+    }
+    if (normalized === 'female' || normalized === 'f') {
+      return '濂�'
+    }
+
+    if (/^-?\d+$/.test(normalized)) {
+      const numeric = Number(normalized)
+      if (numeric === 1) return '鐢�'
+      if (numeric === 0) return '濂�'
+      if (numeric === 2) return '濂�'
+    }
+
+    return gender === undefined || gender === null ? '鏈~鍐�' : String(gender)
+  },
+
+  // 缁熶竴澶勭悊鍑虹敓鏃ユ湡鏄剧ず鏂囨湰
+  getBirthdayText(dateString) {
+    if (!dateString) {
+      return '鏈~鍐�'
+    }
+    const formatted = formatDateUtil(dateString, 'YYYY-MM-DD')
+    return formatted || '鏈~鍐�'
+  },
+
   // 鏍煎紡鍖栨棩鏈�
   formatDate(dateString) {
-    return formatDate(dateString, 'YYYY-MM-DD HH:mm')
+    return formatDateUtil(dateString, 'YYYY-MM-DD HH:mm')
   },
 
   // 鍒嗕韩椤甸潰
@@ -622,4 +628,4 @@
       path: '/pages/index/index'
     }
   }
-})
\ No newline at end of file
+})
diff --git a/wx/pages/judge/review.wxml b/wx/pages/judge/review.wxml
index 1ed466d..fd5c086 100644
--- a/wx/pages/judge/review.wxml
+++ b/wx/pages/judge/review.wxml
@@ -16,32 +16,33 @@
           {{reviewStatus === 'COMPLETED' ? '宸茶瘎瀹�' : '寰呰瘎瀹�'}}
         </view>
       </view>
-      
+
       <view class="submission-detail">
         <text class="submission-title">{{submission.title}}</text>
-        <text class="submission-desc">{{submission.description}}</text>
-        
+        <text class="submission-desc">{{submission.description || '鏆傛棤椤圭洰鎻忚堪'}}</text>
+
         <!-- 鍙傝禌鑰呬俊鎭� -->
         <view class="participant-info">
           <view class="participant-header">
             <text class="participant-label">鍙傝禌鑰咃細</text>
             <view class="contact-btn" bindtap="onContactParticipant">
-              <text class="contact-icon">馃挰</text>
-              <text class="contact-text">鑱旂郴</text>
+              <text class="contact-icon">馃摓</text>
+              <text class="contact-text">鎷ㄦ墦鐢佃瘽</text>
             </view>
           </view>
-          
+
           <view class="participant-detail">
             <image class="participant-avatar" src="{{submission.participant.avatar}}" mode="aspectFill"></image>
             <view class="participant-text">
-              <text class="participant-name">{{submission.participant.name}}</text>
-              <text class="participant-school">{{submission.participant.school}} - {{submission.participant.major}}</text>
+              <text class="participant-name">{{submission.participant.name || '鍖垮悕'}}</text>
+              <text class="participant-meta">鎬у埆锛歿{submission.participant.gender || '鏈~鍐�'}}锛屽嚭鐢熸棩鏈燂細{{submission.participant.birthday || '鏈~鍐�'}}</text>
+              <text class="participant-field">鎵�灞炲尯鍩燂細{{submission.participant.region || '鏈~鍐�'}}</text>
+              <text class="participant-field">瀛﹀巻锛歿{submission.participant.education || '鏈~鍐�'}}</text>
             </view>
           </view>
-          
-          <!-- 鍥㈤槦淇℃伅 -->
-          <view wx:if="{{submission.team}}" class="team-info">
-            <text class="team-label">鍥㈤槦锛歿{submission.team.name}}</text>
+
+          <view wx:if="{{submission.team && submission.team.members}}" class="team-info">
+            <text class="team-label">鍥㈤槦锛歿{submission.team.name || '鏈懡鍚嶅洟闃�'}}</text>
             <view class="team-members">
               <view wx:for="{{submission.team.members}}" wx:key="id" class="team-member">
                 <text class="member-name">{{item.name}}</text>
@@ -50,74 +51,36 @@
             </view>
           </view>
         </view>
-        
-        <text class="submit-time">鎻愪氦鏃堕棿锛歿{formatDate(submission.submittedAt)}}</text>
+
       </view>
     </view>
 
-    <!-- 浣滃搧濯掍綋 -->
-    <view wx:if="{{submission.images.length > 0 || submission.videos.length > 0}}" class="media-section">
-      <text class="section-title">浣滃搧灞曠ず</text>
-      
-      <!-- 鍥剧墖 -->
-      <view wx:if="{{submission.images.length > 0}}" class="media-grid">
-        <view 
-          wx:for="{{submission.images}}" 
-          wx:key="*this"
-          class="media-item image-item"
-          data-url="{{item}}"
-          data-type="image"
-          bindtap="onMediaTap"
-        >
-          <image class="media-image" src="{{item}}" mode="aspectFill"></image>
-        </view>
-      </view>
-      
-      <!-- 瑙嗛 -->
-      <view wx:if="{{submission.videos.length > 0}}" class="media-grid">
-        <view 
-          wx:for="{{submission.videos}}" 
-          wx:key="*this"
-          class="media-item video-item"
-          data-url="{{item}}"
-          data-type="video"
-          bindtap="onMediaTap"
-        >
-          <video class="media-video" src="{{item}}" poster="{{item}}" controls></video>
-          <view class="play-overlay">
-            <text class="play-icon">鈻�</text>
-          </view>
-        </view>
-      </view>
-    </view>
-
-    <!-- 浣滃搧鏂囦欢 -->
-    <view wx:if="{{submission.files.length > 0}}" class="files-section">
-      <text class="section-title">浣滃搧鏂囦欢</text>
-      
-      <view class="file-list">
-        <view 
-          wx:for="{{submission.files}}" 
+    <!-- 浣滃搧绱犳潗 -->
+    <view wx:if="{{submission.mediaList && submission.mediaList.length > 0}}" class="media-section">
+      <text class="section-title">鍙傝禌浣滃搧</text>
+      <view class="media-list">
+        <view
+          wx:for="{{submission.mediaList}}"
           wx:key="id"
-          class="file-item"
+          class="media-item"
+          bindtap="onMediaTap"
+          data-index="{{index}}"
         >
-          <view class="file-info">
-            <text class="file-icon">馃搫</text>
-            <view class="file-detail">
-              <text class="file-name">{{item.name}}</text>
-              <text class="file-size">{{getFileSizeText(item.size)}}</text>
-            </view>
+          <view class="media-thumb-wrapper">
+            <image
+              wx:if="{{item.mediaType === 'image' || item.mediaType === 'video'}}"
+              class="media-thumb"
+              src="{{item.thumbUrl}}"
+              mode="aspectFill"
+            />
+            <view wx:elif="{{item.mediaType === 'pdf'}}" class="media-icon pdf">PDF</view>
+            <view wx:elif="{{item.mediaType === 'word'}}" class="media-icon doc">DOC</view>
+            <view wx:else class="media-icon file">FILE</view>
+            <view wx:if="{{item.mediaType === 'video'}}" class="media-play">鈻�</view>
           </view>
-          
-          <view 
-            class="download-btn {{item.isDownloading ? 'downloading' : ''}}"
-            data-file-id="{{item.id}}"
-            data-file-name="{{item.name}}"
-            data-file-url="{{item.url}}"
-            bindtap="onDownloadFile"
-          >
-            <text class="download-icon">{{item.isDownloading ? '鈴�' : '猬�'}}</text>
-            <text class="download-text">{{item.isDownloading ? '涓嬭浇涓�' : '涓嬭浇'}}</text>
+          <view class="media-info">
+            <text class="media-name">{{item.name}}</text>
+            <text class="media-size">{{getFileSizeText(item.size)}}</text>
           </view>
         </view>
       </view>
@@ -126,84 +89,70 @@
     <!-- 璇勫鏍囧噯 -->
     <view class="criteria-section">
       <text class="section-title">璇勫鏍囧噯</text>
-      
+
       <view class="criteria-list">
         <view wx:for="{{criteria}}" wx:key="id" class="criterion-item">
           <view class="criterion-header">
             <text class="criterion-name">{{item.name}}</text>
-            <text class="criterion-score">{{scores[item.id] || 0}}/{{item.maxScore}}鍒�</text>
+            <text class="criterion-score">{{scores[item.id] || 0}} / {{item.maxScore}} 鍒�</text>
           </view>
-          
-          <text class="criterion-desc">{{item.description}}</text>
-          
-          <!-- 璇勫垎閫夋嫨鍣� -->
-          <view class="score-selector">
-            <picker 
-              range="{{scoreOptions}}" 
-              range-key="label"
-              value="{{(scores[item.id] || 1) - 1}}"
+
+          <text class="criterion-desc">{{item.description || '鏆傛棤璇勫垎璇存槑'}}</text>
+
+          <view class="score-control">
+            <view class="score-btn" data-criterion-id="{{item.id}}" data-index="{{index}}" bindtap="decreaseScore">-</view>
+            <input
+              class="score-input"
+              type="digit"
+              value="{{scores[item.id] || 0}}"
               data-criterion-id="{{item.id}}"
-              bindchange="onScoreChange"
-              disabled="{{reviewStatus === 'COMPLETED'}}"
-            >
-              <view class="score-picker {{reviewStatus === 'COMPLETED' ? 'disabled' : ''}}">
-                <text class="score-text">{{getScoreLabel(scores[item.id] || 0)}}</text>
-                <text class="picker-arrow">{{reviewStatus === 'COMPLETED' ? '' : '鈻�'}}</text>
-              </view>
-            </picker>
+              data-index="{{index}}"
+              bindinput="onScoreChange"
+              placeholder="0"
+            />
+            <view class="score-btn" data-criterion-id="{{item.id}}" data-index="{{index}}" bindtap="increaseScore">+</view>
           </view>
         </view>
       </view>
-      
-      <!-- 鎬诲垎鏄剧ず -->
+
       <view class="total-score">
         <text class="total-label">鎬诲垎锛�</text>
-        <text class="total-value">{{totalScore}}/{{maxScore}}鍒�</text>
+        <text class="total-value">{{totalScore}} / {{maxScore}} 鍒�</text>
       </view>
     </view>
 
     <!-- 璇勫鎰忚 -->
     <view class="comment-section">
       <text class="section-title">璇勫鎰忚</text>
-      
-      <textarea 
-        class="comment-input {{reviewStatus === 'COMPLETED' ? 'disabled' : ''}}"
-        placeholder="璇峰~鍐欒缁嗙殑璇勫鎰忚锛屽寘鎷綔鍝佺殑浼樼偣銆佷笉瓒冲拰鏀硅繘寤鸿..."
+      <textarea
+        class="comment-input"
+        placeholder="璇峰~鍐欒缁嗚瘎瀹℃剰瑙侊紝鍖呮嫭浣滃搧浼樼偣銆佷笉瓒冲拰鏀硅繘寤鸿..."
         value="{{comment}}"
         maxlength="1000"
         show-confirm-bar="{{false}}"
-        disabled="{{reviewStatus === 'COMPLETED'}}"
         bindinput="onCommentInput"
       ></textarea>
-      
       <view class="comment-counter">
-        <text class="counter-text">{{comment.length}}/1000</text>
+        <text class="counter-text">{{comment.length}} / 1000</text>
       </view>
     </view>
 
-    <!-- 宸叉湁璇勫璁板綍 -->
+    <!-- 鏃㈡湁璇勫 -->
     <view wx:if="{{existingReview}}" class="existing-review">
-      <text class="section-title">璇勫璁板綍</text>
-      
+      <text class="section-title">鍘嗗彶璇勫</text>
       <view class="review-info">
-        <text class="review-time">璇勫鏃堕棿锛歿{formatDate(existingReview.reviewedAt)}}</text>
-        <text class="review-total">鎬诲垎锛歿{existingReview.totalScore}}/{{maxScore}}鍒�</text>
+        <text class="review-time">璇勫鏃堕棿锛歿{formatDate(existingReview.reviewedAt) || '鏈煡'}}</text>
+        <text class="review-total">鎬诲垎锛歿{existingReview.totalScore}} / {{maxScore}} 鍒�</text>
       </view>
-      
       <view class="other-reviews-btn" bindtap="onViewOtherReviews">
         <text class="other-reviews-icon">馃懃</text>
-        <text class="other-reviews-text">鏌ョ湅鍏朵粬璇勫</text>
+        <text class="other-reviews-text">鏌ョ湅鍏朵粬璇勫璇勫垎</text>
       </view>
     </view>
 
-    <!-- 搴曢儴鎿嶄綔鏍� -->
-    <view wx:if="{{reviewStatus !== 'COMPLETED'}}" class="bottom-actions">
-      <view class="action-btn draft-btn" bindtap="onSaveDraft">
-        <text class="btn-icon">馃捑</text>
-        <text class="btn-text">淇濆瓨鑽夌</text>
-      </view>
-      
-      <view 
+    <!-- 搴曢儴鎿嶄綔 -->
+    <view class="bottom-actions">
+      <view
         class="action-btn submit-btn {{submitting ? 'submitting' : ''}}"
         bindtap="onSubmitReview"
       >
@@ -212,27 +161,4 @@
       </view>
     </view>
   </view>
-
-  <!-- 瑙嗛棰勮妯℃�佹 -->
-  <view wx:if="{{showMediaPreview}}" class="media-preview-modal">
-    <view class="modal-overlay" bindtap="onCloseMediaPreview"></view>
-    <view class="modal-content">
-      <view class="modal-header">
-        <text class="modal-title">瑙嗛棰勮</text>
-        <view class="close-btn" bindtap="onCloseMediaPreview">
-          <text class="close-icon">鉁�</text>
-        </view>
-      </view>
-      
-      <view class="modal-body">
-        <video 
-          wx:if="{{mediaType === 'video'}}"
-          class="preview-video"
-          src="{{currentMedia}}"
-          controls
-          autoplay
-        ></video>
-      </view>
-    </view>
-  </view>
-</view>
\ No newline at end of file
+</view>
diff --git a/wx/pages/judge/review.wxss b/wx/pages/judge/review.wxss
index fab5a9e..24f04d5 100644
--- a/wx/pages/judge/review.wxss
+++ b/wx/pages/judge/review.wxss
@@ -5,7 +5,7 @@
   padding-bottom: 120rpx;
 }
 
-/* 鍔犺浇鐘舵�� */
+/* Loading */
 .loading-container {
   display: flex;
   flex-direction: column;
@@ -34,12 +34,10 @@
   100% { transform: rotate(360deg); }
 }
 
-/* 璇勫鍐呭 */
 .review-content {
   padding: 30rpx;
 }
 
-/* 閫氱敤鏍峰紡 */
 .section-title {
   font-size: 32rpx;
   font-weight: 600;
@@ -48,8 +46,11 @@
   display: block;
 }
 
-/* 浣滃搧淇℃伅 */
-.submission-info {
+.submission-info,
+.media-section,
+.criteria-section,
+.comment-section,
+.existing-review {
   background-color: #fff;
   border-radius: 16rpx;
   padding: 30rpx;
@@ -87,12 +88,6 @@
   color: #155724;
 }
 
-.submission-detail {
-  display: flex;
-  flex-direction: column;
-  gap: 20rpx;
-}
-
 .submission-title {
   font-size: 30rpx;
   font-weight: 600;
@@ -128,15 +123,12 @@
   display: flex;
   align-items: center;
   gap: 8rpx;
-  padding: 8rpx 16rpx;
-  background-color: #007aff;
+  padding: 10rpx 24rpx;
+  background: linear-gradient(135deg, #34c759 0%, #2ea043 100%);
   color: #fff;
-  border-radius: 16rpx;
-  font-size: 22rpx;
-}
-
-.contact-icon {
-  font-size: 20rpx;
+  border-radius: 24rpx;
+  font-size: 24rpx;
+  box-shadow: 0 6rpx 16rpx rgba(46, 160, 67, 0.25);
 }
 
 .participant-detail {
@@ -146,13 +138,13 @@
 }
 
 .participant-avatar {
-  width: 80rpx;
-  height: 80rpx;
-  border-radius: 40rpx;
+  width: 96rpx;
+  height: 96rpx;
+  border-radius: 48rpx;
+  background-color: #f0f0f0;
 }
 
 .participant-text {
-  flex: 1;
   display: flex;
   flex-direction: column;
   gap: 8rpx;
@@ -160,196 +152,127 @@
 
 .participant-name {
   font-size: 28rpx;
-  font-weight: 500;
+  font-weight: 600;
   color: #333;
 }
 
-.participant-school {
+.participant-meta {
+  font-size: 24rpx;
+  color: #555;
+  line-height: 1.4;
+}
+
+.participant-field {
   font-size: 24rpx;
   color: #666;
+  line-height: 1.4;
 }
 
 .team-info {
   margin-top: 20rpx;
-  padding-top: 20rpx;
-  border-top: 1rpx solid #f0f0f0;
+  background-color: #f8f9fa;
+  border-radius: 12rpx;
+  padding: 20rpx;
 }
 
 .team-label {
   font-size: 26rpx;
-  color: #666;
-  font-weight: 500;
-  margin-bottom: 16rpx;
-  display: block;
+  font-weight: 600;
+  color: #333;
 }
 
 .team-members {
+  margin-top: 12rpx;
   display: flex;
-  flex-wrap: wrap;
-  gap: 16rpx;
+  flex-direction: column;
+  gap: 8rpx;
 }
 
 .team-member {
+  font-size: 24rpx;
+  color: #555;
+}
+
+.media-list {
   display: flex;
   flex-direction: column;
-  align-items: center;
-  padding: 12rpx 20rpx;
-  background-color: #f8f9fa;
-  border-radius: 12rpx;
-}
-
-.member-name {
-  font-size: 24rpx;
-  color: #333;
-  font-weight: 500;
-}
-
-.member-role {
-  font-size: 20rpx;
-  color: #666;
-  margin-top: 4rpx;
-}
-
-.submit-time {
-  font-size: 24rpx;
-  color: #999;
-  margin-top: 10rpx;
-}
-
-/* 濯掍綋灞曠ず */
-.media-section {
-  background-color: #fff;
-  border-radius: 16rpx;
-  padding: 30rpx;
-  margin-bottom: 30rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
-}
-
-.media-grid {
-  display: grid;
-  grid-template-columns: repeat(2, 1fr);
   gap: 20rpx;
 }
 
 .media-item {
-  position: relative;
-  border-radius: 12rpx;
-  overflow: hidden;
-  aspect-ratio: 1;
-}
-
-.media-image,
-.media-video {
-  width: 100%;
-  height: 100%;
-  object-fit: cover;
-}
-
-.play-overlay {
-  position: absolute;
-  top: 50%;
-  left: 50%;
-  transform: translate(-50%, -50%);
-  width: 60rpx;
-  height: 60rpx;
-  background-color: rgba(0, 0, 0, 0.6);
-  border-radius: 50%;
   display: flex;
   align-items: center;
-  justify-content: center;
-}
-
-.play-icon {
-  color: #fff;
-  font-size: 24rpx;
-  margin-left: 4rpx;
-}
-
-/* 鏂囦欢鍒楄〃 */
-.files-section {
-  background-color: #fff;
-  border-radius: 16rpx;
-  padding: 30rpx;
-  margin-bottom: 30rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
-}
-
-.file-list {
-  display: flex;
-  flex-direction: column;
-  gap: 20rpx;
-}
-
-.file-item {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
+  gap: 24rpx;
   padding: 20rpx;
   background-color: #f8f9fa;
   border-radius: 12rpx;
 }
 
-.file-info {
+.media-thumb-wrapper {
+  width: 120rpx;
+  height: 120rpx;
+  border-radius: 12rpx;
+  overflow: hidden;
+  position: relative;
+  background: #eaeaea;
+}
+
+.media-thumb {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.media-icon {
+  width: 100%;
+  height: 100%;
   display: flex;
   align-items: center;
-  gap: 16rpx;
-  flex: 1;
-  min-width: 0;
-}
-
-.file-icon {
+  justify-content: center;
   font-size: 32rpx;
+  font-weight: 600;
+  color: #fff;
 }
 
-.file-detail {
+.media-icon.pdf { background: #e74c3c; }
+.media-icon.doc { background: #1e88e5; }
+.media-icon.file { background: #6c757d; }
+
+.media-play {
+  position: absolute;
+  bottom: 8rpx;
+  right: 8rpx;
+  width: 44rpx;
+  height: 44rpx;
+  background: rgba(0, 0, 0, 0.6);
+  border-radius: 50%;
+  color: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 24rpx;
+}
+
+.media-info {
   flex: 1;
   min-width: 0;
   display: flex;
   flex-direction: column;
-  gap: 4rpx;
+  gap: 6rpx;
 }
 
-.file-name {
-  font-size: 26rpx;
+.media-name {
+  font-size: 28rpx;
+  font-weight: 600;
   color: #333;
-  font-weight: 500;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
 }
 
-.file-size {
+.media-size {
   font-size: 22rpx;
   color: #666;
-}
-
-.download-btn {
-  display: flex;
-  align-items: center;
-  gap: 8rpx;
-  padding: 12rpx 20rpx;
-  background-color: #007aff;
-  color: #fff;
-  border-radius: 20rpx;
-  font-size: 22rpx;
-  transition: all 0.3s ease;
-}
-
-.download-btn.downloading {
-  background-color: #6c757d;
-}
-
-.download-icon {
-  font-size: 20rpx;
-}
-
-/* 璇勫鏍囧噯 */
-.criteria-section {
-  background-color: #fff;
-  border-radius: 16rpx;
-  padding: 30rpx;
-  margin-bottom: 30rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
 }
 
 .criteria-list {
@@ -392,33 +315,35 @@
   display: block;
 }
 
-.score-selector {
-  width: 100%;
-}
-
-.score-picker {
+.score-control {
   display: flex;
-  justify-content: space-between;
   align-items: center;
-  padding: 16rpx 20rpx;
-  background-color: #fff;
-  border: 2rpx solid #e0e0e0;
-  border-radius: 8rpx;
-  font-size: 26rpx;
+  gap: 20rpx;
+  margin-top: 16rpx;
 }
 
-.score-picker.disabled {
-  background-color: #f5f5f5;
-  color: #999;
+.score-btn {
+  width: 60rpx;
+  height: 60rpx;
+  border-radius: 12rpx;
+  background: #eef2ff;
+  color: #4c60ff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 36rpx;
+  font-weight: 600;
 }
 
-.score-text {
-  color: #333;
-}
-
-.picker-arrow {
-  color: #999;
-  font-size: 20rpx;
+.score-input {
+  flex: 1;
+  height: 60rpx;
+  border: 2rpx solid #dcdfe6;
+  border-radius: 12rpx;
+  text-align: center;
+  font-size: 28rpx;
+  padding: 0 12rpx;
+  background: #fff;
 }
 
 .total-score {
@@ -444,15 +369,6 @@
   font-weight: 700;
 }
 
-/* 璇勫鎰忚 */
-.comment-section {
-  background-color: #fff;
-  border-radius: 16rpx;
-  padding: 30rpx;
-  margin-bottom: 30rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
-}
-
 .comment-input {
   width: 100%;
   min-height: 200rpx;
@@ -466,29 +382,16 @@
   box-sizing: border-box;
 }
 
-.comment-input.disabled {
-  background-color: #f5f5f5;
-  color: #999;
-}
-
 .comment-counter {
-  display: flex;
-  justify-content: flex-end;
+  text-align: right;
   margin-top: 12rpx;
-}
-
-.counter-text {
   font-size: 22rpx;
   color: #999;
 }
 
-/* 宸叉湁璇勫璁板綍 */
 .existing-review {
-  background-color: #fff;
-  border-radius: 16rpx;
-  padding: 30rpx;
-  margin-bottom: 30rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
+  background-color: #fff7e6;
+  border: 2rpx solid #ffb347;
 }
 
 .review-info {
@@ -496,49 +399,35 @@
   justify-content: space-between;
   align-items: center;
   margin-bottom: 20rpx;
-  padding: 20rpx;
-  background-color: #f8f9fa;
-  border-radius: 12rpx;
-}
-
-.review-time,
-.review-total {
-  font-size: 24rpx;
+  font-size: 26rpx;
   color: #666;
 }
 
 .other-reviews-btn {
   display: flex;
   align-items: center;
-  justify-content: center;
-  gap: 12rpx;
-  padding: 16rpx;
-  background-color: #007aff;
+  gap: 8rpx;
+  padding: 14rpx 24rpx;
+  border-radius: 24rpx;
+  background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
   color: #fff;
-  border-radius: 12rpx;
   font-size: 26rpx;
 }
 
-.other-reviews-icon {
-  font-size: 24rpx;
-}
-
-/* 搴曢儴鎿嶄綔鏍� */
 .bottom-actions {
   position: fixed;
-  bottom: 0;
   left: 0;
   right: 0;
-  display: flex;
-  gap: 20rpx;
+  bottom: 0;
   padding: 20rpx 30rpx;
-  background-color: #fff;
-  border-top: 1rpx solid #e0e0e0;
-  box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.1);
+  background-color: rgba(255, 255, 255, 0.95);
+  box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
+  display: flex;
+  justify-content: center;
+  align-items: stretch;
 }
 
 .action-btn {
-  flex: 1;
   display: flex;
   align-items: center;
   justify-content: center;
@@ -548,11 +437,8 @@
   font-size: 28rpx;
   font-weight: 500;
   transition: all 0.3s ease;
-}
-
-.draft-btn {
-  background-color: #6c757d;
-  color: #fff;
+  flex: 1;
+  width: 100%;
 }
 
 .submit-btn {
@@ -568,101 +454,25 @@
   font-size: 24rpx;
 }
 
-/* 瑙嗛棰勮妯℃�佹 */
-.media-preview-modal {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  z-index: 1000;
-}
-
-.modal-overlay {
-  position: absolute;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background-color: rgba(0, 0, 0, 0.8);
-}
-
-.modal-content {
-  position: absolute;
-  top: 50%;
-  left: 50%;
-  transform: translate(-50%, -50%);
-  width: 90%;
-  max-width: 600rpx;
-  background-color: #fff;
-  border-radius: 16rpx;
-  overflow: hidden;
-}
-
-.modal-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding: 20rpx 30rpx;
-  border-bottom: 1rpx solid #e0e0e0;
-}
-
-.modal-title {
-  font-size: 30rpx;
-  font-weight: 600;
-  color: #333;
-}
-
-.close-btn {
-  width: 60rpx;
-  height: 60rpx;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  border-radius: 50%;
-  background-color: #f8f9fa;
-}
-
-.close-icon {
-  font-size: 24rpx;
-  color: #666;
-}
-
-.modal-body {
-  padding: 30rpx;
-}
-
-.preview-video {
-  width: 100%;
-  height: 400rpx;
-  border-radius: 12rpx;
-}
-
-/* 鍝嶅簲寮忛�傞厤 */
 @media (max-width: 375px) {
   .container {
     padding-bottom: 100rpx;
   }
-  
+
   .review-content {
     padding: 20rpx;
   }
-  
+
   .submission-info,
   .media-section,
-  .files-section,
   .criteria-section,
   .comment-section,
   .existing-review {
     padding: 20rpx;
     margin-bottom: 20rpx;
   }
-  
-  .media-grid {
-    grid-template-columns: 1fr;
-  }
-  
+
   .bottom-actions {
     padding: 15rpx 20rpx;
   }
-}
\ No newline at end of file
+}
diff --git a/wx/pages/message/message.js b/wx/pages/message/message.js
index 4fa1f29..e390706 100644
--- a/wx/pages/message/message.js
+++ b/wx/pages/message/message.js
@@ -21,8 +21,22 @@
 
   // 鍔犺浇娑堟伅鍒楄〃
   loadMessages() {
-    // 妫�鏌ョ敤鎴锋槸鍚﹀凡鐧诲綍
-    const userInfo = app.globalData.userInfo
+    // 妫�鏌ョ敤鎴锋槸鍚﹀凡鐧诲綍锛屽鏋済lobalData涓病鏈夛紝灏濊瘯浠庢湰鍦板瓨鍌ㄦ仮澶�
+    let userInfo = app.globalData.userInfo
+    if (!userInfo || !userInfo.userId) {
+      console.log('globalData涓病鏈塽serInfo锛屽皾璇曚粠鏈湴瀛樺偍鎭㈠')
+      try {
+        const storedUserInfo = wx.getStorageSync('userInfo')
+        if (storedUserInfo && storedUserInfo.userId) {
+          console.log('浠庢湰鍦板瓨鍌ㄦ仮澶島serInfo鎴愬姛')
+          app.globalData.userInfo = storedUserInfo
+          userInfo = storedUserInfo
+        }
+      } catch (error) {
+        console.error('浠庢湰鍦板瓨鍌ㄦ仮澶島serInfo澶辫触:', error)
+      }
+    }
+    
     if (!userInfo || !userInfo.userId) {
       console.error('鐢ㄦ埛鏈櫥褰曟垨userId涓嶅瓨鍦�')
       wx.showToast({
diff --git a/wx/pages/project/detail.js b/wx/pages/project/detail.js
index 6ce3629..49a5ed8 100644
--- a/wx/pages/project/detail.js
+++ b/wx/pages/project/detail.js
@@ -597,4 +597,4 @@
       path: `/pages/project/detail?id=${this.data.projectId}`
     }
   }
-})
+})
\ No newline at end of file
diff --git a/wx/pages/review/index.js b/wx/pages/review/index.js
index b30b1cd..3a9dd0e 100644
--- a/wx/pages/review/index.js
+++ b/wx/pages/review/index.js
@@ -2,6 +2,14 @@
 const app = getApp()
 const { graphqlRequest, formatDate } = require('../../lib/utils')
 
+const GET_RATING_STATS_QUERY = `
+  query GetRatingStats($activityPlayerId: ID!) {
+    judgeRatingsForPlayer(activityPlayerId: $activityPlayerId) {
+      hasRated
+    }
+  }
+`
+
 Page({
   data: {
     loading: false,
@@ -55,7 +63,7 @@
 
   // 鍒囨崲閫夐」鍗�
   switchTab(e) {
-    const index = e.currentTarget.dataset.index
+    const index = parseInt(e.currentTarget.dataset.index) || 0
     if (index === this.data.currentTab) return
     
     this.setData({
@@ -110,10 +118,29 @@
       ])
     } catch (error) {
       console.error('鍔犺浇鏁版嵁澶辫触:', error)
-      wx.showToast({
-        title: '鍔犺浇澶辫触',
-        icon: 'none'
-      })
+      
+      // 妫�鏌ユ槸鍚︽槸璁よ瘉鐩稿叧閿欒
+      if (error.message && (
+        error.message.includes('娌℃湁鏉冮檺璁块棶') ||
+        error.message.includes('璇峰厛鐧诲綍') ||
+        error.message.includes('閲嶆柊鐧诲綍')
+      )) {
+        wx.showToast({
+          title: '姝e湪閲嶆柊鐧诲綍...',
+          icon: 'loading',
+          duration: 2000
+        })
+        
+        // 绛夊緟涓�娈垫椂闂村悗閲嶈瘯
+        setTimeout(() => {
+          this.loadData()
+        }, 2000)
+      } else {
+        wx.showToast({
+          title: '鍔犺浇澶辫触锛岃閲嶈瘯',
+          icon: 'none'
+        })
+      }
     } finally {
       this.setData({ loading: false })
       wx.stopPullDownRefresh()
@@ -223,13 +250,13 @@
         const projects = data.items.map(item => ({
           ...item,
           submitTime: item.submitTime ? formatDate(item.submitTime) : '',
-          reviewTime: item.reviewTime ? formatDate(item.reviewTime) : '',
-          statusText: this.getStatusText(item.status),
-          statusType: this.getStatusType(item.status)
+          reviewTime: item.reviewTime ? formatDate(item.reviewTime) : ''
         }))
 
+        const projectsWithRatingCount = await this.enrichProjectsWithRatingCounts(projects)
+
         this.setData({
-          projectList: isLoadMore ? [...this.data.projectList, ...projects] : projects,
+          projectList: isLoadMore ? [...this.data.projectList, ...projectsWithRatingCount] : projectsWithRatingCount,
           hasMore: data.hasMore || false,
           currentPage: variables.page
         })
@@ -274,26 +301,39 @@
     }
   },
 
-  // 鑾峰彇鐘舵�佹枃鏈�
-  getStatusText(status) {
-    const statusMap = {
-      'SUBMITTED': '宸叉彁浜�',
-      'UNDER_REVIEW': '璇勫涓�',
-      'REVIEWED': '宸茶瘎瀹�',
-      'REJECTED': '宸叉嫆缁�'
+  async enrichProjectsWithRatingCounts(projects) {
+    if (!Array.isArray(projects) || projects.length === 0) {
+      return projects || []
     }
-    return statusMap[status] || status
+
+    try {
+      const counts = await Promise.all(projects.map(project => this.getProjectRatingCount(project.id)))
+      return projects.map((project, index) => ({
+        ...project,
+        ratingCount: counts[index]
+      }))
+    } catch (error) {
+      console.error('鎵归噺鑾峰彇璇勫娆℃暟澶辫触:', error)
+      return projects.map(project => ({
+        ...project,
+        ratingCount: typeof project.ratingCount === 'number' ? project.ratingCount : 0
+      }))
+    }
   },
 
-  // 鑾峰彇鐘舵�佺被鍨�
-  getStatusType(status) {
-    const typeMap = {
-      'SUBMITTED': 'info',
-      'UNDER_REVIEW': 'warning',
-      'REVIEWED': 'success',
-      'REJECTED': 'danger'
+  async getProjectRatingCount(activityPlayerId) {
+    if (!activityPlayerId) {
+      return 0
     }
-    return typeMap[status] || 'info'
+
+    try {
+      const result = await graphqlRequest(GET_RATING_STATS_QUERY, { activityPlayerId })
+      const ratings = result?.judgeRatingsForPlayer || []
+      return ratings.filter(item => item?.hasRated).length
+    } catch (error) {
+      console.error(`鑾峰彇椤圭洰 ${activityPlayerId} 鐨勮瘎瀹℃鏁板け璐�:`, error)
+      return 0
+    }
   },
 
   // 鑾峰彇绌虹姸鎬佹枃鏈�
@@ -323,4 +363,4 @@
     ]
     return emptyDescs[currentTab] || ''
   }
-})
\ No newline at end of file
+})
diff --git a/wx/pages/review/index.wxml b/wx/pages/review/index.wxml
index a05628d..893f084 100644
--- a/wx/pages/review/index.wxml
+++ b/wx/pages/review/index.wxml
@@ -74,8 +74,9 @@
         <view class="project-info">
           <view class="project-header">
             <text class="project-name">{{project.projectName || '鏈懡鍚嶉」鐩�'}}</text>
-            <view class="project-status" wx:if="{{project.status}}">
-              <text class="status-text status-{{project.statusType}}">{{project.statusText}}</text>
+            <view class="review-count" wx:if="{{project.ratingCount !== undefined}}">
+              <text class="review-count-label">璇勫娆℃暟锛�</text>
+              <text class="review-count-value">{{project.ratingCount}}</text>
             </view>
           </view>
           
@@ -87,13 +88,13 @@
         </view>
 
         <view class="project-actions">
-          <view 
-            class="review-btn"
-            bindtap="goToReviewDetail"
-            data-project="{{project}}"
-          >
-            <text class="btn-text">璇勫</text>
-          </view>
+        <view 
+          class="review-btn"
+          bindtap="goToReviewDetail"
+          data-activity-player-id="{{project.activityPlayerId || project.id}}"
+        >
+          <text class="btn-text">璇勫</text>
+        </view>
         </view>
       </view>
     </view>
@@ -104,4 +105,4 @@
       <text class="load-more-text" wx:else>鍔犺浇涓�...</text>
     </view>
   </view>
-</view>
\ No newline at end of file
+</view>
diff --git a/wx/pages/review/index.wxss b/wx/pages/review/index.wxss
index 5023c95..875f1ae 100644
--- a/wx/pages/review/index.wxss
+++ b/wx/pages/review/index.wxss
@@ -204,30 +204,25 @@
   margin-right: 20rpx;
 }
 
-.project-status {
+.review-count {
   flex-shrink: 0;
-}
-
-.status-text {
-  font-size: 24rpx;
+  display: flex;
+  align-items: center;
   padding: 6rpx 12rpx;
+  background: #f1f5ff;
   border-radius: 12rpx;
-  font-weight: 500;
+  gap: 6rpx;
 }
 
-.status-text.status-warning {
-  background: #fff3e0;
-  color: #f57c00;
+.review-count-label {
+  font-size: 24rpx;
+  color: #5a6c90;
 }
 
-.status-text.status-success {
-  background: #e8f5e8;
-  color: #388e3c;
-}
-
-.status-text.status-info {
-  background: #e3f2fd;
-  color: #1976d2;
+.review-count-value {
+  font-size: 28rpx;
+  color: #1a3a7b;
+  font-weight: 600;
 }
 
 .project-details {
@@ -286,4 +281,4 @@
 .load-more-text {
   font-size: 26rpx;
   color: #999;
-}
\ No newline at end of file
+}

--
Gitblit v1.8.0