瀏覽代碼

SDK Updated

zhangyan 3 月之前
父節點
當前提交
6087ceec0b

+ 6 - 0
card/pages.json

@@ -6,6 +6,12 @@
 				"navigationBarTitleText": "彩图奔跑APP卡片页面"
 			}
 		},
+		{
+			"path": "pages/index/detail",
+			"style": {
+				"navigationBarTitleText": "详情页面"
+			}
+		},
 		{
 			"path": "pages/achievement/index",
 			"style": {

+ 153 - 0
card/pages/index/detail.vue

@@ -0,0 +1,153 @@
+<template>
+	<view class="body">
+		<view class="content uni-column">
+			<view class="top uni-column">
+				<view class="topbar uni-row">
+					<image mode="aspectFit" class="topbar-back" @click="btnBack" src="/static/default/back.png"></image>
+					<text class="mcName">详情页面</text>
+					<view class="topbar-right"></view>
+				</view>
+				<view class="top-main uni-column">
+					<text class="top-title">这里是详情页面的标题</text>
+					<text class="top-desc">这里是详情页面的描述内容,展示详细信息。</text>
+				</view>
+			</view>
+			<view class="main uni-column">
+				<view class="info-box">
+					<text class="info-text">欢迎来到详情页!</text>
+					<text class="info-subtext">这是一个全屏展示的页面。</text>
+				</view>
+				<button class="btnStart" @click="btnAction">执行操作</button>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import tools from '../../common/tools';
+	import Bridge from '../../sdk/bridge'; // 使用 SDK Bridge
+	import {
+		ossUrl
+	} from '../../common/api';
+
+	export default {
+		data() {
+			return {
+				queryObj: {},
+				queryString: "",
+			}
+		},
+		onLoad(query) {
+			this.queryObj = query;
+			this.queryString = tools.objectToQueryString(this.queryObj);
+			console.log("Detail Page Loaded", query);
+		},
+		methods: {
+			btnBack() {
+				// 返回首页或关闭当前 WebView
+				// Bridge.back(); // 或者使用兼容的 tools.appAction
+				const url = `action://to_home/`;
+				Bridge.appAction(url);
+			},
+			btnAction() {
+				uni.showToast({
+					title: '点击了按钮',
+					icon: 'none'
+				});
+			}
+		}
+	}
+</script>
+
+<style scoped>
+	.content {
+		width: 100vw;
+		height: 100vh;
+		background-color: #f5f5f5;
+	}
+
+	.top {
+		width: 100%;
+		height: 400rpx;
+		padding-top: 40px; /* 适配状态栏 */
+		background: linear-gradient(180deg, #178bff 0%, #004d9b 100%);
+		color: white;
+	}
+
+	.topbar {
+		width: 92%;
+		padding: 0 4%;
+		height: 88rpx;
+		align-items: center;
+		justify-content: space-between;
+	}
+
+	.topbar-back {
+		width: 40rpx;
+		height: 40rpx;
+	}
+
+	.mcName {
+		font-size: 36rpx;
+		font-weight: bold;
+	}
+	
+	.topbar-right {
+		width: 40rpx; 
+	}
+
+	.top-main {
+		flex: 1;
+		justify-content: center;
+		align-items: center;
+	}
+
+	.top-title {
+		font-size: 48rpx;
+		font-weight: bold;
+		margin-bottom: 20rpx;
+	}
+
+	.top-desc {
+		font-size: 28rpx;
+		opacity: 0.8;
+	}
+
+	.main {
+		flex: 1;
+		width: 92%;
+		margin: 0 auto;
+		margin-top: -40rpx; /* 上移覆盖 */
+		background-color: white;
+		border-radius: 20rpx 20rpx 0 0;
+		padding: 40rpx;
+		box-sizing: border-box;
+		justify-content: space-between;
+	}
+
+	.info-box {
+		align-items: center;
+	}
+
+	.info-text {
+		font-size: 32rpx;
+		color: #333;
+		margin-bottom: 20rpx;
+	}
+
+	.info-subtext {
+		font-size: 26rpx;
+		color: #666;
+	}
+
+	.btnStart {
+		width: 100%;
+		height: 90rpx;
+		line-height: 90rpx;
+		background-color: #178bff;
+		color: white;
+		border-radius: 45rpx;
+		font-size: 32rpx;
+		margin-bottom: 40rpx;
+	}
+</style>

+ 92 - 2
card/pages/index/index.vue

@@ -1,9 +1,22 @@
 <template>
-	<view></view>
+	<!-- <view></view> -->
+	<view class="body body-radius">
+		<view class="content uni-column" :class="contentBg" @click="btnClick">
+			<view class="main uni-column">
+				<image mode="aspectFit" class="logo" :src="logoSrc"></image>
+				<view class="uni-row" style="position: relative;">
+					<text class="type mod-text">{{type}}</text>
+				</view>
+				<view class="name mod-text">{{name}}</view>
+				<button class="button mod-button">{{btnText}}</button>
+			</view>
+		</view>
+	</view>
 </template>
 
 <script>
 	import tools from '../../common/tools';
+	import Bridge from '../../sdk/bridge';
 	import cardfunc from '../../common/cardfunc';
 	import {
 		token,
@@ -17,6 +30,13 @@
 				queryString: "",
 				token: "",
 				ecId: 0, // 卡片id
+				
+				// Card UI Data
+				contentBg: "content-bg-blue",
+				logoSrc: "/static/logo.png",
+				type: "示例卡片",
+				name: "点击查看详情",
+				btnText: "立即查看",
 			}
 		},
 		onLoad(query) { // 类型非必填,可自动推导
@@ -28,10 +48,14 @@
 			this.token = query["token"] ?? token;
 			this.ecId = query["id"] ?? 0;
 
+			// --- Original Logic Commented Out for Demo ---
+			/*
 			cardfunc.init(this, this.token, this.ecId);
 			cardfunc.userConfigQuery(this.userConfigQueryCallback);
+			*/
 		},
 		methods: {
+			/*
 			userConfigQueryCallback(userconfig) {
 				// console.log("[userConfigQueryCallback] userconfig:", userconfig);
 				userconfig = cardfunc.parseCardConfig(userconfig);
@@ -61,6 +85,72 @@
 					});
 				}
 			},
+			*/
+			btnClick() {
+				// 构造目标 URL,通常是部署后的完整路径
+				// 注意:这里为了演示使用了 ossUrl,实际开发中请根据环境调整
+				// full=true 是给 Flutter 端拦截的信号(约定俗成)
+				const url = `${ossUrl}#/pages/index/detail?${this.queryString}&full=true`;
+				
+				console.log("Navigating to:", url);
+				
+				// 使用 Bridge 触发跳转
+				Bridge.appAction(url);
+			}
 		}
 	}
-</script>
+</script>
+
+<style scoped>
+	.content {
+		width: 100vw;
+		height: 100vh;
+		justify-content: center;
+		align-items: center;
+	}
+	
+	.content-bg-blue {
+		background: linear-gradient(180deg,#178bff 0%,#004d9b 100%);
+	}
+
+	.main {
+		width: 100%;
+		height: 600rpx;
+		justify-content: space-evenly;
+		align-items: center;
+	}
+
+	.logo {
+		width: 200rpx;
+		height: 200rpx;
+		background-color: white;
+		border-radius: 20rpx;
+	}
+	
+	.type {
+		opacity: 0.8;
+		font-family: Roboto;
+		color: #ffffff;
+		font-size: 36rpx;
+		text-align: center;
+	}
+	
+	.name {
+		font-family: Roboto;
+		color: #ffffff;
+		font-size: 48rpx;
+		text-align: center;
+		font-weight: bold;
+	}
+	
+	.button {
+		width: 320rpx;
+		height: 86rpx;
+		color: #0458ad;
+		background: #ffffff;
+		border-radius: 56rpx;
+		font-size: 40rpx;
+		line-height: 86rpx;
+		text-align: center;
+	}
+</style>

+ 5 - 1
card/sdk/api.js

@@ -205,7 +205,11 @@
                     throw new Error('Unauthorized');
                 }
                 var msg = res.message || '请求失败';
-                alert(msg);
+                if (window.Bridge && window.Bridge.showToast) {
+                    window.Bridge.showToast(msg, 'none');
+                } else {
+                    alert(msg);
+                }
                 throw new Error(msg);
             })
             .catch(function(err) { console.error('[API] Error:', err); throw err; });

二進制
card/sdk/bg.jpg


+ 137 - 0
card/sdk/bridge.js

@@ -103,6 +103,23 @@
            }
            return;
 
+        case 'makePhoneCall':
+          url = 'tel:' + data.phoneNumber;
+          break;
+          
+        case 'showToast':
+          // 降级为 alert,体验稍差但保证可见
+          // setTimeout 避免阻塞当前执行流
+          setTimeout(function() { alert(data.title); }, 10);
+          return;
+          
+        case 'showModal':
+           setTimeout(function() { 
+               var result = confirm(data.content || data.title); 
+               // 无法同步返回结果给 App 逻辑,仅做展示
+           }, 10);
+           return;
+
         default:
           console.warn('[Bridge] No legacy fallback for action:', action);
       }
@@ -114,6 +131,58 @@
       }
     },
 
+    // ==============================
+    // Ported from common/tools.js
+    // ==============================
+
+    /**
+     * 对url追加项目版本号
+     */
+    urlAddVer: function(url) {
+      var newUrl = url;
+      try {
+        if (window.uni && window.uni.getSystemInfoSync) {
+          var systemInfo = window.uni.getSystemInfoSync();
+          var version_number = systemInfo.appVersion;
+
+          if (version_number) {
+            if (newUrl.indexOf('_v=') !== -1) {
+              return newUrl;
+            }
+            if (newUrl.indexOf('?') !== -1) {
+              newUrl += "&_v=" + version_number;
+            } else {
+              newUrl += "?_v=" + version_number;
+            }
+          }
+        }
+      } catch (e) {
+        console.warn('[Bridge] urlAddVer error:', e);
+      }
+      console.log("[Bridge] urlAddVer newUrl:", newUrl);
+      return newUrl;
+    },
+
+    /**
+     * 导航到APP内的某个页面或执行APP内部的某些功能
+     */
+    appAction: function(url, actType) {
+      actType = actType || "";
+      console.log("[Bridge] appAction:", url, "actType:", actType);
+
+      if (url.indexOf('http') !== -1) {
+        window.location.href = this.urlAddVer(url);
+      } else if (url == "reload") {
+        window.location.reload();
+      } else if (actType == "uni.navigateTo" && window.uni && window.uni.navigateTo) {
+        window.uni.navigateTo({
+          url: this.urlAddVer(url)
+        });
+      } else {
+        window.location.href = url;
+      }
+    },
+
     // ==============================
     // 公开 API
     // ==============================
@@ -168,6 +237,74 @@
        this._post('saveImage', { base64: base64Str });
     },
     
+    // --- 新增完善功能 ---
+    
+    /**
+     * 预览图片
+     * @param {Array} urls 图片地址数组
+     * @param {String} current 当前显示图片的地址
+     */
+    previewImage: function(urls, current) {
+        this._post('previewImage', { urls: urls, current: current });
+    },
+
+    /**
+     * 拨打电话
+     * @param {String} phoneNumber 电话号码
+     */
+    makePhoneCall: function(phoneNumber) {
+        this._post('makePhoneCall', { phoneNumber: phoneNumber });
+    },
+
+    /**
+     * 设置剪贴板内容
+     * @param {String} data 文本内容
+     */
+    setClipboardData: function(data) {
+        this._post('setClipboardData', { data: data });
+    },
+
+    /**
+     * 显示 Toast 提示
+     * @param {String} title 提示内容
+     * @param {String} icon 图标 (success/loading/none)
+     * @param {Number} duration 持续时间(ms)
+     */
+    showToast: function(title, icon, duration) {
+        this._post('showToast', { 
+            title: title, 
+            icon: icon || 'none', 
+            duration: duration || 1500 
+        });
+    },
+
+    /**
+     * 显示 Loading 提示框
+     * @param {String} title 提示内容
+     */
+    showLoading: function(title) {
+        this._post('showLoading', { title: title || '加载中' });
+    },
+
+    /**
+     * 隐藏 Loading 提示框
+     */
+    hideLoading: function() {
+        this._post('hideLoading');
+    },
+    
+    /**
+     * 显示模态确认框
+     * @param {Object} options { title, content, showCancel, confirmText, cancelText }
+     * @param {Function} successCallback 点击确定/取消的回调 (仅在支持的双向通信环境有效)
+     */
+    showModal: function(options, successCallback) {
+        // TODO: 这里的 callback 在单向 Bridge 中难以实现,通常需要 App 回调 JS 方法
+        this._post('showModal', options);
+    },
+    
+    // ------------------
+
     getToken: function () {
         this._post('getToken');
     },

+ 5 - 1
card/sdk/demo_project/api.js

@@ -205,7 +205,11 @@
                     throw new Error('Unauthorized');
                 }
                 var msg = res.message || '请求失败';
-                alert(msg);
+                if (window.Bridge && window.Bridge.showToast) {
+                    window.Bridge.showToast(msg, 'none');
+                } else {
+                    alert(msg);
+                }
                 throw new Error(msg);
             })
             .catch(function(err) { console.error('[API] Error:', err); throw err; });

二進制
card/sdk/demo_project/bg.jpg


+ 137 - 0
card/sdk/demo_project/bridge.js

@@ -103,6 +103,23 @@
            }
            return;
 
+        case 'makePhoneCall':
+          url = 'tel:' + data.phoneNumber;
+          break;
+          
+        case 'showToast':
+          // 降级为 alert,体验稍差但保证可见
+          // setTimeout 避免阻塞当前执行流
+          setTimeout(function() { alert(data.title); }, 10);
+          return;
+          
+        case 'showModal':
+           setTimeout(function() { 
+               var result = confirm(data.content || data.title); 
+               // 无法同步返回结果给 App 逻辑,仅做展示
+           }, 10);
+           return;
+
         default:
           console.warn('[Bridge] No legacy fallback for action:', action);
       }
@@ -114,6 +131,58 @@
       }
     },
 
+    // ==============================
+    // Ported from common/tools.js
+    // ==============================
+
+    /**
+     * 对url追加项目版本号
+     */
+    urlAddVer: function(url) {
+      var newUrl = url;
+      try {
+        if (window.uni && window.uni.getSystemInfoSync) {
+          var systemInfo = window.uni.getSystemInfoSync();
+          var version_number = systemInfo.appVersion;
+
+          if (version_number) {
+            if (newUrl.indexOf('_v=') !== -1) {
+              return newUrl;
+            }
+            if (newUrl.indexOf('?') !== -1) {
+              newUrl += "&_v=" + version_number;
+            } else {
+              newUrl += "?_v=" + version_number;
+            }
+          }
+        }
+      } catch (e) {
+        console.warn('[Bridge] urlAddVer error:', e);
+      }
+      console.log("[Bridge] urlAddVer newUrl:", newUrl);
+      return newUrl;
+    },
+
+    /**
+     * 导航到APP内的某个页面或执行APP内部的某些功能
+     */
+    appAction: function(url, actType) {
+      actType = actType || "";
+      console.log("[Bridge] appAction:", url, "actType:", actType);
+
+      if (url.indexOf('http') !== -1) {
+        window.location.href = this.urlAddVer(url);
+      } else if (url == "reload") {
+        window.location.reload();
+      } else if (actType == "uni.navigateTo" && window.uni && window.uni.navigateTo) {
+        window.uni.navigateTo({
+          url: this.urlAddVer(url)
+        });
+      } else {
+        window.location.href = url;
+      }
+    },
+
     // ==============================
     // 公开 API
     // ==============================
@@ -168,6 +237,74 @@
        this._post('saveImage', { base64: base64Str });
     },
     
