|
|
@@ -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
|
|
|
}
|
|
|
}
|
|
|
|