Rockz-Home 1 месяц назад
Родитель
Сommit
17e83d64f1
10 измененных файлов с 801 добавлено и 380 удалено
  1. 40 7
      card/sdk/api.js
  2. 44 10
      card/sdk/bridge.js
  3. 107 46
      card/sdk/detail.html
  4. 30 1
      card/sdk/index.html
  5. 40 7
      card/sdk/old/api.js
  6. 44 10
      card/sdk/old/bridge.js
  7. BIN
      card/sdk/old/card.png
  8. 226 64
      card/sdk/old/detail.html
  9. 91 177
      card/sdk/old/index.html
  10. 179 58
      card/sdk/old2/detail.html

+ 40 - 7
card/sdk/api.js

@@ -1,6 +1,39 @@
 (function (window) {
     'use strict';
 
+    // Logger Utility
+    const Logger = {
+        _isDev: false,
+
+        init: function(isDev) {
+            this._isDev = isDev;
+        },
+
+        log: function() {
+            if (this._isDev) {
+                console.log.apply(console, arguments);
+            }
+        },
+
+        warn: function() {
+            if (this._isDev) {
+                console.warn.apply(console, arguments);
+            }
+        },
+
+        error: function() {
+            console.error.apply(console, arguments); // Always log errors
+        }
+    };
+
+    // Determine _isDev status from main window's URL query params
+    function getQueryParam(name) {
+        const params = new URLSearchParams(window.location.search);
+        return params.get(name);
+    }
+    const env = (getQueryParam('env') || '').toLowerCase();
+    Logger.init(env === 'mock'); // Initialize Logger
+
     /**
      * ColorMapRun API SDK (Full Version with Mock)
      * 封装了与后端服务器的所有交互
@@ -160,7 +193,7 @@
             if (options.ossUrl) Config.ossUrl = options.ossUrl;
             if (options.token) Config.token = options.token;
             if (typeof options.useMock === 'boolean') Config.useMock = options.useMock;
-            if (Config.useMock) console.warn('%c [API] Mock 模式已开启 ', 'background: orange; color: white;');
+            if (Config.useMock) Logger.warn('%c [API] Mock 模式已开启 ', 'background: orange; color: white;');
         },
 
         getOssUrl: function() {
@@ -174,13 +207,13 @@
         request: function(endpoint, data) {
             if (Config.useMock) {
                 return new Promise(function(resolve, reject) {
-                    console.log('[API-Mock] Request:', endpoint, data);
+                    Logger.log('[API-Mock] Request:', endpoint, data);
                     setTimeout(function() {
                         var mockData = MOCK_DB[endpoint];
                         if (endpoint === 'OnlineMcSignUp') MOCK_DB['UserJoinCardQuery'].isJoin = true;
                         if (endpoint === 'ScoreExchangeGoods') MOCK_DB['OnlineScoreQuery'].score -= 100;
                         
-                        console.log('[API-Mock] Response:', endpoint, mockData || {});
+                        Logger.log('[API-Mock] Response:', endpoint, mockData || {});
                         resolve(mockData || {});
                     }, 300); 
                 });
@@ -191,15 +224,15 @@
             var formData = new URLSearchParams();
             for (var key in data) { if (data.hasOwnProperty(key)) formData.append(key, data[key]); }
 
-            console.log('[API] Request:', endpoint, data);
+            Logger.log('[API] Request:', endpoint, data);
 
             return fetch(url, { method: 'POST', headers: headers, body: formData, mode: 'cors', credentials: 'omit' })
             .then(function(response) { return response.json(); })
             .then(function(res) {
-                console.log('[API] Response:', endpoint, res);
+                Logger.log('[API] Response:', endpoint, res);
                 if (res.code === 0) return res.data;
                 if (res.code === 401 || res.statusCode === 401) {
-                    console.warn('[API] Token invalid');
+                    Logger.warn('[API] Token invalid');
                     if (window.Bridge && window.Bridge._post) window.Bridge._post('toLogin');
                     else alert('登录已过期');
                     throw new Error('Unauthorized');
@@ -212,7 +245,7 @@
                 }
                 throw new Error(msg);
             })
-            .catch(function(err) { console.error('[API] Error:', err); throw err; });
+            .catch(function(err) { Logger.error('[API] Error:', err); throw err; });
         },
 
         // ==============================

+ 44 - 10
card/sdk/bridge.js

@@ -1,6 +1,40 @@
 (function (window) {
   'use strict';
 
+  // Logger Utility
+  const Logger = {
+      _isDev: false,
+
+      init: function(isDev) {
+          this._isDev = isDev;
+      },
+
+      log: function() {
+          if (this._isDev) {
+              console.log.apply(console, arguments);
+          }
+      },
+
+      warn: function() {
+          if (this._isDev) {
+              console.warn.apply(console, arguments);
+          }
+      },
+
+      error: function() {
+          console.error.apply(console, arguments); // Always log errors
+      }
+  };
+
+  // Determine _isDev status from main window's URL query params
+  // This function is defined here to be self-contained for bridge.js
+  function getQueryParam(name) {
+      const params = new URLSearchParams(window.location.search);
+      return params.get(name);
+  }
+  const env = (getQueryParam('env') || '').toLowerCase();
+  Logger.init(env === 'mock'); // Initialize Logger
+
   /**
    * ColorMapRun JSBridge SDK (Compatible Version)
    * 用于 H5 页面与 Flutter App 进行交互
@@ -17,7 +51,7 @@
      */
     _post: function (action, data) {
       data = data || {};
-      console.log('[Bridge] Call:', action, data);
+      Logger.log('[Bridge] Call:', action, data);
 
       // 1. 优先尝试标准 uni 通道 (新版 App)
       if (window.uni && window.uni.postMessage) {
@@ -80,7 +114,7 @@
           if (window.share_wx && window.share_wx.postMessage) {
              window.share_wx.postMessage(JSON.stringify(data));
           } else {
-             console.error('[Bridge] share_wx injection not found');
+             Logger.error('[Bridge] share_wx injection not found');
              alert('微信分享功能不可用(环境不支持)');
           }
           return; // shareWx 不需要走 URL 拦截
@@ -90,7 +124,7 @@
            if (window.wx_launch_mini && window.wx_launch_mini.postMessage) {
                window.wx_launch_mini.postMessage(JSON.stringify(data));
            } else {
-               console.error('[Bridge] wx_launch_mini injection not found');
+               Logger.error('[Bridge] wx_launch_mini injection not found');
            }
            return;
 
@@ -99,7 +133,7 @@
            if (window.save_base64 && window.save_base64.postMessage) {
                window.save_base64.postMessage(data.base64);
            } else {
-               console.error('[Bridge] save_base64 injection not found');
+               Logger.error('[Bridge] save_base64 injection not found');
            }
            return;
 
@@ -121,11 +155,11 @@
            return;
 
         default:
-          console.warn('[Bridge] No legacy fallback for action:', action);
+          Logger.warn('[Bridge] No legacy fallback for action:', action);
       }
 
       if (url) {
-        console.log('[Bridge] Legacy URL jump:', url);
+        Logger.log('[Bridge] Legacy URL jump:', url);
         // 触发 URL 拦截
         window.location.href = url;
       }
@@ -157,9 +191,9 @@
           }
         }
       } catch (e) {
-        console.warn('[Bridge] urlAddVer error:', e);
+        Logger.warn('[Bridge] urlAddVer error:', e);
       }
-      console.log("[Bridge] urlAddVer newUrl:", newUrl);
+      Logger.log("[Bridge] urlAddVer newUrl:", newUrl);
       return newUrl;
     },
 