+    // --- 新增完善功能 ---
+    
+    /**
+     * 预览图片
+     * @param {Array} urls 图片地址数组
+     * @param {String} current 当前显示图片的地址
+     */
+    previewImage: function(urls, current) {
+        this._post('previewImage', { urls: urls, current: current });
+    },
+
+    /**
+     * 拨打电话
+     * @param {String} phoneNumber 电话号码
+     */
+    makePhoneCall: function(phoneNumber) {
+        this._post('makePhoneCall', { phoneNumber: phoneNumber });
+    },
+
+    /**
+     * 设置剪贴板内容
+     * @param {String} data 文本内容
+     */
+    setClipboardData: function(data) {
+        this._post('setClipboardData', { data: data });
+    },
+
+    /**
+     * 显示 Toast 提示
+     * @param {String} title 提示内容
+     * @param {String} icon 图标 (success/loading/none)
+     * @param {Number} duration 持续时间(ms)
+     */
+    showToast: function(title, icon, duration) {
+        this._post('showToast', { 
+            title: title, 
+            icon: icon || 'none', 
+            duration: duration || 1500 
+        });
+    },
+
+    /**
+     * 显示 Loading 提示框
+     * @param {String} title 提示内容
+     */
+    showLoading: function(title) {
+        this._post('showLoading', { title: title || '加载中' });
+    },
+
+    /**
+     * 隐藏 Loading 提示框
+     */
+    hideLoading: function() {
+        this._post('hideLoading');
+    },
+    
+    /**
+     * 显示模态确认框
+     * @param {Object} options { title, content, showCancel, confirmText, cancelText }
+     * @param {Function} successCallback 点击确定/取消的回调 (仅在支持的双向通信环境有效)
+     */
+    showModal: function(options, successCallback) {
+        // TODO: 这里的 callback 在单向 Bridge 中难以实现,通常需要 App 回调 JS 方法
+        this._post('showModal', options);
+    },
+    
+    // ------------------
+
     getToken: function () {
         this._post('getToken');
     },

+ 722 - 0
card/sdk/demo_project/detail.html

