zhangyan 1 miesiąc temu
rodzic
commit
07c44cc59a

+ 29 - 0
card/sdk/AGENTS.md

@@ -0,0 +1,29 @@
+# Repository Guidelines
+
+## 项目结构与模块组织
+- 核心库:`bridge.js` 负责原生桥接,`api.js` 封装服务器请求;每个页面都需优先加载。`mock_flutter.js` 仅限桌面调试使用。
+- 根目录的 `index.html` 与 `demo.html` 展示接入方式;接口文档位于 `API.md`、`API_SERVER.md`。
+- `demo_project/` 提供完整示例流程(`index.html`、`rank.html`、`signup.html`、`detail.html`),复用同一 SDK;新增页面可按其目录布局,页面私有资源放在对应 HTML 同目录。
+
+## 构建、测试与开发命令
+- 静态项目;直接在浏览器打开 `index.html` 或 `demo_project/index.html` 即可快速查看。
+- 推荐本地起服务避免 CORS:`python -m http.server 8000`,访问 `http://localhost:8000/demo_project/index.html`。
+- 交付前:移除 `<script src="./mock_flutter.js"></script>`,并在 API 初始化时填入真实 token 后再打包压缩。
+
+## 编码风格与命名约定
+- JavaScript 保持 ES5 兼容:使用 IIFE、`var`、4 空格缩进、分号,文件顶部写 `'use strict'`。
+- 函数/变量用 lowerCamelCase,常量用 UPPER_SNAKE_CASE,HTML/CSS 类名用 kebab-case。
+- 网络与桥接工具函数集中在现有文件内;未经约定不要引入打包器或模块加载器。
+
+## 测试指南
+- 以手动验证为主:开启 `mock_flutter.js` 先跑 Mock 流程,再关闭它并连真实 App Bridge/API 复测。
+- 重点检查 token 处理(URL 参数与 `Bridge.getToken()` 兜底)、`demo_project/rank.html` 与 `signup.html` 的排行榜/报名流程、以及通过 `API.getOssUrl` 拼接的 OSS 链接。
+- 如新增自动化,请在文档中写明运行方式,并与上述手动步骤并行保留。
+
+## 提交与 Pull Request 指南
+- 当前无历史记录,建议沿用 Conventional Commit 前缀(`feat:`、`fix:`、`chore:`),范围简洁。
+- PR 内容需包含:摘要、影响的页面/文件、手动测试记录(浏览器 + mock/真实环境)、UI 变更的截图或录屏,以及关联的文档更新。
+
+## 安全与配置提示
+- 不要在控制台或仓库中暴露 token 和用户数据;本地服务时优先用环境变量注入配置。
+- 交付前确认 `Config.useMock` 设为 `false`,并检查生产接口地址配置正确。

+ 0 - 0
card/sdk_delivery/API.md → card/sdk/API.md


+ 22 - 1
card/sdk_delivery/API_SERVER.md → card/sdk/API_SERVER.md

@@ -171,9 +171,30 @@
                 }
             ],
             "teamRankRs": [], // 队伍榜