@@ -168,7 +202,7 @@
      */
     appAction: function(url, actType) {
       actType = actType || "";
-      console.log("[Bridge] appAction:", url, "actType:", actType);
+      Logger.log("[Bridge] appAction:", url, "actType:", actType);
 
       if (url.indexOf('http') !== -1) {
         window.location.href = this.urlAddVer(url);
@@ -314,7 +348,7 @@
         this._tokenCallback = callback;
     },
     receiveToken: function(token) {
-        console.log('[Bridge] Received token:', token);
+        Logger.log('[Bridge] Received token:', token);
         if (this._tokenCallback) {
             this._tokenCallback(token);
         }

+ 107 - 46
card/sdk/detail.html

@@ -39,62 +39,62 @@
 
         /* 顶部 Header */
         .header-area {
-            height: 280px; 
+            height: 210px; 
             background: linear-gradient(to bottom, rgba(72, 48, 85, 0.7), rgba(45, 52, 54, 0.95)), 
                         url('./bg.jpg') center/cover;
             padding: 20px;
-            padding-top: max(20px, env(safe-area-inset-top));
+            padding-top: max(25px, env(safe-area-inset-top));
             color: white;
             border-bottom-left-radius: 30px; border-bottom-right-radius: 30px;
             position: relative; flex-shrink: 0;
             z-index: 1; 
         }
 
-        .nav-bar { display: flex; justify-content: space-between; align-items: center; margin-top: 10px; }
+        .nav-bar { display: flex; justify-content: space-between; align-items: center; margin-top: 5px; }
         .icon-btn {
-            width: 36px; height: 36px; background: rgba(255,255,255,0.2); backdrop-filter: blur(5px);
+            width: 32px; height: 32px; background: rgba(255,255,255,0.2); backdrop-filter: blur(5px);
             border-radius: 50%; display: flex; align-items: center; justify-content: center;
-            cursor: pointer; border: 1px solid rgba(255,255,255,0.3);
+            cursor: pointer; border: 1px solid rgba(255,255,255,0.3); font-size: 14px;
         }
-        .month-select { font-size: 18px; font-weight: bold; display: flex; align-items: center; gap: 6px; cursor: pointer; text-shadow: 0 2px 4px rgba(0,0,0,0.5); }
+        .month-select { font-size: 16px; font-weight: bold; display: flex; align-items: center; gap: 6px; cursor: pointer; text-shadow: 0 2px 4px rgba(0,0,0,0.5); }
 
         /* 仪表盘卡片 */
         .dashboard-card {
-            margin-top: 25px;
+            margin-top: 10px;
             background: rgba(0, 0, 0, 0.4); 
             backdrop-filter: blur(10px);
-            border-radius: 20px;
-            padding: 15px 20px;
+            border-radius: 16px;
+            padding: 10px 15px;
             border: 1px solid rgba(255, 255, 255, 0.1);
             box-shadow: 0 8px 20px rgba(0,0,0,0.2);
             position: relative;
             z-index: 5; 
         }
 
-        .dash-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
-        .dash-title { font-size: 16px; font-weight: bold; display: flex; align-items: center; gap: 8px; color: #fff; }
+        .dash-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
+        .dash-title { font-size: 14px; font-weight: bold; display: flex; align-items: center; gap: 6px; color: #fff; }
         .dash-icon { color: var(--primary-orange); }
-        .dash-badge { background: var(--primary-orange); color: #2d3436; font-size: 12px; font-weight: 800; padding: 4px 12px; border-radius: 12px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); transition: all 0.3s;}
+        .dash-badge { background: var(--primary-orange); color: #2d3436; font-size: 11px; font-weight: 800; padding: 3px 10px; border-radius: 10px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); transition: all 0.3s;}
         
         .dash-badge.completed { background: linear-gradient(135deg, #55efc4, #00b894); color: white; }
 
-        .trophy-track-container { position: relative; display: flex; justify-content: space-between; align-items: center; padding: 0 10px; }
+        .trophy-track-container { position: relative; display: flex; justify-content: space-between; align-items: center; padding: 0 5px; }
         
-        .track-line-bg { position: absolute; top: 50%; left: 10px; right: 10px; height: 4px; background: rgba(255,255,255,0.2); border-radius: 2px; transform: translateY(-50%); z-index: 0; }
+        .track-line-bg { position: absolute; top: 50%; left: 10px; right: 10px; height: 3px; background: rgba(255,255,255,0.2); border-radius: 2px; transform: translateY(-50%); z-index: 0; }
         
         .track-line-active { 
             position: absolute; top: 50%; left: 10px; 
             width: 25%; 
-            height: 4px; background: var(--primary-orange); border-radius: 2px; transform: translateY(-50%); z-index: 0; 
+            height: 3px; background: var(--primary-orange); border-radius: 2px; transform: translateY(-50%); z-index: 0; 
             box-shadow: 0 0 8px rgba(253, 203, 110, 0.6); 
             transition: width 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
         }
 
         .trophy-item { 
-            position: relative; z-index: 1; width: 32px; height: 32px; 
+            position: relative; z-index: 1; width: 28px; height: 28px; 
             background: #2d3436; border: 2px solid rgba(255,255,255,0.3); border-radius: 50%; 
             display: flex; justify-content: center; align-items: center; 
-            color: #636e72; font-size: 12px; 
+            color: #636e72; font-size: 10px; 
             transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); 
         }
         
@@ -103,7 +103,7 @@
             transform: scale(1.15); box-shadow: 0 0 10px rgba(253, 203, 110, 0.5); 
         }
 
-        .trophy-item.final { width: 40px; height: 40px; border-color: rgba(255,255,255,0.5); font-size: 16px; }
+        .trophy-item.final { width: 34px; height: 34px; border-color: rgba(255,255,255,0.5); font-size: 14px; }
         
         .trophy-item.final.active { 
             background: linear-gradient(135deg, #f1c40f, #e67e22); 
@@ -121,9 +121,9 @@
 
         /* 领奖台 */
         .podium-wrap {
-            height: 140px;
+            height: 120px;
             display: flex; justify-content: center; align-items: flex-end; 
-            margin-top: -60px; 
+            margin-top: -50px; 
             position: relative; 
             z-index: 10; 
             padding-bottom: 0px; 
@@ -133,27 +133,27 @@
         .p-1 { z-index: 3; } 
         .p-3 { z-index: 1; margin-left: -15px; }
         
-        .p-img { width: 50px; height: 50px; border-radius: 50%; border: 3px solid white; background: #eee; margin-bottom: -10px; position: relative; z-index: 2; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.2);}
-        .p-1 .p-img { width: 70px; height: 70px; border-color: #f1c40f; margin-bottom: -15px;}
+        .p-img { width: 44px; height: 44px; border-radius: 50%; border: 2px solid white; background: #eee; margin-bottom: -10px; position: relative; z-index: 2; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.2);}
+        .p-1 .p-img { width: 60px; height: 60px; border-color: #f1c40f; margin-bottom: -12px;}
         .p-img img { width: 100%; height: 100%; object-fit: cover; }
 
         .crown {
-            position: absolute; top: -28px; color: #f1c40f; font-size: 32px;
+            position: absolute; top: -24px; color: #f1c40f; font-size: 26px;
             filter: drop-shadow(0 2px 4px rgba(0,0,0,0.4));
             animation: crownFloat 2s ease-in-out infinite;
             z-index: 20; 
         }
         @keyframes crownFloat {
             0%, 100% { transform: translateY(0) rotate(-5deg); }
-            50% { transform: translateY(-8px) rotate(5deg); }
+            50% { transform: translateY(-6px) rotate(5deg); }
         }
 
         .p-box { width: 100%; text-align: center; padding-top: 15px; border-radius: 8px 8px 0 0; color: white; box-shadow: 0 5px 15px rgba(0,0,0,0.2); }
-        .p-1 .p-box { height: 90px; background: linear-gradient(180deg, #f1c40f, #f39c12); padding-top: 20px; }
-        .p-2 .p-box { height: 70px; background: linear-gradient(180deg, #bdc3c7, #95a5a6); }
-        .p-3 .p-box { height: 55px; background: linear-gradient(180deg, #e67e22, #d35400); }
-        .p-name { font-size: 12px; margin-bottom: 2px; text-shadow: 0 1px 1px rgba(0,0,0,0.3); white-space: nowrap; overflow: hidden; max-width: 80px; margin: 0 auto; text-overflow: ellipsis;}
-        .p-score { font-size: 14px; font-weight: bold; }
+        .p-1 .p-box { height: 76px; background: linear-gradient(180deg, #f1c40f, #f39c12); padding-top: 18px; }
+        .p-2 .p-box { height: 60px; background: linear-gradient(180deg, #bdc3c7, #95a5a6); }
+        .p-3 .p-box { height: 50px; background: linear-gradient(180deg, #e67e22, #d35400); }
+        .p-name { font-size: 11px; margin-bottom: 2px; text-shadow: 0 1px 1px rgba(0,0,0,0.3); white-space: nowrap; overflow: hidden; max-width: 70px; margin: 0 auto; text-overflow: ellipsis;}
+        .p-score { font-size: 13px; font-weight: bold; }
 
         /* 列表容器 */
         .list-container {
@@ -395,6 +395,32 @@
             allMonthsData: null
         };
 
+        // Logger Utility
+        const Logger = {
+            _isDev: false, // Will be set by initPage based on useMock
+
+            init: function(isDev) {
+                this._isDev = isDev;
+            },
+
+            log: function() {
+                if (this._isDev) {
+                    console.log.apply(console, arguments);
+                }
+            },
+
+            warn: function() {
+                if (this._isDev) {
+                    console.warn.apply(console, arguments);
+                }
+            },
+
+            error: function() {
+                // Always log errors, regardless of dev mode
+                console.error.apply(console, arguments);
+            }
+        };
+
         function getQuery(name) {
             const params = new URLSearchParams(window.location.search);
             return params.get(name);
@@ -624,6 +650,35 @@
             myNameEl.innerText = name;
             myTeamEl.innerText = hasRank ? '继续加油' : '';
             myAvatarEl.innerHTML = buildAvatar(name, 'me');
+            
+            // Remove click listener if exists (clean slate)
+            document.querySelector('.my-rank-bar').onclick = null;
+        }
+
+        function renderGuestState() {
+            const rankNumEl = document.getElementById('myRankNum');
+            const myScoreEl = document.getElementById('myScoreValue');
+            const myNameEl = document.getElementById('myName');
+            const myTeamEl = document.getElementById('myTeam');
+            const myAvatarEl = document.getElementById('myAvatar');
+            const myRankBar = document.querySelector('.my-rank-bar');
+
+            rankNumEl.innerText = '--';
+            myScoreEl.innerText = '';
+            myNameEl.innerText = '您还未登录';
+            myTeamEl.innerText = '点击去登录';
+            myTeamEl.style.color = '#fdcb6e'; 
+            
+            // Random avatar
+            const randomSeed = 'guest_' + Math.floor(Math.random() * 10000);
+            myAvatarEl.innerHTML = buildAvatar('Guest', randomSeed);
+
+            // Add click listener
+            myRankBar.onclick = function() {
+                if (window.Bridge && Bridge.toLogin) {
+                    Bridge.toLogin();
+                }
+            };
         }
 
         function renderRules(config) {
@@ -635,7 +690,7 @@
                     ruleContent.innerHTML = rules[0].data.content;
                 }
             } catch (err) {
-                console.warn('[Rule] parse error', err);
+                Logger.warn('[Rule] parse error', err);
             }
         }
 
@@ -669,7 +724,7 @@
                 renderPodium(state.activeTab === 'venue' ? state.venueList : state.scoreList, state.activeTab);
                 switchTab(state.activeTab, document.querySelector('.tab.active'));
             } catch (err) {
-                console.error('[Month] 加载失败', err);
+                Logger.error('[Month] 加载失败', err);
                 rankListEl.innerHTML = '<div style="padding:20px; color:#d63031;">数据加载失败,请重试</div>';
             } finally {
                 setLoading(false);
@@ -692,6 +747,7 @@
             let baseUrl = getQuery('baseUrl') || undefined;
             const env = (getQuery('env') || '').toLowerCase();
             const useMock = env === 'mock';
+            Logger.init(useMock); // Initialize logger based on mock status
             const ym = getYearMonth();
             
             // 1. Initialize with 6 months
@@ -707,7 +763,7 @@
 
             async function safeCall(promiseFactory) {
                 try { return await promiseFactory(); }
-                catch (err) { console.warn('[Optional API] ignore error', err); return null; }
+                catch (err) { Logger.warn('[Optional API] ignore error', err); return null; }
             }
 
             // Helper to load data for a specific month
@@ -788,22 +844,27 @@
                 renderPodium(state.scoreList, 'score');
                 switchTab(state.activeTab, document.querySelector('.tab.active'));
                 
-                let hasRenderedMyInfo = false;
-                if (allowLogin && myRank) {
-                    const rankVal = Number(myRank.rankNum);
-                    if (rankVal > 0) {
-                        renderMyInfo({ rankNum: rankVal }, myScore, userInfo);
-                    } else {
-                        renderMyInfo({ rankNum: null }, null, userInfo);
+                if (!token && !useMock) {
+                    renderGuestState();
+                } else {
+                    let hasRenderedMyInfo = false;
+                    if (allowLogin && myRank) {
+                        const rankVal = Number(myRank.rankNum);
+                        if (rankVal > 0) {
+                            renderMyInfo({ rankNum: rankVal }, myScore, userInfo);
+                        } else {
+                            renderMyInfo({ rankNum: null }, null, userInfo);
+                        }
+                        hasRenderedMyInfo = true;
+                    } else if (selfRow) {
+                        renderMyInfo({ rankNum: selfRow.rankNum }, { score: selfRow.inRankNum || selfRow.score }, { nickName: selfRow.userName });
+                        hasRenderedMyInfo = true;
+                    }
+                    if (!hasRenderedMyInfo) {
+                        renderMyInfo(myRank, myScore, userInfo);
                     }
-                    hasRenderedMyInfo = true;
-                } else if (selfRow) {
-                    renderMyInfo({ rankNum: selfRow.rankNum }, { score: selfRow.inRankNum || selfRow.score }, { nickName: selfRow.userName });
-                    hasRenderedMyInfo = true;
-                }
-                if (!hasRenderedMyInfo) {
-                    renderMyInfo(myRank, myScore, userInfo);
                 }
+
                 renderRules(cardConfig);
             } finally {
                 setLoading(false);

+ 30 - 1
card/sdk/index.html

@@ -129,11 +129,40 @@
 
       <script src="./bridge.js"></script>
     <script>
+        // Logger Utility
+        const Logger = {
+            _isDev: false,
+
+            init: function(isDev) {
+                this._isDev = isDev;
+            },
+
+            log: function() {
+                if (this._isDev) {
+                    console.log.apply(console, arguments);
+                }
+            },
+
+            warn: function() {
+                if (this._isDev) {
+                    console.warn.apply(console, arguments);
+                }
+            },
+
+            error: function() {
+                console.error.apply(console, arguments);
+            }
+        };
+
         function getQueryParam(name) {
             const params = new URLSearchParams(window.location.search);
             return params.get(name);
         }
 
+        // Initialize Logger
+        const env = (getQueryParam('env') || '').toLowerCase();
+        Logger.init(env === 'mock'); // Only log if env=mock
+
         function redirectToDetail() {
             const token = getQueryParam('token');
             const id = getQueryParam('id');
@@ -153,7 +182,7 @@
             
             detailUrl += (detailUrl.includes('?') ? '&' : '?') + 'full=true';
 
-            console.log("Navigating to:", detailUrl);
+            Logger.log("Navigating to:", detailUrl);
             
             if (window.Bridge && window.Bridge.appAction) {
                 Bridge.appAction(detailUrl);

+ 40 - 7
card/sdk/old/api.js

@@ -1,6 +1,39 @@
 (function (window) {
     'use strict';
 
+    // Logger Utility
+    const Logger = {
+        _isDev: false,
+
+        init: function(isDev) {
+            this._isDev = isDev;
+        },
+
+        log: function() {
+            if (this._isDev) {
+                console.log.apply(console, arguments);
+            }
+        },
+
+        warn: function() {
+            if (this._isDev) {
+                console.warn.apply(console, arguments);
+            }
+        },
+
+        error: function() {
+            console.error.apply(console, arguments); // Always log errors
+        }
+    };
+
+    // Determine _isDev status from main window's URL query params
+    function getQueryParam(name) {
+        const params = new URLSearchParams(window.location.search);
+        return params.get(name);
+    }
+    const env = (getQueryParam('env') || '').toLowerCase();
+    Logger.init(env === 'mock'); // Initialize Logger
+
     /**
      * ColorMapRun API SDK (Full Version with Mock)
      * 封装了与后端服务器的所有交互
@@ -160,7 +193,7 @@
             if (options.ossUrl) Config.ossUrl = options.ossUrl;
             if (options.token) Config.token = options.token;
             if (typeof options.useMock === 'boolean') Config.useMock = options.useMock;
-            if (Config.useMock) console.warn('%c [API] Mock 模式已开启 ', 'background: orange; color: white;');
+            if (Config.useMock) Logger.warn('%c [API] Mock 模式已开启 ', 'background: orange; color: white;');
         },
 
         getOssUrl: function() {
@@ -174,13 +207,13 @@
         request: function(endpoint, data) {
             if (Config.useMock) {
                 return new Promise(function(resolve, reject) {
-                    console.log('[API-Mock] Request:', endpoint, data);
+                    Logger.log('[API-Mock] Request:', endpoint, data);
                     setTimeout(function() {
                         var mockData = MOCK_DB[endpoint];
                         if (endpoint === 'OnlineMcSignUp') MOCK_DB['UserJoinCardQuery'].isJoin = true;
                         if (endpoint === 'ScoreExchangeGoods') MOCK_DB['OnlineScoreQuery'].score -= 100;
                         
-                        console.log('[API-Mock] Response:', endpoint, mockData || {});
+                        Logger.log('[API-Mock] Response:', endpoint, mockData || {});
                         resolve(mockData || {});
                     }, 300); 
                 });
@@ -191,15 +224,15 @@
             var formData = new URLSearchParams();
             for (var key in data) { if (data.hasOwnProperty(key)) formData.append(key, data[key]); }
 
-            console.log('[API] Request:', endpoint, data);
+            Logger.log('[API] Request:', endpoint, data);
 
             return fetch(url, { method: 'POST', headers: headers, body: formData, mode: 'cors', credentials: 'omit' })
             .then(function(response) { return response.json(); })
             .then(function(res) {
-                console.log('[API] Response:', endpoint, res);
+                Logger.log('[API] Response:', endpoint, res);
                 if (res.code === 0) return res.data;
                 if (res.code === 401 || res.statusCode === 401) {
-                    console.warn('[API] Token invalid');
+                    Logger.warn('[API] Token invalid');
                     if (window.Bridge && window.Bridge._post) window.Bridge._post('toLogin');
                     else alert('登录已过期');
                     throw new Error('Unauthorized');
@@ -212,7 +245,7 @@
                 }
                 throw new Error(msg);
             })
-            .catch(function(err) { console.error('[API] Error:', err); throw err; });
+            .catch(function(err) { Logger.error('[API] Error:', err); throw err; });
         },
 
         // ==============================

+ 44 - 10
card/sdk/old/bridge.js

@@ -1,6 +1,40 @@
 (function (window) {
   'use strict';
 
+  // Logger Utility
+  const Logger = {
+      _isDev: false,
+
+      init: function(isDev) {
+          this._isDev = isDev;
+      },
+
+      log: function() {
+          if (this._isDev) {
+              console.log.apply(console, arguments);
+          }
+      },
+
+      warn: function() {
+          if (this._isDev) {
+              console.warn.apply(console, arguments);
+          }
+      },
+
+      error: function() {
+          console.error.apply(console, arguments); // Always log errors
+      }
+  };
+
+  // Determine _isDev status from main window's URL query params
+  // This function is defined here to be self-contained for bridge.js
+  function getQueryParam(name) {
+      const params = new URLSearchParams(window.location.search);
+      return params.get(name);
+  }
+  const env = (getQueryParam('env') || '').toLowerCase();
+  Logger.init(env === 'mock'); // Initialize Logger
+
   /**
    * ColorMapRun JSBridge SDK (Compatible Version)
    * 用于 H5 页面与 Flutter App 进行交互
@@ -17,7 +51,7 @@
      */
     _post: function (action, data) {
       data = data || {};
-      console.log('[Bridge] Call:', action, data);
+      Logger.log('[Bridge] Call:', action, data);
 
       // 1. 优先尝试标准 uni 通道 (新版 App)
       if (window.uni && window.uni.postMessage) {
@@ -80,7 +114,7 @@
           if (window.share_wx && window.share_wx.postMessage) {
              window.share_wx.postMessage(JSON.stringify(data));
           } else {
-             console.error('[Bridge] share_wx injection not found');
+             Logger.error('[Bridge] share_wx injection not found');
              alert('微信分享功能不可用(环境不支持)');
           }
           return; // shareWx 不需要走 URL 拦截
@@ -90,7 +124,7 @@
            if (window.wx_launch_mini && window.wx_launch_mini.postMessage) {
                window.wx_launch_mini.postMessage(JSON.stringify(data));
            } else {
-               console.error('[Bridge] wx_launch_mini injection not found');
+               Logger.error('[Bridge] wx_launch_mini injection not found');
            }
            return;
 
@@ -99,7 +133,7 @@
            if (window.save_base64 && window.save_base64.postMessage) {
                window.save_base64.postMessage(data.base64);
            } else {
-               console.error('[Bridge] save_base64 injection not found');
+               Logger.error('[Bridge] save_base64 injection not found');
            }
            return;
 
@@ -121,11 +155,11 @@
            return;
 
         default:
-          console.warn('[Bridge] No legacy fallback for action:', action);
+          Logger.warn('[Bridge] No legacy fallback for action:', action);
       }
 
       if (url) {
-        console.log('[Bridge] Legacy URL jump:', url);
+        Logger.log('[Bridge] Legacy URL jump:', url);
         // 触发 URL 拦截
         window.location.href = url;
       }
@@ -157,9 +191,9 @@
           }
         }
       } catch (e) {
-        console.warn('[Bridge] urlAddVer error:', e);
+        Logger.warn('[Bridge] urlAddVer error:', e);
       }
-      console.log("[Bridge] urlAddVer newUrl:", newUrl);
+      Logger.log("[Bridge] urlAddVer newUrl:", newUrl);
       return newUrl;
     },
 
@@ -168,7 +202,7 @@
      */
     appAction: function(url, actType) {
       actType = actType || "";
-      console.log("[Bridge] appAction:", url, "actType:", actType);
+      Logger.log("[Bridge] appAction:", url, "actType:", actType);
 
       if (url.indexOf('http') !== -1) {
         window.location.href = this.urlAddVer(url);
@@ -314,7 +348,7 @@
         this._tokenCallback = callback;
     },
     receiveToken: function(token) {
-        console.log('[Bridge] Received token:', token);
+        Logger.log('[Bridge] Received token:', token);
         if (this._tokenCallback) {
             this._tokenCallback(token);
         }

BIN
card/sdk/old/card.png


+ 226 - 64
card/sdk/old/detail.html

@@ -3,12 +3,13 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
-    <title>11月挑战赛</title>
+    <title>月挑战赛</title>
     <!-- 本地调试保留 mock_flutter.js,正式接入时移除 -->
-    <script src="./mock_flutter.js"></script>
+    <!-- <script src="./mock_flutter.js"></script> -->
     <script src="./bridge.js"></script>
     <script src="./api.js"></script>
-    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
+    <script src="./js/multiavatar.min.js"></script>
+    <link href="./css/all.min.css" rel="stylesheet">
     <style>
         :root {
             --primary-purple: #593259; 
@@ -40,7 +41,7 @@
         .header-area {
             height: 280px; 
             background: linear-gradient(to bottom, rgba(72, 48, 85, 0.7), rgba(45, 52, 54, 0.95)), 
-                        url('./bg.jpg?w=800') center/cover;
+                        url('./bg.jpg') center/cover;
             padding: 20px;
             padding-top: max(20px, env(safe-area-inset-top));
             color: white;
@@ -137,7 +138,7 @@
         .p-img img { width: 100%; height: 100%; object-fit: cover; }
 
         .crown {
-            position: absolute; top: -38px; color: #f1c40f; font-size: 32px;
+            position: absolute; top: -28px; color: #f1c40f; font-size: 32px;
             filter: drop-shadow(0 2px 4px rgba(0,0,0,0.4));
             animation: crownFloat 2s ease-in-out infinite;
             z-index: 20; 
@@ -185,11 +186,21 @@
 
         /* 底部我的排名 */
         .my-rank-bar {
-            position: fixed; bottom: 0; left: 0; width: 100%; height: 55px; 
+            position: fixed; bottom: 0; left: 0; width: 100%;
+            /* 移除固定高度,改用 padding 撑开,更灵活且紧凑 */
             background: var(--footer-bg); color: white;
-            display: flex; align-items: center; padding: 0 20px;
-            padding-bottom: env(safe-area-inset-bottom);
-            box-sizing: border-box; 
+            display: flex; align-items: center; 
+            padding: 0 20px;
+            
+            /* 上下留出 8px 间距 */
+            padding-top: 8px;
+            /* 底部间距 = 8px + constant(safe-area-inset-bottom) 用于兼容旧版 iOS */
+            padding-bottom: calc(8px + constant(safe-area-inset-bottom));
+            /* 底部间距 = 8px + 安全区高度 */
+            padding-bottom: calc(8px + env(safe-area-inset-bottom));
+            
+            box-sizing: border-box;
+            
             border-radius: 24px 24px 0 0; box-shadow: 0 -5px 20px rgba(0,0,0,0.2); z-index: 99; 
         }
         .my-rank-bar .rank { font-size: 16px; }
@@ -230,13 +241,39 @@
         .rule-item:last-child { margin-bottom: 0; }
         .rule-item i { color: var(--primary-orange); margin-top: 2px; }
 
-        /* 加载遮罩 */
-        .loading-mask {
+        .my-rank-bar {
+            position: fixed; bottom: 0; left: 0; width: 100%;
+            background: var(--footer-bg); color: white;
+            display: flex; align-items: center; 
+            padding: 0 20px;
+            
+            /* Android / 默认: 稍大一点的间距以防贴边 */
+            padding-top: 6px;
+            padding-bottom: 10px;
+            
+            box-sizing: border-box;
+            border-radius: 24px 24px 0 0; box-shadow: 0 -5px 20px rgba(0,0,0,0.2); z-index: 99; 
+        }
+
+        /* iOS 专属优化: 极致压缩高度 */
+        @supports (-webkit-touch-callout: none) {
+            .my-rank-bar {
+                /* 顶部仅留 4px */
+                padding-top: 4px !important;
+                /* 底部严格贴合安全区,不加任何额外间距 */
+                padding-bottom: env(safe-area-inset-bottom) !important; 
+            }
+        }
+
+        .my-rank-bar .rank { font-size: 16px; }
+        /* 稍微缩小底部头像以节省高度 */
+        .my-rank-bar .avatar { width: 30px; height: 30px; border-width: 2px; }
+        .my-rank-bar .name { font-size: 14px; }
             position: fixed; inset: 0; background: rgba(0,0,0,0.35);
             display: none; align-items: center; justify-content: center;
             z-index: 300; color: #fff; font-size: 14px; backdrop-filter: blur(2px);
         }
-        .loading-mask.show { display: flex; }
+        .loading-mask.show { display: none !important; }
         .loading-spinner {
             border: 4px solid rgba(255,255,255,0.3);
             border-top-color: #fdcb6e;
@@ -304,7 +341,7 @@
     <!-- 底部我的排名 -->
     <div class="my-rank-bar">
         <div class="rank" id="myRankNum">--</div>
-        <div class="avatar" id="myAvatar" style="border:2px solid #fdcb6e"><img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Me" alt=""></div>
+        <div class="avatar" id="myAvatar" style="border:2px solid #fdcb6e; overflow:hidden;"></div>
         <div class="info"><div class="name" id="myName" style="color:white">我</div><div class="team" id="myTeam" style="color:#b2bec3">正在加载...</div></div>
         <div class="my-score" id="myScoreValue">--</div>
     </div>
@@ -334,7 +371,7 @@
     </div>
 
     <!-- 加载中遮罩 -->
-    <div class="loading-mask" id="loadingMask">
+    <div class="loading-mask" id="loadingMask" style="display: none !important;">
         <div class="loading-spinner"></div>
         <div>数据加载中...</div>
     </div>
@@ -358,6 +395,32 @@
             allMonthsData: null
         };
 
+        // Logger Utility
+        const Logger = {
+            _isDev: false, // Will be set by initPage based on useMock
+
+            init: function(isDev) {
+                this._isDev = isDev;
+            },
+
+            log: function() {
+                if (this._isDev) {
+                    console.log.apply(console, arguments);
+                }
+            },
+
+            warn: function() {
+                if (this._isDev) {
+                    console.warn.apply(console, arguments);
+                }
+            },
+
+            error: function() {
+                // Always log errors, regardless of dev mode
+                console.error.apply(console, arguments);
+            }
+        };
+
         function getQuery(name) {
             const params = new URLSearchParams(window.location.search);
             return params.get(name);
@@ -398,9 +461,19 @@
         }
 
         function buildAvatar(name, salt) {
+            // Use local Multiavatar library (Pure JS, no external requests)
+            // It generates high-quality SVG avatars
             const seedBase = name || 'user';
-            const seed = encodeURIComponent(seedBase + (salt || ''));
-            return `<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=${seed}" alt="">`;
+            const seed = seedBase + (salt || '');
+            
+            // multiavatar(seed) returns an SVG string
+            const svgCode = multiavatar(seed);
+            
+            // We need to wrap it in a container or return it as a data URI or direct HTML
+            // Since the existing code expects an <img> tag or innerHTML, putting SVG directly is best for crispness.
+            // However, the existing styling puts it inside a small circle div.
+            // Multiavatar SVG is square, so we rely on parent CSS (overflow: hidden) to clip it to a circle.
+            return svgCode;
         }
 
         function setProgress(real, target) {
@@ -480,14 +553,19 @@
         function renderPodium(list, tabType) {
             const type = tabType || state.activeTab || 'score';
             podiumWrap.innerHTML = '';
-            if (!list || list.length === 0) {
-                podiumWrap.innerHTML = '<div style="color:#fff;">暂无榜单数据</div>';
-                return;
+            
+            // Ensure we have at least 3 elements for the podium, filling with null if less.
+            // This allows the podium structure to always be rendered, even if slots are empty.
+            const podiumData = Array(3).fill(null);
+            if (list && list.length > 0) {
+                list.slice(0, 3).forEach((item, index) => {
+                    podiumData[index] = item;
+                });
             }
-            // Ensure we have at least 3 elements, fill with null if less
-            const p1 = list[0]; // Actual Rank 1
-            const p2 = list[1]; // Actual Rank 2
-            const p3 = list[2]; // Actual Rank 3
+
+            const p1 = podiumData[0]; // Actual Rank 1
+            const p2 = podiumData[1]; // Actual Rank 2
+            const p3 = podiumData[2]; // Actual Rank 3
 
             // Define the visual order for rendering (2nd, 1st, 3rd)
             const podiumItems = [
@@ -498,21 +576,20 @@
 
             podiumItems.forEach(itemConfig => {
                 const person = itemConfig.person;
-                if (!person) return; // Skip if no person for this position
-
                 const col = document.createElement('div');
                 col.className = 'p-col ' + itemConfig.className;
                 
-                const name = person.nickName || person.name || person.userName || '选手';
-                const score = person.score != null ? person.score : (person.inRankNum != null ? person.inRankNum : '--');
-                const rankNum = person.rankNum; // Use actual rank from item
+                const name = person ? (person.nickName || person.name || person.userName) : '虚位以待';
+                const score = person ? (person.score != null ? person.score : (person.inRankNum != null ? person.inRankNum : '')) : '';
+                // Placeholder avatar for empty slots or Multiavatar for actual people
+                const avatarContent = person ? buildAvatar(name, person.rankNum) : '<div style="width:100%;height:100%;background:#ddd;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#666;font-size:12px;font-weight:bold;">?</div>';
 
-                // Only the actual first place gets the crown
-                const isActualFirst = (person === p1); 
+                // Only the actual first place gets the crown, and only if that person exists
+                const isActualFirst = (person === p1 && p1 !== null); 
 
                 col.innerHTML = `
                     ${isActualFirst ? '<div class="crown"><i class="fa-solid fa-crown"></i></div>' : ''}
-                    <div class="p-img">${buildAvatar(name, rankNum)}</div>
+                    <div class="p-img">${avatarContent}</div>
                     <div class="p-box">
                         <div class="p-name">${name}</div>
                         <div class="p-score">${score}</div>
@@ -520,7 +597,9 @@
                 `;
                 podiumWrap.appendChild(col);
             });
-            const remaining = list.slice(3);
+
+            // The remaining items for the list below the podium
+            const remaining = list && list.length > 3 ? list.slice(3) : [];
             if (type === 'venue') {
                 state.venueListRendered = remaining;
             } else {
@@ -571,6 +650,35 @@
             myNameEl.innerText = name;
             myTeamEl.innerText = hasRank ? '继续加油' : '';
             myAvatarEl.innerHTML = buildAvatar(name, 'me');
+            
+            // Remove click listener if exists (clean slate)
+            document.querySelector('.my-rank-bar').onclick = null;
+        }
+
+        function renderGuestState() {
+            const rankNumEl = document.getElementById('myRankNum');
+            const myScoreEl = document.getElementById('myScoreValue');
+            const myNameEl = document.getElementById('myName');
+            const myTeamEl = document.getElementById('myTeam');
+            const myAvatarEl = document.getElementById('myAvatar');
+            const myRankBar = document.querySelector('.my-rank-bar');
+
+            rankNumEl.innerText = '--';
+            myScoreEl.innerText = '';
+            myNameEl.innerText = '您还未登录';
+            myTeamEl.innerText = '点击去登录';
+            myTeamEl.style.color = '#fdcb6e'; 
+            
+            // Random avatar
+            const randomSeed = 'guest_' + Math.floor(Math.random() * 10000);
+            myAvatarEl.innerHTML = buildAvatar('Guest', randomSeed);
+
+            // Add click listener
+            myRankBar.onclick = function() {
+                if (window.Bridge && Bridge.toLogin) {
+                    Bridge.toLogin();
+                }
+            };
         }
 
         function renderRules(config) {
@@ -582,7 +690,7 @@
                     ruleContent.innerHTML = rules[0].data.content;
                 }
             } catch (err) {
-                console.warn('[Rule] parse error', err);
+                Logger.warn('[Rule] parse error', err);
             }
         }
 
@@ -616,7 +724,7 @@
                 renderPodium(state.activeTab === 'venue' ? state.venueList : state.scoreList, state.activeTab);
                 switchTab(state.activeTab, document.querySelector('.tab.active'));
             } catch (err) {
-                console.error('[Month] 加载失败', err);
+                Logger.error('[Month] 加载失败', err);
                 rankListEl.innerHTML = '<div style="padding:20px; color:#d63031;">数据加载失败,请重试</div>';
             } finally {
                 setLoading(false);
@@ -629,14 +737,22 @@
         }
 
         async function initPage() {
+            // Platform detection for CSS adjustments
+            if (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream) {
+                document.body.classList.add('platform-ios');
+            }
+
             const ecId = getQuery('ecId') || '4';
             const token = getQuery('token');
             let baseUrl = getQuery('baseUrl') || undefined;
             const env = (getQuery('env') || '').toLowerCase();
             const useMock = env === 'mock';
+            Logger.init(useMock); // Initialize logger based on mock status
             const ym = getYearMonth();
-            state.currentYM = ym;
-            state.months = getRecentMonths(3);
+            
+            // 1. Initialize with 6 months
+            state.months = getRecentMonths(6); 
+            
             if (!baseUrl && !useMock) baseUrl = 'https://colormaprun.com/api/card/';
             if (window.Bridge && window.Bridge.onToken) Bridge.onToken(API.setToken);
             API.init({ token: token || '', useMock: useMock, baseUrl: baseUrl });
@@ -647,11 +763,50 @@
 
             async function safeCall(promiseFactory) {
                 try { return await promiseFactory(); }
-                catch (err) { console.warn('[Optional API] ignore error', err); return null; }
+                catch (err) { Logger.warn('[Optional API] ignore error', err); return null; }
+            }
+
+            // Helper to load data for a specific month
+            async function fetchMonthData(year, month) {
+                return await API.request('MonthRankDetailQuery', { year: year, month: month, dispArrStr: 'grad,mapNum' });
             }
 
             try {
-                const monthRank = await API.request('MonthRankDetailQuery', { year: ym.year, month: ym.month, dispArrStr: 'grad,mapNum' });
+                // 2. Try loading current month first
+                let targetYM = { year: ym.year, month: ym.month };
+                let monthRank = await fetchMonthData(targetYM.year, targetYM.month);
+                
+                // 3. Check if current month has data
+                let monthGrad = monthRank && monthRank.gradRs ? monthRank.gradRs : [];
+                let monthMap = monthRank && monthRank.mapNumRs ? monthRank.mapNumRs : [];
+                let hasData = monthGrad.length > 0 || monthMap.length > 0;
+
+                // 4. If no data, search backwards in the 6-month list
+                if (!hasData) {
+                    // Start from index 1 because index 0 (current month) is already checked
+                    for (let i = 1; i < state.months.length; i++) {
+                        const checkYM = state.months[i];
+                        const checkRank = await fetchMonthData(checkYM.year, checkYM.month);
+                        const checkGrad = checkRank && checkRank.gradRs ? checkRank.gradRs : [];
+                        const checkMap = checkRank && checkRank.mapNumRs ? checkRank.mapNumRs : [];
+                        
+                        if (checkGrad.length > 0 || checkMap.length > 0) {
+                            // Found data! Update target and data
+                            targetYM = checkYM;
+                            monthRank = checkRank;
+                            monthGrad = checkGrad;
+                            monthMap = checkMap;
+                            hasData = true;
+                            break; 
+                        }
+                    }
+                }
+
+                // Update state with the found (or original empty) data
+                state.currentYM = targetYM;
+                state.scoreList = monthGrad;
+                state.venueList = monthMap;
+
                 let base, currentMonth, allMonths, myRank, myScore, userInfo, cardConfig;
                 if (useMock || allowLogin) {
                     [base, currentMonth, allMonths, myRank, myScore, userInfo, cardConfig] = await Promise.all([
@@ -664,49 +819,56 @@
                         safeCall(() => API.getCardConfig(ecId, 'rank'))
                     ]);
                 } else {
-                    base = { ecName: `${ym.month}月挑战赛` };
-                    currentMonth = { monthRs: [{ month: ym.month, realNum: 0, targetNum: 4 }] };
+                    base = { ecName: `${targetYM.month}月挑战赛` };
+                    currentMonth = { monthRs: [{ month: targetYM.month, realNum: 0, targetNum: 4 }] };
                     allMonths = [];
                     myRank = null;
                     myScore = null;
                     userInfo = null;
                     cardConfig = null;
                 }
-                const monthGrad = monthRank && monthRank.gradRs ? monthRank.gradRs : [];
-                const monthMap = monthRank && monthRank.mapNumRs ? monthRank.mapNumRs : [];
-                state.scoreList = monthGrad;
-                state.venueList = monthMap;
+
                 const selfRow = monthGrad.find && monthGrad.find(item => item.isSelf === 1);
-                if (!base && monthGrad.length) base = { ecName: `${ym.month}月挑战赛` };
+                if (!base && monthGrad.length) base = { ecName: `${targetYM.month}月挑战赛` };
                 state.currentMonthData = currentMonth || null;
                 state.allMonthsData = allMonths || null;
-                const prog = findMonthProgress(ym.year, ym.month, state.currentMonthData, state.allMonthsData);
-                document.getElementById('currentMonthText').innerText = `${ym.month}月挑战赛`;
+                
+                // IMPORTANT: Render with the actually selected month (targetYM)
+                const prog = findMonthProgress(targetYM.year, targetYM.month, state.currentMonthData, state.allMonthsData);
+                document.getElementById('currentMonthText').innerText = `${targetYM.month}月挑战赛`;
+                
+                // Re-render dropdown to highlight correct month
+                renderMonths(state.months);
+
                 renderBadge(prog.realNum, prog.targetNum);
                 renderPodium(state.scoreList, 'score');
                 switchTab(state.activeTab, document.querySelector('.tab.active'));
-                let hasRenderedMyInfo = false;
-                if (allowLogin && myRank) {
-                    const rankVal = Number(myRank.rankNum);
-                    if (rankVal > 0) {
-                        renderMyInfo({ rankNum: rankVal }, myScore, userInfo);
-                    } else {
-                        renderMyInfo({ rankNum: null }, null, userInfo);
+                
+                if (!token && !useMock) {
+                    renderGuestState();
+                } else {
+                    let hasRenderedMyInfo = false;
+                    if (allowLogin && myRank) {
+                        const rankVal = Number(myRank.rankNum);
+                        if (rankVal > 0) {
+                            renderMyInfo({ rankNum: rankVal }, myScore, userInfo);
+                        } else {
+                            renderMyInfo({ rankNum: null }, null, userInfo);
+                        }
+                        hasRenderedMyInfo = true;
+                    } else if (selfRow) {
+                        renderMyInfo({ rankNum: selfRow.rankNum }, { score: selfRow.inRankNum || selfRow.score }, { nickName: selfRow.userName });
+                        hasRenderedMyInfo = true;
+                    }
+                    if (!hasRenderedMyInfo) {
+                        renderMyInfo(myRank, myScore, userInfo);
                     }
-                    hasRenderedMyInfo = true;
-                } else if (selfRow) {
-                    renderMyInfo({ rankNum: selfRow.rankNum }, { score: selfRow.inRankNum || selfRow.score }, { nickName: selfRow.userName });
-                    hasRenderedMyInfo = true;
-                }
-                if (!hasRenderedMyInfo) {
-                    renderMyInfo(myRank, myScore, userInfo);
                 }
+
                 renderRules(cardConfig);
-            } catch (err) {
-                console.error('[Init] 加载失败', err);
-                rankListEl.innerHTML = '<div style="padding:20px; color:#d63031;">数据加载失败,请重试</div>';
             } finally {
                 setLoading(false);
+                loadingMask.classList.remove('show'); // Force remove show class
             }
         }
 

+ 91 - 177
card/sdk/old/index.html

@@ -2,15 +2,10 @@
 <html lang="zh-CN">
 <head>
     <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
-    <title>11月挑战赛 - 智能缩放版</title>
-    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+    <title>月度排行榜</title>
+    <link href="./css/all.min.css" rel="stylesheet">
     <style>
-        :root {
-            --primary-purple: #593259;
-            --primary-orange: #ffeaa7;
-        }
-
         * {
             box-sizing: border-box;
             margin: 0;
@@ -21,234 +16,153 @@
         }
 
         html, body {
-            margin: 0;
-            padding: 0;
-            width: 100vw;
-            height: 100vh;
+            width: 100%;
+            height: 100%;
             overflow: hidden;
-            background-color: transparent !important;
-            background: rgba(0, 0, 0, 0) !important;
-        }
-
-        body {
+            background: transparent;
             display: flex;
             justify-content: center;
             align-items: center;
         }
 
-        /* 2. 外层容器 */
+        /* 容器:100% 充满嵌入框 */
         .card-container {
-            width: 96%;
-            height: 96%;
-            /* 圆角也需要自适应:最大24px,或者视口最小边的6% */
-            border-radius: min(24px, 6vmin);
+            width: 100%;
+            height: 100%;
+            border-radius: 20px;
             overflow: hidden;
             position: relative;
             cursor: pointer;
-            
-            background: linear-gradient(to bottom, 
-                            rgba(162, 155, 254, 0.2) 0%, 
-                            rgba(45, 52, 54, 0.95) 100%), 
-                        url('./bg.jpg?w=800') center/cover no-repeat;
-            
-            box-shadow: 0 15px 40px rgba(0, 0, 0, 0.4);
-            
-            transform: translateZ(0);
-            -webkit-mask-image: -webkit-radial-gradient(white, black);
+            /* 背景图 */
+            background: url('./card.png') center/cover no-repeat;
+        }
+
+        .card-container:active {
+            transform: scale(0.98);
+            transition: transform 0.1s;
         }
 
-        /* 3. 内层毛玻璃 */
-        .glass-layer {
+        .content-layer {
             position: absolute;
-            top: 4%; left: 4%; right: 4%; bottom: 4%;
-            
-            /* 圆角自适应 */
-            border-radius: min(20px, 5vmin);
-            
-            border: 1.5px solid rgba(255, 255, 255, 0.3);
-            border-bottom: 1.5px solid rgba(255, 255, 255, 0.1);
-            
-            background: linear-gradient(to bottom, 
-                rgba(255, 255, 255, 0.15) 0%, 
-                rgba(255, 255, 255, 0.02) 50%, 
-                rgba(0, 0, 0, 0.1) 100%
-            );
-            backdrop-filter: blur(6px);
-            -webkit-backdrop-filter: blur(6px);
-            
+            top: 0; left: 0; width: 100%; height: 100%;
             display: flex;
             flex-direction: column;
             align-items: center;
-            justify-content: flex-start;
-            text-align: center;
-            
-            box-shadow: inset 0 0 15px rgba(255,255,255,0.05);
-        }
-
-        .card-container:active {
-            transform: scale(0.98);
-            transition: transform 0.1s;
+            z-index: 2;
         }
 
-        /* 顶部年份 */
-        .tag-year {
-            /* 字体大小:最大20px,缩小时占视口最小边的5.5% */
-            font-size: min(30px, 7.5vmin); 
-            font-weight: 400;
-            letter-spacing: 1px;
-            color: #fff;
-            background: rgba(0, 0, 0, 0.2);
+        /* 1. 年份标签 - 修改:去背景、放大字体、单位改 vmin */
+        .year-tag {
+            /* 距离顶部约 14% */
+            margin-top: 13%; 
             
-            /* Padding 使用 em 单位,随字体大小缩放 */
-            padding: 0.2em 0.8em;
+            /* vmin 单位:确保随容器大小自动缩放 */
+            font-size: 8vmin; /* 字体加大 */
+            font-weight: 800;
+            color: #fff;
+            letter-spacing: 2px;
             
-            border-radius: 16px;
-            border: 1px solid rgba(255,255,255,0.2);
-            position: absolute;
+            /* 去掉背景和边框 */
+            background: none;
+            border: none;
+            padding: 0;
             
-            /* 距离顶部的距离也自适应 */
-            top: min(10px, 2vh); 
-            z-index: 10;
+            /* 增加投影以保证清晰度 */
+            text-shadow: 0 2px 4px rgba(0,0,0,0.5);
         }
 
-        /* 核心数字:11 */
-        .title-month {
-            /* 字体大小:最大80px,缩小时占视口最小边的22% */
-            font-size: min(140px, 32vmin); 
+        /* 2. 月份数字 - 修改:单位改 vmin */
+        .month-num {
+            /* 字体极大,随容器缩放 */
+            font-size: 32vmin; 
             font-weight: 900;
             line-height: 1;
+            margin-top: 9%;
             
-            background: linear-gradient(to bottom, #ffffff 10%, #ffd700 50%, #ff9f43 100%);
+            /* 金色渐变字 */
+            background: linear-gradient(to bottom, #ffffff 20%, #ffd700 60%, #ff9f43 100%);
             -webkit-background-clip: text;
             -webkit-text-fill-color: transparent;
             
-            filter: drop-shadow(0 4px 0px rgba(0, 0, 0, 0.2));
-            
-            /* 顶部间距:最大55px,缩小时占视口高度的12% */
-            margin-top: min(75px, 16vh); 
-            z-index: 2;
-        }
-
-        /* 月度挑战赛 */
-        .title-sub {
-            /* 字体大小:最大16px,缩小时占视口最小边的4.5% */
-            font-size: min(20px, 6.5vmin); 
-            font-weight: 800;
-            color: rgba(255, 255, 255, 0.95);
-            letter-spacing: 2px;
-            text-transform: uppercase;
-            text-shadow: 0 1px 2px rgba(0,0,0,0.5);
-            
-            margin-top: min(5px, 1vh); 
-            margin-bottom: auto; 
+            filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
         }
 
-        /* 奖杯图标 */
-        .deco-icon {
-            /* 底部间距:最大22px,缩小时占视口高度的4% */
-            margin-bottom: min(22px, 8vh);
+        /* 3. 奖杯动画图标 - 修改:大幅下移 */
+        .trophy-icon {
+            /* 修改:往下移动 (从原来的 4% 增加到 15%) */
+            margin-top: 13%; 
             
-            /* 宽高:最大40px,缩小时占视口最小边的11% */
-            width: min(40px, 11vmin); 
-            height: min(40px, 11vmin);
+            /* 图标大小也随容器缩放 */
+            width: 15vmin; 
+            height: 15vmin;
             
-            border-radius: 50%;
             display: flex; justify-content: center; align-items: center;
-            background: rgba(255,255,255,0.1);
-            border: 1px solid rgba(255,255,255,0.2);
-            box-shadow: 0 5px 15px rgba(0,0,0,0.3);
-            
             animation: trophy-pulse 2s infinite ease-in-out;
         }
 
-        .deco-icon i {
-            /* 图标大小:最大20px,随容器缩小 */
-            font-size: min(20px, 5.5vmin);
+        .trophy-icon i {
+            /* 图标字体大小随容器缩放 */
+            font-size: 10vmin; 
             color: #ffd700; 
             filter: drop-shadow(0 0 5px rgba(253, 203, 110, 0.8));
         }
 
-        /* 查看排行按钮 */
-        .action-btn {
-            /* 底部间距:最大20px,缩小时自适应 */
-            margin-bottom: min(30px, 6vh);
-            
-            /* 宽度:最大110px,缩小时占视口宽度的30% */
-            width: min(180px, 50vmin); 
-            
-            padding: min(8px, 2vh) 0;
-            border-radius: 25px;
-            border: none;
-            
-            background: linear-gradient(90deg, #ffeaa7 0%, #fab1a0 100%);
-            color: #593259;
-            
-            /* 字体:最大12px */
-            font-size: min(18px, 5.5vmin);
-            font-weight: 900;
-            
-            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
-            
-            display: flex;
-            align-items: center;
-            justify-content: center;
-            gap: 4px;
-            z-index: 10;
-        }
-        
-        .action-btn i { font-size: 0.8em; }
-
         @keyframes trophy-pulse {
-            0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(253, 203, 110, 0.2); }
-            50% { transform: scale(1.15); box-shadow: 0 0 15px 5px rgba(253, 203, 110, 0.4); }
-            100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(253, 203, 110, 0); }
-        }
-
-        /* 顶部反光 */
-        .shine {
-            position: absolute;
-            top: -20px; left: -20px;
-            width: 150px; height: 100px;
-            background: radial-gradient(circle, rgba(255,255,255,0.3) 0%, transparent 70%);
-            z-index: 3;
-            pointer-events: none;
-            filter: blur(20px);
+            0% { transform: scale(1); filter: drop-shadow(0 0 0 rgba(253, 203, 110, 0)); }
+            50% { transform: scale(1.2); filter: drop-shadow(0 0 8px rgba(253, 203, 110, 0.6)); }
+            100% { transform: scale(1); filter: drop-shadow(0 0 0 rgba(253, 203, 110, 0)); }
         }
 
     </style>
 </head>
 <body>
 
-    <!-- 外层容器 -->
     <div class="card-container" onclick="redirectToDetail()">
-        
-        <!-- 内层容器 -->
-        <div class="glass-layer">
-            <div class="shine"></div>
-            
-            <div class="tag-year">2025 年</div>
-            
-            <div class="title-month">11</div>
-            <div class="title-sub">月度挑战赛</div>
-            
-            <div class="deco-icon">
+        <div class="content-layer">
+            <div class="year-tag">2025年</div>
+            <div class="month-num">12</div>
+            <div class="trophy-icon">
                 <i class="fa-solid fa-trophy"></i>
             </div>
-
-            <button class="action-btn" onclick="redirectToDetail()">
-                查看排行 <i class="fa-solid fa-arrow-right"></i>
-            </button>
         </div>
-        
     </div>
 
-    <script src="./bridge.js"></script>
+      <script src="./bridge.js"></script>
     <script>
+        // Logger Utility
+        const Logger = {
+            _isDev: false,
+
+            init: function(isDev) {
+                this._isDev = isDev;
+            },
+
+            log: function() {
+                if (this._isDev) {
+                    console.log.apply(console, arguments);
+                }
+            },
+
+            warn: function() {
+                if (this._isDev) {
+                    console.warn.apply(console, arguments);
+                }
+            },
+
+            error: function() {
+                console.error.apply(console, arguments);
+            }
+        };
+
         function getQueryParam(name) {
             const params = new URLSearchParams(window.location.search);
             return params.get(name);
         }
 
+        // Initialize Logger
+        const env = (getQueryParam('env') || '').toLowerCase();
+        Logger.init(env === 'mock'); // Only log if env=mock
+
         function redirectToDetail() {
             const token = getQueryParam('token');
             const id = getQueryParam('id');
@@ -268,7 +182,7 @@
             
             detailUrl += (detailUrl.includes('?') ? '&' : '?') + 'full=true';
 
-            console.log("Navigating to:", detailUrl);
+            Logger.log("Navigating to:", detailUrl);
             
             if (window.Bridge && window.Bridge.appAction) {
                 Bridge.appAction(detailUrl);

+ 179 - 58
card/sdk/old2/detail.html

@@ -138,7 +138,7 @@
         .p-img img { width: 100%; height: 100%; object-fit: cover; }
 
         .crown {
-            position: absolute; top: -38px; color: #f1c40f; font-size: 32px;
+            position: absolute; top: -28px; color: #f1c40f; font-size: 32px;
             filter: drop-shadow(0 2px 4px rgba(0,0,0,0.4));
             animation: crownFloat 2s ease-in-out infinite;
             z-index: 20; 
@@ -186,11 +186,21 @@
 
         /* 底部我的排名 */
         .my-rank-bar {
-            position: fixed; bottom: 0; left: 0; width: 100%; height: 55px; 
+            position: fixed; bottom: 0; left: 0; width: 100%;
+            /* 移除固定高度,改用 padding 撑开,更灵活且紧凑 */
             background: var(--footer-bg); color: white;
-            display: flex; align-items: center; padding: 0 20px;
-            padding-bottom: env(safe-area-inset-bottom);
-            box-sizing: border-box; 
+            display: flex; align-items: center; 
+            padding: 0 20px;
+            
+            /* 上下留出 8px 间距 */
+            padding-top: 8px;
+            /* 底部间距 = 8px + constant(safe-area-inset-bottom) 用于兼容旧版 iOS */
+            padding-bottom: calc(8px + constant(safe-area-inset-bottom));
+            /* 底部间距 = 8px + 安全区高度 */
+            padding-bottom: calc(8px + env(safe-area-inset-bottom));
+            
+            box-sizing: border-box;
+            
             border-radius: 24px 24px 0 0; box-shadow: 0 -5px 20px rgba(0,0,0,0.2); z-index: 99; 
         }
         .my-rank-bar .rank { font-size: 16px; }
@@ -231,13 +241,39 @@
         .rule-item:last-child { margin-bottom: 0; }
         .rule-item i { color: var(--primary-orange); margin-top: 2px; }
 
-        /* 加载遮罩 */
-        .loading-mask {
+        .my-rank-bar {
+            position: fixed; bottom: 0; left: 0; width: 100%;
+            background: var(--footer-bg); color: white;
+            display: flex; align-items: center; 
+            padding: 0 20px;
+            
+            /* Android / 默认: 稍大一点的间距以防贴边 */
+            padding-top: 6px;
+            padding-bottom: 10px;
+            
+            box-sizing: border-box;
+            border-radius: 24px 24px 0 0; box-shadow: 0 -5px 20px rgba(0,0,0,0.2); z-index: 99; 
+        }
+
+        /* iOS 专属优化: 极致压缩高度 */
+        @supports (-webkit-touch-callout: none) {
+            .my-rank-bar {
+                /* 顶部仅留 4px */
+                padding-top: 4px !important;
+                /* 底部严格贴合安全区,不加任何额外间距 */
+                padding-bottom: env(safe-area-inset-bottom) !important; 
+            }
+        }
+
+        .my-rank-bar .rank { font-size: 16px; }
+        /* 稍微缩小底部头像以节省高度 */
+        .my-rank-bar .avatar { width: 30px; height: 30px; border-width: 2px; }
+        .my-rank-bar .name { font-size: 14px; }
             position: fixed; inset: 0; background: rgba(0,0,0,0.35);
             display: none; align-items: center; justify-content: center;
             z-index: 300; color: #fff; font-size: 14px; backdrop-filter: blur(2px);
         }
-        .loading-mask.show { display: flex; }
+        .loading-mask.show { display: none !important; }
         .loading-spinner {
             border: 4px solid rgba(255,255,255,0.3);
             border-top-color: #fdcb6e;
@@ -305,7 +341,7 @@
     <!-- 底部我的排名 -->
     <div class="my-rank-bar">
         <div class="rank" id="myRankNum">--</div>
-        <div class="avatar" id="myAvatar" style="border:2px solid #fdcb6e"><img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Me" alt=""></div>
+        <div class="avatar" id="myAvatar" style="border:2px solid #fdcb6e; overflow:hidden;"></div>
         <div class="info"><div class="name" id="myName" style="color:white">我</div><div class="team" id="myTeam" style="color:#b2bec3">正在加载...</div></div>
         <div class="my-score" id="myScoreValue">--</div>
     </div>
@@ -335,7 +371,7 @@
     </div>
 
     <!-- 加载中遮罩 -->
-    <div class="loading-mask" id="loadingMask">
+    <div class="loading-mask" id="loadingMask" style="display: none !important;">
         <div class="loading-spinner"></div>
         <div>数据加载中...</div>
     </div>
@@ -491,17 +527,19 @@
         function renderPodium(list, tabType) {
             const type = tabType || state.activeTab || 'score';
             podiumWrap.innerHTML = '';
-            if (!list || list.length === 0) {
-                podiumWrap.innerHTML = '<div style="color:#fff;">暂无榜单数据</div>';
-                if (type === 'venue') state.venueListRendered = [];
-                else state.scoreListRendered = [];
-                if (state.activeTab === type) renderRankList([]);
-                return;
+            
+            // Ensure we have at least 3 elements for the podium, filling with null if less.
+            // This allows the podium structure to always be rendered, even if slots are empty.
+            const podiumData = Array(3).fill(null);
+            if (list && list.length > 0) {
+                list.slice(0, 3).forEach((item, index) => {
+                    podiumData[index] = item;
+                });
             }
-            // Ensure we have at least 3 elements, fill with null if less
-            const p1 = list[0]; // Actual Rank 1
-            const p2 = list[1]; // Actual Rank 2
-            const p3 = list[2]; // Actual Rank 3
+
+            const p1 = podiumData[0]; // Actual Rank 1
+            const p2 = podiumData[1]; // Actual Rank 2
+            const p3 = podiumData[2]; // Actual Rank 3
 
             // Define the visual order for rendering (2nd, 1st, 3rd)
             const podiumItems = [
@@ -512,21 +550,20 @@
 
             podiumItems.forEach(itemConfig => {
                 const person = itemConfig.person;
-                if (!person) return; // Skip if no person for this position
-
                 const col = document.createElement('div');
                 col.className = 'p-col ' + itemConfig.className;
                 
-                const name = person.nickName || person.name || person.userName || '选手';
-                const score = person.score != null ? person.score : (person.inRankNum != null ? person.inRankNum : '--');
-                const rankNum = person.rankNum; // Use actual rank from item
+                const name = person ? (person.nickName || person.name || person.userName) : '虚位以待';
+                const score = person ? (person.score != null ? person.score : (person.inRankNum != null ? person.inRankNum : '')) : '';
+                // Placeholder avatar for empty slots or Multiavatar for actual people
+                const avatarContent = person ? buildAvatar(name, person.rankNum) : '<div style="width:100%;height:100%;background:#ddd;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#666;font-size:12px;font-weight:bold;">?</div>';
 
-                // Only the actual first place gets the crown
-                const isActualFirst = (person === p1); 
+                // Only the actual first place gets the crown, and only if that person exists
+                const isActualFirst = (person === p1 && p1 !== null); 
 
                 col.innerHTML = `
                     ${isActualFirst ? '<div class="crown"><i class="fa-solid fa-crown"></i></div>' : ''}
-                    <div class="p-img">${buildAvatar(name, rankNum)}</div>
+                    <div class="p-img">${avatarContent}</div>
                     <div class="p-box">
                         <div class="p-name">${name}</div>
                         <div class="p-score">${score}</div>
@@ -534,7 +571,9 @@
                 `;
                 podiumWrap.appendChild(col);
             });
-            const remaining = list.slice(3);
+
+            // The remaining items for the list below the podium
+            const remaining = list && list.length > 3 ? list.slice(3) : [];
             if (type === 'venue') {
                 state.venueListRendered = remaining;
             } else {
@@ -585,6 +624,35 @@
             myNameEl.innerText = name;
             myTeamEl.innerText = hasRank ? '继续加油' : '';
             myAvatarEl.innerHTML = buildAvatar(name, 'me');
+            
+            // Remove click listener if exists (clean slate)
+            document.querySelector('.my-rank-bar').onclick = null;
+        }
+
+        function renderGuestState() {
+            const rankNumEl = document.getElementById('myRankNum');
+            const myScoreEl = document.getElementById('myScoreValue');
+            const myNameEl = document.getElementById('myName');
+            const myTeamEl = document.getElementById('myTeam');
+            const myAvatarEl = document.getElementById('myAvatar');
+            const myRankBar = document.querySelector('.my-rank-bar');
+
+            rankNumEl.innerText = '--';
+            myScoreEl.innerText = '';
+            myNameEl.innerText = '您还未登录';
+            myTeamEl.innerText = '点击去登录';
+            myTeamEl.style.color = '#fdcb6e'; 
+            
+            // Random avatar
+            const randomSeed = 'guest_' + Math.floor(Math.random() * 10000);
+            myAvatarEl.innerHTML = buildAvatar('Guest', randomSeed);
+
+            // Add click listener
+            myRankBar.onclick = function() {
+                if (window.Bridge && Bridge.toLogin) {
+                    Bridge.toLogin();
+                }
+            };
         }
 
         function renderRules(config) {
@@ -643,14 +711,21 @@
         }
 
         async function initPage() {
+            // Platform detection for CSS adjustments
+            if (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream) {
+                document.body.classList.add('platform-ios');
+            }
+
             const ecId = getQuery('ecId') || '4';
             const token = getQuery('token');
             let baseUrl = getQuery('baseUrl') || undefined;
             const env = (getQuery('env') || '').toLowerCase();
             const useMock = env === 'mock';
             const ym = getYearMonth();
-            state.currentYM = ym;
-            state.months = getRecentMonths(3);
+            
+            // 1. Initialize with 6 months
+            state.months = getRecentMonths(6); 
+            
             if (!baseUrl && !useMock) baseUrl = 'https://colormaprun.com/api/card/';
             if (window.Bridge && window.Bridge.onToken) Bridge.onToken(API.setToken);
             API.init({ token: token || '', useMock: useMock, baseUrl: baseUrl });
@@ -664,8 +739,47 @@
                 catch (err) { console.warn('[Optional API] ignore error', err); return null; }
             }
 
+            // Helper to load data for a specific month
+            async function fetchMonthData(year, month) {
+                return await API.request('MonthRankDetailQuery', { year: year, month: month, dispArrStr: 'grad,mapNum' });
+            }
+
             try {
-                const monthRank = await API.request('MonthRankDetailQuery', { year: ym.year, month: ym.month, dispArrStr: 'grad,mapNum' });
+                // 2. Try loading current month first
+                let targetYM = { year: ym.year, month: ym.month };
+                let monthRank = await fetchMonthData(targetYM.year, targetYM.month);
+                
+                // 3. Check if current month has data
+                let monthGrad = monthRank && monthRank.gradRs ? monthRank.gradRs : [];
+                let monthMap = monthRank && monthRank.mapNumRs ? monthRank.mapNumRs : [];
+                let hasData = monthGrad.length > 0 || monthMap.length > 0;
+
+                // 4. If no data, search backwards in the 6-month list
+                if (!hasData) {
+                    // Start from index 1 because index 0 (current month) is already checked
+                    for (let i = 1; i < state.months.length; i++) {
+                        const checkYM = state.months[i];
+                        const checkRank = await fetchMonthData(checkYM.year, checkYM.month);
+                        const checkGrad = checkRank && checkRank.gradRs ? checkRank.gradRs : [];
+                        const checkMap = checkRank && checkRank.mapNumRs ? checkRank.mapNumRs : [];
+                        
+                        if (checkGrad.length > 0 || checkMap.length > 0) {
+                            // Found data! Update target and data
+                            targetYM = checkYM;
+                            monthRank = checkRank;
+                            monthGrad = checkGrad;
+                            monthMap = checkMap;
+                            hasData = true;
+                            break; 
+                        }
+                    }
+                }
+
+                // Update state with the found (or original empty) data
+                state.currentYM = targetYM;
+                state.scoreList = monthGrad;
+                state.venueList = monthMap;
+
                 let base, currentMonth, allMonths, myRank, myScore, userInfo, cardConfig;
                 if (useMock || allowLogin) {
                     [base, currentMonth, allMonths, myRank, myScore, userInfo, cardConfig] = await Promise.all([
@@ -678,49 +792,56 @@
                         safeCall(() => API.getCardConfig(ecId, 'rank'))
                     ]);
                 } else {
-                    base = { ecName: `${ym.month}月挑战赛` };
-                    currentMonth = { monthRs: [{ month: ym.month, realNum: 0, targetNum: 4 }] };
+                    base = { ecName: `${targetYM.month}月挑战赛` };
+                    currentMonth = { monthRs: [{ month: targetYM.month, realNum: 0, targetNum: 4 }] };
                     allMonths = [];
                     myRank = null;
                     myScore = null;
                     userInfo = null;
                     cardConfig = null;
                 }
-                const monthGrad = monthRank && monthRank.gradRs ? monthRank.gradRs : [];
-                const monthMap = monthRank && monthRank.mapNumRs ? monthRank.mapNumRs : [];
-                state.scoreList = monthGrad;
-                state.venueList = monthMap;
+
                 const selfRow = monthGrad.find && monthGrad.find(item => item.isSelf === 1);
-                if (!base && monthGrad.length) base = { ecName: `${ym.month}月挑战赛` };
+                if (!base && monthGrad.length) base = { ecName: `${targetYM.month}月挑战赛` };
                 state.currentMonthData = currentMonth || null;
                 state.allMonthsData = allMonths || null;
-                const prog = findMonthProgress(ym.year, ym.month, state.currentMonthData, state.allMonthsData);
-                document.getElementById('currentMonthText').innerText = `${ym.month}月挑战赛`;
+                
+                // IMPORTANT: Render with the actually selected month (targetYM)
+                const prog = findMonthProgress(targetYM.year, targetYM.month, state.currentMonthData, state.allMonthsData);
+                document.getElementById('currentMonthText').innerText = `${targetYM.month}月挑战赛`;
+                
+                // Re-render dropdown to highlight correct month
+                renderMonths(state.months);
+
                 renderBadge(prog.realNum, prog.targetNum);
                 renderPodium(state.scoreList, 'score');
                 switchTab(state.activeTab, document.querySelector('.tab.active'));
-                let hasRenderedMyInfo = false;
-                if (allowLogin && myRank) {
-                    const rankVal = Number(myRank.rankNum);
-                    if (rankVal > 0) {
-                        renderMyInfo({ rankNum: rankVal }, myScore, userInfo);
-                    } else {
-                        renderMyInfo({ rankNum: null }, null, userInfo);
+                
+                if (!token && !useMock) {
+                    renderGuestState();
+                } else {
+                    let hasRenderedMyInfo = false;
+                    if (allowLogin && myRank) {
+                        const rankVal = Number(myRank.rankNum);
+                        if (rankVal > 0) {
+                            renderMyInfo({ rankNum: rankVal }, myScore, userInfo);
+                        } else {
+                            renderMyInfo({ rankNum: null }, null, userInfo);
+                        }
+                        hasRenderedMyInfo = true;
+                    } else if (selfRow) {
+                        renderMyInfo({ rankNum: selfRow.rankNum }, { score: selfRow.inRankNum || selfRow.score }, { nickName: selfRow.userName });
+                        hasRenderedMyInfo = true;
+                    }
+                    if (!hasRenderedMyInfo) {
+                        renderMyInfo(myRank, myScore, userInfo);
                     }
-                    hasRenderedMyInfo = true;
-                } else if (selfRow) {
-                    renderMyInfo({ rankNum: selfRow.rankNum }, { score: selfRow.inRankNum || selfRow.score }, { nickName: selfRow.userName });
-                    hasRenderedMyInfo = true;
-                }
-                if (!hasRenderedMyInfo) {
-                    renderMyInfo(myRank, myScore, userInfo);
                 }
+
                 renderRules(cardConfig);
-            } catch (err) {
-                console.error('[Init] 加载失败', err);
-                rankListEl.innerHTML = '<div style="padding:20px; color:#d63031;">数据加载失败,请重试</div>';
             } finally {
                 setLoading(false);
+                loadingMask.classList.remove('show'); // Force remove show class
             }
         }