@@ -0,0 +1,722 @@
+<!DOCTYPE html>
+<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>
+    <!-- 本地调试保留 mock_flutter.js,正式接入时移除 -->
+    <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">
+    <style>
+        :root {
+            --primary-purple: #593259; 
+            --primary-orange: #fdcb6e;
+            --primary-red: #d63031;
+            --text-dark: #2d3436;
+            --footer-bg: #483055;
+        }
+
+        * {
+            box-sizing: border-box;
+            margin: 0;
+            padding: 0;
+            font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
+            -webkit-tap-highlight-color: transparent;
+            user-select: none;
+        }
+
+        body {
+            background: #f5f6fa;
+            width: 100%;
+            height: 100vh;
+            overflow: hidden;
+            display: flex;
+            flex-direction: column;
+        }
+
+        /* 顶部 Header */
+        .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;
+            padding: 20px;
+            padding-top: max(20px, 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; }
+        .icon-btn {
+            width: 36px; height: 36px; 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);
+        }
+        .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); }
+
+        /* 仪表盘卡片 */
+        .dashboard-card {
+            margin-top: 25px;
+            background: rgba(0, 0, 0, 0.4); 
+            backdrop-filter: blur(10px);
+            border-radius: 20px;
+            padding: 15px 20px;
+            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-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.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; }
+        
+        .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-active { 
+            position: absolute; top: 50%; left: 10px; 
+            width: 25%; 
+            height: 4px; 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; 
+            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; 
+            transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); 
+        }
+        
+        .trophy-item.active { 
+            background: #fff; border-color: var(--primary-orange); color: var(--primary-orange); 
+            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.active { 
+            background: linear-gradient(135deg, #f1c40f, #e67e22); 
+            color: white; border: none; 
+            transform: scale(1.3); 
+            box-shadow: 0 0 20px rgba(241, 196, 15, 0.6);
+            animation: pulseTrophy 2s infinite;
+        }
+
+        @keyframes pulseTrophy {
+            0% { box-shadow: 0 0 0 0 rgba(241, 196, 15, 0.7); }
+            70% { box-shadow: 0 0 0 10px rgba(241, 196, 15, 0); }
+            100% { box-shadow: 0 0 0 0 rgba(241, 196, 15, 0); }
+        }
+
+        /* 领奖台 */
+        .podium-wrap {
+            height: 140px;
+            display: flex; justify-content: center; align-items: flex-end; 
+            margin-top: -60px; 
+            position: relative; 
+            z-index: 10; 
+            padding-bottom: 0px; 
+        }
+        .p-col { display: flex; flex-direction: column; align-items: center; width: 30%; position: relative;}
+        .p-2 { z-index: 2; margin-right: -15px; }
+        .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 img { width: 100%; height: 100%; object-fit: cover; }
+
+        .crown {
+            position: absolute; top: -38px; 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; 
+        }
+        @keyframes crownFloat {
+            0%, 100% { transform: translateY(0) rotate(-5deg); }
+            50% { transform: translateY(-8px) 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; }
+
+        /* 列表容器 */
+        .list-container {
+            flex: 1; background: white; border-radius: 24px 24px 0 0;
+            padding: 0 20px 120px 20px; 
+            overflow-y: auto; 
+            margin-top: -10px;
+            box-shadow: 0 -5px 20px rgba(0,0,0,0.05);
+            position: relative; z-index: 8; 
+            -webkit-overflow-scrolling: touch; 
+        }
+        
+        .tabs { 
+            display: flex; justify-content: center; gap: 15px; 
+            position: sticky; top: 0; background: white; z-index: 9;
+            padding-top: 20px; padding-bottom: 10px;
+        }
+        
+        .tab { padding: 8px 20px; border-radius: 20px; font-size: 14px; color: #636e72; background: #f1f2f6; cursor: pointer; transition: 0.2s; }
+        .tab.active { background: var(--text-dark); color: #fdcb6e; font-weight: bold; }
+        
+        .list-item { display: flex; align-items: center; padding: 12px 0; border-bottom: 1px solid #f1f2f6; }
+        .rank { width: 30px; text-align: center; font-weight: bold; color: #b2bec3; font-style: italic;}
+        .avatar { width: 40px; height: 40px; border-radius: 50%; margin: 0 12px; background: #eee; overflow: hidden;}
+        .avatar img { width: 100%; height: 100%; object-fit: cover;}
+        .info { flex: 1; }
+        .name { font-size: 14px; color: #2d3436; font-weight: bold; }
+        .team { font-size: 11px; color: #636e72; display: flex; align-items: center; gap: 4px; }
+        .score { font-size: 16px; font-weight: bold; color: var(--primary-purple); }
+
+        /* 底部我的排名 */
+        .my-rank-bar {
+            position: fixed; bottom: 0; left: 0; width: 100%; height: 55px; 
+            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; 
+            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; }
+        .my-rank-bar .avatar { width: 34px; height: 34px; border-width: 2px; }
+        .my-rank-bar .name { font-size: 14px; }
+        .my-rank-bar .team { font-size: 10px; }
+        .my-score { font-size: 18px; font-weight: bold; color: #ffffff !important; margin-left: auto; }
+
+        /* 演示按钮样式 (模态框内) */
+        .demo-section {
+            margin: 20px 0 0 0;
+            border-top: 1px dashed #ddd;
+            padding-top: 15px;
+        }
+        .demo-label { font-size: 12px; color: #999; margin-bottom: 10px; }
+        .demo-controls {
+            display: flex; justify-content: center; gap: 10px;
+        }
+        .demo-btn {
+            background: #eee; border: none; padding: 6px 12px; border-radius: 8px; font-size: 12px; color: #555; cursor: pointer;
+        }
+        .demo-btn:active { background: #ddd; color: #000; }
+
+        /* 下拉菜单 & 模态框 */
+        .dropdown { position: absolute; top: 70px; left: 50%; transform: translateX(-50%); width: 200px; background: white; border-radius: 12px; box-shadow: 0 10px 50px rgba(0,0,0,0.4); display: none; flex-direction: column; overflow: hidden; z-index: 200; }
+        .dropdown.show { display: flex; animation: dropIn 0.2s ease-out; }
+        .dd-item { padding: 12px; color: #636e72; font-size: 14px; text-align: center; border-bottom: 1px solid #f1f2f6; cursor: pointer; }
+        .dd-active { color: var(--primary-purple); font-weight: bold; background: #f9f0ff; }
+        @keyframes dropIn { from{opacity:0; transform:translateX(-50%) translateY(-10px);} to{opacity:1; transform:translateX(-50%) translateY(0);} }
+
+        .modal-mask { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 999; display: flex; justify-content: center; align-items: center; opacity: 0; pointer-events: none; transition: opacity 0.3s; }
+        .modal-mask.show { opacity: 1; pointer-events: auto; }
+        .modal-body { width: 80%; max-width: 320px; background: white; border-radius: 24px; padding: 25px; text-align: center; transform: scale(0.8); transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); border: 4px solid var(--primary-purple); box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
+        .modal-mask.show .modal-body { transform: scale(1); }
+        .m-close { background: var(--primary-purple); color: white; padding: 10px 25px; border-radius: 20px; border: none; font-weight: bold; cursor: pointer; margin-top: 15px; }
+        .rule-box { text-align: left; background: #f9f4ff; border: 1px solid #e3d7ff; border-radius: 12px; padding: 12px; color: #4b3a67; font-size: 13px; line-height: 1.6; box-shadow: inset 0 1px 0 rgba(255,255,255,0.7); }
+        .rule-item { display: flex; gap: 10px; align-items: flex-start; margin-bottom: 8px; }
+        .rule-item:last-child { margin-bottom: 0; }
+        .rule-item i { color: var(--primary-orange); margin-top: 2px; }
+
+        /* 加载遮罩 */
+        .loading-mask {
+            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-spinner {
+            border: 4px solid rgba(255,255,255,0.3);
+            border-top-color: #fdcb6e;
+            border-radius: 50%;
+            width: 36px; height: 36px;
+            animation: spin 1s linear infinite;
+            margin-right: 10px;
+        }
+        @keyframes spin { to { transform: rotate(360deg); } }
+    </style>
+</head>
+<body>
+
+    <!-- 头部 -->
+    <div class="header-area">
+        <div class="nav-bar">
+            <div class="icon-btn" onclick="handleBack()"><i class="fa-solid fa-chevron-left" style="color:white"></i></div>
+            <div class="month-select" onclick="toggleDropdown()">
+                <span id="currentMonthText">11月挑战赛</span> <i class="fa-solid fa-caret-down"></i>
+            </div>
+            <div class="icon-btn" onclick="openModal()"><i class="fa-solid fa-question" style="color:white"></i></div>
+        </div>
+
+        <!-- 仪表盘卡片 -->
+        <div class="dashboard-card">
+            <div class="dash-header">
+                <div class="dash-title">
+                    <i class="fa-solid fa-medal dash-icon"></i> 定向达人
+                </div>
+                <div class="dash-badge" id="dashBadge">挑战 1 / 4</div>
+            </div>
+
+            <div class="trophy-track-container">
+                <div class="track-line-bg"></div>
+                <div class="track-line-active" id="progressLine"></div>
+                
+                <!-- 图标1 -->
+                <div class="trophy-item active" id="t1"><i class="fa-solid fa-check"></i></div>
+                <!-- 图标2 -->
+                <div class="trophy-item" id="t2"><i class="fa-solid fa-lock"></i></div>
+                <!-- 图标3 -->
+                <div class="trophy-item" id="t3"><i class="fa-solid fa-lock"></i></div>
+                <!-- 图标4 (Final) -->
+                <div class="trophy-item final" id="t4"><i class="fa-solid fa-trophy"></i></div>
+            </div>
+        </div>
+    </div>
+
+    <!-- 下拉菜单 -->
+    <div class="dropdown" id="dropdown"></div>
+
+    <!-- 领奖台 -->
+    <div class="podium-wrap" id="podiumWrap"></div>
+
+    <!-- 列表区 -->
+    <div class="list-container">
+        <div class="tabs">
+            <div class="tab active" onclick="switchTab('score', this)">积分排行</div>
+            <div class="tab" onclick="switchTab('venue', this)">场地排行</div>
+        </div>
+        
+        <div id="rankList"></div>
+    </div>
+
+    <!-- 底部我的排名 -->
+    <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="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>
+
+    <!-- 模态框 -->
+    <div class="modal-mask" id="infoModal">
+        <div class="modal-body">
+                        <h3 style="color:#593259; margin-bottom:15px;">📜 规则说明</h3>
+            <div id="ruleContent" class="rule-box">
+                <div class="rule-item"><i class="fa-solid fa-bullseye"></i><div><strong>积分规则:</strong> 在指定公园找到打卡点,耗时越短积分越高。</div></div>
+                <div class="rule-item"><i class="fa-solid fa-map-location-dot"></i><div><strong>场地排行:</strong> 按解锁的公园场地数量排名。</div></div>
+                <div class="rule-item"><i class="fa-solid fa-trophy"></i><div><strong>奖杯获取:</strong> 完成4次有效挑战点亮全部奖杯。</div></div>
+            </div>
+
+            <!-- 演示功能区 -->
+            <div class="demo-section">
+                <div class="demo-label">✨ 功能演示</div>
+                <div class="demo-controls">
+                    <button class="demo-btn" onclick="demoProgress(1);closeModal()">1/4</button>
+                    <button class="demo-btn" onclick="demoProgress(3);closeModal()">3/4</button>
+                    <button class="demo-btn" onclick="demoProgress(4);closeModal()">4/4</button>
+                </div>
+            </div>
+
+            <button class="m-close" onclick="closeModal()">知道了</button>
+        </div>
+    </div>
+
+    <!-- 加载中遮罩 -->
+    <div class="loading-mask" id="loadingMask">
+        <div class="loading-spinner"></div>
+        <div>数据加载中...</div>
+    </div>
+
+    <script>
+        const dropdown = document.getElementById('dropdown');
+        const rankListEl = document.getElementById('rankList');
+        const trackLine = document.getElementById('progressLine');
+        const podiumWrap = document.getElementById('podiumWrap');
+        const ruleContent = document.getElementById('ruleContent');
+        const loadingMask = document.getElementById('loadingMask');
+        const state = {
+            activeTab: 'score',
+            scoreList: [],
+            scoreListRendered: null,
+            venueList: [],
+            venueListRendered: null,
+            months: [],
+            currentYM: null,
+            currentMonthData: null,
+            allMonthsData: null
+        };
+
+        function getQuery(name) {
+            const params = new URLSearchParams(window.location.search);
+            return params.get(name);
+        }
+
+        function getYearMonth() {
+            const now = new Date();
+            const year = parseInt(getQuery('year'), 10) || now.getFullYear();
+            const month = parseInt(getQuery('month'), 10) || (now.getMonth() + 1);
+            return { year: year, month: month };
+        }
+        
+        function getRecentMonths(count) {
+            const ym = getYearMonth();
+            const list = [];
+            let y = ym.year;
+            let m = ym.month;
+            for (let i = 0; i < (count || 3); i++) {
+                list.push({ year: y, month: m });
+                m -= 1;
+                if (m === 0) { m = 12; y -= 1; }
+            }
+            return list;
+        }
+
+        function handleBack() {
+            if (window.Bridge && Bridge.back) Bridge.toHome();
+            else window.history.back(); // Fallback for web environment
+        }
+
+        function openModal() { document.getElementById('infoModal').classList.add('show'); }
+        function closeModal() { document.getElementById('infoModal').classList.remove('show'); }
+        function toggleDropdown() { dropdown.style.display = (dropdown.style.display === 'flex') ? 'none' : 'flex'; }
+
+        function setLoading(isLoading) {
+            rankListEl.style.opacity = isLoading ? '0.4' : '1';
+            loadingMask.classList.toggle('show', isLoading);
+        }
+
+        function buildAvatar(name, salt) {
+            const seedBase = name || 'user';
+            const seed = encodeURIComponent(seedBase + (salt || ''));
+            return `<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=${seed}" alt="">`;
+        }
+
+        function setProgress(real, target) {
+            const badge = document.getElementById('dashBadge');
+            const t1 = document.getElementById('t1');
+            const t2 = document.getElementById('t2');
+            const t3 = document.getElementById('t3');
+            const t4 = document.getElementById('t4');
+            const safeReal = Math.max(real || 0, 0);
+            const safeTarget = target && target > 0 ? target : 4;
+            const ratio = Math.min(safeReal / safeTarget, 1);
+            const percent = Math.max(0, Math.min(1, ratio)) * 100;
+            trackLine.style.width = percent + '%';
+            const textReal = safeReal >= safeTarget ? safeTarget : safeReal;
+            badge.innerText = safeReal >= safeTarget ? '挑战成功' : `挑战 ${textReal} / ${safeTarget}`;
+            badge.classList.toggle('completed', safeReal >= safeTarget);
+            function setIconActive(el, active) {
+                if (active) {
+                    el.classList.add('active');
+                    if (!el.classList.contains('final')) el.innerHTML = '<i class="fa-solid fa-check"></i>';
+                } else {
+                    el.classList.remove('active');
+                    if (!el.classList.contains('final')) el.innerHTML = '<i class="fa-solid fa-lock"></i>';
+                }
+            }
+            setIconActive(t1, safeReal >= 1);
+            setIconActive(t2, safeReal >= 2);
+            setIconActive(t3, safeReal >= 3);
+            setIconActive(t4, safeReal >= safeTarget);
+        }
+
+        function renderBadge(real, target) {
+            setProgress(real, target);
+        }
+
+        function findMonthProgress(year, month, currentMonthData, allMonthsData) {
+            const result = { realNum: 0, targetNum: 4 };
+            const pick = (arr) => {
+                if (!arr || !arr.length) return null;
+                for (let i = 0; i < arr.length; i++) {
+                    if (Number(arr[i].month) === Number(month)) return arr[i];
+                }
+                return null;
+            };
+            const fromCurrent = currentMonthData && pick(currentMonthData.monthRs || []);
+            if (fromCurrent) return { realNum: fromCurrent.realNum || 0, targetNum: fromCurrent.targetNum || 4 };
+            if (allMonthsData && allMonthsData.length) {
+                for (let i = 0; i < allMonthsData.length; i++) {
+                    const item = allMonthsData[i];
+                    if (item.year && Number(item.year) !== Number(year)) continue;
+                    const found = pick(item.monthRs || []);
+                    if (found) return { realNum: found.realNum || 0, targetNum: found.targetNum || 4 };
+                }
+            }
+            return result;
+        }
+
+        function renderMonths(list) {
+            dropdown.innerHTML = '';
+            if (!list || list.length === 0) {
+                dropdown.innerHTML = '<div class="dd-item">暂无月份数据</div>';
+                return;
+            }
+            list.forEach((item, idx) => {
+                const title = `${item.month}月挑战赛`;
+                const div = document.createElement('div');
+                const isCurrent = state.currentYM && state.currentYM.year === item.year && state.currentYM.month === item.month;
+                div.className = 'dd-item' + (isCurrent ? ' dd-active' : '');
+                div.innerText = (idx === 0 && !isCurrent) ? `${title} (本月)` : title;
+                div.onclick = () => {
+                    selectMonth(item.year, item.month, title);
+                };
+                dropdown.appendChild(div);
+            });
+        }
+
+        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, 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
+
+            // Define the visual order for rendering (2nd, 1st, 3rd)
+            const podiumItems = [
+                { person: p2, className: 'p-2' }, // Left column - Second place
+                { person: p1, className: 'p-1' }, // Middle column - First place
+                { person: p3, className: 'p-3' }  // Right column - Third place
+            ];
+
+            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
+
+                // Only the actual first place gets the crown
+                const isActualFirst = (person === p1); 
+
+                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-box">
+                        <div class="p-name">${name}</div>
+                        <div class="p-score">${score}</div>
+                    </div>
+                `;
+                podiumWrap.appendChild(col);
+            });
+            const remaining = list.slice(3);
+            if (type === 'venue') {
+                state.venueListRendered = remaining;
+            } else {
+                state.scoreListRendered = remaining;
+            }
+            if (state.activeTab === type) renderRankList(remaining);
+        }
+
+        function renderRankList(list) {
+            rankListEl.innerHTML = '';
+            if (!list || list.length === 0) {
+                rankListEl.innerHTML = '<div style="padding:20px; color:#b2bec3; text-align:center;">暂无数据</div>';
+                return;
+            }
+            list.forEach((item, idx) => {
+                const rankNum = item.rankNum || idx + 1;
+                const name = item.nickName || item.name || item.userName || item.teamName || '选手';
+                const scoreVal = (item.score != null ? item.score : item.inRankNum);
+                const score = scoreVal != null ? scoreVal : '--';
+                const team = item.teamName || item.coiName || '';
+                const teamIcon = team ? '<i class="fa-solid fa-user-group"></i> ' : '<i class="fa-solid fa-user"></i> ';
+                const row = document.createElement('div');
+                row.className = 'list-item';
+                row.innerHTML = `
+                    <div class="rank">${rankNum}</div>
+                    <div class="avatar">${buildAvatar(name, rankNum)}</div>
+                    <div class="info">
+                        <div class="name">${name}</div>
+                        <div class="team">${teamIcon}${team || '个人'}</div>
+                    </div>
+                    <div class="score">${score}</div>
+                `;
+                rankListEl.appendChild(row);
+            });
+        }
+
+        function renderMyInfo(myRank, myScore, userInfo) {
+            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 rankVal = myRank && Number(myRank.rankNum);
+            const hasRank = rankVal > 0;
+            rankNumEl.innerText = hasRank ? rankVal : '--';
+            myScoreEl.innerText = hasRank && myScore && myScore.score != null ? myScore.score : '--';
+            const name = (userInfo && (userInfo.nickName || userInfo.userName)) || '我';
+            myNameEl.innerText = name;
+            myTeamEl.innerText = hasRank ? '继续加油' : '';
+            myAvatarEl.innerHTML = buildAvatar(name, 'me');
+        }
+
+        function renderRules(config) {
+            if (!config || !config.configJson) return;
+            try {
+                const parsed = JSON.parse(config.configJson);
+                const rules = parsed.popupRuleList || [];
+                if (rules.length > 0 && rules[0].data && rules[0].data.content) {
+                    ruleContent.innerHTML = rules[0].data.content;
+                }
+            } catch (err) {
+                console.warn('[Rule] parse error', err);
+            }
+        }
+
+        function switchTab(type, tabElement) {
+            state.activeTab = type;
+            document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
+            if (tabElement) tabElement.classList.add('active');
+            const baseList = type === 'venue' ? state.venueList : state.scoreList;
+            renderPodium(baseList, type);
+            const rendered = type === 'venue' ? (state.venueListRendered || baseList.slice(3)) : (state.scoreListRendered || baseList.slice(3));
+            renderRankList(rendered);
+        }
+
+        function demoProgress(step) {
+            setProgress(step, 4);
+        }
+
+        async function loadMonthData(year, month) {
+            state.currentYM = { year: year, month: month };
+            document.getElementById('currentMonthText').innerText = `${month}月挑战赛`;
+            renderMonths(state.months);
+            setLoading(true);
+            try {
+                const monthRank = await API.request('MonthRankDetailQuery', { year: year, month: month, dispArrStr: 'grad,mapNum' });
+                const monthGrad = monthRank && monthRank.gradRs ? monthRank.gradRs : [];
+                const monthMap = monthRank && monthRank.mapNumRs ? monthRank.mapNumRs : [];
+                state.scoreList = monthGrad;
+                state.venueList = monthMap;
+                const prog = findMonthProgress(year, month, state.currentMonthData, state.allMonthsData);
+                renderBadge(prog.realNum, prog.targetNum);
+                renderPodium(state.activeTab === 'venue' ? state.venueList : state.scoreList, state.activeTab);
+                switchTab(state.activeTab, document.querySelector('.tab.active'));
+            } catch (err) {
+                console.error('[Month] 加载失败', err);
+                rankListEl.innerHTML = '<div style="padding:20px; color:#d63031;">数据加载失败,请重试</div>';
+            } finally {
+                setLoading(false);
+            }
+        }
+
+        function selectMonth(year, month, title) {
+            dropdown.style.display = 'none';
+            loadMonthData(year, month);
+        }
+
+        async function initPage() {
+            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);
+            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 });
+            const allowLogin = !useMock && token;
+
+            renderMonths(state.months);
+            setLoading(true);
+
+            async function safeCall(promiseFactory) {
+                try { return await promiseFactory(); }
+                catch (err) { console.warn('[Optional API] ignore error', err); return null; }
+            }
+
+            try {
+                const monthRank = await API.request('MonthRankDetailQuery', { year: ym.year, month: ym.month, dispArrStr: 'grad,mapNum' });
+                let base, currentMonth, allMonths, myRank, myScore, userInfo, cardConfig;
+                if (useMock || allowLogin) {
+                    [base, currentMonth, allMonths, myRank, myScore, userInfo, cardConfig] = await Promise.all([
+                        safeCall(() => API.getCardBase(ecId, 'rank')),
+                        safeCall(() => API.request('CurrentMonthlyChallengeQuery', { ecId: ecId })),
+                        safeCall(() => API.getMonthlyChallenge()),
+                        safeCall(() => API.getUserCurrentRank(ecId)),
+                        safeCall(() => API.getScore(ecId)),
+                        safeCall(() => API.getUserInfo()),
+                        safeCall(() => API.getCardConfig(ecId, 'rank'))
+                    ]);
+                } else {
+                    base = { ecName: `${ym.month}月挑战赛` };
+                    currentMonth = { monthRs: [{ month: ym.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}月挑战赛` };
+                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}月挑战赛`;
+                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);
+                    }
+                    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);
+            }
+        }
+
+        document.addEventListener('click', (e) => {
+            if(!e.target.closest('.month-select') && !e.target.closest('.dropdown')) { dropdown.style.display = 'none'; }
+        });
+
+        document.addEventListener('DOMContentLoaded', initPage);
+    </script>
+</body>
+</html>
+
+

+ 200 - 636
card/sdk/demo_project/index.html

@@ -3,19 +3,12 @@
 <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>
-    <!-- 本地调试保留 mock_flutter.js,正式接入时移除 -->
-    <script src="./mock_flutter.js"></script>
-    <script src="./bridge.js"></script>
-    <script src="./api.js"></script>
+    <title>11月挑战赛 - 最终封面</title>
     <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
     <style>
         :root {
-            --primary-purple: #593259; 
-            --primary-orange: #fdcb6e;
-            --primary-red: #d63031;
-            --text-dark: #2d3436;
-            --footer-bg: #483055;
+            --primary-purple: #593259;
+            --primary-orange: #ffeaa7;
         }
 
         * {
@@ -27,669 +20,240 @@
             user-select: none;
         }
 
+        /* 1. 背景层:全屏铺满 */
         body {
-            background: #f5f6fa;
-            width: 100%;
+            margin: 0;
+            padding: 0;
             height: 100vh;
+            width: 100vw;
             overflow: hidden;
+            
+            background: linear-gradient(to bottom, rgba(162, 155, 254, 0.2) 0%, rgba(45, 52, 54, 0.95) 100%), 
+                        url('https://img.freepik.com/free-vector/silhouette-trail-runner-running-forest-at-night_105940-705.jpg?w=800') center/cover no-repeat;
+            background-color: #2d3436;
+            
             display: flex;
-            flex-direction: column;
-        }
-
-        /* 顶部 Header */
-        .header-area {
-            height: 280px; 
-            background: linear-gradient(to bottom, rgba(72, 48, 85, 0.7), rgba(45, 52, 54, 0.95)), 
-                        url('https://img.freepik.com/free-vector/silhouette-trail-runner-running-forest-at-night_105940-705.jpg?w=800') center/cover;
-            padding: 20px;
-            padding-top: max(20px, 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; }
-        .icon-btn {
-            width: 36px; height: 36px; 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);
-        }
-        .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); }
-
-        /* 仪表盘卡片 */
-        .dashboard-card {
-            margin-top: 25px;
-            background: rgba(0, 0, 0, 0.4); 
+            justify-content: center;
+            align-items: center;
+        }
+
+        /* 2. 毛玻璃卡片层:距边 2% */
+        .glass-card {
+            width: 96%;
+            height: 96%;
+            
+            /* 描边 & 圆角 */
+            border: 2px solid rgba(255, 255, 255, 0.25);
+            border-bottom: 2px solid rgba(255, 255, 255, 0.1);
+            border-radius: 40px;
+            
+            /* 毛玻璃效果 */
+            background: linear-gradient(to bottom, 
+                rgba(255, 255, 255, 0.15) 0%, 
+                rgba(255, 255, 255, 0.05) 40%, 
+                rgba(0, 0, 0, 0.2) 100%
+            );
             backdrop-filter: blur(10px);
-            border-radius: 20px;
-            padding: 15px 20px;
-            border: 1px solid rgba(255, 255, 255, 0.1);
-            box-shadow: 0 8px 20px rgba(0,0,0,0.2);
+            -webkit-backdrop-filter: blur(10px);
+            
+            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), inset 0 0 0 1px rgba(255,255,255,0.1);
+            
             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-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.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; }
-        
-        .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-active { 
-            position: absolute; top: 50%; left: 10px; 
-            width: 25%; 
-            height: 4px; 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; 
-            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; 
-            transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); 
-        }
-        
-        .trophy-item.active { 
-            background: #fff; border-color: var(--primary-orange); color: var(--primary-orange); 
-            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.active { 
-            background: linear-gradient(135deg, #f1c40f, #e67e22); 
-            color: white; border: none; 
-            transform: scale(1.3); 
-            box-shadow: 0 0 20px rgba(241, 196, 15, 0.6);
-            animation: pulseTrophy 2s infinite;
-        }
-
-        @keyframes pulseTrophy {
-            0% { box-shadow: 0 0 0 0 rgba(241, 196, 15, 0.7); }
-            70% { box-shadow: 0 0 0 10px rgba(241, 196, 15, 0); }
-            100% { box-shadow: 0 0 0 0 rgba(241, 196, 15, 0); }
-        }
-
-        /* 领奖台 */
-        .podium-wrap {
-            height: 140px;
-            display: flex; justify-content: center; align-items: flex-end; 
-            margin-top: -60px; 
-            position: relative; 
-            z-index: 10; 
-            padding-bottom: 0px; 
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+            text-align: center;
+            cursor: pointer;
+            overflow: hidden;
         }
-        .p-col { display: flex; flex-direction: column; align-items: center; width: 30%; position: relative;}
-        .p-2 { z-index: 2; margin-right: -15px; }
-        .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 img { width: 100%; height: 100%; object-fit: cover; }
 
-        .crown {
-            position: absolute; top: -38px; 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; 
-        }
-        @keyframes crownFloat {
-            0%, 100% { transform: translateY(0) rotate(-5deg); }
-            50% { transform: translateY(-8px) rotate(5deg); }
+        .glass-card:active {
+            transform: scale(0.99);
+            transition: transform 0.1s;
+        }
+
+        /* 顶部年份 */
+        .tag-year {
+            font-size: 18px;
+            font-weight: 800;
+            letter-spacing: 3px;
+            color: #fff;
+            margin-bottom: 2vh;
+            background: rgba(255, 255, 255, 0.2);
+            padding: 8px 24px;
+            border-radius: 30px;
+            text-shadow: 0 1px 2px rgba(0,0,0,0.3);
+            border: 1px solid rgba(255,255,255,0.3);
+            box-shadow: 0 4px 15px rgba(0,0,0,0.2);
+        }
+
+        /* 月份数字 - 超大号金色 */
+        .title-month {
+            font-size: 35vmin;
+            font-weight: 900;
+            line-height: 1;
+            
+            /* 金色金属渐变 */
+            background: linear-gradient(to bottom, 
+                #ffffff 10%, 
+                #ffd700 50%, 
+                #ff9f43 100%
+            );
+            -webkit-background-clip: text;
+            -webkit-text-fill-color: transparent;
+            
+            filter: drop-shadow(0 4px 0px rgba(255, 215, 0, 0.3));
+            
+            margin: 1vh 0;
+            z-index: 2;
+        }
+
+        .title-sub {
+            font-size: 20px;
+            font-weight: 800;
+            color: rgba(255, 255, 255, 0.95);
+            letter-spacing: 6px;
+            text-transform: uppercase;
+            margin-top: -1vh;
+            text-shadow: 0 2px 4px rgba(0,0,0,0.6);
+        }
+
+        /* 底部图标容器 */
+        .deco-icon {
+            margin-top: 5vh;
+            width: 80px; height: 80px;
+            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 10px 30px rgba(0,0,0,0.3);
+            animation: trophy-pulse 2s infinite ease-in-out;
+        }
+
+        .deco-icon i {
+            font-size: 40px;
+            color: #ffd700; 
+            filter: drop-shadow(0 0 5px rgba(253, 203, 110, 0.8));
+        }
+
+        /* ================= 新增:查看排行按钮 ================= */
+        .action-btn {
+            position: absolute;
+            bottom: 8%; /* 距离底部的位置 */
+            
+            /* 按钮尺寸与样式 */
+            padding: 14px 40px;
+            border-radius: 50px;
+            border: none;
+            
+            /* 渐变背景:浅金 -> 珊瑚色 */
+            background: linear-gradient(135deg, #ffeaa7 0%, #fab1a0 100%);
+            
+            /* 文字样式:深紫色,高对比度 */
+            color: #593259;
+            font-size: 18px;
+            font-weight: 900;
+            letter-spacing: 1px;
+            
+            /* 阴影与光泽 */
+            box-shadow: 0 10px 25px rgba(255, 118, 117, 0.4), 
+                        inset 0 1px 0 rgba(255,255,255,0.4);
+            
+            display: flex;
+            align-items: center;
+            gap: 10px;
+            
+            z-index: 10;
+            animation: btn-float 3s infinite ease-in-out;
         }
 
-        .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; }
-
-        /* 列表容器 */
-        .list-container {
-            flex: 1; background: white; border-radius: 24px 24px 0 0;
-            padding: 0 20px 120px 20px; 
-            overflow-y: auto; 
-            margin-top: -10px;
-            box-shadow: 0 -5px 20px rgba(0,0,0,0.05);
-            position: relative; z-index: 8; 
-            -webkit-overflow-scrolling: touch; 
-        }
-        
-        .tabs { 
-            display: flex; justify-content: center; gap: 15px; 
-            position: sticky; top: 0; background: white; z-index: 9;
-            padding-top: 20px; padding-bottom: 10px;
+        /* 按钮悬浮动画 */
+        @keyframes btn-float {
+            0%, 100% { transform: translateY(0); }
+            50% { transform: translateY(-5px); }
         }
-        
-        .tab { padding: 8px 20px; border-radius: 20px; font-size: 14px; color: #636e72; background: #f1f2f6; cursor: pointer; transition: 0.2s; }
-        .tab.active { background: var(--text-dark); color: #fdcb6e; font-weight: bold; }
-        
-        .list-item { display: flex; align-items: center; padding: 12px 0; border-bottom: 1px solid #f1f2f6; }
-        .rank { width: 30px; text-align: center; font-weight: bold; color: #b2bec3; font-style: italic;}
-        .avatar { width: 40px; height: 40px; border-radius: 50%; margin: 0 12px; background: #eee; overflow: hidden;}
-        .avatar img { width: 100%; height: 100%; object-fit: cover;}
-        .info { flex: 1; }
-        .name { font-size: 14px; color: #2d3436; font-weight: bold; }
-        .team { font-size: 11px; color: #636e72; display: flex; align-items: center; gap: 4px; }
-        .score { font-size: 16px; font-weight: bold; color: var(--primary-purple); }
 
-        /* 底部我的排名 */
-        .my-rank-bar {
-            position: fixed; bottom: 0; left: 0; width: 100%; height: 55px; 
-            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; 
-            border-radius: 24px 24px 0 0; box-shadow: 0 -5px 20px rgba(0,0,0,0.2); z-index: 99; 
+        @keyframes trophy-pulse {
+            0% { transform: scale(1) translateY(0); box-shadow: 0 0 0 0 rgba(253, 203, 110, 0.2); }
+            50% { transform: scale(1.1) translateY(-5px); box-shadow: 0 0 30px 10px rgba(253, 203, 110, 0.4); }
+            100% { transform: scale(1) translateY(0); box-shadow: 0 0 0 0 rgba(253, 203, 110, 0); }
         }
-        .my-rank-bar .rank { font-size: 16px; }
-        .my-rank-bar .avatar { width: 34px; height: 34px; border-width: 2px; }
-        .my-rank-bar .name { font-size: 14px; }
-        .my-rank-bar .team { font-size: 10px; }
-        .my-score { font-size: 18px; font-weight: bold; color: #ffffff !important; margin-left: auto; }
 
-        /* 演示按钮样式 (模态框内) */
-        .demo-section {
-            margin: 20px 0 0 0;
-            border-top: 1px dashed #ddd;
-            padding-top: 15px;
-        }
-        .demo-label { font-size: 12px; color: #999; margin-bottom: 10px; }
-        .demo-controls {
-            display: flex; justify-content: center; gap: 10px;
-        }
-        .demo-btn {
-            background: #eee; border: none; padding: 6px 12px; border-radius: 8px; font-size: 12px; color: #555; cursor: pointer;
+        /* 顶部反光效果 */
+        .shine {
+            position: absolute;
+            top: -15%; left: -15%;
+            width: 60%; height: 40%;
+            background: radial-gradient(circle, rgba(255,255,255,0.4) 0%, transparent 70%);
+            z-index: 3;
+            pointer-events: none;
+            filter: blur(30px);
+            opacity: 0.6;
         }
-        .demo-btn:active { background: #ddd; color: #000; }
 
-        /* 下拉菜单 & 模态框 */
-        .dropdown { position: absolute; top: 70px; left: 50%; transform: translateX(-50%); width: 200px; background: white; border-radius: 12px; box-shadow: 0 10px 50px rgba(0,0,0,0.4); display: none; flex-direction: column; overflow: hidden; z-index: 200; }
-        .dropdown.show { display: flex; animation: dropIn 0.2s ease-out; }
-        .dd-item { padding: 12px; color: #636e72; font-size: 14px; text-align: center; border-bottom: 1px solid #f1f2f6; cursor: pointer; }
-        .dd-active { color: var(--primary-purple); font-weight: bold; background: #f9f0ff; }
-        @keyframes dropIn { from{opacity:0; transform:translateX(-50%) translateY(-10px);} to{opacity:1; transform:translateX(-50%) translateY(0);} }
-
-        .modal-mask { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 999; display: flex; justify-content: center; align-items: center; opacity: 0; pointer-events: none; transition: opacity 0.3s; }
-        .modal-mask.show { opacity: 1; pointer-events: auto; }
-        .modal-body { width: 80%; max-width: 320px; background: white; border-radius: 24px; padding: 25px; text-align: center; transform: scale(0.8); transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); border: 4px solid var(--primary-purple); box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
-        .modal-mask.show .modal-body { transform: scale(1); }
-        .m-close { background: var(--primary-purple); color: white; padding: 10px 25px; border-radius: 20px; border: none; font-weight: bold; cursor: pointer; margin-top: 15px; }
-
-        /* 加载遮罩 */
-        .loading-mask {
-            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-spinner {
-            border: 4px solid rgba(255,255,255,0.3);
-            border-top-color: #fdcb6e;
-            border-radius: 50%;
-            width: 36px; height: 36px;
-            animation: spin 1s linear infinite;
-            margin-right: 10px;
-        }
-        @keyframes spin { to { transform: rotate(360deg); } }
     </style>
 </head>
 <body>
 
-    <!-- 头部 -->
-    <div class="header-area">
-        <div class="nav-bar">
-            <div class="icon-btn" onclick="handleBack()"><i class="fa-solid fa-chevron-left" style="color:white"></i></div>
-            <div class="month-select" onclick="toggleDropdown()">
-                <span id="currentMonthText">11月挑战赛</span> <i class="fa-solid fa-caret-down"></i>
-            </div>
-            <div class="icon-btn" onclick="openModal()"><i class="fa-solid fa-question" style="color:white"></i></div>
-        </div>
-
-        <!-- 仪表盘卡片 -->
-        <div class="dashboard-card">
-            <div class="dash-header">
-                <div class="dash-title">
-                    <i class="fa-solid fa-medal dash-icon"></i> 定向达人
-                </div>
-                <div class="dash-badge" id="dashBadge">挑战 1 / 4</div>
-            </div>
-
-            <div class="trophy-track-container">
-                <div class="track-line-bg"></div>
-                <div class="track-line-active" id="progressLine"></div>
-                
-                <!-- 图标1 -->
-                <div class="trophy-item active" id="t1"><i class="fa-solid fa-check"></i></div>
-                <!-- 图标2 -->
-                <div class="trophy-item" id="t2"><i class="fa-solid fa-lock"></i></div>
-                <!-- 图标3 -->
-                <div class="trophy-item" id="t3"><i class="fa-solid fa-lock"></i></div>
-                <!-- 图标4 (Final) -->
-                <div class="trophy-item final" id="t4"><i class="fa-solid fa-trophy"></i></div>
-            </div>
-        </div>
-    </div>
-
-    <!-- 下拉菜单 -->
-    <div class="dropdown" id="dropdown"></div>
-
-    <!-- 领奖台 -->
-    <div class="podium-wrap" id="podiumWrap"></div>
-
-    <!-- 列表区 -->
-    <div class="list-container">
-        <div class="tabs">
-            <div class="tab active" onclick="switchTab('score', this)">积分排行</div>
-            <div class="tab" onclick="switchTab('venue', this)">场地排行</div>
-        </div>
+    <!-- 点击整个区域都能跳转 -->
+    <div class="glass-card" onclick="redirectToDetail()">
+        <div class="shine"></div>
         
-        <div id="rankList"></div>
-    </div>
-
-    <!-- 底部我的排名 -->
-    <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="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>
-
-    <!-- 模态框 -->
-    <div class="modal-mask" id="infoModal">
-        <div class="modal-body">
-            <h3 style="color:#593259; margin-bottom:15px;">📜 规则说明</h3>
-            <p id="ruleContent" style="text-align:left; color:#636e72; font-size:14px; line-height:1.6;">数据加载中...</p>
-
-            <!-- 演示功能区 -->
-            <div class="demo-section">
-                <div class="demo-label">✨ 功能演示</div>
-                <div class="demo-controls">
-                    <button class="demo-btn" onclick="demoProgress(1);closeModal()">1/4</button>
-                    <button class="demo-btn" onclick="demoProgress(3);closeModal()">3/4</button>
-                    <button class="demo-btn" onclick="demoProgress(4);closeModal()">4/4</button>
-                </div>
-            </div>
-
-            <button class="m-close" onclick="closeModal()">知道了</button>
+        <div class="tag-year">2025 年</div>
+        
+        <div class="title-month">11</div>
+        <div class="title-sub">月度挑战赛</div>
+        
+        <div class="deco-icon">
+            <i class="fa-solid fa-trophy"></i>
         </div>
-    </div>
 
-    <!-- 加载中遮罩 -->
-    <div class="loading-mask" id="loadingMask">
-        <div class="loading-spinner"></div>
-        <div>数据加载中...</div>
+        <!-- 替代原来的文字提示,使用显眼的按钮 -->
+        <button class="action-btn">
+            查看排行 <i class="fa-solid fa-arrow-right"></i>
+        </button>
     </div>
 
+    <!-- 本地调试保留 mock_flutter.js,正式接入时移除 -->
+    <script src="./mock_flutter.js"></script>
+    <script src="./bridge.js"></script>
     <script>
-        const dropdown = document.getElementById('dropdown');
-        const rankListEl = document.getElementById('rankList');
-        const trackLine = document.getElementById('progressLine');
-        const podiumWrap = document.getElementById('podiumWrap');
-        const ruleContent = document.getElementById('ruleContent');
-        const loadingMask = document.getElementById('loadingMask');
-        const state = {
-            activeTab: 'score',
-            scoreList: [],
-            scoreListRendered: null,
-            venueList: [],
-            venueListRendered: null,
-            months: [],
-            currentYM: null,
-            currentMonthData: null,
-            allMonthsData: null
-        };
-
-        function getQuery(name) {
+        function getQueryParam(name) {
             const params = new URLSearchParams(window.location.search);
             return params.get(name);
         }
 
-        function getYearMonth() {
-            const now = new Date();
-            const year = parseInt(getQuery('year'), 10) || now.getFullYear();
-            const month = parseInt(getQuery('month'), 10) || (now.getMonth() + 1);
-            return { year: year, month: month };
-        }
-        
-        function getRecentMonths(count) {
-            const ym = getYearMonth();
-            const list = [];
-            let y = ym.year;
-            let m = ym.month;
-            for (let i = 0; i < (count || 3); i++) {
-                list.push({ year: y, month: m });
-                m -= 1;
-                if (m === 0) { m = 12; y -= 1; }
-            }
-            return list;
-        }
-
-        function handleBack() {
-            if (window.Bridge && Bridge.back) Bridge.back();
-            else window.history.back();
-        }
-
-        function openModal() { document.getElementById('infoModal').classList.add('show'); }
-        function closeModal() { document.getElementById('infoModal').classList.remove('show'); }
-        function toggleDropdown() { dropdown.style.display = (dropdown.style.display === 'flex') ? 'none' : 'flex'; }
-
-        function setLoading(isLoading) {
-            rankListEl.style.opacity = isLoading ? '0.4' : '1';
-            loadingMask.classList.toggle('show', isLoading);
-        }
+        function redirectToDetail() {
+            const token = getQueryParam('token');
+            const id = getQueryParam('id');
+            let detailUrl = 'detail.html';
 
-        function buildAvatar(name, salt) {
-            const seedBase = name || 'user';
-            const seed = encodeURIComponent(seedBase + (salt || ''));
-            return `<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=${seed}" alt="">`;
-        }
-
-        function setProgress(real, target) {
-            const badge = document.getElementById('dashBadge');
-            const t1 = document.getElementById('t1');
-            const t2 = document.getElementById('t2');
-            const t3 = document.getElementById('t3');
-            const t4 = document.getElementById('t4');
-            const safeReal = Math.max(real || 0, 0);
-            const safeTarget = target && target > 0 ? target : 4;
-            const ratio = Math.min(safeReal / safeTarget, 1);
-            const percent = Math.max(0, Math.min(1, ratio)) * 100;
-            trackLine.style.width = percent + '%';
-            const textReal = safeReal >= safeTarget ? safeTarget : safeReal;
-            badge.innerText = safeReal >= safeTarget ? '挑战成功' : `挑战 ${textReal} / ${safeTarget}`;
-            badge.classList.toggle('completed', safeReal >= safeTarget);
-            function setIconActive(el, active) {
-                if (active) {
-                    el.classList.add('active');
-                    if (!el.classList.contains('final')) el.innerHTML = '<i class="fa-solid fa-check"></i>';
-                } else {
-                    el.classList.remove('active');
-                    if (!el.classList.contains('final')) el.innerHTML = '<i class="fa-solid fa-lock"></i>';
-                }
+            const queryParams = [];
+            if (token) {
+                queryParams.push(`token=${encodeURIComponent(token)}`);
             }
-            setIconActive(t1, safeReal >= 1);
-            setIconActive(t2, safeReal >= 2);
-            setIconActive(t3, safeReal >= 3);
-            setIconActive(t4, safeReal >= safeTarget);
-        }
-
-        function renderBadge(real, target) {
-            setProgress(real, target);
-        }
-
-        function findMonthProgress(year, month, currentMonthData, allMonthsData) {
-            const result = { realNum: 0, targetNum: 4 };
-            const pick = (arr) => {
-                if (!arr || !arr.length) return null;
-                for (let i = 0; i < arr.length; i++) {
-                    if (Number(arr[i].month) === Number(month)) return arr[i];
-                }
-                return null;
-            };
-            const fromCurrent = currentMonthData && pick(currentMonthData.monthRs || []);
-            if (fromCurrent) return { realNum: fromCurrent.realNum || 0, targetNum: fromCurrent.targetNum || 4 };
-            if (allMonthsData && allMonthsData.length) {
-                for (let i = 0; i < allMonthsData.length; i++) {
-                    const item = allMonthsData[i];
-                    if (item.year && Number(item.year) !== Number(year)) continue;
-                    const found = pick(item.monthRs || []);
-                    if (found) return { realNum: found.realNum || 0, targetNum: found.targetNum || 4 };
-                }
+            if (id) {
+                queryParams.push(`id=${encodeURIComponent(id)}`);
             }
-            return result;
-        }
 
-        function renderMonths(list) {
-            dropdown.innerHTML = '';
-            if (!list || list.length === 0) {
-                dropdown.innerHTML = '<div class="dd-item">暂无月份数据</div>';
-                return;
+            if (queryParams.length > 0) {
+                detailUrl += `?${queryParams.join('&')}`;
             }
-            list.forEach((item, idx) => {
-                const title = `${item.month}月挑战赛`;
-                const div = document.createElement('div');
-                const isCurrent = state.currentYM && state.currentYM.year === item.year && state.currentYM.month === item.month;
-                div.className = 'dd-item' + (isCurrent ? ' dd-active' : '');
-                div.innerText = (idx === 0 && !isCurrent) ? `${title} (本月)` : title;
-                div.onclick = () => {
-                    selectMonth(item.year, item.month, title);
-                };
-                dropdown.appendChild(div);
-            });
-        }
-
-        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;
-            }
-            const top3 = list.slice(0, 3);
-            const cols = ['p-2', 'p-1', 'p-3'];
-            top3.forEach((item, idx) => {
-                const col = document.createElement('div');
-                col.className = 'p-col ' + cols[idx];
-                const name = item.nickName || item.name || item.userName || '选手';
-                const score = item.score != null ? item.score : (item.inRankNum != null ? item.inRankNum : '--');
-                const rankNum = item.rankNum || idx + 1;
-                const isFirst = idx === 1;
-                col.innerHTML = `
-                    ${isFirst ? '<div class="crown"><i class="fa-solid fa-crown"></i></div>' : ''}
-                    <div class="p-img">${buildAvatar(name, rankNum)}</div>
-                    <div class="p-box">
-                        <div class="p-name">${name}</div>
-                        <div class="p-score">${score}</div>
-                    </div>
-                `;
-                podiumWrap.appendChild(col);
-            });
-            const remaining = list.slice(3);
-            if (type === 'venue') {
-                state.venueListRendered = remaining;
+            
+            // Add full=true for Flutter interception
+            detailUrl += (detailUrl.includes('?') ? '&' : '?') + 'full=true';
+
+            console.log("Navigating from sdk/index.html to:", detailUrl);
+            
+            // Use Bridge.appAction for navigation
+            if (window.Bridge && window.Bridge.appAction) {
+                Bridge.appAction(detailUrl);
             } else {
-                state.scoreListRendered = remaining;
-            }
-            if (state.activeTab === type) renderRankList(remaining);
-        }
-
-        function renderRankList(list) {
-            rankListEl.innerHTML = '';
-            if (!list || list.length === 0) {
-                rankListEl.innerHTML = '<div style="padding:20px; color:#b2bec3; text-align:center;">暂无数据</div>';
-                return;
-            }
-            list.forEach((item, idx) => {
-                const rankNum = item.rankNum || idx + 1;
-                const name = item.nickName || item.name || item.userName || item.teamName || '选手';
-                const scoreVal = (item.score != null ? item.score : item.inRankNum);
-                const score = scoreVal != null ? scoreVal : '--';
-                const team = item.teamName || item.coiName || '';
-                const teamIcon = team ? '<i class="fa-solid fa-user-group"></i> ' : '<i class="fa-solid fa-user"></i> ';
-                const row = document.createElement('div');
-                row.className = 'list-item';
-                row.innerHTML = `
-                    <div class="rank">${rankNum}</div>
-                    <div class="avatar">${buildAvatar(name, rankNum)}</div>
-                    <div class="info">
-                        <div class="name">${name}</div>
-                        <div class="team">${teamIcon}${team || '个人'}</div>
-                    </div>
-                    <div class="score">${score}</div>
-                `;
-                rankListEl.appendChild(row);
-            });
-        }
-
-        function renderMyInfo(myRank, myScore, userInfo) {
-            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 rankVal = myRank && Number(myRank.rankNum);
-            const hasRank = rankVal > 0;
-            rankNumEl.innerText = hasRank ? rankVal : '--';
-            myScoreEl.innerText = hasRank && myScore && myScore.score != null ? myScore.score : '--';
-            const name = (userInfo && (userInfo.nickName || userInfo.userName)) || '我';
-            myNameEl.innerText = name;
-            myTeamEl.innerText = hasRank ? '继续加油' : '';
-            myAvatarEl.innerHTML = buildAvatar(name, 'me');
-        }
-
-        function renderRules(config) {
-            if (!config || !config.configJson) return;
-            try {
-                const parsed = JSON.parse(config.configJson);
-                const rules = parsed.popupRuleList || [];
-                if (rules.length > 0 && rules[0].data && rules[0].data.content) {
-                    ruleContent.innerHTML = rules[0].data.content;
-                }
-            } catch (err) {
-                console.warn('[Rule] parse error', err);
-            }
-        }
-
-        function switchTab(type, tabElement) {
-            state.activeTab = type;
-            document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
-            if (tabElement) tabElement.classList.add('active');
-            const baseList = type === 'venue' ? state.venueList : state.scoreList;
-            renderPodium(baseList, type);
-            const rendered = type === 'venue' ? (state.venueListRendered || baseList.slice(3)) : (state.scoreListRendered || baseList.slice(3));
-            renderRankList(rendered);
-        }
-
-        function demoProgress(step) {
-            setProgress(step, 4);
-        }
-
-        async function loadMonthData(year, month) {
-            state.currentYM = { year: year, month: month };
-            document.getElementById('currentMonthText').innerText = `${month}月挑战赛`;
-            renderMonths(state.months);
-            setLoading(true);
-            try {
-                const monthRank = await API.request('MonthRankDetailQuery', { year: year, month: month, dispArrStr: 'grad,mapNum' });
-                const monthGrad = monthRank && monthRank.gradRs ? monthRank.gradRs : [];
-                const monthMap = monthRank && monthRank.mapNumRs ? monthRank.mapNumRs : [];
-                state.scoreList = monthGrad;
-                state.venueList = monthMap;
-                const prog = findMonthProgress(year, month, state.currentMonthData, state.allMonthsData);
-                renderBadge(prog.realNum, prog.targetNum);
-                renderPodium(state.activeTab === 'venue' ? state.venueList : state.scoreList, state.activeTab);
-                switchTab(state.activeTab, document.querySelector('.tab.active'));
-            } catch (err) {
-                console.error('[Month] 加载失败', err);
-                rankListEl.innerHTML = '<div style="padding:20px; color:#d63031;">数据加载失败,请重试</div>';
-            } finally {
-                setLoading(false);
-            }
-        }
-
-        function selectMonth(year, month, title) {
-            dropdown.style.display = 'none';
-            loadMonthData(year, month);
-        }
-
-        async function initPage() {
-            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);
-            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 });
-            const allowLogin = !useMock && token;
-            if (!token && !useMock && window.Bridge && Bridge.getToken) Bridge.getToken();
-            renderMonths(state.months);
-            setLoading(true);
-
-            async function safeCall(promiseFactory) {
-                try { return await promiseFactory(); }
-                catch (err) { console.warn('[Optional API] ignore error', err); return null; }
-            }
-
-            try {
-                const monthRank = await API.request('MonthRankDetailQuery', { year: ym.year, month: ym.month, dispArrStr: 'grad,mapNum' });
-                let base, currentMonth, allMonths, myRank, myScore, userInfo, cardConfig;
-                if (useMock || allowLogin) {
-                    [base, currentMonth, allMonths, myRank, myScore, userInfo, cardConfig] = await Promise.all([
-                        safeCall(() => API.getCardBase(ecId, 'rank')),
-                        safeCall(() => API.request('CurrentMonthlyChallengeQuery', { ecId: ecId })),
-                        safeCall(() => API.getMonthlyChallenge()),
-                        safeCall(() => API.getUserCurrentRank(ecId)),
-                        safeCall(() => API.getScore(ecId)),
-                        safeCall(() => API.getUserInfo()),
-                        safeCall(() => API.getCardConfig(ecId, 'rank'))
-                    ]);
-                } else {
-                    base = { ecName: `${ym.month}月挑战赛` };
-                    currentMonth = { monthRs: [{ month: ym.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}月挑战赛` };
-                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}月挑战赛`;
-                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);
-                    }
-                    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);
+                console.error("Bridge or Bridge.appAction is not defined. Falling back to window.location.href.");
+                window.location.href = detailUrl; // Fallback for pure browser environment
             }
         }
-
-        document.addEventListener('click', (e) => {
-            if(!e.target.closest('.month-select') && !e.target.closest('.dropdown')) { dropdown.style.display = 'none'; }
-        });
-
-        document.addEventListener('DOMContentLoaded', initPage);
     </script>
 </body>
-</html>
+</html>

+ 722 - 0
card/sdk/detail.html

@@ -0,0 +1,722 @@
+<!DOCTYPE html>
+<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>
+    <!-- 本地调试保留 mock_flutter.js,正式接入时移除 -->
+    <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">
+    <style>
+        :root {
+            --primary-purple: #593259; 
+            --primary-orange: #fdcb6e;
+            --primary-red: #d63031;
+            --text-dark: #2d3436;
+            --footer-bg: #483055;
+        }
+
+        * {
+            box-sizing: border-box;
+            margin: 0;
+            padding: 0;
+            font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
+            -webkit-tap-highlight-color: transparent;
+            user-select: none;
+        }
+
+        body {
+            background: #f5f6fa;
+            width: 100%;
+            height: 100vh;
+            overflow: hidden;
+            display: flex;
+            flex-direction: column;
+        }
+
+        /* 顶部 Header */
+        .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;
+            padding: 20px;
+            padding-top: max(20px, 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; }
+        .icon-btn {
+            width: 36px; height: 36px; 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);
+        }
+        .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); }
+
+        /* 仪表盘卡片 */
+        .dashboard-card {
+            margin-top: 25px;
+            background: rgba(0, 0, 0, 0.4); 
+            backdrop-filter: blur(10px);
+            border-radius: 20px;
+            padding: 15px 20px;
+            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-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.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; }
+        
+        .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-active { 
+            position: absolute; top: 50%; left: 10px; 
+            width: 25%; 
+            height: 4px; 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; 
+            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; 
+            transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); 
+        }
+        
+        .trophy-item.active { 
+            background: #fff; border-color: var(--primary-orange); color: var(--primary-orange); 
+            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.active { 
+            background: linear-gradient(135deg, #f1c40f, #e67e22); 
+            color: white; border: none; 
+            transform: scale(1.3); 
+            box-shadow: 0 0 20px rgba(241, 196, 15, 0.6);
+            animation: pulseTrophy 2s infinite;
+        }
+
+        @keyframes pulseTrophy {
+            0% { box-shadow: 0 0 0 0 rgba(241, 196, 15, 0.7); }
+            70% { box-shadow: 0 0 0 10px rgba(241, 196, 15, 0); }
+            100% { box-shadow: 0 0 0 0 rgba(241, 196, 15, 0); }
+        }
+
+        /* 领奖台 */
+        .podium-wrap {
+            height: 140px;
+            display: flex; justify-content: center; align-items: flex-end; 
+            margin-top: -60px; 
+            position: relative; 
+            z-index: 10; 
+            padding-bottom: 0px; 
+        }
+        .p-col { display: flex; flex-direction: column; align-items: center; width: 30%; position: relative;}
+        .p-2 { z-index: 2; margin-right: -15px; }
+        .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 img { width: 100%; height: 100%; object-fit: cover; }
+
+        .crown {
+            position: absolute; top: -38px; 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; 
+        }
+        @keyframes crownFloat {
+            0%, 100% { transform: translateY(0) rotate(-5deg); }
+            50% { transform: translateY(-8px) 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; }
+
+        /* 列表容器 */
+        .list-container {
+            flex: 1; background: white; border-radius: 24px 24px 0 0;
+            padding: 0 20px 120px 20px; 
+            overflow-y: auto; 
+            margin-top: -10px;
+            box-shadow: 0 -5px 20px rgba(0,0,0,0.05);
+            position: relative; z-index: 8; 
+            -webkit-overflow-scrolling: touch; 
+        }
+        
+        .tabs { 
+            display: flex; justify-content: center; gap: 15px; 
+            position: sticky; top: 0; background: white; z-index: 9;
+            padding-top: 20px; padding-bottom: 10px;
+        }
+        
+        .tab { padding: 8px 20px; border-radius: 20px; font-size: 14px; color: #636e72; background: #f1f2f6; cursor: pointer; transition: 0.2s; }
+        .tab.active { background: var(--text-dark); color: #fdcb6e; font-weight: bold; }
+        
+        .list-item { display: flex; align-items: center; padding: 12px 0; border-bottom: 1px solid #f1f2f6; }
+        .rank { width: 30px; text-align: center; font-weight: bold; color: #b2bec3; font-style: italic;}
+        .avatar { width: 40px; height: 40px; border-radius: 50%; margin: 0 12px; background: #eee; overflow: hidden;}
+        .avatar img { width: 100%; height: 100%; object-fit: cover;}
+        .info { flex: 1; }
+        .name { font-size: 14px; color: #2d3436; font-weight: bold; }
+        .team { font-size: 11px; color: #636e72; display: flex; align-items: center; gap: 4px; }
+        .score { font-size: 16px; font-weight: bold; color: var(--primary-purple); }
+
+        /* 底部我的排名 */
+        .my-rank-bar {
+            position: fixed; bottom: 0; left: 0; width: 100%; height: 55px; 
+            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; 
+            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; }
+        .my-rank-bar .avatar { width: 34px; height: 34px; border-width: 2px; }
+        .my-rank-bar .name { font-size: 14px; }
+        .my-rank-bar .team { font-size: 10px; }
+        .my-score { font-size: 18px; font-weight: bold; color: #ffffff !important; margin-left: auto; }
+
+        /* 演示按钮样式 (模态框内) */
+        .demo-section {
+            margin: 20px 0 0 0;
+            border-top: 1px dashed #ddd;
+            padding-top: 15px;
+        }
+        .demo-label { font-size: 12px; color: #999; margin-bottom: 10px; }
+        .demo-controls {
+            display: flex; justify-content: center; gap: 10px;
+        }
+        .demo-btn {
+            background: #eee; border: none; padding: 6px 12px; border-radius: 8px; font-size: 12px; color: #555; cursor: pointer;
+        }
+        .demo-btn:active { background: #ddd; color: #000; }
+
+        /* 下拉菜单 & 模态框 */
+        .dropdown { position: absolute; top: 70px; left: 50%; transform: translateX(-50%); width: 200px; background: white; border-radius: 12px; box-shadow: 0 10px 50px rgba(0,0,0,0.4); display: none; flex-direction: column; overflow: hidden; z-index: 200; }
+        .dropdown.show { display: flex; animation: dropIn 0.2s ease-out; }
+        .dd-item { padding: 12px; color: #636e72; font-size: 14px; text-align: center; border-bottom: 1px solid #f1f2f6; cursor: pointer; }
+        .dd-active { color: var(--primary-purple); font-weight: bold; background: #f9f0ff; }
+        @keyframes dropIn { from{opacity:0; transform:translateX(-50%) translateY(-10px);} to{opacity:1; transform:translateX(-50%) translateY(0);} }
+
+        .modal-mask { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 999; display: flex; justify-content: center; align-items: center; opacity: 0; pointer-events: none; transition: opacity 0.3s; }
+        .modal-mask.show { opacity: 1; pointer-events: auto; }
+        .modal-body { width: 80%; max-width: 320px; background: white; border-radius: 24px; padding: 25px; text-align: center; transform: scale(0.8); transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); border: 4px solid var(--primary-purple); box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
+        .modal-mask.show .modal-body { transform: scale(1); }
+        .m-close { background: var(--primary-purple); color: white; padding: 10px 25px; border-radius: 20px; border: none; font-weight: bold; cursor: pointer; margin-top: 15px; }
+        .rule-box { text-align: left; background: #f9f4ff; border: 1px solid #e3d7ff; border-radius: 12px; padding: 12px; color: #4b3a67; font-size: 13px; line-height: 1.6; box-shadow: inset 0 1px 0 rgba(255,255,255,0.7); }
+        .rule-item { display: flex; gap: 10px; align-items: flex-start; margin-bottom: 8px; }
+        .rule-item:last-child { margin-bottom: 0; }
+        .rule-item i { color: var(--primary-orange); margin-top: 2px; }
+
+        /* 加载遮罩 */
+        .loading-mask {
+            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-spinner {
+            border: 4px solid rgba(255,255,255,0.3);
+            border-top-color: #fdcb6e;
+            border-radius: 50%;
+            width: 36px; height: 36px;
+            animation: spin 1s linear infinite;
+            margin-right: 10px;
+        }
+        @keyframes spin { to { transform: rotate(360deg); } }
+    </style>
+</head>
+<body>
+
+    <!-- 头部 -->
+    <div class="header-area">
+        <div class="nav-bar">
+            <div class="icon-btn" onclick="handleBack()"><i class="fa-solid fa-chevron-left" style="color:white"></i></div>
+            <div class="month-select" onclick="toggleDropdown()">
+                <span id="currentMonthText">11月挑战赛</span> <i class="fa-solid fa-caret-down"></i>
+            </div>
+            <div class="icon-btn" onclick="openModal()"><i class="fa-solid fa-question" style="color:white"></i></div>
+        </div>
+
+        <!-- 仪表盘卡片 -->
+        <div class="dashboard-card">
+            <div class="dash-header">
+                <div class="dash-title">
+                    <i class="fa-solid fa-medal dash-icon"></i> 定向达人
+                </div>
+                <div class="dash-badge" id="dashBadge">挑战 1 / 4</div>
+            </div>
+
+            <div class="trophy-track-container">
+                <div class="track-line-bg"></div>
+                <div class="track-line-active" id="progressLine"></div>
+                
+                <!-- 图标1 -->
+                <div class="trophy-item active" id="t1"><i class="fa-solid fa-check"></i></div>
+                <!-- 图标2 -->
+                <div class="trophy-item" id="t2"><i class="fa-solid fa-lock"></i></div>
+                <!-- 图标3 -->
+                <div class="trophy-item" id="t3"><i class="fa-solid fa-lock"></i></div>
+                <!-- 图标4 (Final) -->
+                <div class="trophy-item final" id="t4"><i class="fa-solid fa-trophy"></i></div>
+            </div>
+        </div>
+    </div>
+
+    <!-- 下拉菜单 -->
+    <div class="dropdown" id="dropdown"></div>
+
+    <!-- 领奖台 -->
+    <div class="podium-wrap" id="podiumWrap"></div>
+
+    <!-- 列表区 -->
+    <div class="list-container">
+        <div class="tabs">
+            <div class="tab active" onclick="switchTab('score', this)">积分排行</div>
+            <div class="tab" onclick="switchTab('venue', this)">场地排行</div>
+        </div>
+        
+        <div id="rankList"></div>
+    </div>
+
+    <!-- 底部我的排名 -->
+    <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="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>
+
+    <!-- 模态框 -->
+    <div class="modal-mask" id="infoModal">
+        <div class="modal-body">
+                        <h3 style="color:#593259; margin-bottom:15px;">📜 规则说明</h3>
+            <div id="ruleContent" class="rule-box">
+                <div class="rule-item"><i class="fa-solid fa-bullseye"></i><div><strong>积分规则:</strong> 在指定公园找到打卡点,耗时越短积分越高。</div></div>
+                <div class="rule-item"><i class="fa-solid fa-map-location-dot"></i><div><strong>场地排行:</strong> 按解锁的公园场地数量排名。</div></div>
+                <div class="rule-item"><i class="fa-solid fa-trophy"></i><div><strong>奖杯获取:</strong> 完成4次有效挑战点亮全部奖杯。</div></div>
+            </div>
+
+            <!-- 演示功能区 -->
+            <div class="demo-section">
+                <div class="demo-label">✨ 功能演示</div>
+                <div class="demo-controls">
+                    <button class="demo-btn" onclick="demoProgress(1);closeModal()">1/4</button>
+                    <button class="demo-btn" onclick="demoProgress(3);closeModal()">3/4</button>
+                    <button class="demo-btn" onclick="demoProgress(4);closeModal()">4/4</button>
+                </div>
+            </div>
+
+            <button class="m-close" onclick="closeModal()">知道了</button>
+        </div>
+    </div>
+
+    <!-- 加载中遮罩 -->
+    <div class="loading-mask" id="loadingMask">
+        <div class="loading-spinner"></div>
+        <div>数据加载中...</div>
+    </div>
+
+    <script>
+        const dropdown = document.getElementById('dropdown');
+        const rankListEl = document.getElementById('rankList');
+        const trackLine = document.getElementById('progressLine');
+        const podiumWrap = document.getElementById('podiumWrap');
+        const ruleContent = document.getElementById('ruleContent');
+        const loadingMask = document.getElementById('loadingMask');
+        const state = {
+            activeTab: 'score',
+            scoreList: [],
+            scoreListRendered: null,
+            venueList: [],
+            venueListRendered: null,
+            months: [],
+            currentYM: null,
+            currentMonthData: null,
+            allMonthsData: null
+        };
+
+        function getQuery(name) {
+            const params = new URLSearchParams(window.location.search);
+            return params.get(name);
+        }
+
+        function getYearMonth() {
+            const now = new Date();
+            const year = parseInt(getQuery('year'), 10) || now.getFullYear();
+            const month = parseInt(getQuery('month'), 10) || (now.getMonth() + 1);
+            return { year: year, month: month };
+        }
+        
+        function getRecentMonths(count) {
+            const ym = getYearMonth();
+            const list = [];
+            let y = ym.year;
+            let m = ym.month;
+            for (let i = 0; i < (count || 3); i++) {
+                list.push({ year: y, month: m });
+                m -= 1;
+                if (m === 0) { m = 12; y -= 1; }
+            }
+            return list;
+        }
+
+        function handleBack() {
+            if (window.Bridge && Bridge.back) Bridge.toHome();
+            else window.history.back(); // Fallback for web environment
+        }
+
+        function openModal() { document.getElementById('infoModal').classList.add('show'); }
+        function closeModal() { document.getElementById('infoModal').classList.remove('show'); }
+        function toggleDropdown() { dropdown.style.display = (dropdown.style.display === 'flex') ? 'none' : 'flex'; }
+
+        function setLoading(isLoading) {
+            rankListEl.style.opacity = isLoading ? '0.4' : '1';
+            loadingMask.classList.toggle('show', isLoading);
+        }
+
+        function buildAvatar(name, salt) {
+            const seedBase = name || 'user';
+            const seed = encodeURIComponent(seedBase + (salt || ''));
+            return `<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=${seed}" alt="">`;
+        }
+
+        function setProgress(real, target) {
+            const badge = document.getElementById('dashBadge');
+            const t1 = document.getElementById('t1');
+            const t2 = document.getElementById('t2');
+            const t3 = document.getElementById('t3');
+            const t4 = document.getElementById('t4');
+            const safeReal = Math.max(real || 0, 0);
+            const safeTarget = target && target > 0 ? target : 4;
+            const ratio = Math.min(safeReal / safeTarget, 1);
+            const percent = Math.max(0, Math.min(1, ratio)) * 100;
+            trackLine.style.width = percent + '%';
+            const textReal = safeReal >= safeTarget ? safeTarget : safeReal;
+            badge.innerText = safeReal >= safeTarget ? '挑战成功' : `挑战 ${textReal} / ${safeTarget}`;
+            badge.classList.toggle('completed', safeReal >= safeTarget);
+            function setIconActive(el, active) {
+                if (active) {
+                    el.classList.add('active');
+                    if (!el.classList.contains('final')) el.innerHTML = '<i class="fa-solid fa-check"></i>';
+                } else {
+                    el.classList.remove('active');
+                    if (!el.classList.contains('final')) el.innerHTML = '<i class="fa-solid fa-lock"></i>';
+                }
+            }
+            setIconActive(t1, safeReal >= 1);
+            setIconActive(t2, safeReal >= 2);
+            setIconActive(t3, safeReal >= 3);
+            setIconActive(t4, safeReal >= safeTarget);
+        }
+
+        function renderBadge(real, target) {
+            setProgress(real, target);
+        }
+
+        function findMonthProgress(year, month, currentMonthData, allMonthsData) {
+            const result = { realNum: 0, targetNum: 4 };
+            const pick = (arr) => {
+                if (!arr || !arr.length) return null;
+                for (let i = 0; i < arr.length; i++) {
+                    if (Number(arr[i].month) === Number(month)) return arr[i];
+                }
+                return null;
+            };
+            const fromCurrent = currentMonthData && pick(currentMonthData.monthRs || []);
+            if (fromCurrent) return { realNum: fromCurrent.realNum || 0, targetNum: fromCurrent.targetNum || 4 };
+            if (allMonthsData && allMonthsData.length) {
+                for (let i = 0; i < allMonthsData.length; i++) {
+                    const item = allMonthsData[i];
+                    if (item.year && Number(item.year) !== Number(year)) continue;
+                    const found = pick(item.monthRs || []);
+                    if (found) return { realNum: found.realNum || 0, targetNum: found.targetNum || 4 };
+                }
+            }
+            return result;
+        }
+
+        function renderMonths(list) {
+            dropdown.innerHTML = '';
+            if (!list || list.length === 0) {
+                dropdown.innerHTML = '<div class="dd-item">暂无月份数据</div>';
+                return;
+            }
+            list.forEach((item, idx) => {
+                const title = `${item.month}月挑战赛`;
+                const div = document.createElement('div');
+                const isCurrent = state.currentYM && state.currentYM.year === item.year && state.currentYM.month === item.month;
+                div.className = 'dd-item' + (isCurrent ? ' dd-active' : '');
+                div.innerText = (idx === 0 && !isCurrent) ? `${title} (本月)` : title;
+                div.onclick = () => {
+                    selectMonth(item.year, item.month, title);
+                };
+                dropdown.appendChild(div);
+            });
+        }
+
+        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, 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
+
+            // Define the visual order for rendering (2nd, 1st, 3rd)
+            const podiumItems = [
+                { person: p2, className: 'p-2' }, // Left column - Second place
+                { person: p1, className: 'p-1' }, // Middle column - First place
+                { person: p3, className: 'p-3' }  // Right column - Third place
+            ];
+
+            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
+
+                // Only the actual first place gets the crown
+                const isActualFirst = (person === p1); 
+
+                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-box">
+                        <div class="p-name">${name}</div>
+                        <div class="p-score">${score}</div>
+                    </div>
+                `;
+                podiumWrap.appendChild(col);
+            });
+            const remaining = list.slice(3);
+            if (type === 'venue') {
+                state.venueListRendered = remaining;
+            } else {
+                state.scoreListRendered = remaining;
+            }
+            if (state.activeTab === type) renderRankList(remaining);
+        }
+
+        function renderRankList(list) {
+            rankListEl.innerHTML = '';
+            if (!list || list.length === 0) {
+                rankListEl.innerHTML = '<div style="padding:20px; color:#b2bec3; text-align:center;">暂无数据</div>';
+                return;
+            }
+            list.forEach((item, idx) => {
+                const rankNum = item.rankNum || idx + 1;
+                const name = item.nickName || item.name || item.userName || item.teamName || '选手';
+                const scoreVal = (item.score != null ? item.score : item.inRankNum);
+                const score = scoreVal != null ? scoreVal : '--';
+                const team = item.teamName || item.coiName || '';
+                const teamIcon = team ? '<i class="fa-solid fa-user-group"></i> ' : '<i class="fa-solid fa-user"></i> ';
+                const row = document.createElement('div');
+                row.className = 'list-item';
+                row.innerHTML = `
+                    <div class="rank">${rankNum}</div>
+                    <div class="avatar">${buildAvatar(name, rankNum)}</div>
+                    <div class="info">
+                        <div class="name">${name}</div>
+                        <div class="team">${teamIcon}${team || '个人'}</div>
+                    </div>
+                    <div class="score">${score}</div>
+                `;
+                rankListEl.appendChild(row);
+            });
+        }
+
+        function renderMyInfo(myRank, myScore, userInfo) {
+            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 rankVal = myRank && Number(myRank.rankNum);
+            const hasRank = rankVal > 0;
+            rankNumEl.innerText = hasRank ? rankVal : '--';
+            myScoreEl.innerText = hasRank && myScore && myScore.score != null ? myScore.score : '--';
+            const name = (userInfo && (userInfo.nickName || userInfo.userName)) || '我';
+            myNameEl.innerText = name;
+            myTeamEl.innerText = hasRank ? '继续加油' : '';
+            myAvatarEl.innerHTML = buildAvatar(name, 'me');
+        }
+
+        function renderRules(config) {
+            if (!config || !config.configJson) return;
+            try {
+                const parsed = JSON.parse(config.configJson);
+                const rules = parsed.popupRuleList || [];
+                if (rules.length > 0 && rules[0].data && rules[0].data.content) {
+                    ruleContent.innerHTML = rules[0].data.content;
+                }
+            } catch (err) {
+                console.warn('[Rule] parse error', err);
+            }
+        }
+
+        function switchTab(type, tabElement) {
+            state.activeTab = type;
+            document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
+            if (tabElement) tabElement.classList.add('active');
+            const baseList = type === 'venue' ? state.venueList : state.scoreList;
+            renderPodium(baseList, type);
+            const rendered = type === 'venue' ? (state.venueListRendered || baseList.slice(3)) : (state.scoreListRendered || baseList.slice(3));
+            renderRankList(rendered);
+        }
+
+        function demoProgress(step) {
+            setProgress(step, 4);
+        }
+
+        async function loadMonthData(year, month) {
+            state.currentYM = { year: year, month: month };
+            document.getElementById('currentMonthText').innerText = `${month}月挑战赛`;
+            renderMonths(state.months);
+            setLoading(true);
+            try {
+                const monthRank = await API.request('MonthRankDetailQuery', { year: year, month: month, dispArrStr: 'grad,mapNum' });
+                const monthGrad = monthRank && monthRank.gradRs ? monthRank.gradRs : [];
+                const monthMap = monthRank && monthRank.mapNumRs ? monthRank.mapNumRs : [];
+                state.scoreList = monthGrad;
+                state.venueList = monthMap;
+                const prog = findMonthProgress(year, month, state.currentMonthData, state.allMonthsData);
+                renderBadge(prog.realNum, prog.targetNum);
+                renderPodium(state.activeTab === 'venue' ? state.venueList : state.scoreList, state.activeTab);
+                switchTab(state.activeTab, document.querySelector('.tab.active'));
+            } catch (err) {
+                console.error('[Month] 加载失败', err);
+                rankListEl.innerHTML = '<div style="padding:20px; color:#d63031;">数据加载失败,请重试</div>';
+            } finally {
+                setLoading(false);
+            }
+        }
+
+        function selectMonth(year, month, title) {
+            dropdown.style.display = 'none';
+            loadMonthData(year, month);
+        }
+
+        async function initPage() {
+            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);
+            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 });
+            const allowLogin = !useMock && token;
+
+            renderMonths(state.months);
+            setLoading(true);
+
+            async function safeCall(promiseFactory) {
+                try { return await promiseFactory(); }
+                catch (err) { console.warn('[Optional API] ignore error', err); return null; }
+            }
+
+            try {
+                const monthRank = await API.request('MonthRankDetailQuery', { year: ym.year, month: ym.month, dispArrStr: 'grad,mapNum' });
+                let base, currentMonth, allMonths, myRank, myScore, userInfo, cardConfig;
+                if (useMock || allowLogin) {
+                    [base, currentMonth, allMonths, myRank, myScore, userInfo, cardConfig] = await Promise.all([
+                        safeCall(() => API.getCardBase(ecId, 'rank')),
+                        safeCall(() => API.request('CurrentMonthlyChallengeQuery', { ecId: ecId })),
+                        safeCall(() => API.getMonthlyChallenge()),
+                        safeCall(() => API.getUserCurrentRank(ecId)),
+                        safeCall(() => API.getScore(ecId)),
+                        safeCall(() => API.getUserInfo()),
+                        safeCall(() => API.getCardConfig(ecId, 'rank'))
+                    ]);
+                } else {
+                    base = { ecName: `${ym.month}月挑战赛` };
+                    currentMonth = { monthRs: [{ month: ym.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}月挑战赛` };
+                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}月挑战赛`;
+                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);
+                    }
+                    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);
+            }
+        }
+
+        document.addEventListener('click', (e) => {
+            if(!e.target.closest('.month-select') && !e.target.closest('.dropdown')) { dropdown.style.display = 'none'; }
+        });
+
+        document.addEventListener('DOMContentLoaded', initPage);
+    </script>
+</body>
+</html>
+
+

+ 200 - 646
card/sdk/index.html

@@ -3,19 +3,12 @@
 <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>
-    <!-- 本地调试保留 mock_flutter.js,正式接入时移除 -->
-    <script src="./mock_flutter.js"></script>
-    <script src="./bridge.js"></script>
-    <script src="./api.js"></script>
+    <title>11月挑战赛 - 最终封面</title>
     <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
     <style>
         :root {
-            --primary-purple: #593259; 
-            --primary-orange: #fdcb6e;
-            --primary-red: #d63031;
-            --text-dark: #2d3436;
-            --footer-bg: #483055;
+            --primary-purple: #593259;
+            --primary-orange: #ffeaa7;
         }
 
         * {
@@ -27,679 +20,240 @@
             user-select: none;
         }
 
+        /* 1. 背景层:全屏铺满 */
         body {
-            background: #f5f6fa;
-            width: 100%;
+            margin: 0;
+            padding: 0;
             height: 100vh;
+            width: 100vw;
             overflow: hidden;
+            
+            background: linear-gradient(to bottom, rgba(162, 155, 254, 0.2) 0%, rgba(45, 52, 54, 0.95) 100%), 
+                        url('https://img.freepik.com/free-vector/silhouette-trail-runner-running-forest-at-night_105940-705.jpg?w=800') center/cover no-repeat;
+            background-color: #2d3436;
+            
             display: flex;
-            flex-direction: column;
-        }
-
-        /* 顶部 Header */
-        .header-area {
-            height: 280px; 
-            background: linear-gradient(to bottom, rgba(72, 48, 85, 0.7), rgba(45, 52, 54, 0.95)), 
-                        url('https://img.freepik.com/free-vector/silhouette-trail-runner-running-forest-at-night_105940-705.jpg?w=800') center/cover;
-            padding: 20px;
-            padding-top: max(20px, 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; }
-        .icon-btn {
-            width: 36px; height: 36px; 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);
-        }
-        .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); }
-
-        /* 仪表盘卡片 */
-        .dashboard-card {
-            margin-top: 25px;
-            background: rgba(0, 0, 0, 0.4); 
+            justify-content: center;
+            align-items: center;
+        }
+
+        /* 2. 毛玻璃卡片层:距边 2% */
+        .glass-card {
+            width: 96%;
+            height: 96%;
+            
+            /* 描边 & 圆角 */
+            border: 2px solid rgba(255, 255, 255, 0.25);
+            border-bottom: 2px solid rgba(255, 255, 255, 0.1);
+            border-radius: 40px;
+            
+            /* 毛玻璃效果 */
+            background: linear-gradient(to bottom, 
+                rgba(255, 255, 255, 0.15) 0%, 
+                rgba(255, 255, 255, 0.05) 40%, 
+                rgba(0, 0, 0, 0.2) 100%
+            );
             backdrop-filter: blur(10px);
-            border-radius: 20px;
-            padding: 15px 20px;
-            border: 1px solid rgba(255, 255, 255, 0.1);
-            box-shadow: 0 8px 20px rgba(0,0,0,0.2);
+            -webkit-backdrop-filter: blur(10px);
+            
+            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), inset 0 0 0 1px rgba(255,255,255,0.1);
+            
             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-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.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; }
-        
-        .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-active { 
-            position: absolute; top: 50%; left: 10px; 
-            width: 25%; 
-            height: 4px; 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; 
-            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; 
-            transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); 
-        }
-        
-        .trophy-item.active { 
-            background: #fff; border-color: var(--primary-orange); color: var(--primary-orange); 
-            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.active { 
-            background: linear-gradient(135deg, #f1c40f, #e67e22); 
-            color: white; border: none; 
-            transform: scale(1.3); 
-            box-shadow: 0 0 20px rgba(241, 196, 15, 0.6);
-            animation: pulseTrophy 2s infinite;
-        }
-
-        @keyframes pulseTrophy {
-            0% { box-shadow: 0 0 0 0 rgba(241, 196, 15, 0.7); }
-            70% { box-shadow: 0 0 0 10px rgba(241, 196, 15, 0); }
-            100% { box-shadow: 0 0 0 0 rgba(241, 196, 15, 0); }
-        }
-
-        /* 领奖台 */
-        .podium-wrap {
-            height: 140px;
-            display: flex; justify-content: center; align-items: flex-end; 
-            margin-top: -60px; 
-            position: relative; 
-            z-index: 10; 
-            padding-bottom: 0px; 
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+            text-align: center;
+            cursor: pointer;
+            overflow: hidden;
         }
-        .p-col { display: flex; flex-direction: column; align-items: center; width: 30%; position: relative;}
-        .p-2 { z-index: 2; margin-right: -15px; }
-        .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 img { width: 100%; height: 100%; object-fit: cover; }
 
-        .crown {
-            position: absolute; top: -38px; 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; 
-        }
-        @keyframes crownFloat {
-            0%, 100% { transform: translateY(0) rotate(-5deg); }
-            50% { transform: translateY(-8px) rotate(5deg); }
+        .glass-card:active {
+            transform: scale(0.99);
+            transition: transform 0.1s;
+        }
+
+        /* 顶部年份 */
+        .tag-year {
+            font-size: 18px;
+            font-weight: 800;
+            letter-spacing: 3px;
+            color: #fff;
+            margin-bottom: 2vh;
+            background: rgba(255, 255, 255, 0.2);
+            padding: 8px 24px;
+            border-radius: 30px;
+            text-shadow: 0 1px 2px rgba(0,0,0,0.3);
+            border: 1px solid rgba(255,255,255,0.3);
+            box-shadow: 0 4px 15px rgba(0,0,0,0.2);
+        }
+
+        /* 月份数字 - 超大号金色 */
+        .title-month {
+            font-size: 35vmin;
+            font-weight: 900;
+            line-height: 1;
+            
+            /* 金色金属渐变 */
+            background: linear-gradient(to bottom, 
+                #ffffff 10%, 
+                #ffd700 50%, 
+                #ff9f43 100%
+            );
+            -webkit-background-clip: text;
+            -webkit-text-fill-color: transparent;
+            
+            filter: drop-shadow(0 4px 0px rgba(255, 215, 0, 0.3));
+            
+            margin: 1vh 0;
+            z-index: 2;
+        }
+
+        .title-sub {
+            font-size: 20px;
+            font-weight: 800;
+            color: rgba(255, 255, 255, 0.95);
+            letter-spacing: 6px;
+            text-transform: uppercase;
+            margin-top: -1vh;
+            text-shadow: 0 2px 4px rgba(0,0,0,0.6);
+        }
+
+        /* 底部图标容器 */
+        .deco-icon {
+            margin-top: 5vh;
+            width: 80px; height: 80px;
+            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 10px 30px rgba(0,0,0,0.3);
+            animation: trophy-pulse 2s infinite ease-in-out;
+        }
+
+        .deco-icon i {
+            font-size: 40px;
+            color: #ffd700; 
+            filter: drop-shadow(0 0 5px rgba(253, 203, 110, 0.8));
+        }
+
+        /* ================= 新增:查看排行按钮 ================= */
+        .action-btn {
+            position: absolute;
+            bottom: 8%; /* 距离底部的位置 */
+            
+            /* 按钮尺寸与样式 */
+            padding: 14px 40px;
+            border-radius: 50px;
+            border: none;
+            
+            /* 渐变背景:浅金 -> 珊瑚色 */
+            background: linear-gradient(135deg, #ffeaa7 0%, #fab1a0 100%);
+            
+            /* 文字样式:深紫色,高对比度 */
+            color: #593259;
+            font-size: 18px;
+            font-weight: 900;
+            letter-spacing: 1px;
+            
+            /* 阴影与光泽 */
+            box-shadow: 0 10px 25px rgba(255, 118, 117, 0.4), 
+                        inset 0 1px 0 rgba(255,255,255,0.4);
+            
+            display: flex;
+            align-items: center;
+            gap: 10px;
+            
+            z-index: 10;
+            animation: btn-float 3s infinite ease-in-out;
         }
 
-        .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; }
-
-        /* 列表容器 */
-        .list-container {
-            flex: 1; background: white; border-radius: 24px 24px 0 0;
-            padding: 0 20px 120px 20px; 
-            overflow-y: auto; 
-            margin-top: -10px;
-            box-shadow: 0 -5px 20px rgba(0,0,0,0.05);
-            position: relative; z-index: 8; 
-            -webkit-overflow-scrolling: touch; 
+        /* 按钮悬浮动画 */
+        @keyframes btn-float {
+            0%, 100% { transform: translateY(0); }
+            50% { transform: translateY(-5px); }
         }
-        
-        .tabs { 
-            display: flex; justify-content: center; gap: 15px; 
-            position: sticky; top: 0; background: white; z-index: 9;
-            padding-top: 20px; padding-bottom: 10px;
-        }
-        
-        .tab { padding: 8px 20px; border-radius: 20px; font-size: 14px; color: #636e72; background: #f1f2f6; cursor: pointer; transition: 0.2s; }
-        .tab.active { background: var(--text-dark); color: #fdcb6e; font-weight: bold; }
-        
-        .list-item { display: flex; align-items: center; padding: 12px 0; border-bottom: 1px solid #f1f2f6; }
-        .rank { width: 30px; text-align: center; font-weight: bold; color: #b2bec3; font-style: italic;}
-        .avatar { width: 40px; height: 40px; border-radius: 50%; margin: 0 12px; background: #eee; overflow: hidden;}
-        .avatar img { width: 100%; height: 100%; object-fit: cover;}
-        .info { flex: 1; }
-        .name { font-size: 14px; color: #2d3436; font-weight: bold; }
-        .team { font-size: 11px; color: #636e72; display: flex; align-items: center; gap: 4px; }
-        .score { font-size: 16px; font-weight: bold; color: var(--primary-purple); }
 
-        /* 底部我的排名 */
-        .my-rank-bar {
-            position: fixed; bottom: 0; left: 0; width: 100%; height: 55px; 
-            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; 
-            border-radius: 24px 24px 0 0; box-shadow: 0 -5px 20px rgba(0,0,0,0.2); z-index: 99; 
+        @keyframes trophy-pulse {
+            0% { transform: scale(1) translateY(0); box-shadow: 0 0 0 0 rgba(253, 203, 110, 0.2); }
+            50% { transform: scale(1.1) translateY(-5px); box-shadow: 0 0 30px 10px rgba(253, 203, 110, 0.4); }
+            100% { transform: scale(1) translateY(0); box-shadow: 0 0 0 0 rgba(253, 203, 110, 0); }
         }
-        .my-rank-bar .rank { font-size: 16px; }
-        .my-rank-bar .avatar { width: 34px; height: 34px; border-width: 2px; }
-        .my-rank-bar .name { font-size: 14px; }
-        .my-rank-bar .team { font-size: 10px; }
-        .my-score { font-size: 18px; font-weight: bold; color: #ffffff !important; margin-left: auto; }
 
-        /* 演示按钮样式 (模态框内) */
-        .demo-section {
-            margin: 20px 0 0 0;
-            border-top: 1px dashed #ddd;
-            padding-top: 15px;
+        /* 顶部反光效果 */
+        .shine {
+            position: absolute;
+            top: -15%; left: -15%;
+            width: 60%; height: 40%;
+            background: radial-gradient(circle, rgba(255,255,255,0.4) 0%, transparent 70%);
+            z-index: 3;
+            pointer-events: none;
+            filter: blur(30px);
+            opacity: 0.6;
         }
-        .demo-label { font-size: 12px; color: #999; margin-bottom: 10px; }
-        .demo-controls {
-            display: flex; justify-content: center; gap: 10px;
-        }
-        .demo-btn {
-            background: #eee; border: none; padding: 6px 12px; border-radius: 8px; font-size: 12px; color: #555; cursor: pointer;
-        }
-        .demo-btn:active { background: #ddd; color: #000; }
-
-        /* 下拉菜单 & 模态框 */
-        .dropdown { position: absolute; top: 70px; left: 50%; transform: translateX(-50%); width: 200px; background: white; border-radius: 12px; box-shadow: 0 10px 50px rgba(0,0,0,0.4); display: none; flex-direction: column; overflow: hidden; z-index: 200; }
-        .dropdown.show { display: flex; animation: dropIn 0.2s ease-out; }
-        .dd-item { padding: 12px; color: #636e72; font-size: 14px; text-align: center; border-bottom: 1px solid #f1f2f6; cursor: pointer; }
-        .dd-active { color: var(--primary-purple); font-weight: bold; background: #f9f0ff; }
-        @keyframes dropIn { from{opacity:0; transform:translateX(-50%) translateY(-10px);} to{opacity:1; transform:translateX(-50%) translateY(0);} }
 
-        .modal-mask { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 999; display: flex; justify-content: center; align-items: center; opacity: 0; pointer-events: none; transition: opacity 0.3s; }
-        .modal-mask.show { opacity: 1; pointer-events: auto; }
-        .modal-body { width: 80%; max-width: 320px; background: white; border-radius: 24px; padding: 25px; text-align: center; transform: scale(0.8); transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); border: 4px solid var(--primary-purple); box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
-        .modal-mask.show .modal-body { transform: scale(1); }
-        .m-close { background: var(--primary-purple); color: white; padding: 10px 25px; border-radius: 20px; border: none; font-weight: bold; cursor: pointer; margin-top: 15px; }
-        .rule-box { text-align: left; background: #f9f4ff; border: 1px solid #e3d7ff; border-radius: 12px; padding: 12px; color: #4b3a67; font-size: 13px; line-height: 1.6; box-shadow: inset 0 1px 0 rgba(255,255,255,0.7); }
-        .rule-item { display: flex; gap: 10px; align-items: flex-start; margin-bottom: 8px; }
-        .rule-item:last-child { margin-bottom: 0; }
-        .rule-item i { color: var(--primary-orange); margin-top: 2px; }
-
-        /* 加载遮罩 */
-        .loading-mask {
-            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-spinner {
-            border: 4px solid rgba(255,255,255,0.3);
-            border-top-color: #fdcb6e;
-            border-radius: 50%;
-            width: 36px; height: 36px;
-            animation: spin 1s linear infinite;
-            margin-right: 10px;
-        }
-        @keyframes spin { to { transform: rotate(360deg); } }
     </style>
 </head>
 <body>
 
-    <!-- 头部 -->
-    <div class="header-area">
-        <div class="nav-bar">
-            <div class="icon-btn" onclick="handleBack()"><i class="fa-solid fa-chevron-left" style="color:white"></i></div>
-            <div class="month-select" onclick="toggleDropdown()">
-                <span id="currentMonthText">11月挑战赛</span> <i class="fa-solid fa-caret-down"></i>
-            </div>
-            <div class="icon-btn" onclick="openModal()"><i class="fa-solid fa-question" style="color:white"></i></div>
-        </div>
-
-        <!-- 仪表盘卡片 -->
-        <div class="dashboard-card">
-            <div class="dash-header">
-                <div class="dash-title">
-                    <i class="fa-solid fa-medal dash-icon"></i> 定向达人
-                </div>
-                <div class="dash-badge" id="dashBadge">挑战 1 / 4</div>
-            </div>
-
-            <div class="trophy-track-container">
-                <div class="track-line-bg"></div>
-                <div class="track-line-active" id="progressLine"></div>
-                
-                <!-- 图标1 -->
-                <div class="trophy-item active" id="t1"><i class="fa-solid fa-check"></i></div>
-                <!-- 图标2 -->
-                <div class="trophy-item" id="t2"><i class="fa-solid fa-lock"></i></div>
-                <!-- 图标3 -->
-                <div class="trophy-item" id="t3"><i class="fa-solid fa-lock"></i></div>
-                <!-- 图标4 (Final) -->
-                <div class="trophy-item final" id="t4"><i class="fa-solid fa-trophy"></i></div>
-            </div>
-        </div>
-    </div>
-
-    <!-- 下拉菜单 -->
-    <div class="dropdown" id="dropdown"></div>
-
-    <!-- 领奖台 -->
-    <div class="podium-wrap" id="podiumWrap"></div>
-
-    <!-- 列表区 -->
-    <div class="list-container">
-        <div class="tabs">
-            <div class="tab active" onclick="switchTab('score', this)">积分排行</div>
-            <div class="tab" onclick="switchTab('venue', this)">场地排行</div>
-        </div>
+    <!-- 点击整个区域都能跳转 -->
+    <div class="glass-card" onclick="redirectToDetail()">
+        <div class="shine"></div>
         
-        <div id="rankList"></div>
-    </div>
-
-    <!-- 底部我的排名 -->
-    <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="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>
-
-    <!-- 模态框 -->
-    <div class="modal-mask" id="infoModal">
-        <div class="modal-body">
-                        <h3 style="color:#593259; margin-bottom:15px;">📜 规则说明</h3>
-            <div id="ruleContent" class="rule-box">
-                <div class="rule-item"><i class="fa-solid fa-bullseye"></i><div><strong>积分规则:</strong> 在指定公园找到打卡点,耗时越短积分越高。</div></div>
-                <div class="rule-item"><i class="fa-solid fa-map-location-dot"></i><div><strong>场地排行:</strong> 按解锁的公园场地数量排名。</div></div>
-                <div class="rule-item"><i class="fa-solid fa-trophy"></i><div><strong>奖杯获取:</strong> 完成4次有效挑战点亮全部奖杯。</div></div>
-            </div>
-
-            <!-- 演示功能区 -->
-            <div class="demo-section">
-                <div class="demo-label">✨ 功能演示</div>
-                <div class="demo-controls">
-                    <button class="demo-btn" onclick="demoProgress(1);closeModal()">1/4</button>
-                    <button class="demo-btn" onclick="demoProgress(3);closeModal()">3/4</button>
-                    <button class="demo-btn" onclick="demoProgress(4);closeModal()">4/4</button>
-                </div>
-            </div>
-
-            <button class="m-close" onclick="closeModal()">知道了</button>
+        <div class="tag-year">2025 年</div>
+        
+        <div class="title-month">11</div>
+        <div class="title-sub">月度挑战赛</div>
+        
+        <div class="deco-icon">
+            <i class="fa-solid fa-trophy"></i>
         </div>
-    </div>
 
-    <!-- 加载中遮罩 -->
-    <div class="loading-mask" id="loadingMask">
-        <div class="loading-spinner"></div>
-        <div>数据加载中...</div>
+        <!-- 替代原来的文字提示,使用显眼的按钮 -->
+        <button class="action-btn">
+            查看排行 <i class="fa-solid fa-arrow-right"></i>
+        </button>
     </div>
 
+    <!-- 本地调试保留 mock_flutter.js,正式接入时移除 -->
+    <script src="./mock_flutter.js"></script>
+    <script src="./bridge.js"></script>
     <script>
-        const dropdown = document.getElementById('dropdown');
-        const rankListEl = document.getElementById('rankList');
-        const trackLine = document.getElementById('progressLine');
-        const podiumWrap = document.getElementById('podiumWrap');
-        const ruleContent = document.getElementById('ruleContent');
-        const loadingMask = document.getElementById('loadingMask');
-        const state = {
-            activeTab: 'score',
-            scoreList: [],
-            scoreListRendered: null,
-            venueList: [],
-            venueListRendered: null,
-            months: [],
-            currentYM: null,
-            currentMonthData: null,
-            allMonthsData: null
-        };
-
-        function getQuery(name) {
+        function getQueryParam(name) {
             const params = new URLSearchParams(window.location.search);
             return params.get(name);
         }
 
-        function getYearMonth() {
-            const now = new Date();
-            const year = parseInt(getQuery('year'), 10) || now.getFullYear();
-            const month = parseInt(getQuery('month'), 10) || (now.getMonth() + 1);
-            return { year: year, month: month };
-        }
-        
-        function getRecentMonths(count) {
-            const ym = getYearMonth();
-            const list = [];
-            let y = ym.year;
-            let m = ym.month;
-            for (let i = 0; i < (count || 3); i++) {
-                list.push({ year: y, month: m });
-                m -= 1;
-                if (m === 0) { m = 12; y -= 1; }
-            }
-            return list;
-        }
-
-        function handleBack() {
-            if (window.Bridge && Bridge.back) Bridge.back();
-            else window.history.back();
-        }
-
-        function openModal() { document.getElementById('infoModal').classList.add('show'); }
-        function closeModal() { document.getElementById('infoModal').classList.remove('show'); }
-        function toggleDropdown() { dropdown.style.display = (dropdown.style.display === 'flex') ? 'none' : 'flex'; }
-
-        function setLoading(isLoading) {
-            rankListEl.style.opacity = isLoading ? '0.4' : '1';
-            loadingMask.classList.toggle('show', isLoading);
-        }
-
-        function buildAvatar(name, salt) {
-            const seedBase = name || 'user';
-            const seed = encodeURIComponent(seedBase + (salt || ''));
-            return `<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=${seed}" alt="">`;
-        }
+        function redirectToDetail() {
+            const token = getQueryParam('token');
+            const id = getQueryParam('id');
+            let detailUrl = 'detail.html';
 
-        function setProgress(real, target) {
-            const badge = document.getElementById('dashBadge');
-            const t1 = document.getElementById('t1');
-            const t2 = document.getElementById('t2');
-            const t3 = document.getElementById('t3');
-            const t4 = document.getElementById('t4');
-            const safeReal = Math.max(real || 0, 0);
-            const safeTarget = target && target > 0 ? target : 4;
-            const ratio = Math.min(safeReal / safeTarget, 1);
-            const percent = Math.max(0, Math.min(1, ratio)) * 100;
-            trackLine.style.width = percent + '%';
-            const textReal = safeReal >= safeTarget ? safeTarget : safeReal;
-            badge.innerText = safeReal >= safeTarget ? '挑战成功' : `挑战 ${textReal} / ${safeTarget}`;
-            badge.classList.toggle('completed', safeReal >= safeTarget);
-            function setIconActive(el, active) {
-                if (active) {
-                    el.classList.add('active');
-                    if (!el.classList.contains('final')) el.innerHTML = '<i class="fa-solid fa-check"></i>';
-                } else {
-                    el.classList.remove('active');
-                    if (!el.classList.contains('final')) el.innerHTML = '<i class="fa-solid fa-lock"></i>';
-                }
+            const queryParams = [];
+            if (token) {
+                queryParams.push(`token=${encodeURIComponent(token)}`);
             }
-            setIconActive(t1, safeReal >= 1);
-            setIconActive(t2, safeReal >= 2);
-            setIconActive(t3, safeReal >= 3);
-            setIconActive(t4, safeReal >= safeTarget);
-        }
-
-        function renderBadge(real, target) {
-            setProgress(real, target);
-        }
-
-        function findMonthProgress(year, month, currentMonthData, allMonthsData) {
-            const result = { realNum: 0, targetNum: 4 };
-            const pick = (arr) => {
-                if (!arr || !arr.length) return null;
-                for (let i = 0; i < arr.length; i++) {
-                    if (Number(arr[i].month) === Number(month)) return arr[i];
-                }
-                return null;
-            };
-            const fromCurrent = currentMonthData && pick(currentMonthData.monthRs || []);
-            if (fromCurrent) return { realNum: fromCurrent.realNum || 0, targetNum: fromCurrent.targetNum || 4 };
-            if (allMonthsData && allMonthsData.length) {
-                for (let i = 0; i < allMonthsData.length; i++) {
-                    const item = allMonthsData[i];
-                    if (item.year && Number(item.year) !== Number(year)) continue;
-                    const found = pick(item.monthRs || []);
-                    if (found) return { realNum: found.realNum || 0, targetNum: found.targetNum || 4 };
-                }
-            }
-            return result;
-        }
-
-        function renderMonths(list) {
-            dropdown.innerHTML = '';
-            if (!list || list.length === 0) {
-                dropdown.innerHTML = '<div class="dd-item">暂无月份数据</div>';
-                return;
+            if (id) {
+                queryParams.push(`id=${encodeURIComponent(id)}`);
             }
-            list.forEach((item, idx) => {
-                const title = `${item.month}月挑战赛`;
-                const div = document.createElement('div');
-                const isCurrent = state.currentYM && state.currentYM.year === item.year && state.currentYM.month === item.month;
-                div.className = 'dd-item' + (isCurrent ? ' dd-active' : '');
-                div.innerText = (idx === 0 && !isCurrent) ? `${title} (本月)` : title;
-                div.onclick = () => {
-                    selectMonth(item.year, item.month, title);
-                };
-                dropdown.appendChild(div);
-            });
-        }
 
-        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;
+            if (queryParams.length > 0) {
+                detailUrl += `?${queryParams.join('&')}`;
             }
-            const top3 = list.slice(0, 3);
-            const cols = ['p-2', 'p-1', 'p-3'];
-            top3.forEach((item, idx) => {
-                const col = document.createElement('div');
-                col.className = 'p-col ' + cols[idx];
-                const name = item.nickName || item.name || item.userName || '选手';
-                const score = item.score != null ? item.score : (item.inRankNum != null ? item.inRankNum : '--');
-                const rankNum = item.rankNum || idx + 1;
-                const isFirst = idx === 1;
-                col.innerHTML = `
-                    ${isFirst ? '<div class="crown"><i class="fa-solid fa-crown"></i></div>' : ''}
-                    <div class="p-img">${buildAvatar(name, rankNum)}</div>
-                    <div class="p-box">
-                        <div class="p-name">${name}</div>
-                        <div class="p-score">${score}</div>
-                    </div>
-                `;
-                podiumWrap.appendChild(col);
-            });
-            const remaining = list.slice(3);
-            if (type === 'venue') {
-                state.venueListRendered = remaining;
+            
+            // Add full=true for Flutter interception
+            detailUrl += (detailUrl.includes('?') ? '&' : '?') + 'full=true';
+
+            console.log("Navigating from sdk/index.html to:", detailUrl);
+            
+            // Use Bridge.appAction for navigation
+            if (window.Bridge && window.Bridge.appAction) {
+                Bridge.appAction(detailUrl);
             } else {
-                state.scoreListRendered = remaining;
-            }
-            if (state.activeTab === type) renderRankList(remaining);
-        }
-
-        function renderRankList(list) {
-            rankListEl.innerHTML = '';
-            if (!list || list.length === 0) {
-                rankListEl.innerHTML = '<div style="padding:20px; color:#b2bec3; text-align:center;">暂无数据</div>';
-                return;
+                console.error("Bridge or Bridge.appAction is not defined. Falling back to window.location.href.");
+                window.location.href = detailUrl; // Fallback for pure browser environment
             }
-            list.forEach((item, idx) => {
-                const rankNum = item.rankNum || idx + 1;
-                const name = item.nickName || item.name || item.userName || item.teamName || '选手';
-                const scoreVal = (item.score != null ? item.score : item.inRankNum);
-                const score = scoreVal != null ? scoreVal : '--';
-                const team = item.teamName || item.coiName || '';
-                const teamIcon = team ? '<i class="fa-solid fa-user-group"></i> ' : '<i class="fa-solid fa-user"></i> ';
-                const row = document.createElement('div');
-                row.className = 'list-item';
-                row.innerHTML = `
-                    <div class="rank">${rankNum}</div>
-                    <div class="avatar">${buildAvatar(name, rankNum)}</div>
-                    <div class="info">
-                        <div class="name">${name}</div>
-                        <div class="team">${teamIcon}${team || '个人'}</div>
-                    </div>
-                    <div class="score">${score}</div>
-                `;
-                rankListEl.appendChild(row);
-            });
-        }
-
-        function renderMyInfo(myRank, myScore, userInfo) {
-            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 rankVal = myRank && Number(myRank.rankNum);
-            const hasRank = rankVal > 0;
-            rankNumEl.innerText = hasRank ? rankVal : '--';
-            myScoreEl.innerText = hasRank && myScore && myScore.score != null ? myScore.score : '--';
-            const name = (userInfo && (userInfo.nickName || userInfo.userName)) || '我';
-            myNameEl.innerText = name;
-            myTeamEl.innerText = hasRank ? '继续加油' : '';
-            myAvatarEl.innerHTML = buildAvatar(name, 'me');
         }
-
-        function renderRules(config) {
-            if (!config || !config.configJson) return;
-            try {
-                const parsed = JSON.parse(config.configJson);
-                const rules = parsed.popupRuleList || [];
-                if (rules.length > 0 && rules[0].data && rules[0].data.content) {
-                    ruleContent.innerHTML = rules[0].data.content;
-                }
-            } catch (err) {
-                console.warn('[Rule] parse error', err);
-            }
-        }
-
-        function switchTab(type, tabElement) {
-            state.activeTab = type;
-            document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
-            if (tabElement) tabElement.classList.add('active');
-            const baseList = type === 'venue' ? state.venueList : state.scoreList;
-            renderPodium(baseList, type);
-            const rendered = type === 'venue' ? (state.venueListRendered || baseList.slice(3)) : (state.scoreListRendered || baseList.slice(3));
-            renderRankList(rendered);
-        }
-
-        function demoProgress(step) {
-            setProgress(step, 4);
-        }
-
-        async function loadMonthData(year, month) {
-            state.currentYM = { year: year, month: month };
-            document.getElementById('currentMonthText').innerText = `${month}月挑战赛`;
-            renderMonths(state.months);
-            setLoading(true);
-            try {
-                const monthRank = await API.request('MonthRankDetailQuery', { year: year, month: month, dispArrStr: 'grad,mapNum' });
-                const monthGrad = monthRank && monthRank.gradRs ? monthRank.gradRs : [];
-                const monthMap = monthRank && monthRank.mapNumRs ? monthRank.mapNumRs : [];
-                state.scoreList = monthGrad;
-                state.venueList = monthMap;
-                const prog = findMonthProgress(year, month, state.currentMonthData, state.allMonthsData);
-                renderBadge(prog.realNum, prog.targetNum);
-                renderPodium(state.activeTab === 'venue' ? state.venueList : state.scoreList, state.activeTab);
-                switchTab(state.activeTab, document.querySelector('.tab.active'));
-            } catch (err) {
-                console.error('[Month] 加载失败', err);
-                rankListEl.innerHTML = '<div style="padding:20px; color:#d63031;">数据加载失败,请重试</div>';
-            } finally {
-                setLoading(false);
-            }
-        }
-
-        function selectMonth(year, month, title) {
-            dropdown.style.display = 'none';
-            loadMonthData(year, month);
-        }
-
-        async function initPage() {
-            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);
-            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 });
-            const allowLogin = !useMock && token;
-            if (!token && !useMock && window.Bridge && Bridge.getToken) Bridge.getToken();
-            renderMonths(state.months);
-            setLoading(true);
-
-            async function safeCall(promiseFactory) {
-                try { return await promiseFactory(); }
-                catch (err) { console.warn('[Optional API] ignore error', err); return null; }
-            }
-
-            try {
-                const monthRank = await API.request('MonthRankDetailQuery', { year: ym.year, month: ym.month, dispArrStr: 'grad,mapNum' });
-                let base, currentMonth, allMonths, myRank, myScore, userInfo, cardConfig;
-                if (useMock || allowLogin) {
-                    [base, currentMonth, allMonths, myRank, myScore, userInfo, cardConfig] = await Promise.all([
-                        safeCall(() => API.getCardBase(ecId, 'rank')),
-                        safeCall(() => API.request('CurrentMonthlyChallengeQuery', { ecId: ecId })),
-                        safeCall(() => API.getMonthlyChallenge()),
-                        safeCall(() => API.getUserCurrentRank(ecId)),
-                        safeCall(() => API.getScore(ecId)),
-                        safeCall(() => API.getUserInfo()),
-                        safeCall(() => API.getCardConfig(ecId, 'rank'))
-                    ]);
-                } else {
-                    base = { ecName: `${ym.month}月挑战赛` };
-                    currentMonth = { monthRs: [{ month: ym.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}月挑战赛` };
-                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}月挑战赛`;
-                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);
-                    }
-                    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);
-            }
-        }
-
-        document.addEventListener('click', (e) => {
-            if(!e.target.closest('.month-select') && !e.target.closest('.dropdown')) { dropdown.style.display = 'none'; }
-        });
-
-        document.addEventListener('DOMContentLoaded', initPage);
     </script>
 </body>
-</html>
-
-
+</html>

+ 25 - 0
card/sdk/mock_flutter.js

@@ -62,6 +62,31 @@
                 }
             }, 1000);
             break;
+
+        // --- 新增 Mock 方法 ---
+        case 'previewImage':
+            alert(`[模拟App] 预览图片\n当前: ${data.current}\n列表: ${JSON.stringify(data.urls)}`);
+            break;
+        case 'makePhoneCall':
+            alert(`[模拟App] 拨打电话: ${data.phoneNumber}`);
+            break;
+        case 'setClipboardData':
+            alert(`[模拟App] 剪贴板内容已设置: ${data.data}`);
+            break;
+        case 'showToast':
+            console.log(`[模拟App] ShowToast: ${data.title} (icon: ${data.icon})`);
+            break;
+        case 'showLoading':
+            console.log(`[模拟App] ShowLoading: ${data.title}`);
+            break;
+        case 'hideLoading':
+            console.log(`[模拟App] HideLoading`);
+            break;
+        case 'showModal':
+            const confirmed = confirm(`[模拟App] Modal: ${data.title}\n${data.content || ''}`);
+            console.log(`[模拟App] Modal result: ${confirmed ? 'Confirm' : 'Cancel'}`);
+            break;
+
         default:
           console.log(`[模拟App] 收到未知指令: ${action}, 数据: `, data);
           // alert(`[模拟App] 收到未知指令: ${action}`);