-            "inTeamRs": []    // 队内榜
+              "inTeamRs": []    // 队内榜
+          }
+          ```
+
+*   **3.1.1 月挑战排名查询 (月榜)**
+    *   **API 方法**: `API.request('MonthRankDetailQuery', { year, month, dispArrStr })`
+    *   **参数**:
+        *   `year` (int): 年份,如 2025
+        *   `month` (int): 月份,1-12
+        *   `dispArrStr` (string): 固定传 `grad,mapNum`(分别返回个人榜、场地榜)
+    *   **示例**:
+        ```bash
+        curl -X POST -H "Content-Type: application/x-www-form-urlencoded" \
+          -d "year=2025&month=11&dispArrStr=grad,mapNum" \
+          https://colormaprun.com/api/card/MonthRankDetailQuery
+        ```
+    *   **返回数据**:
+        ```json
+        {
+          "gradRs": [ { "userName": "张三", "rankNum": 1, "inRankNum": 264, "isSelf": 0, "isInGame": 0 }, ... ],
+          "mapNumRs": [ { "userName": "Q", "rankNum": 1, "inRankNum": 1, "isSelf": 0, "isInGame": 0 }, ... ]
         }
         ```
+        *说明*: 部分环境允许不带 token 访问月榜;若需鉴权同样在 Header 携带 `token`。
 
 *   **3.2 卡片用户当前排名查询**
     *   **API 方法**: `API.getUserCurrentRank(ecId)`

+ 0 - 0
card/sdk_delivery/GUIDE.md → card/sdk/GUIDE.md


+ 2 - 2
card/sdk_delivery/demo_project/api.js → card/sdk/api.js

@@ -9,7 +9,7 @@
 
     // 基础配置
     var Config = {
-        baseUrl: 'https://t-mapi.colormaprun.com/api/card/', 
+        baseUrl: 'https://colormaprun.com/api/card/', 
         ossUrl: 'http://oss-card.colormaprun.com/card/',
         token: '',
         useMock: false 
@@ -193,7 +193,7 @@
 
             console.log('[API] Request:', endpoint, data);
 
-            return fetch(url, { method: 'POST', headers: headers, body: formData })
+            return fetch(url, { method: 'POST', headers: headers, body: formData, mode: 'cors', credentials: 'omit' })
             .then(function(response) { return response.json(); })
             .then(function(res) {
                 console.log('[API] Response:', endpoint, res);

+ 0 - 0
card/sdk_delivery/bridge.js → card/sdk/bridge.js


+ 0 - 0
card/sdk_delivery/demo.html → card/sdk/demo.html


+ 2 - 2
card/sdk_delivery/api.js → card/sdk/demo_project/api.js

@@ -9,7 +9,7 @@
 
     // 基础配置
     var Config = {
-        baseUrl: 'https://t-mapi.colormaprun.com/api/card/', 
+        baseUrl: 'https://colormaprun.com/api/card/', 
         ossUrl: 'http://oss-card.colormaprun.com/card/',
         token: '',
         useMock: false 
@@ -193,7 +193,7 @@
 
             console.log('[API] Request:', endpoint, data);
 
-            return fetch(url, { method: 'POST', headers: headers, body: formData })
+            return fetch(url, { method: 'POST', headers: headers, body: formData, mode: 'cors', credentials: 'omit' })
             .then(function(response) { return response.json(); })
             .then(function(res) {
                 console.log('[API] Response:', endpoint, res);

+ 0 - 0
card/sdk_delivery/demo_project/bridge.js → card/sdk/demo_project/bridge.js


+ 695 - 0
card/sdk/demo_project/index.html

@@ -0,0 +1,695 @@
+<!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('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); 
+            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; }
+
+        /* 加载遮罩 */
+        .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>
+            <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>
+    </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.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 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;
+            }
+            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;
+            } 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);
+            }
+        }
+
+        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>

+ 695 - 0
card/sdk/index.html

@@ -0,0 +1,695 @@
+<!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('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); 
+            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; }
+
+        /* 加载遮罩 */
+        .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>
+            <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>
+    </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.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 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;
+            }
+            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;
+            } 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);
+            }
+        }
+
+        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>

+ 0 - 0
card/sdk_delivery/demo_project/mock_flutter.js → card/sdk/mock_flutter.js


+ 0 - 219
card/sdk_delivery/demo_project/demo.html

@@ -1,219 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>ColorMapRun SDK 全功能测试台</title>
-    <style>
-        body { font-family: sans-serif; padding: 20px; background: #f0f2f5; display: flex; gap: 20px; }
-        .sidebar { width: 300px; flex-shrink: 0; display: flex; flex-direction: column; gap: 10px; height: 90vh; overflow-y: auto; padding-right: 10px; }
-        .main { flex: 1; display: flex; flex-direction: column; height: 90vh; }
-        
-        h2, h3 { margin-top: 0; color: #333; }
-        h3 { font-size: 16px; margin-bottom: 10px; padding-bottom: 5px; border-bottom: 2px solid #007aff; display: inline-block; }
-        
-        .group { margin-bottom: 20px; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.05); }
-        .group-title { font-weight: bold; margin-bottom: 10px; color: #555; }
-        
-        .btn-grid { display: grid; grid-template-columns: 1fr; gap: 8px; }
-        button { padding: 8px 12px; font-size: 13px; border: 1px solid #ddd; background: #fff; border-radius: 4px; cursor: pointer; text-align: left; transition: all 0.2s; }
-        button:hover { background: #f0f8ff; border-color: #007aff; color: #007aff; }
-        button.active { background: #007aff; color: white; border-color: #007aff; }
-        
-        .log-area { flex: 1; background: #1e1e1e; color: #0f0; padding: 15px; border-radius: 8px; font-family: 'Consolas', monospace; font-size: 13px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; line-height: 1.5; }
-        .log-entry { margin-bottom: 10px; border-bottom: 1px dashed #444; padding-bottom: 5px; }
-        .log-time { color: #888; font-size: 11px; }
-        .log-label { color: #fff; font-weight: bold; }
-        .log-error { color: #ff4d4f; }
-    </style>
-
-    <!-- SDK -->
-    <script src="./mock_flutter.js"></script> <!-- 确保 mock_flutter.js 在 bridge.js 和 api.js 之前引入,以便正确模拟全局对象 -->
-    <script src="./bridge.js"></script>
-    <script src="./api.js"></script>
-</head>
-<body>
-
-    <div class="sidebar">
-        <h2>SDK 测试台</h2>
-        <div style="font-size: 12px; color: #666; margin-bottom: 10px;">当前环境: <span id="envInfo">检测中...</span></div>
-        <button onclick="clearLog()" style="text-align: center; margin-bottom: 10px;">🗑️ 清空日志</button>
-
-        <!-- App Bridge -->
-        <div class="group">
-            <div class="group-title">📱 App 交互 (Bridge)</div>
-            <div class="btn-grid">
-                <button onclick="run('Bridge.openMap(latitude, longitude, name)', Bridge.openMap, 39.9, 116.4, '北京')">打开地图</button>
-                <button onclick="run('Bridge.openMatch(id, type)', Bridge.openMatch, MOCK_MCID, 1)">打开赛事详情</button>
-                <button onclick="run('Bridge.openActivityList(id, mapName)', Bridge.openActivityList, 202, '奥体')">打开活动列表</button>
-                <button onclick="run('Bridge.shareWx({title, url, image, scene})', Bridge.shareWx, {title:'Demo分享', url:location.href, image:'https://picsum.photos/100/100'})">微信分享</button>
-                <button onclick="run('Bridge.launchWxMini(username, path)', Bridge.launchWxMini, 'gh_bea09156da8d', 'pages/index/index')">打开小程序</button>
-                <button onclick="run('Bridge.saveImage(base64Str)', Bridge.saveImage, 'data:image/png;base64,iVBORw0KGgoAAA...')">保存图片</button>
-                <button onclick="run('Bridge.back()', Bridge.back)">返回上一页</button>
-                <button onclick="run('Bridge.toHome()', Bridge.toHome)">返回App首页</button>
-                <button onclick="run('Bridge.toLogin()', Bridge.toLogin)">跳转登录</button>
-                <button onclick="run('Bridge.setTitle(title)', Bridge.setTitle, '新标题')">设置标题</button>
-                <button onclick="getToken()">获取 Token (Bridge)</button>
-            </div>
-        </div>
-
-        <!-- API: Config -->
-        <div class="group">
-            <div class="group-title">1. 配置与基础</div>
-            <div class="btn-grid">
-                <button onclick="api('API.getCardBase(ecId, pageName)', API.getCardBase, MOCK_ECID, 'index')">getCardBase</button>
-                <button onclick="api('API.getCardConfig(ecId, pageName)', API.getCardConfig, MOCK_ECID, 'index')">getCardConfig</button>
-                <button onclick="api('API.getUserConfig(ecId, pageName)', API.getUserConfig, MOCK_ECID, 'index')">getUserConfig</button>
-            </div>
-        </div>
-
-        <!-- API: Activity -->
-        <div class="group">
-            <div class="group-title">2. 活动与报名</div>
-            <div class="btn-grid">
-                <button onclick="api('API.getCardDetail(ecId)', API.getCardDetail, MOCK_ECID)">getCardDetail</button>
-                <button onclick="api('API.getMatchRsDetail(ecId)', API.getMatchRsDetail, MOCK_ECID)">getMatchRsDetail</button>
-                <button onclick="api('API.getUserJoinStatus(ecId)', API.getUserJoinStatus, MOCK_ECID)">getUserJoinStatus</button>
-                <button onclick="api('API.isNewUserInCardComp(ecId)', API.isNewUserInCardComp, MOCK_ECID)">isNewUserInCardComp</button>
-                <button onclick="api('API.getOnlineMcSignUpDetail(ecId)', API.getOnlineMcSignUpDetail, MOCK_ECID)">getOnlineMcSignUpDetail</button>
-                <button onclick="api('API.signUpOnline(mcId, coiId, selectTeam, nickName)', API.signUpOnline, MOCK_MCID, 0, 0, '测试用户')">signUpOnline (报名)</button>
-                <button onclick="api('API.isAllowMcSignUp(ecId)', API.isAllowMcSignUp, MOCK_ECID)">isAllowMcSignUp</button>
-            </div>
-        </div>
-
-        <!-- API: Rank -->
-        <div class="group">
-            <div class="group-title">3. 排名与成就</div>
-            <div class="btn-grid">
-                <button onclick="api('API.getRankDetail(mcIdListStr, mcType, dispArrStr)', API.getRankDetail, String(MOCK_MCID), 1, 'total')">getRankDetail</button>
-                <button onclick="api('API.getUserCurrentRank(ecId)', API.getUserCurrentRank, MOCK_ECID)">getUserCurrentRank</button>
-                <button onclick="api('API.getCurrentMonthlyChallenge()', API.getCurrentMonthlyChallenge)">getCurrentMonthlyChallenge</button>
-                <button onclick="api('API.getMonthlyChallenge()', API.getMonthlyChallenge)">getMonthlyChallenge</button>
-                <button onclick="api('API.getMonthRankDetail()', API.getMonthRankDetail)">getMonthRankDetail</button>
-                <button onclick="api('API.getAchievement()', API.getAchievement)">getAchievement</button>
-                <button onclick="api('API.getCompStatistic(ecId)', API.getCompStatistic, MOCK_ECID)">getCompStatistic</button>
-            </div>
-        </div>
-
-        <!-- API: Exchange -->
-        <div class="group">
-            <div class="group-title">4. 积分与兑换</div>
-            <div class="btn-grid">
-                <button onclick="api('API.getScore(ecId)', API.getScore, MOCK_ECID)">getScore</button>
-                <button onclick="api('API.getGoodsList(ecId)', API.getGoodsList, MOCK_ECID)">getGoodsList</button>
-                <button onclick="api('API.getGoodsDetail(goodsId)', API.getGoodsDetail, 1)">getGoodsDetail</button>
-                <button onclick="api('API.exchangeGoods(ecId, goodsId)', API.exchangeGoods, MOCK_ECID, 1)">exchangeGoods</button>
-                <button onclick="api('API.getExchangeList(ecId)', API.getExchangeList, MOCK_ECID)">getExchangeList</button>
-                <button onclick="api('API.getExchangeDetail(ecId, exchangeId)', API.getExchangeDetail, MOCK_ECID, 1)">getExchangeDetail</button>
-            </div>
-        </div>
-
-        <!-- API: Message -->
-        <div class="group">
-            <div class="group-title">5. 消息与通知</div>
-            <div class="btn-grid">
-                <button onclick="api('API.getUnReadMessages(relationId, relationType)', API.getUnReadMessages, MOCK_ECID, 2)">getUnReadMessages</button>
-                <button onclick="api('API.readMessage(mqIdListStr)', API.readMessage, '1,2,3')">readMessage</button>
-                <button onclick="api('API.getWarnMessage(ecId)', API.getWarnMessage, MOCK_ECID)">getWarnMessage</button>
-            </div>
-        </div>
-
-        <!-- API: Cert -->
-        <div class="group">
-            <div class="group-title">6. 电子证书</div>
-            <div class="btn-grid">
-                <button onclick="api('API.getCertStyle(ecId)', API.getCertStyle, MOCK_ECID)">getCertStyle</button>
-                <button onclick="api('API.getUserBaseInCertificate(ecId)', API.getUserBaseInCertificate, MOCK_ECID)">getUserBaseInCertificate</button>
-                <button onclick="api('API.createCertificate({ecId, certStyleId, ...})', API.createCertificate, {ecId:MOCK_ECID, certStyleId:1, userName:'测试用户'})">createCertificate</button>
-            </div>
-        </div>
-
-        <!-- API: Other -->
-        <div class="group">
-            <div class="group-title">7. 其他</div>
-            <div class="btn-grid">
-                <button onclick="api('API.getUserInfo()', API.getUserInfo)">getUserInfo</button>
-                <button onclick="api('API.getMapList(ecId)', API.getMapList, MOCK_ECID)">getMapList</button>
-                <button onclick="api('API.getGrids(ecId)', API.getGrids, MOCK_ECID)">getGrids</button>
-            </div>
-        </div>
-    </div>
-
-    <div class="main">
-        <h3>执行日志</h3>
-        <div class="log-area" id="log">
-            <div class="log-entry" style="color: #aaa;">等待操作...</div>
-        </div>
-    </div>
-
-    <script>
-        var TOKEN = '96ba3c924394934f7d30fa869a94ce0d'; 
-        var MOCK_ECID = 1; // 模拟卡片/活动ID (ecId)
-        var MOCK_MCID = 101; // 模拟赛事ID (mcId)
-
-        var isLocal = window.location.protocol === 'file:' || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
-        
-        document.getElementById('envInfo').innerText = isLocal ? '本地开发 (Mock ON)' : '生产环境';
-
-        // 初始化 API SDK
-        API.init({
-            token: TOKEN,
-            useMock: isLocal
-        });
-
-        // ===========================================
-        // 日志功能
-        // ===========================================
-        function log(label, data, isError = false) {
-            var el = document.getElementById('log');
-            var div = document.createElement('div');
-            div.className = 'log-entry';
-            var time = new Date().toLocaleTimeString();
-            var content = typeof data === 'object' ? JSON.stringify(data, null, 2) : data;
-            div.innerHTML = `<div class="log-time">[${time}]</div><div class="log-label ${isError?'log-error':''}">${label}</div><div>${content}</div>`;
-            el.prepend(div);
-        }
-
-        function clearLog() {
-            document.getElementById('log').innerHTML = '';
-        }
-
-        // ===========================================
-        // Bridge 调用通用函数
-        // fnDesc: Bridge 方法的描述字符串,如 'Bridge.openMap(latitude, longitude, name)'
-        // fn: 实际要调用的 Bridge 方法
-        // ...args: 实际参数
-        // ===========================================
-        function run(fnDesc, fn, ...args) {
-            log('调用 ' + fnDesc, '参数: ' + JSON.stringify(args));
-            fn.apply(Bridge, args);
-        }
-
-        function getToken() {
-            log('调用 Bridge.getToken', '等待 App 回调 Token...');
-            Bridge.onToken(function(t) {
-                log('✅ Bridge 回调成功', 'Token: ' + t);
-            });
-            Bridge.getToken();
-        }
-
-        // ===========================================
-        // API 调用通用函数
-        // fnDesc: API 方法的描述字符串,如 'API.getCardDetail(ecId)'
-        // fn: 实际要调用的 API 方法
-        // ...args: 实际参数
-        // ===========================================
-        function api(fnDesc, fn, ...args) {
-            log('请求 ' + fnDesc, '实际参数: ' + JSON.stringify(args));
-            
-            fn.apply(API, args)
-                .then(function(res) {
-                    log('✅ 成功', res);
-                })
-                .catch(function(err) {
-                    log('❌ 失败', err.message || err, true);
-                });
-        }
-    </script>
-</body>
-</html>

+ 0 - 88
card/sdk_delivery/demo_project/detail.html

@@ -1,88 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
-    <title>活动详情</title>
-    <style>
-        body { margin: 0; padding: 0; font-family: -apple-system, sans-serif; background-color: white; }
-        .hero { width: 100%; height: 200px; background: #ddd url('https://via.placeholder.com/400x200/007aff/ffffff?text=Activity') center/cover; position: relative; }
-        .back-btn { position: absolute; top: 40px; left: 20px; width: 32px; height: 32px; background: rgba(0,0,0,0.5); border-radius: 50%; color: white; display: flex; align-items: center; justify-content: center; font-size: 18px; cursor: pointer; }
-        .content { padding: 20px; }
-        .title { font-size: 24px; font-weight: bold; margin-bottom: 10px; }
-        .tags { display: flex; gap: 10px; margin-bottom: 20px; }
-        .tag { background: #f0f2f5; color: #666; padding: 4px 8px; border-radius: 4px; font-size: 12px; }
-        .desc { color: #666; line-height: 1.6; margin-bottom: 30px; }
-        
-        .action-bar { position: fixed; bottom: 0; left: 0; width: 100%; padding: 10px 20px; background: white; box-shadow: 0 -2px 10px rgba(0,0,0,0.05); box-sizing: border-box; display: flex; gap: 10px; }
-        .btn { flex: 1; padding: 12px; border-radius: 24px; text-align: center; font-weight: bold; font-size: 16px; cursor: pointer; }
-        .btn-primary { background: #007aff; color: white; }
-        .btn-outline { border: 1px solid #ddd; color: #333; background: white; }
-    </style>
-
-    <!-- SDK -->
-    <script src="./mock_flutter.js"></script>
-    <script src="./bridge.js"></script>
-</head>
-<body>
-
-    <div class="hero">
-        <div class="back-btn" onclick="Bridge.back()">←</div>
-    </div>
-
-    <div class="content">
-        <div class="title" id="title">活动标题</div>
-        <div class="tags">
-            <div class="tag">线下活动</div>
-            <div class="tag">2025.11.26</div>
-        </div>
-        <div class="desc">
-            这是一个非常精彩的线下活动。通过这个页面,演示了如何调用 App 原生的能力。<br><br>
-            1. 点击左上角返回按钮,调用 Bridge.back()<br>
-            2. 点击底部“导航”,调用 Bridge.openMap()<br>
-            3. 点击底部“详情”,调用 Bridge.openMatch() 跳转原生页面
-        </div>
-        
-        <div style="text-align: center; margin-top: 20px;">
-             <button onclick="doShare()" style="padding: 8px 20px; background: #4cd964; color: white; border: none; border-radius: 20px;">分享给朋友</button>
-        </div>
-    </div>
-
-    <div class="action-bar">
-        <div class="btn btn-outline" onclick="doNav()">🗺️ 导航</div>
-        <div class="btn btn-primary" onclick="doNativeDetail()">📱 原生详情</div>
-    </div>
-
-    <script>
-        // 获取 URL 参数
-        function getQuery(name) {
-            var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
-            var r = window.location.search.substr(1).match(reg);
-            if (r != null) return decodeURIComponent(r[2]); return null;
-        }
-
-        var id = getQuery('id') || '0';
-        var name = getQuery('name') || '未知活动';
-        document.getElementById('title').innerText = name;
-
-        function doNav() {
-            // 调用地图
-            Bridge.openMap(39.9042, 116.4074, name);
-        }
-
-        function doNativeDetail() {
-            // 跳转 App 原生详情页
-            // type=1: 普通活动
-            Bridge.openMatch(id, 1);
-        }
-        
-        function doShare() {
-            Bridge.shareWx({
-                title: '快来参加: ' + name,
-                url: window.location.href,
-                image: 'https://via.placeholder.com/100'
-            });
-        }
-    </script>
-</body>
-</html>

+ 0 - 113
card/sdk_delivery/demo_project/index.html

@@ -1,113 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
-    <title>彩图奔跑 - 首页</title>
-    <style>
-        body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; background-color: #f7f8fa; }
-        .header { background: #007aff; color: white; padding: 20px; padding-top: 40px; border-bottom-left-radius: 20px; border-bottom-right-radius: 20px; }
-        .user-info { display: flex; align-items: center; justify-content: space-between; }
-        .score-box { text-align: center; background: rgba(255,255,255,0.2); padding: 10px 20px; border-radius: 12px; backdrop-filter: blur(5px); }
-        .score-num { font-size: 24px; font-weight: bold; }
-        .section-title { margin: 20px; font-size: 18px; font-weight: bold; color: #333; }
-        .card-list { padding: 0 15px; display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
-        .card { background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.05); transition: transform 0.2s; }
-        .card:active { transform: scale(0.98); }
-        .card-img { width: 100%; height: 100px; background-color: #eee; object-fit: cover; }
-        .card-body { padding: 10px; }
-        .card-title { font-size: 14px; font-weight: bold; margin-bottom: 5px; }
-        .card-desc { font-size: 12px; color: #888; }
-        .loading { text-align: center; margin-top: 50px; color: #999; }
-    </style>
-
-    <!-- SDK 引入 -->
-    <script src="./mock_flutter.js"></script> <!-- 调试用,打包请删除 -->
-    <script src="./bridge.js"></script>
-    <script src="./api.js"></script>
-</head>
-<body>
-
-    <div class="header">
-        <div class="user-info">
-            <div>
-                <div style="font-size: 14px; opacity: 0.8;">欢迎回来</div>
-                <div style="font-size: 20px; font-weight: bold;">运动达人</div>
-            </div>
-            <div class="score-box">
-                <div style="font-size: 12px;">我的积分</div>
-                <div class="score-num" id="scoreVal">--</div>
-            </div>
-        </div>
-        <div style="margin-top: 20px; display: flex; gap: 10px;">
-            <button onclick="location.href='rank.html'" style="flex: 1; background: rgba(255,255,255,0.2); border: 1px solid white; color: white; border-radius: 20px; padding: 8px;">🏆 排行榜</button>
-            <button onclick="location.href='signup.html'" style="flex: 1; background: white; color: #007aff; border: none; border-radius: 20px; padding: 8px;">📝 去报名</button>
-        </div>
-    </div>
-
-    <div class="section-title">热门活动</div>
-    
-    <div class="card-list" id="listContainer">
-        <!-- 动态生成 -->
-    </div>
-    
-    <div class="loading" id="loading">加载数据中...</div>
-
-    <script>
-        // 模拟 Token,真实环境请从 URL 获取: getUrlParam('token')
-        var TOKEN = '96ba3c924394934f7d30fa869a94ce0d'; 
-
-        // 1. 初始化
-        // 增强本地环境检测:包含 file:, localhost, 127.0.0.1
-        var isLocal = window.location.protocol === 'file:' || 
-                      window.location.hostname === 'localhost' || 
-                      window.location.hostname === '127.0.0.1';
-        
-        API.init({ 
-            token: TOKEN,
-            useMock: isLocal
-        });
-
-        // 2. 获取积分
-        API.getScore(0).then(function(data) {
-            document.getElementById('scoreVal').innerText = data.score;
-        }).catch(function(err) {
-            console.error('积分获取失败', err);
-            document.getElementById('scoreVal').innerText = 'Err';
-        });
-
-        // 3. 模拟获取活动列表 (API中没有直接的活动列表接口,这里用静态数据模拟业务逻辑)
-        var mockActivities = [
-            { id: 101, name: '奥体中心定向赛', desc: '探索城市地标', img: 'https://via.placeholder.com/150/007aff/ffffff?text=Aoti' },
-            { id: 102, name: '千佛山登山节', desc: '登高望远', img: 'https://via.placeholder.com/150/ff9500/ffffff?text=Mountain' },
-            { id: 103, name: '大明湖健步走', desc: '湖畔漫步', img: 'https://via.placeholder.com/150/34c759/ffffff?text=Lake' },
-            { id: 104, name: '校园寻宝', desc: '重返校园时光', img: 'https://via.placeholder.com/150/af52de/ffffff?text=School' }
-        ];
-
-        setTimeout(function() {
-            document.getElementById('loading').style.display = 'none';
-            renderList(mockActivities);
-        }, 500);
-
-        function renderList(list) {
-            var html = '';
-            list.forEach(function(item) {
-                html += `
-                <div class="card" onclick="goDetail(${item.id}, '${item.name}')">
-                    <img src="${item.img}" class="card-img">
-                    <div class="card-body">
-                        <div class="card-title">${item.name}</div>
-                        <div class="card-desc">${item.desc}</div>
-                    </div>
-                </div>`;
-            });
-            document.getElementById('listContainer').innerHTML = html;
-        }
-
-        function goDetail(id, name) {
-            // 跳转到 H5 详情页 (演示页面间传参)
-            window.location.href = `detail.html?id=${id}&name=${encodeURIComponent(name)}`;
-        }
-    </script>
-</body>
-</html>

+ 0 - 120
card/sdk_delivery/demo_project/rank.html

@@ -1,120 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
-    <title>排行榜</title>
-    <style>
-        body { margin: 0; padding: 0; font-family: -apple-system, sans-serif; background-color: #f7f8fa; }
-        .header { background: linear-gradient(135deg, #FF9500, #FFCC00); color: white; padding: 20px; padding-top: 40px; text-align: center; }
-        .my-rank-box { background: rgba(255,255,255,0.2); padding: 15px; border-radius: 10px; margin-top: 20px; display: flex; justify-content: space-between; align-items: center; }
-        
-        .list-container { padding: 15px; margin-top: -20px; }
-        .rank-item { background: white; border-radius: 12px; padding: 15px; margin-bottom: 10px; display: flex; align-items: center; box-shadow: 0 2px 5px rgba(0,0,0,0.05); }
-        .rank-num { width: 30px; font-size: 18px; font-weight: bold; color: #999; text-align: center; margin-right: 10px; }
-        .rank-num.top3 { color: #FF9500; font-size: 22px; }
-        .avatar { width: 40px; height: 40px; border-radius: 50%; background: #eee; margin-right: 15px; object-fit: cover; }
-        .info { flex: 1; }
-        .name { font-weight: bold; font-size: 16px; color: #333; }
-        .score { font-weight: bold; color: #FF9500; font-size: 18px; }
-        .unit { font-size: 12px; color: #999; margin-left: 4px; }
-
-        .loading { text-align: center; padding: 20px; color: #999; }
-        .back-btn { position: absolute; top: 20px; left: 20px; color: white; font-size: 24px; cursor: pointer; }
-    </style>
-
-    <script src="./mock_flutter.js"></script>
-    <script src="./bridge.js"></script>
-    <script src="./api.js"></script>
-</head>
-<body>
-
-    <div class="header">
-        <div class="back-btn" onclick="Bridge.back()">←</div>
-        <h2>实时排行榜</h2>
-        <div class="my-rank-box">
-            <div>我的排名</div>
-            <div style="font-size: 24px; font-weight: bold;" id="myRankNum">--</div>
-        </div>
-    </div>
-
-    <div class="list-container" id="list">
-        <div class="loading">正在加载排名数据...</div>
-    </div>
-
-    <script>
-        var TOKEN = '96ba3c924394934f7d30fa869a94ce0d';
-        // 模拟参数
-        var MC_ID = '1001'; // 假设的赛事ID
-        var MC_TYPE = 1;
-
-        // 检测是否本地开发环境
-        var isLocal = window.location.protocol === 'file:' || 
-                      window.location.hostname === 'localhost' || 
-                      window.location.hostname === '127.0.0.1';
-
-        API.init({ 
-            token: TOKEN,
-            useMock: isLocal // 本地开发自动开启 Mock
-        });
-
-        // 1. 并行请求:我的排名 + 总榜单
-        Promise.all([
-            API.getUserCurrentRank(0), // ecId=0 示例
-            API.getRankDetail(MC_ID, MC_TYPE, 'total') // 获取总榜
-        ]).then(function(results) {
-            var myRankData = results[0];
-            var rankListData = results[1];
-
-            // 渲染我的排名
-            document.getElementById('myRankNum').innerText = myRankData.rankNum > 0 ? myRankData.rankNum : '未上榜';
-
-            // 渲染列表
-            // 注意:这里假设后端返回的 structure 是 { totalRankRs: [...] }
-            // 根据之前的分析,API 返回的数据结构可能需要适配
-            var list = rankListData.totalRankRs || []; 
-            renderList(list);
-
-        }).catch(function(err) {
-            console.error(err);
-            document.getElementById('list').innerHTML = '<div class="loading">加载失败,请重试<br>' + err.message + '</div>';
-        });
-
-        function renderList(list) {
-            if (!list || list.length === 0) {
-                // 如果没有真实数据,为了演示效果,生成假数据
-                list = generateMockData();
-            }
-
-            var html = '';
-            list.forEach(function(item, index) {
-                var rank = index + 1;
-                var isTop3 = rank <= 3 ? 'top3' : '';
-                var avatarUrl = item.headUrl || 'https://via.placeholder.com/40?text=' + item.nickName.charAt(0);
-                
-                html += `
-                <div class="rank-item">
-                    <div class="rank-num ${isTop3}">${rank}</div>
-                    <img src="${avatarUrl}" class="avatar">
-                    <div class="info">
-                        <div class="name">${item.nickName}</div>
-                    </div>
-                    <div class="score">${item.score}<span class="unit">分</span></div>
-                </div>`;
-            });
-            document.getElementById('list').innerHTML = html;
-        }
-
-        // 生成模拟数据 (仅当 API 无数据时使用)
-        function generateMockData() {
-            return [
-                { nickName: '张三', score: 9800, headUrl: '' },
-                { nickName: '李四', score: 9500, headUrl: '' },
-                { nickName: '王五', score: 8900, headUrl: '' },
-                { nickName: '赵六', score: 8200, headUrl: '' },
-                { nickName: '运动小达人', score: 7800, headUrl: '' },
-            ];
-        }
-    </script>
-</body>
-</html>

+ 0 - 176
card/sdk_delivery/demo_project/signup.html

@@ -1,176 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
-    <title>活动报名</title>
-    <style>
-        body { margin: 0; padding: 0; font-family: -apple-system, sans-serif; background-color: white; padding-bottom: 80px; }
-        .hero { width: 100%; height: 240px; background: #ddd url('https://via.placeholder.com/400x240/34c759/ffffff?text=Running') center/cover; position: relative; }
-        .back-btn { position: absolute; top: 40px; left: 20px; width: 36px; height: 36px; background: rgba(0,0,0,0.4); border-radius: 50%; color: white; display: flex; align-items: center; justify-content: center; font-size: 20px; cursor: pointer; backdrop-filter: blur(4px); }
-        
-        .container { padding: 20px; }
-        .title { font-size: 26px; font-weight: bold; margin-bottom: 10px; color: #333; }
-        .info-row { display: flex; margin-bottom: 15px; align-items: center; color: #666; font-size: 14px; }
-        .icon { margin-right: 8px; }
-
-        .desc-box { margin-top: 30px; border-top: 1px solid #eee; padding-top: 20px; }
-        .desc-title { font-weight: bold; font-size: 18px; margin-bottom: 10px; }
-        .desc-text { line-height: 1.6; color: #666; font-size: 15px; }
-
-        .bottom-bar { position: fixed; bottom: 0; left: 0; width: 100%; background: white; border-top: 1px solid #eee; padding: 10px 20px; box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; }
-        .status-text { font-size: 12px; color: #999; }
-        .btn { width: 100%; padding: 14px; border-radius: 30px; text-align: center; color: white; font-weight: bold; font-size: 18px; cursor: pointer; border: none; transition: background 0.3s; }
-        
-        .btn-blue { background: #007aff; box-shadow: 0 4px 12px rgba(0,122,255,0.3); }
-        .btn-blue:active { background: #005bb5; }
-        
-        .btn-green { background: #34c759; box-shadow: 0 4px 12px rgba(52,199,89,0.3); }
-        .btn-green:active { background: #248a3d; }
-
-        .btn-disabled { background: #ccc; cursor: not-allowed; box-shadow: none; }
-    </style>
-
-    <script src="./mock_flutter.js"></script>
-    <script src="./bridge.js"></script>
-    <script src="./api.js"></script>
-</head>
-<body>
-
-    <div class="hero">
-        <div class="back-btn" onclick="Bridge.back()">←</div>
-    </div>
-
-    <div class="container">
-        <div class="title" id="eventName">加载中...</div>
-        
-        <div class="info-row">
-            <span class="icon">🕒</span>
-            <span id="eventTime">--</span>
-        </div>
-        <div class="info-row">
-            <span class="icon">📍</span>
-            <span>全国线上活动</span>
-        </div>
-
-        <div class="desc-box">
-            <div class="desc-title">活动简介</div>
-            <div class="desc-text">
-                这是一场激动人心的线上挑战赛!无论你在哪里,只要迈开双腿,就能参与其中。<br><br>
-                <b>参赛规则:</b><br>
-                1. 点击下方按钮报名。<br>
-                2. 使用 App 记录运动轨迹。<br>
-                3. 完赛后获得电子证书。
-            </div>
-        </div>
-    </div>
-
-    <div class="bottom-bar">
-        <button id="actionBtn" class="btn btn-disabled" onclick="handleAction()">加载状态...</button>
-    </div>
-
-    <script>
-        var TOKEN = '96ba3c924394934f7d30fa869a94ce0d';
-        var EC_ID = 0; // 卡片ID
-        var MC_ID = 101; // 赛事ID (用于报名)
-
-        var state = {
-            isJoin: false,
-            isLoading: true
-        };
-
-        // 检测是否本地开发环境
-        var isLocal = window.location.protocol === 'file:' || 
-                      window.location.hostname === 'localhost' || 
-                      window.location.hostname === '127.0.0.1';
-        
-        API.init({ 
-            token: TOKEN,
-            useMock: isLocal // 本地开发自动开启 Mock
-        });
-
-        // 初始化页面
-        initPage();
-
-        function initPage() {
-            // 1. 获取活动详情
-            API.getCardDetail(EC_ID).then(function(data) {
-                document.getElementById('eventName').innerText = data.mcName || '演示活动:线上马拉松';
-                // 简单格式化时间,真实项目建议用 momentjs 或 tools
-                document.getElementById('eventTime').innerText = '活动时间: 近期有效'; 
-            }).catch(function(err) {
-                console.warn('详情加载失败(可能用mock数据)', err);
-                document.getElementById('eventName').innerText = '演示活动:线上马拉松';
-            });
-
-            // 2. 检查报名状态
-            checkJoinStatus();
-        }
-
-        function checkJoinStatus() {
-            setBtnLoading(true);
-            API.getUserJoinStatus(EC_ID).then(function(data) {
-                state.isJoin = data.isJoin;
-                updateBtnState();
-            }).catch(function(err) {
-                console.error(err);
-                alert('获取报名状态失败');
-                // 默认未报名
-                state.isJoin = false;
-                updateBtnState();
-            });
-        }
-
-        function updateBtnState() {
-            var btn = document.getElementById('actionBtn');
-            state.isLoading = false;
-            btn.className = state.isJoin ? 'btn btn-green' : 'btn btn-blue';
-            btn.innerText = state.isJoin ? '已报名,进入比赛' : '立即报名';
-        }
-
-        function setBtnLoading(loading) {
-            state.isLoading = loading;
-            var btn = document.getElementById('actionBtn');
-            if (loading) {
-                btn.className = 'btn btn-disabled';
-                btn.innerText = '处理中...';
-            }
-        }
-
-        function handleAction() {
-            if (state.isLoading) return;
-
-            if (state.isJoin) {
-                // 已报名 -> 进入比赛 (调用 Bridge 打开原生页面)
-                Bridge.openMatch(MC_ID, 1); // type=1 普通活动
-            } else {
-                // 未报名 -> 执行报名
-                doSignUp();
-            }
-        }
-
-        function doSignUp() {
-            if (!confirm('确定要报名参加这个活动吗?')) return;
-
-            setBtnLoading(true);
-            
-            API.signUpOnline(MC_ID).then(function(res) {
-                alert('🎉 报名成功!');
-                // 刷新状态
-                checkJoinStatus();
-            }).catch(function(err) {
-                setBtnLoading(false);
-                console.error(err);
-                // Mock 模式下 API 可能会失败(没有真实后端),我们模拟成功
-                if (window.location.protocol === 'file:') {
-                    alert('🎉 报名成功!(Mock模式)');
-                    state.isJoin = true;
-                    updateBtnState();
-                } else {
-                    alert('报名失败: ' + err.message);
-                }
-            });
-        }
-    </script>
-</body>
-</html>

+ 0 - 102
card/sdk_delivery/mock_flutter.js

@@ -1,102 +0,0 @@
-(function (window) {
-    'use strict';
-  
-    console.log('%c [MockFlutter] 已加载,当前处于开发调试模式 ', 'background: #42b983; color: white; font-size: 14px; padding: 4px;');
-  
-    // 1. 模拟 uni.postMessage
-    if (!window.uni) {
-      window.uni = {};
-    }
-  
-    window.uni.postMessage = function (message) {
-      const payload = message.data || {};
-      const action = payload.action;
-      const data = payload.data;
-  
-      console.group('%c [MockFlutter] 收到 App 指令 ', 'color: #1aad19; font-weight: bold;');
-      console.log('Action:', action);
-      console.log('Data:', data);
-      console.groupEnd();
-  
-      // 模拟具体行为反馈
-      switch (action) {
-        case 'openMap':
-          alert(`[模拟App] 正在打开地图\n纬度: ${data.latitude}\n经度: ${data.longitude}\n名称: ${data.name}`);
-          break;
-        case 'openMatch':
-          alert(`[模拟App] 正在打开赛事详情\nID: ${data.id}\n类型: ${data.type}`);
-          break;
-        case 'openActivityList':
-          alert(`[模拟App] 正在打开活动列表\nID: ${data.id}\n名称: ${data.mapName}`);
-          break;
-        case 'back':
-          console.log('[模拟App] 执行返回操作');
-          // alert('[模拟App] 执行返回操作'); // 弹窗太多会烦,这里只打印
-          break;
-        case 'toHome':
-          alert('[模拟App] 执行返回 App 首页');
-          break;
-        case 'toLogin':
-          alert('[模拟App] 执行跳转登录页');
-          break;
-        case 'setTitle':
-          alert(`[模拟App] 设置标题为: ${data.title}`);
-          break;
-        case 'shareWx':
-          alert(`[模拟App] 正在调起微信分享\n标题: ${data.title}\n链接: ${data.url}`);
-          break;
-        case 'launchWxMini':
-          alert(`[模拟App] 正在打开微信小程序\n原始ID: ${data.username}\n路径: ${data.path}`);
-          break;
-        case 'saveImage':
-          alert(`[模拟App] 正在保存图片 (Base64长度: ${data.base64 ? data.base64.length : '0'})`);
-          break;
-        case 'getToken':
-            console.log('[模拟App] 收到获取Token请求,1秒后模拟回调...');
-            setTimeout(function() {
-                if (window.Bridge && window.Bridge.receiveToken) {
-                    window.Bridge.receiveToken('MOCK_TOKEN_FOR_BRIDGE_12345');
-                    console.log('[模拟App] 已通过 Bridge.receiveToken 回调 Token');
-                } else {
-                    console.warn('[模拟App] 无法回调 Token,Bridge.receiveToken 未定义');
-                }
-            }, 1000);
-            break;
-        default:
-          console.log(`[模拟App] 收到未知指令: ${action}, 数据: `, data);
-          // alert(`[模拟App] 收到未知指令: ${action}`);
-      }
-    };
-  
-    // 2. 模拟旧版注入对象 (防止报错,并使用可读中文)
-    if (!window.share_wx) {
-        window.share_wx = {
-            postMessage: function(jsonStr) {
-                console.log('[MockFlutter] share_wx.postMessage 收到:', jsonStr);
-                alert('[模拟App/旧通道] 微信分享:\n' + jsonStr);
-            }
-        };
-    }
-    
-    if (!window.wx_launch_mini) {
-        window.wx_launch_mini = {
-            postMessage: function(jsonStr) {
-                console.log('[MockFlutter] wx_launch_mini.postMessage 收到:', jsonStr);
-                alert('[模拟App/旧通道] 打开小程序:\n' + jsonStr);
-            }
-        };
-    }
-
-    if (!window.save_base64) {
-        window.save_base64 = {
-            postMessage: function(base64Str) {
-                console.log('[MockFlutter] save_base64.postMessage 收到 (Base64长度):', base64Str ? base64Str.length : '0');
-                alert(`[模拟App/旧通道] 保存图片 (Base64长度: ${base64Str ? base64Str.length : '0'})`);
-            }
-        };
-    }
-  
-    // 3. 模拟 UserAgent (可选)
-    // 某些逻辑可能会检查 navigator.userAgent,这里可以根据需要注入,但目前 SDK 不依赖这个判断
-  
-  })(window);