Browse Source

New: Sdk Created

Rockz-Home 1 tháng trước cách đây
mục cha
commit
342dea2546

+ 5 - 0
card/.gemini/settings.json

@@ -0,0 +1,5 @@
+{
+  "general": {
+    "previewFeatures": true
+  }
+}

+ 148 - 0
card/sdk_delivery/API.md

@@ -0,0 +1,148 @@
+# ColorMapRun H5 交互 SDK 开发文档 (Bridge)
+
+本文档描述了 H5 页面如何与彩图奔跑 App (Flutter) 进行交互。所有的交互方法都封装在 `bridge.js` 中。
+
+## 1. 快速开始
+
+### 1.1 文件引入
+
+在您的 `index.html` 或项目入口文件中,引入 `bridge.js`。
+
+```html
+<!-- 引入通信桥梁 -->
+<script src="./bridge.js"></script>
+```
+
+**注意**:本 SDK 内部会自动处理兼容性(优先检测 `uni.webView`,自动降级适配旧版 App URL 拦截),因此您**不需要**关心 App 是新版还是旧版,直接调用 API 即可。
+
+### 1.2 本地调试 (Mock)
+
+在开发阶段(浏览器环境),App 环境不可用。请在 `<head>` 中引入 `mock_flutter.js` 来模拟 App 行为。
+
+```html
+<head>
+    <!-- ... 其他标签 ... -->
+    
+    <!-- 【仅开发环境引入】 模拟 App 通信环境 -->
+    <script src="./mock_flutter.js"></script>
+    
+    <script src="./bridge.js"></script>
+</head>
+```
+
+**⚠️ 重要**:在打包交付给 App 端集成前,**请务必注释或删除对 `mock_flutter.js` 的引用**。
+
+---
+
+## 2. API 列表
+
+所有的 API 都挂载在全局对象 `window.Bridge` 上。
+
+### 2.1 基础导航
+
+#### 返回上一页
+通知 App 用户点击了返回按钮 (相当于浏览器的后退)。
+```javascript
+Bridge.back();
+```
+
+#### 返回 App 首页
+强制关闭当前 Webview,返回 App 的原生首页。
+```javascript
+Bridge.toHome();
+```
+
+#### 跳转登录
+当 API 返回 401 未授权时,调用此方法唤起 App 原生登录页。
+```javascript
+Bridge.toLogin();
+```
+
+#### 设置标题
+修改 App 顶部导航栏的标题。
+```javascript
+Bridge.setTitle('我的页面标题');
+```
+
+### 2.2 业务跳转
+
+#### 打开地图导航
+调起 App 原生导航(支持高德、百度、腾讯地图)。
+```javascript
+/**
+ * @param {number} latitude 纬度
+ * @param {number} longitude 经度
+ * @param {string} name 地点名称
+ */
+Bridge.openMap(36.666, 117.123, '济南奥体中心');
+```
+
+#### 打开比赛详情 (原生页)
+跳转到 App 内的原生比赛详情页。
+```javascript
+/**
+ * @param {string|number} id 活动ID (mcId)
+ * @param {string} type 活动类型 (默认 1)
+ */
+Bridge.openMatch(101, 1);
+```
+
+#### 打开活动列表 (原生页)
+跳转到 App 内的原生多地图/活动列表页。
+```javascript
+/**
+ * @param {string|number} id 关联ID (mapId)
+ * @param {string} mapName 地图名称
+ */
+Bridge.openActivityList(202, '奥体公园定向');
+```
+
+### 2.3 功能调用
+
+#### 微信分享
+调起 App 的微信分享面板。
+```javascript
+Bridge.shareWx({
+    title: '快来参加彩图奔跑!',
+    url: 'https://colormaprun.com/activity/123',
+    image: 'https://colormaprun.com/logo.png',
+    scene: 'session' // session: 好友, timeline: 朋友圈
+});
+```
+
+#### 跳转微信小程序
+```javascript
+Bridge.launchWxMini('gh_bea09156da8d', 'pages/index/index');
+```
+
+#### 保存图片
+将 Base64 格式的图片保存到手机相册。
+```javascript
+Bridge.saveImage('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...');
+```
+
+### 2.4 获取数据
+
+#### 获取用户 Token (异步)
+**注意**: 通常 H5 页面加载时 URL 会携带 Token。此方法主要用于特殊情况下的主动获取。
+
+```javascript
+// 1. 注册回调
+Bridge.onToken(function(token) {
+    console.log('收到 Token:', token);
+    // 使用 Token 发起 API 请求...
+});
+
+// 2. 发起获取请求
+Bridge.getToken();
+```
+
+---
+
+## 3. 常见问题
+
+**Q: 为什么 `Bridge.back()` 没有反应?**
+A: 请检查是否在浏览器环境中。如果在浏览器中,确保引入了 `mock_flutter.js`。如果在 App 中,请确认 App 的 WebView 能够响应 `javascript:history.back()` 或 `uni.postMessage`。
+
+**Q: 怎么判断当前是在 App 内?**
+A: 推荐通过 UserAgent 判断,或者尝试调用 `Bridge.getToken()` 看是否有回调。但在使用本 SDK 时,建议假设处于 App 环境编写代码,并在本地开启 Mock 进行调试。

+ 404 - 0
card/sdk_delivery/API_SERVER.md

@@ -0,0 +1,404 @@
+# ColorMapRun 后端 API 接口文档
+
+本文档描述了 H5 页面需要调用的后端接口。
+
+## 1. 基础说明
+
+*   **接口域名**:
+    *   开发环境: `https://t-mapi.colormaprun.com/api/card/`
+    *   生产环境: `https://colormaprun.com/api/card/`
+*   **OSS 资源域名**: `http://oss-card.colormaprun.com/card/`
+*   **请求方式**: `POST`
+*   **Content-Type**: `application/x-www-form-urlencoded`
+*   **鉴权**: 所有接口需要在 Header 中携带 `token`。
+
+### 1.1 请求头 (Headers)
+
+| 参数名 | 必选 | 说明 | 示例 |
+| :--- | :--- | :--- | :--- |
+| `Content-Type` | 是 | 固定值 | `application/x-www-form-urlencoded` |
+| `token` | 是 | 用户身份令牌 | `96ba3c924394934f7d30fa869a94ce0d` |
+
+### 1.2 响应结构 (Response)
+
+所有接口返回 JSON 格式数据:
+
+```json
+{
+  "code": 0,          // 0 表示成功,非 0 表示失败
+  "message": "成功",   // 提示信息
+  "data": { ... }     // 业务数据,下文中的“返回数据”均指此 data 字段的内容
+}
+```
+
+## 2. API 封装方法 (调用 `window.API` 对象)
+
+### 2.1 获取 OSS 资源基础地址
+
+*   **API 方法**: `API.getOssUrl()`
+*   **说明**: 获取图片、视频等静态资源的访问基础 URL。第三方开发者可自行拼接完整资源路径。
+*   **参数**: 无
+*   **返回**: `string` (OSS 基础 URL)
+
+---
+
+### 2.2 接口列表
+
+#### 1. 卡片与配置
+
+*   **1.1 卡片基本信息查询**
+    *   **API 方法**: `API.getCardBase(ecId, pageName)`
+    *   **参数**: `ecId` (int), `pageName` (string)
+    *   **返回数据**:
+        ```json
+        {
+            "ecName": "活动名称",
+            "ecDesc": "活动描述",
+            "beginSecond": 1700000000, // 开始时间戳
+            "endSecond": 1700090000,   // 结束时间戳
+            "secondCardName": "跳转页面名称"
+        }
+        ```
+
+*   **1.2 卡片通用配置查询**
+    *   **API 方法**: `API.getCardConfig(ecId, pageName)`
+    *   **参数**:
+        *   `ecId` (int): 卡片/活动 ID
+        *   `pageName` (string): 页面名称 (如 "all", "rankList", "index")
+    *   **返回数据**:
+        ```json
+        {
+            "configJson": "{\"css\": \"string\", \"tabActiveColor\": \"string\", \"popupRuleConfig\": {...}, \"popupRuleList\": [...]}"
+        }
+        ```
+    *   **configJson 字段说明 (示例)**:
+        *   `css`: `string`,动态注入到页面的 CSS 样式。
+        *   `tabActiveColor`: `string`,Tab 栏选中颜色,如 "#FF5733"。
+        *   `popupRuleConfig`: `object`,弹窗组件的样式配置,如 `{ "height": "500px", "theme": "light" }`。
+        *   `popupHelpConfig`, `popupWarnConfig`, `popupExchgConfig`, `popupMessageConfig`: `object`,其他类型弹窗的配置。
+        *   `popupRuleList`: `array<object|string>`,规则弹窗的内容列表,元素可以是 `{ "type": 1, "data": { "title": "标题", "content": "HTML 内容", "logo": {"src": "...", "width": "..."} } }`,也可以是字符串 "default" 或 "default2" (表示加载预设内容)。
+        *   `popupExchgList`, `popupHelpList`: `array<object>`,其他类型弹窗的内容列表。
+        *   *注意:`configJson` 结构灵活,字段是动态的,取决于后台配置,开发者应做好判空处理。*
+
+*   **1.3 用户个性化配置查询**
+    *   **API 方法**: `API.getUserConfig(ecId, pageName)`
+    *   **参数**: `ecId` (int), `pageName` (string)
+    *   **返回数据**:
+        ```json
+        {
+            "configJson": "{\"tplInfo\": {...}, \"mapInfo\": [...], \"popupRuleList\": [...]}"
+        }
+        ```
+    *   **configJson 字段说明 (示例)**:
+        *   `tplInfo`: `object`,模板信息,如 `{ "tplTypeId": 1, "ssctId": 1 }`。
+        *   `mapInfo`: `array<object>`,地图相关信息,如 `[{"activityList": [{"showName": "路线名称", "point": {"longitude": 117, "latitude": 36}}]}]`。
+        *   `popupRuleList`: `array<object>`,用户专属规则弹窗内容。
+        *   *注意:结构灵活,字段是动态的,取决于后台配置,开发者应做好判空处理。*
+
+#### 2. 活动与报名
+
+*   **2.1 卡片对应活动或赛事详情查询**
+    *   **API 方法**: `API.getCardDetail(ecId)`
+    *   **参数**: `ecId` (int)
+    *   **返回数据**:
+        ```json
+        {
+            "mcId": 101,
+            "mcName": "赛事名称",
+            "mcType": 1, // 1:普通 2:线下 3:线上
+            "beginSecond": 1700000000,
+            "endSecond": 1700090000,
+            "teamNum": 0, // 0:个人, >0:团队
+            "coiName": "已报名单位名称"
+        }
+        ```
+
+*   **2.2 用户是否已报名卡片对应赛事查询**
+    *   **API 方法**: `API.getUserJoinStatus(ecId)`
+    *   **参数**: `ecId` (int)
+    *   **返回数据**:
+        ```json
+        {
+            "isJoin": true // true: 已报名, false: 未报名
+        }
+        ```
+
+*   **2.3 线上赛报名 (重新分组)**
+    *   **API 方法**: `API.signUpOnline(mcId, coiId, selectTeam, nickName)`
+    *   **参数**: `mcId` (int), `coiId` (opt), `selectTeam` (opt), `nickName` (opt)
+    *   **返回数据**: `{}` (空对象表示成功)
+
+*   **2.4 是否允许重新分组(报名)**
+    *   **API 方法**: `API.isAllowMcSignUp(ecId)`
+    *   **参数**: `ecId` (int)
+    *   **返回数据**:
+        ```json
+        {
+            "allowSignUp": true
+        }
+        ```
+
+*   **2.5 线上赛报名页面信息详情**
+    *   **API 方法**: `API.getOnlineMcSignUpDetail(ecId)`
+    *   **参数**: `ecId` (int)
+    *   **返回数据**:
+        ```json
+        {
+            "teamList": [ // 可选的团队列表
+                { "teamId": 1, "teamName": "红队" },
+                { "teamId": 2, "teamName": "蓝队" }
+            ],
+            "signupFields": [ // 报名所需字段,示例
+                { "name": "phone", "label": "手机号", "type": "text", "required": true }
+            ]
+        }
+        ```
+
+#### 3. 排名与成就
+
+*   **3.1 排名查询**
+    *   **API 方法**: `API.getRankDetail(mcIdListStr, mcType, dispArrStr)`
+    *   **参数**: `mcIdListStr` (string), `mcType` (int), `dispArrStr` (string)
+    *   **返回数据**:
+        ```json
+        {
+            "totalRankRs": [ // 总榜
+                {
+                    "nickName": "张三",
+                    "score": 10000,
+                    "headUrl": "http://...",
+                    "rankNum": 1
+                }
+            ],
+            "teamRankRs": [], // 队伍榜
+            "inTeamRs": []    // 队内榜
+        }
+        ```
+
+*   **3.2 卡片用户当前排名查询**
+    *   **API 方法**: `API.getUserCurrentRank(ecId)`
+    *   **参数**: `ecId` (int)
+    *   **返回数据**:
+        ```json
+        {
+            "rankNum": 5 // 当前排名,0表示未上榜
+        }
+        ```
+
+*   **3.3 玩家所有月挑战记录查询**
+    *   **API 方法**: `API.getMonthlyChallenge()`
+    *   **参数**: 无
+    *   **返回数据**: (Array)
+        ```json
+        [
+            {
+                "year": "2023",
+                "monthRs": [
+                    {
+                        "month": 10,
+                        "realNum": 5, // 实际完成
+                        "targetNum": 10 // 目标
+                    }
+                ]
+            }
+        ]
+        ```
+
+*   **3.4 玩家活动成就查询**
+    *   **API 方法**: `API.getAchievement()`
+    *   **参数**: 无
+    *   **返回数据**: (Array)
+        ```json
+        [
+            {
+                "year": "2023",
+                "aiRs": [
+                    {
+                        "aiName": "完赛奖牌",
+                        "aiTime": 1700000000,
+                        "iconUrl": "http://..."
+                    }
+                ]
+            }
+        ]
+        ```
+
+#### 4. 积分与兑换
+
+*   **4.1 卡片内可用积分查询**
+    *   **API 方法**: `API.getScore(ecId)`
+    *   **参数**: `ecId` (int)
+    *   **返回数据**:
+        ```json
+        {
+            "score": 880,
+            "extTime": 1700000000 // 过期时间
+        }
+        ```
+
+*   **4.2 积分可兑换商品列表查询**
+    *   **API 方法**: `API.getGoodsList(ecId)`
+    *   **参数**: `ecId` (int)
+    *   **返回数据**: (Array)
+        ```json
+        [
+            {
+                "goodsId": 1,
+                "goodsName": "商品A",
+                "goodsPic": "/static/...", // 需拼接 OSS Url
+                "goodsLeftNum": 99,
+                "corrScore": 100
+            }
+        ]
+        ```
+
+*   **4.3 积分兑换商品**
+    *   **API 方法**: `API.exchangeGoods(ecId, goodsId)`
+    *   **参数**: `ecId` (int), `goodsId` (int)
+    *   **返回数据**: `{}` (成功)
+
+*   **4.4 玩家兑换记录查询**
+    *   **API 方法**: `API.getExchangeList(ecId)`
+    *   **参数**: `ecId` (int)
+    *   **返回数据**: (Array)
+        ```json
+        [
+            {
+                "exchangeId": 123,
+                "goodsName": "商品A",
+                "createTime": 1700000000,
+                "status": 1 // 1:成功
+            }
+        ]
+        ```
+
+#### 5. 消息与通知
+
+*   **5.1 未读消息列表查询**
+    *   **API 方法**: `API.getUnReadMessages(relationId, relationType)`
+    *   **参数**: `relationId` (int), `relationType` (int)
+    *   **返回数据**: (Array)
+        ```json
+        [
+            {
+                "mqId": 1,
+                "mqType": 1, // 1:成就, 3:系统
+                "mqTitle": "标题",
+                "mqMessage": "内容",
+                "iconUrl": "http://..."
+            }
+        ]
+        ```
+
+*   **5.2 警告列表查询**
+    *   **API 方法**: `API.getWarnMessage(ecId)`
+    *   **参数**: `ecId` (int)
+    *   **返回数据**: (Array)
+        ```json
+        [
+            {
+                "warnType": 1,
+                "warnTitle": "黄牌警告",
+                "warnMessage": "您违规了...",
+                "iconUrl": "http://..."
+            }
+        ]
+        ```
+
+#### 6. 网格游戏
+
+*   **6.1 网格卡片信息查询**
+    *   **API 方法**: `API.getGrids(ecId)`
+    *   **参数**: `ecId` (int)
+    *   **返回数据**:
+        ```json
+        {
+            "compId": 101,
+            "compName": "网格赛",
+            "widthNum": 3,
+            "heightNum": 3,
+            "maskImgPic": "http://...",
+            "actualImgPic": "http://...",
+            "state": 2, // 1:未开始 2:进行中 3:已结束
+            "detailRs": [
+                {
+                    "orderNum": 1, // 格子序号
+                    "isComplete": 1, // 1:点亮 0:未点亮
+                    "showName": "格子1",
+                    "relationType": 1, // 1:活动详情 2:列表
+                    "ocaId": 201 // 关联ID
+                }
+            ]
+        }
+        ```
+
+#### 7. 电子证书
+
+*   **7.1 查询电子证书样式**
+    *   **API 方法**: `API.getCertStyle(ecId)`
+    *   **参数**: `ecId` (int)
+    *   **返回数据**:
+        ```json
+        {
+            "styleId": 1,
+            "styleName": "默认样式",
+            "templateUrl": "http://...", // 证书模板图片 URL
+            "elements": [
+                { "type": "text", "field": "userName", "x": 100, "y": 200, "fontSize": 20, "color": "#000" }
+                // 更多元素配置,如位置、字体、颜色
+            ]
+        }
+        ```
+
+*   **7.2 查询电子证书成就对应用户基本信息**
+    *   **API 方法**: `API.getUserBaseInCertificate(ecId)`
+    *   **参数**: `ecId` (int)
+    *   **返回数据**:
+        ```json
+        {
+            "userName": "用户名称",
+            "activityName": "活动名称",
+            "completionTime": 1700000000 // 完成时间戳
+        }
+        ```
+
+*   **7.3 根据成就信息确认生成电子证书**
+    *   **API 方法**: `API.createCertificate(data)`
+    *   **参数**:
+        *   `data` (object): 包含生成证书所需的所有参数,通常包括 `ecId`, `certStyleId`, `userName`, `activityName`, `completionTime` 等。具体结构请参考后端文档或 `api.js` 源码中的请求示例。
+    *   **返回数据**:
+        ```json
+        {
+            "certUrl": "http://example.com/generated_cert.png" // 生成的证书图片 URL
+        }
+        ```
+
+#### 8. 其他辅助接口
+
+*   **8.1 用户基本信息查询**
+    *   **API 方法**: `API.getUserInfo()`
+    *   **参数**: 无
+    *   **返回数据**:
+        ```json
+        {
+            "nickName": "用户昵称",
+            "headUrl": "http://..."
+        }
+        ```
+*   **8.2 卡片对应地图列表详情查询**
+    *   **API 方法**: `API.getMapList(ecId)`
+    *   **参数**: `ecId` (int)
+    *   **返回数据**: (Array)
+        ```json
+        [
+            {
+                "mapId": 1,
+                "mapName": "奥体中心",
+                "latitude": 36.666,
+                "longitude": 117.123,
+                "activityList": [
+                    { "ocaId": 101, "showName": "迷你跑" }
+                ]
+            }
+        ]
+        ```

+ 55 - 0
card/sdk_delivery/GUIDE.md

@@ -0,0 +1,55 @@
+# 第三方 H5 对接指南
+
+## 1. 目录结构说明
+
+交付包包含以下核心文件:
+
+*   `bridge.js`: **核心通信库**,封装了与 App 的交互,必须在所有页面引入。
+*   `api.js`: **后端 API 库**,封装了所有服务器请求接口。
+*   `mock_flutter.js`: **调试工具**,仅在本地开发使用。
+*   `demo_project/`: **完整示例项目**,包含首页、排行榜、报名等真实场景代码。
+*   `API.md`: 详细的 App 交互接口文档 (Bridge)。
+*   `API_SERVER.md`: 后端服务器接口文档 (API)。
+
+## 2. 开发流程
+
+### 第一步:参考 Demo
+建议直接打开 `demo_project/index.html` (本地双击即可运行),体验完整的交互流程。您可以参考其中的 `rank.html` 和 `signup.html` 来构建您的业务页面。
+
+### 第二步:初始化 SDK
+在您的 H5 项目(无论是 Vue, React 还是原生 HTML)的入口 HTML 文件中引入核心库:
+
+```html
+<script src="./bridge.js"></script>
+<script src="./api.js"></script>
+```
+
+### 第三步:开启调试模式 (Mock)
+为了方便在 Chrome/Edge 浏览器中开发,请在 `bridge.js` 之前引入 `mock_flutter.js`。
+此外,在初始化 API 时,如果是本地环境,建议开启 Mock 模式:
+
+```javascript
+// 检测是否本地开发环境
+var isLocal = window.location.protocol === 'file:' || window.location.hostname === 'localhost';
+
+API.init({
+    token: '...', 
+    useMock: isLocal // 本地开发自动开启 Mock,直接返回模拟数据
+});
+```
+
+### 第四步:打包交付
+**关键步骤**:
+在构建生产环境包(Build)之前,请**移除**对 `mock_flutter.js` 的引用。
+App 环境会自动注入真实的通信通道。
+
+## 3. 交付物清单
+
+请向我们提供:
+1.  构建后的静态资源包 (dist 目录)。
+2.  部署说明(如果需要特殊的 Nginx 配置)。
+
+## 4. 注意事项
+
+*   **OSS 图片**: 后端接口返回的图片路径通常不带域名,请使用 `API.getOssUrl()` 获取基础域名并进行拼接。
+*   **Token**: 在 App 环境中,H5 页面加载时 URL 参数中通常会携带 `token`。如果 URL 中没有,请使用 `Bridge.getToken()` 获取。

+ 319 - 0
card/sdk_delivery/api.js

@@ -0,0 +1,319 @@
+(function (window) {
+    'use strict';
+
+    /**
+     * ColorMapRun API SDK (Full Version with Mock)
+     * 封装了与后端服务器的所有交互
+     * 依赖: bridge.js (用于 401 跳转登录)
+     */
+
+    // 基础配置
+    var Config = {
+        baseUrl: 'https://t-mapi.colormaprun.com/api/card/', 
+        ossUrl: 'http://oss-card.colormaprun.com/card/',
+        token: '',
+        useMock: false 
+    };
+
+    // 模拟数据定义 (涵盖所有接口)
+    var MOCK_DB = {
+        'CardBaseQuery': {
+            ecName: '[Mock]跑向大明湖卡片',
+            ecDesc: '欢迎参加彩图奔跑活动!',
+            beginSecond: Date.now() / 1000 - 86400 * 5,
+            endSecond: Date.now() / 1000 + 86400 * 10,
+            secondCardName: '地图导航'
+        },
+        'CardDetailQuery': {
+            mcId: 101,
+            mcName: '[Mock]线上马拉松',
+            mcType: 1,
+            beginSecond: Date.now() / 1000 - 86400 * 2,
+            endSecond: Date.now() / 1000 + 86400 * 5,
+            teamNum: 0,
+            coiName: '个人组'
+        },
+        'MatchRsDetailQuery': [
+            { id: 1, name: '[Mock]活动1', status: 1 },
+            { id: 2, name: '[Mock]活动2', status: 0 }
+        ],
+        'CardRankDetailQuery': {
+            totalRankRs: [
+                { nickName: 'Mock张三', score: 10000, headUrl: 'https://picsum.photos/40/40?random=1', rankNum: 1 },
+                { nickName: 'Mock李四', score: 9500, headUrl: 'https://picsum.photos/40/40?random=2', rankNum: 2 },
+                { nickName: 'Mock王五', score: 8800, headUrl: 'https://picsum.photos/40/40?random=3', rankNum: 3 },
+                { nickName: 'Mock赵六', score: 7200, headUrl: 'https://picsum.photos/40/40?random=4', rankNum: 4 },
+                { nickName: 'Mock小明', score: 6500, headUrl: 'https://picsum.photos/40/40?random=5', rankNum: 5 }
+            ],
+            teamRankRs: [],
+            inTeamRs: [],
+            otherRs: []
+        },
+        'UserCurrentRankNumQuery': { rankNum: 5 },
+        'UserJoinCardQuery': { isJoin: false }, // 默认未报名
+        'IsNewUserInCardComp': { isNew: true },
+        'OnlineMcSignUpDetail': {
+            teamList: [
+                { teamId: 1, teamName: '[Mock]个人组' },
+                { teamId: 2, teamName: '[Mock]团队组' }
+            ],
+            signupFields: [
+                { name: 'realName', label: '真实姓名', type: 'text', required: true, value: '' },
+                { name: 'phone', label: '手机号码', type: 'tel', required: true, value: '' }
+            ]
+        },
+        'OnlineMcSignUp': {}, // 报名成功返回空 data
+        'IsAllowMcSignUp': { allowSignUp: true },
+        'CurrentMonthlyChallengeQuery': {
+            year: '2023',
+            monthRs: [{ month: 11, realNum: 10, targetNum: 20 }]
+        },
+        'CardConfigQuery': {
+            configJson: JSON.stringify({
+                css: ".custom-header { background-color: #f0f8ff; }",
+                tabActiveColor: "#007bff",
+                popupRuleConfig: { height: "60%", theme: "light" },
+                popupRuleList: [
+                    { type: 1, data: { title: '规则1', content: '这是<b>Mock</b>的活动规则内容。' } },
+                    { type: 1, data: { title: '规则2', content: '第二条规则。', logo: { src: 'https://picsum.photos/100/50', width: '100px', height: '50px' } } }
+                ]
+            })
+        },
+        'UserConfigQuery': {
+            configJson: JSON.stringify({
+                tplInfo: { tplTypeId: 1, ssctId: 1 },
+                mapInfo: [
+                    { activityList: [{ showName: '迷你路线', pathImg: '', ocaId: 1, matchType: 1, point: { longitude: 117.0, latitude: 36.6, name: '起点' } }] }
+                ],
+                popupRuleList: [{ type: 1, data: { title: '用户个性化', content: '这是用户专属内容' } }]
+            })
+        },
+        'MonthlyChallengeQuery': [
+            { year: '2023', monthRs: [{ month: 10, realNum: 15, targetNum: 20 }] },
+            { year: '2022', monthRs: [{ month: 12, realNum: 25, targetNum: 20 }] }
+        ],
+        'MonthRankDetailQuery': [
+            { nickName: '月榜冠军', score: 500, headUrl: 'https://picsum.photos/40/40?random=6' }
+        ],
+        'AchievementQuery': [
+            {
+                year: '2023',
+                aiRs: [
+                    { aiName: '初次登场', aiTime: Date.now() / 1000 - 86400 * 30, iconUrl: 'https://picsum.photos/60/60?random=7' },
+                    { aiName: '跑步达人', aiTime: Date.now() / 1000 - 86400 * 10, iconUrl: 'https://picsum.photos/60/60?random=8' }
+                ]
+            }
+        ],
+        'ExchangeListQuery': [
+            { exchangeId: 1, goodsName: '[Mock]运动手环', createTime: Date.now() / 1000 - 86400 * 7, status: 1 },
+            { exchangeId: 2, goodsName: '[Mock]定制水杯', createTime: Date.now() / 1000 - 86400 * 15, status: 0 }
+        ],
+        'ExchangeDetailQuery': {
+            exchangeId: 1, goodsName: '[Mock]运动手环', createTime: Date.now() / 1000 - 86400 * 7, status: 1,
+            address: 'Mock地址', receiver: 'Mock收件人', phone: '138****8888'
+        },
+        'UnReadMessageQuery': [
+            { mqId: 1, mqType: 1, mqTitle: '[Mock]新成就', mqMessage: '恭喜您解锁新成就!', iconUrl: 'https://picsum.photos/50/50?random=9' }
+        ],
+        'ReadMessage': {},
+        'MapListQuery': [
+            { mapId: 1, mapName: '[Mock]公园地图', latitude: 36.6, longitude: 117.0, activityList: [] }
+        ],
+        'CompStatisticQuery': { totalDistance: 123.45, totalPeople: 1000 },
+        'WarnMessageQuery': [
+            { warnType: 1, warnTitle: '[Mock]黄牌警告', warnMessage: '您的成绩异常', iconUrl: 'https://picsum.photos/50/50?random=10' }
+        ],
+        'CertStyleQuery': {
+            styleId: 1, styleName: '简约风', templateUrl: 'https://picsum.photos/600/400?random=11',
+            elements: [
+                { type: 'text', field: 'userName', x: 100, y: 150, fontSize: 24, color: '#333' }
+            ]
+        },
+        'UserBaseQueryInCertificate': { userName: '[Mock]证书用户', activityName: '[Mock]活动名', completionTime: Date.now() / 1000 },
+        'CertificateCreateByUserAi': { certUrl: 'https://picsum.photos/600/400?random=12' },
+        'OnlineScoreQuery': { score: 880, extTime: Date.now() / 1000 + 86400 * 30 },
+        'CanExchangeGoodsList': [
+            { goodsId: 1, goodsName: '[Mock]运动手环', goodsPic: 'https://picsum.photos/100/100?random=13', goodsLeftNum: 50, corrScore: 500 },
+            { goodsId: 2, goodsName: '[Mock]定制水杯', goodsPic: 'https://picsum.photos/100/100?random=14', goodsLeftNum: 0, corrScore: 300 }
+        ],
+        'CanExchangeGoodsDetail': {
+            goodsId: 1, goodsName: '[Mock]运动手环', goodsPic: 'https://picsum.photos/200/200?random=15', corrScore: 500,
+            desc: '这是<b>Mock</b>的运动手环详情,功能强大,是您运动的好伙伴!'
+        },
+        'ScoreExchangeGoods': {},
+        'UserBasicInformationQuery': { nickName: 'Mock用户', headUrl: 'https://picsum.photos/50/50?random=16' },
+        'GridsQuery': {
+            compId: 201, compName: '[Mock]网格挑战', widthNum: 3, heightNum: 3,
+            maskImgPic: 'https://picsum.photos/300/300?random=17',
+            actualImgPic: 'https://picsum.photos/300/300?random=18',
+            state: 2, // 1:未开始 2:进行中 3:已结束
+            detailRs: [
+                { orderNum: 1, isComplete: 1, showName: '完成1', relationType: 1, ocaId: 10, longitude: 117.1, latitude: 36.6, popupImg: 'https://picsum.photos/100/100?random=19' },
+                { orderNum: 2, isComplete: 0, showName: '未完2', relationType: 1, ocaId: 11, longitude: 117.2, latitude: 36.7, popupImg: 'https://picsum.photos/100/100?random=20' }
+            ]
+        }
+    };
+
+    var API = {
+        init: function(options) {
+            if (options.baseUrl) Config.baseUrl = options.baseUrl;
+            if (options.ossUrl) Config.ossUrl = options.ossUrl;
+            if (options.token) Config.token = options.token;
+            if (typeof options.useMock === 'boolean') Config.useMock = options.useMock;
+            if (Config.useMock) console.warn('%c [API] Mock 模式已开启 ', 'background: orange; color: white;');
+        },
+
+        getOssUrl: function() {
+            return Config.ossUrl;
+        },
+
+        setToken: function(token) {
+            Config.token = token;
+        },
+
+        request: function(endpoint, data) {
+            if (Config.useMock) {
+                return new Promise(function(resolve, reject) {
+                    console.log('[API-Mock] Request:', endpoint, data);
+                    setTimeout(function() {
+                        var mockData = MOCK_DB[endpoint];
+                        if (endpoint === 'OnlineMcSignUp') MOCK_DB['UserJoinCardQuery'].isJoin = true;
+                        if (endpoint === 'ScoreExchangeGoods') MOCK_DB['OnlineScoreQuery'].score -= 100;
+                        
+                        console.log('[API-Mock] Response:', endpoint, mockData || {});
+                        resolve(mockData || {});
+                    }, 300); 
+                });
+            }
+
+            var url = Config.baseUrl + endpoint;
+            var headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'token': Config.token };
+            var formData = new URLSearchParams();
+            for (var key in data) { if (data.hasOwnProperty(key)) formData.append(key, data[key]); }
+
+            console.log('[API] Request:', endpoint, data);
+
+            return fetch(url, { method: 'POST', headers: headers, body: formData })
+            .then(function(response) { return response.json(); })
+            .then(function(res) {
+                console.log('[API] Response:', endpoint, res);
+                if (res.code === 0) return res.data;
+                if (res.code === 401 || res.statusCode === 401) {
+                    console.warn('[API] Token invalid');
+                    if (window.Bridge && window.Bridge._post) window.Bridge._post('toLogin');
+                    else alert('登录已过期');
+                    throw new Error('Unauthorized');
+                }
+                var msg = res.message || '请求失败';
+                alert(msg);
+                throw new Error(msg);
+            })
+            .catch(function(err) { console.error('[API] Error:', err); throw err; });
+        },
+
+        // ==============================
+        // 完整业务接口封装 (按原始 api.js 顺序)
+        // ==============================
+
+        // 1. 卡片基本信息查询
+        getCardBase: function(ecId, pageName) { return this.request('CardBaseQuery', { ecId: ecId, pageName: pageName }); },
+
+        // 2. 卡片对应活动或赛事详情查询
+        getCardDetail: function(ecId) { return this.request('CardDetailQuery', { ecId: ecId }); },
+
+        // 3. 卡片对应线上赛多个活动查询
+        getMatchRsDetail: function(ecId) { return this.request('MatchRsDetailQuery', { ecId: ecId }); },
+
+        // 4. 排名查询
+        getRankDetail: function(mcIdListStr, mcType, dispArrStr) { return this.request('CardRankDetailQuery', { mcIdListStr: mcIdListStr, mcType: mcType, dispArrStr: dispArrStr }); },
+
+        // 5. 卡片用户当前排名查询
+        getUserCurrentRank: function(ecId) { return this.request('UserCurrentRankNumQuery', { ecId: ecId }); },
+
+        // 6. 用户是否已经报名卡片对应赛事查询
+        getUserJoinStatus: function(ecId) { return this.request('UserJoinCardQuery', { ecId: ecId }); },
+
+        // 7. 用户在卡片对应赛事是否新用户
+        isNewUserInCardComp: function(ecId) { return this.request('IsNewUserInCardComp', { ecId: ecId }); },
+
+        // 8. 线上赛报名页面信息详情
+        getOnlineMcSignUpDetail: function(ecId) { return this.request('OnlineMcSignUpDetail', { ecId: ecId }); },
+
+        // 9. 线上赛报名(重新分组)
+        signUpOnline: function(mcId, coiId, selectTeam, nickName) { 
+            return this.request('OnlineMcSignUp', { mcId: mcId, coiId: coiId, selectTeam: selectTeam, nickName: nickName }); 
+        },
+
+        // 10. 是否允许重新分组(报名)
+        isAllowMcSignUp: function(ecId) { return this.request('IsAllowMcSignUp', { ecId: ecId }); },
+
+        // 11. 玩家当前月挑战记录查询
+        getCurrentMonthlyChallenge: function() { return this.request('CurrentMonthlyChallengeQuery', {}); },
+
+        // 12. 卡片配置信息查询
+        getCardConfig: function(ecId, pageName) { return this.request('CardConfigQuery', { ecId: ecId, pageName: pageName }); },
+
+        // 13. 用户自定义配置信息查询
+        getUserConfig: function(ecId, pageName) { return this.request('UserConfigQuery', { ecId: ecId, pageName: pageName }); },
+
+        // 14. 玩家所有月挑战记录查询
+        getMonthlyChallenge: function() { return this.request('MonthlyChallengeQuery', {}); },
+
+        // 15. 月挑战排名查询
+        getMonthRankDetail: function() { return this.request('MonthRankDetailQuery', {}); },
+
+        // 16. 玩家活动成就查询
+        getAchievement: function() { return this.request('AchievementQuery', {}); },
+
+        // 17. 玩家兑换记录查询
+        getExchangeList: function(ecId) { return this.request('ExchangeListQuery', { ecId: ecId }); },
+
+        // 18. 玩家兑换详情查询
+        getExchangeDetail: function(ecId, exchangeId) { return this.request('ExchangeDetailQuery', { ecId: ecId, exchangeId: exchangeId }); },
+
+        // 19. 未读消息列表查询
+        getUnReadMessages: function(relationId, relationType) { return this.request('UnReadMessageQuery', { relationId: relationId, relationType: relationType || 2 }); },
+
+        // 20. 标记消息已读
+        readMessage: function(mqIdListStr) { return this.request('ReadMessage', { mqIdListStr: mqIdListStr }); },
+
+        // 21. 卡片对应地图列表详情查询
+        getMapList: function(ecId) { return this.request('MapListQuery', { ecId: ecId }); },
+
+        // 22. 赛事总成绩统计查询
+        getCompStatistic: function(ecId) { return this.request('CompStatisticQuery', { ecId: ecId }); },
+
+        // 23. 警告列表查询
+        getWarnMessage: function(ecId) { return this.request('WarnMessageQuery', { ecId: ecId }); },
+
+        // 24. 查询电子证书样式
+        getCertStyle: function(ecId) { return this.request('CertStyleQuery', { ecId: ecId }); },
+
+        // 25. 查询电子证书成就对应用户基本信息
+        getUserBaseInCertificate: function(ecId) { return this.request('UserBaseQueryInCertificate', { ecId: ecId }); },
+
+        // 26. 根据成就信息确认生成电子证书
+        createCertificate: function(data) { return this.request('CertificateCreateByUserAi', data); },
+
+        // 27. 卡片内可用积分查询
+        getScore: function(ecId) { return this.request('OnlineScoreQuery', { ecId: ecId }); },
+
+        // 28. 积分可兑换商品列表查询
+        getGoodsList: function(ecId) { return this.request('CanExchangeGoodsList', { ecId: ecId }); },
+
+        // 29. 积分可兑换商品详情
+        getGoodsDetail: function(goodsId) { return this.request('CanExchangeGoodsDetail', { goodsId: goodsId }); },
+
+        // 30. 积分兑换商品
+        exchangeGoods: function(ecId, goodsId) { return this.request('ScoreExchangeGoods', { ecId: ecId, goodsId: goodsId }); },
+
+        // 31. 用户基本信息查询
+        getUserInfo: function() { return this.request('UserBasicInformationQuery', {}); },
+
+        // 32. 网格卡片信息查询
+        getGrids: function(ecId) { return this.request('GridsQuery', { ecId: ecId }); }
+    };
+
+    window.API = API;
+
+})(window);

+ 189 - 0
card/sdk_delivery/bridge.js

@@ -0,0 +1,189 @@
+(function (window) {
+  'use strict';
+
+  /**
+   * ColorMapRun JSBridge SDK (Compatible Version)
+   * 用于 H5 页面与 Flutter App 进行交互
+   * 兼容性说明:
+   * - 优先检测 uni.webView 标准通道
+   * - 降级适配旧版 App 的 action:// 协议拦截和 window.share_wx 注入对象
+   */
+
+  var Bridge = {
+    version: '1.0.2',
+    
+    /**
+     * 内部核心发送方法
+     */
+    _post: function (action, data) {
+      data = data || {};
+      console.log('[Bridge] Call:', action, data);
+
+      // 1. 优先尝试标准 uni 通道 (新版 App)
+      if (window.uni && window.uni.postMessage) {
+        window.uni.postMessage({
+          data: {
+            action: action,
+            data: data
+          }
+        });
+        return;
+      }
+
+      // 2. 降级适配 (旧版 App)
+      this._fallback(action, data);
+    },
+
+    /**
+     * 旧版 App 适配逻辑
+     */
+    _fallback: function (action, data) {
+      var url = '';
+
+      switch (action) {
+        case 'back':
+          // 尝试关闭页面或返回
+          window.history.back();
+          break;
+          
+        case 'toHome':
+          // 协议: action://to_home/
+          url = 'action://to_home/';
+          break;
+
+        case 'toLogin':
+          // 协议: action://to_login/
+          url = 'action://to_login/';
+          break;
+
+        case 'openMap':
+          // 协议: action://to_map_app?title=xxx&latitude=xxx&longitude=xxx
+          url = 'action://to_map_app?title=' + encodeURIComponent(data.name || '') +
+                '&latitude=' + data.latitude +
+                '&longitude=' + data.longitude;
+          break;
+
+        case 'openMatch':
+          // 协议: action://to_detail/?id=xxx&matchType=xxx
+          url = 'action://to_detail/?id=' + data.id +
+                '&matchType=' + (data.type || 1);
+          break;
+          
+        case 'openActivityList':
+           // 协议: action://to_activity_list/?id=xxx&mapName=xxx
+           url = 'action://to_activity_list/?id=' + data.id +
+                 '&mapName=' + encodeURIComponent(data.mapName || '');
+           break;
+
+        case 'shareWx':
+          // 旧版使用注入对象 share_wx
+          if (window.share_wx && window.share_wx.postMessage) {
+             window.share_wx.postMessage(JSON.stringify(data));
+          } else {
+             console.error('[Bridge] share_wx injection not found');
+             alert('微信分享功能不可用(环境不支持)');
+          }
+          return; // shareWx 不需要走 URL 拦截
+
+        case 'launchWxMini':
+           // 旧版使用注入对象 wx_launch_mini
+           if (window.wx_launch_mini && window.wx_launch_mini.postMessage) {
+               window.wx_launch_mini.postMessage(JSON.stringify(data));
+           } else {
+               console.error('[Bridge] wx_launch_mini injection not found');
+           }
+           return;
+
+        case 'saveImage':
+           // 旧版使用注入对象 save_base64
+           if (window.save_base64 && window.save_base64.postMessage) {
+               window.save_base64.postMessage(data.base64);
+           } else {
+               console.error('[Bridge] save_base64 injection not found');
+           }
+           return;
+
+        default:
+          console.warn('[Bridge] No legacy fallback for action:', action);
+      }
+
+      if (url) {
+        console.log('[Bridge] Legacy URL jump:', url);
+        // 触发 URL 拦截
+        window.location.href = url;
+      }
+    },
+
+    // ==============================
+    // 公开 API
+    // ==============================
+
+    back: function () {
+      this._post('back');
+    },
+    
+    toHome: function() {
+        this._post('toHome');
+    },
+    
+    toLogin: function() {
+        this._post('toLogin');
+    },
+
+    setTitle: function (title) {
+      this._post('setTitle', { title: title });
+    },
+
+    openMap: function (latitude, longitude, name) {
+      this._post('openMap', {
+        latitude: latitude,
+        longitude: longitude,
+        name: name
+      });
+    },
+
+    openMatch: function (id, type) {
+      this._post('openMatch', {
+        id: id,
+        type: type
+      });
+    },
+    
+    openActivityList: function(id, mapName) {
+        this._post('openActivityList', {
+            id: id,
+            mapName: mapName
+        });
+    },
+
+    shareWx: function (options) {
+      this._post('shareWx', options);
+    },
+
+    launchWxMini: function (username, path) {
+      this._post('launchWxMini', { username: username, path: path });
+    },
+    
+    saveImage: function (base64Str) {
+       this._post('saveImage', { base64: base64Str });
+    },
+    
+    getToken: function () {
+        this._post('getToken');
+    },
+    
+    _tokenCallback: null,
+    onToken: function(callback) {
+        this._tokenCallback = callback;
+    },
+    receiveToken: function(token) {
+        console.log('[Bridge] Received token:', token);
+        if (this._tokenCallback) {
+            this._tokenCallback(token);
+        }
+    }
+  };
+
+  window.Bridge = Bridge;
+
+})(window);

+ 219 - 0
card/sdk_delivery/demo.html

@@ -0,0 +1,219 @@
+<!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>

+ 319 - 0
card/sdk_delivery/demo_project/api.js

@@ -0,0 +1,319 @@
+(function (window) {
+    'use strict';
+
+    /**
+     * ColorMapRun API SDK (Full Version with Mock)
+     * 封装了与后端服务器的所有交互
+     * 依赖: bridge.js (用于 401 跳转登录)
+     */
+
+    // 基础配置
+    var Config = {
+        baseUrl: 'https://t-mapi.colormaprun.com/api/card/', 
+        ossUrl: 'http://oss-card.colormaprun.com/card/',
+        token: '',
+        useMock: false 
+    };
+
+    // 模拟数据定义 (涵盖所有接口)
+    var MOCK_DB = {
+        'CardBaseQuery': {
+            ecName: '[Mock]跑向大明湖卡片',
+            ecDesc: '欢迎参加彩图奔跑活动!',
+            beginSecond: Date.now() / 1000 - 86400 * 5,
+            endSecond: Date.now() / 1000 + 86400 * 10,
+            secondCardName: '地图导航'
+        },
+        'CardDetailQuery': {
+            mcId: 101,
+            mcName: '[Mock]线上马拉松',
+            mcType: 1,
+            beginSecond: Date.now() / 1000 - 86400 * 2,
+            endSecond: Date.now() / 1000 + 86400 * 5,
+            teamNum: 0,
+            coiName: '个人组'
+        },
+        'MatchRsDetailQuery': [
+            { id: 1, name: '[Mock]活动1', status: 1 },
+            { id: 2, name: '[Mock]活动2', status: 0 }
+        ],
+        'CardRankDetailQuery': {
+            totalRankRs: [
+                { nickName: 'Mock张三', score: 10000, headUrl: 'https://picsum.photos/40/40?random=1', rankNum: 1 },
+                { nickName: 'Mock李四', score: 9500, headUrl: 'https://picsum.photos/40/40?random=2', rankNum: 2 },
+                { nickName: 'Mock王五', score: 8800, headUrl: 'https://picsum.photos/40/40?random=3', rankNum: 3 },
+                { nickName: 'Mock赵六', score: 7200, headUrl: 'https://picsum.photos/40/40?random=4', rankNum: 4 },
+                { nickName: 'Mock小明', score: 6500, headUrl: 'https://picsum.photos/40/40?random=5', rankNum: 5 }
+            ],
+            teamRankRs: [],
+            inTeamRs: [],
+            otherRs: []
+        },
+        'UserCurrentRankNumQuery': { rankNum: 5 },
+        'UserJoinCardQuery': { isJoin: false }, // 默认未报名
+        'IsNewUserInCardComp': { isNew: true },
+        'OnlineMcSignUpDetail': {
+            teamList: [
+                { teamId: 1, teamName: '[Mock]个人组' },
+                { teamId: 2, teamName: '[Mock]团队组' }
+            ],
+            signupFields: [
+                { name: 'realName', label: '真实姓名', type: 'text', required: true, value: '' },
+                { name: 'phone', label: '手机号码', type: 'tel', required: true, value: '' }
+            ]
+        },
+        'OnlineMcSignUp': {}, // 报名成功返回空 data
+        'IsAllowMcSignUp': { allowSignUp: true },
+        'CurrentMonthlyChallengeQuery': {
+            year: '2023',
+            monthRs: [{ month: 11, realNum: 10, targetNum: 20 }]
+        },
+        'CardConfigQuery': {
+            configJson: JSON.stringify({
+                css: ".custom-header { background-color: #f0f8ff; }",
+                tabActiveColor: "#007bff",
+                popupRuleConfig: { height: "60%", theme: "light" },
+                popupRuleList: [
+                    { type: 1, data: { title: '规则1', content: '这是<b>Mock</b>的活动规则内容。' } },
+                    { type: 1, data: { title: '规则2', content: '第二条规则。', logo: { src: 'https://picsum.photos/100/50', width: '100px', height: '50px' } } }
+                ]
+            })
+        },
+        'UserConfigQuery': {
+            configJson: JSON.stringify({
+                tplInfo: { tplTypeId: 1, ssctId: 1 },
+                mapInfo: [
+                    { activityList: [{ showName: '迷你路线', pathImg: '', ocaId: 1, matchType: 1, point: { longitude: 117.0, latitude: 36.6, name: '起点' } }] }
+                ],
+                popupRuleList: [{ type: 1, data: { title: '用户个性化', content: '这是用户专属内容' } }]
+            })
+        },
+        'MonthlyChallengeQuery': [
+            { year: '2023', monthRs: [{ month: 10, realNum: 15, targetNum: 20 }] },
+            { year: '2022', monthRs: [{ month: 12, realNum: 25, targetNum: 20 }] }
+        ],
+        'MonthRankDetailQuery': [
+            { nickName: '月榜冠军', score: 500, headUrl: 'https://picsum.photos/40/40?random=6' }
+        ],
+        'AchievementQuery': [
+            {
+                year: '2023',
+                aiRs: [
+                    { aiName: '初次登场', aiTime: Date.now() / 1000 - 86400 * 30, iconUrl: 'https://picsum.photos/60/60?random=7' },
+                    { aiName: '跑步达人', aiTime: Date.now() / 1000 - 86400 * 10, iconUrl: 'https://picsum.photos/60/60?random=8' }
+                ]
+            }
+        ],
+        'ExchangeListQuery': [
+            { exchangeId: 1, goodsName: '[Mock]运动手环', createTime: Date.now() / 1000 - 86400 * 7, status: 1 },
+            { exchangeId: 2, goodsName: '[Mock]定制水杯', createTime: Date.now() / 1000 - 86400 * 15, status: 0 }
+        ],
+        'ExchangeDetailQuery': {
+            exchangeId: 1, goodsName: '[Mock]运动手环', createTime: Date.now() / 1000 - 86400 * 7, status: 1,
+            address: 'Mock地址', receiver: 'Mock收件人', phone: '138****8888'
+        },
+        'UnReadMessageQuery': [
+            { mqId: 1, mqType: 1, mqTitle: '[Mock]新成就', mqMessage: '恭喜您解锁新成就!', iconUrl: 'https://picsum.photos/50/50?random=9' }
+        ],
+        'ReadMessage': {},
+        'MapListQuery': [
+            { mapId: 1, mapName: '[Mock]公园地图', latitude: 36.6, longitude: 117.0, activityList: [] }
+        ],
+        'CompStatisticQuery': { totalDistance: 123.45, totalPeople: 1000 },
+        'WarnMessageQuery': [
+            { warnType: 1, warnTitle: '[Mock]黄牌警告', warnMessage: '您的成绩异常', iconUrl: 'https://picsum.photos/50/50?random=10' }
+        ],
+        'CertStyleQuery': {
+            styleId: 1, styleName: '简约风', templateUrl: 'https://picsum.photos/600/400?random=11',
+            elements: [
+                { type: 'text', field: 'userName', x: 100, y: 150, fontSize: 24, color: '#333' }
+            ]
+        },
+        'UserBaseQueryInCertificate': { userName: '[Mock]证书用户', activityName: '[Mock]活动名', completionTime: Date.now() / 1000 },
+        'CertificateCreateByUserAi': { certUrl: 'https://picsum.photos/600/400?random=12' },
+        'OnlineScoreQuery': { score: 880, extTime: Date.now() / 1000 + 86400 * 30 },
+        'CanExchangeGoodsList': [
+            { goodsId: 1, goodsName: '[Mock]运动手环', goodsPic: 'https://picsum.photos/100/100?random=13', goodsLeftNum: 50, corrScore: 500 },
+            { goodsId: 2, goodsName: '[Mock]定制水杯', goodsPic: 'https://picsum.photos/100/100?random=14', goodsLeftNum: 0, corrScore: 300 }
+        ],
+        'CanExchangeGoodsDetail': {
+            goodsId: 1, goodsName: '[Mock]运动手环', goodsPic: 'https://picsum.photos/200/200?random=15', corrScore: 500,
+            desc: '这是<b>Mock</b>的运动手环详情,功能强大,是您运动的好伙伴!'
+        },
+        'ScoreExchangeGoods': {},
+        'UserBasicInformationQuery': { nickName: 'Mock用户', headUrl: 'https://picsum.photos/50/50?random=16' },
+        'GridsQuery': {
+            compId: 201, compName: '[Mock]网格挑战', widthNum: 3, heightNum: 3,
+            maskImgPic: 'https://picsum.photos/300/300?random=17',
+            actualImgPic: 'https://picsum.photos/300/300?random=18',
+            state: 2, // 1:未开始 2:进行中 3:已结束
+            detailRs: [
+                { orderNum: 1, isComplete: 1, showName: '完成1', relationType: 1, ocaId: 10, longitude: 117.1, latitude: 36.6, popupImg: 'https://picsum.photos/100/100?random=19' },
+                { orderNum: 2, isComplete: 0, showName: '未完2', relationType: 1, ocaId: 11, longitude: 117.2, latitude: 36.7, popupImg: 'https://picsum.photos/100/100?random=20' }
+            ]
+        }
+    };
+
+    var API = {
+        init: function(options) {
+            if (options.baseUrl) Config.baseUrl = options.baseUrl;
+            if (options.ossUrl) Config.ossUrl = options.ossUrl;
+            if (options.token) Config.token = options.token;
+            if (typeof options.useMock === 'boolean') Config.useMock = options.useMock;
+            if (Config.useMock) console.warn('%c [API] Mock 模式已开启 ', 'background: orange; color: white;');
+        },
+
+        getOssUrl: function() {
+            return Config.ossUrl;
+        },
+
+        setToken: function(token) {
+            Config.token = token;
+        },
+
+        request: function(endpoint, data) {
+            if (Config.useMock) {
+                return new Promise(function(resolve, reject) {
+                    console.log('[API-Mock] Request:', endpoint, data);
+                    setTimeout(function() {
+                        var mockData = MOCK_DB[endpoint];
+                        if (endpoint === 'OnlineMcSignUp') MOCK_DB['UserJoinCardQuery'].isJoin = true;
+                        if (endpoint === 'ScoreExchangeGoods') MOCK_DB['OnlineScoreQuery'].score -= 100;
+                        
+                        console.log('[API-Mock] Response:', endpoint, mockData || {});
+                        resolve(mockData || {});
+                    }, 300); 
+                });
+            }
+
+            var url = Config.baseUrl + endpoint;
+            var headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'token': Config.token };
+            var formData = new URLSearchParams();
+            for (var key in data) { if (data.hasOwnProperty(key)) formData.append(key, data[key]); }
+
+            console.log('[API] Request:', endpoint, data);
+
+            return fetch(url, { method: 'POST', headers: headers, body: formData })
+            .then(function(response) { return response.json(); })
+            .then(function(res) {
+                console.log('[API] Response:', endpoint, res);
+                if (res.code === 0) return res.data;
+                if (res.code === 401 || res.statusCode === 401) {
+                    console.warn('[API] Token invalid');
+                    if (window.Bridge && window.Bridge._post) window.Bridge._post('toLogin');
+                    else alert('登录已过期');
+                    throw new Error('Unauthorized');
+                }
+                var msg = res.message || '请求失败';
+                alert(msg);
+                throw new Error(msg);
+            })
+            .catch(function(err) { console.error('[API] Error:', err); throw err; });
+        },
+
+        // ==============================
+        // 完整业务接口封装 (按原始 api.js 顺序)
+        // ==============================
+
+        // 1. 卡片基本信息查询
+        getCardBase: function(ecId, pageName) { return this.request('CardBaseQuery', { ecId: ecId, pageName: pageName }); },
+
+        // 2. 卡片对应活动或赛事详情查询
+        getCardDetail: function(ecId) { return this.request('CardDetailQuery', { ecId: ecId }); },
+
+        // 3. 卡片对应线上赛多个活动查询
+        getMatchRsDetail: function(ecId) { return this.request('MatchRsDetailQuery', { ecId: ecId }); },
+
+        // 4. 排名查询
+        getRankDetail: function(mcIdListStr, mcType, dispArrStr) { return this.request('CardRankDetailQuery', { mcIdListStr: mcIdListStr, mcType: mcType, dispArrStr: dispArrStr }); },
+
+        // 5. 卡片用户当前排名查询
+        getUserCurrentRank: function(ecId) { return this.request('UserCurrentRankNumQuery', { ecId: ecId }); },
+
+        // 6. 用户是否已经报名卡片对应赛事查询
+        getUserJoinStatus: function(ecId) { return this.request('UserJoinCardQuery', { ecId: ecId }); },
+
+        // 7. 用户在卡片对应赛事是否新用户
+        isNewUserInCardComp: function(ecId) { return this.request('IsNewUserInCardComp', { ecId: ecId }); },
+
+        // 8. 线上赛报名页面信息详情
+        getOnlineMcSignUpDetail: function(ecId) { return this.request('OnlineMcSignUpDetail', { ecId: ecId }); },
+
+        // 9. 线上赛报名(重新分组)
+        signUpOnline: function(mcId, coiId, selectTeam, nickName) { 
+            return this.request('OnlineMcSignUp', { mcId: mcId, coiId: coiId, selectTeam: selectTeam, nickName: nickName }); 
+        },
+
+        // 10. 是否允许重新分组(报名)
+        isAllowMcSignUp: function(ecId) { return this.request('IsAllowMcSignUp', { ecId: ecId }); },
+
+        // 11. 玩家当前月挑战记录查询
+        getCurrentMonthlyChallenge: function() { return this.request('CurrentMonthlyChallengeQuery', {}); },
+
+        // 12. 卡片配置信息查询
+        getCardConfig: function(ecId, pageName) { return this.request('CardConfigQuery', { ecId: ecId, pageName: pageName }); },
+
+        // 13. 用户自定义配置信息查询
+        getUserConfig: function(ecId, pageName) { return this.request('UserConfigQuery', { ecId: ecId, pageName: pageName }); },
+
+        // 14. 玩家所有月挑战记录查询
+        getMonthlyChallenge: function() { return this.request('MonthlyChallengeQuery', {}); },
+
+        // 15. 月挑战排名查询
+        getMonthRankDetail: function() { return this.request('MonthRankDetailQuery', {}); },
+
+        // 16. 玩家活动成就查询
+        getAchievement: function() { return this.request('AchievementQuery', {}); },
+
+        // 17. 玩家兑换记录查询
+        getExchangeList: function(ecId) { return this.request('ExchangeListQuery', { ecId: ecId }); },
+
+        // 18. 玩家兑换详情查询
+        getExchangeDetail: function(ecId, exchangeId) { return this.request('ExchangeDetailQuery', { ecId: ecId, exchangeId: exchangeId }); },
+
+        // 19. 未读消息列表查询
+        getUnReadMessages: function(relationId, relationType) { return this.request('UnReadMessageQuery', { relationId: relationId, relationType: relationType || 2 }); },
+
+        // 20. 标记消息已读
+        readMessage: function(mqIdListStr) { return this.request('ReadMessage', { mqIdListStr: mqIdListStr }); },
+
+        // 21. 卡片对应地图列表详情查询
+        getMapList: function(ecId) { return this.request('MapListQuery', { ecId: ecId }); },
+
+        // 22. 赛事总成绩统计查询
+        getCompStatistic: function(ecId) { return this.request('CompStatisticQuery', { ecId: ecId }); },
+
+        // 23. 警告列表查询
+        getWarnMessage: function(ecId) { return this.request('WarnMessageQuery', { ecId: ecId }); },
+
+        // 24. 查询电子证书样式
+        getCertStyle: function(ecId) { return this.request('CertStyleQuery', { ecId: ecId }); },
+
+        // 25. 查询电子证书成就对应用户基本信息
+        getUserBaseInCertificate: function(ecId) { return this.request('UserBaseQueryInCertificate', { ecId: ecId }); },
+
+        // 26. 根据成就信息确认生成电子证书
+        createCertificate: function(data) { return this.request('CertificateCreateByUserAi', data); },
+
+        // 27. 卡片内可用积分查询
+        getScore: function(ecId) { return this.request('OnlineScoreQuery', { ecId: ecId }); },
+
+        // 28. 积分可兑换商品列表查询
+        getGoodsList: function(ecId) { return this.request('CanExchangeGoodsList', { ecId: ecId }); },
+
+        // 29. 积分可兑换商品详情
+        getGoodsDetail: function(goodsId) { return this.request('CanExchangeGoodsDetail', { goodsId: goodsId }); },
+
+        // 30. 积分兑换商品
+        exchangeGoods: function(ecId, goodsId) { return this.request('ScoreExchangeGoods', { ecId: ecId, goodsId: goodsId }); },
+
+        // 31. 用户基本信息查询
+        getUserInfo: function() { return this.request('UserBasicInformationQuery', {}); },
+
+        // 32. 网格卡片信息查询
+        getGrids: function(ecId) { return this.request('GridsQuery', { ecId: ecId }); }
+    };
+
+    window.API = API;
+
+})(window);

+ 189 - 0
card/sdk_delivery/demo_project/bridge.js

@@ -0,0 +1,189 @@
+(function (window) {
+  'use strict';
+
+  /**
+   * ColorMapRun JSBridge SDK (Compatible Version)
+   * 用于 H5 页面与 Flutter App 进行交互
+   * 兼容性说明:
+   * - 优先检测 uni.webView 标准通道
+   * - 降级适配旧版 App 的 action:// 协议拦截和 window.share_wx 注入对象
+   */
+
+  var Bridge = {
+    version: '1.0.2',
+    
+    /**
+     * 内部核心发送方法
+     */
+    _post: function (action, data) {
+      data = data || {};
+      console.log('[Bridge] Call:', action, data);
+
+      // 1. 优先尝试标准 uni 通道 (新版 App)
+      if (window.uni && window.uni.postMessage) {
+        window.uni.postMessage({
+          data: {
+            action: action,
+            data: data
+          }
+        });
+        return;
+      }
+
+      // 2. 降级适配 (旧版 App)
+      this._fallback(action, data);
+    },
+
+    /**
+     * 旧版 App 适配逻辑
+     */
+    _fallback: function (action, data) {
+      var url = '';
+
+      switch (action) {
+        case 'back':
+          // 尝试关闭页面或返回
+          window.history.back();
+          break;
+          
+        case 'toHome':
+          // 协议: action://to_home/
+          url = 'action://to_home/';
+          break;
+
+        case 'toLogin':
+          // 协议: action://to_login/
+          url = 'action://to_login/';
+          break;
+
+        case 'openMap':
+          // 协议: action://to_map_app?title=xxx&latitude=xxx&longitude=xxx
+          url = 'action://to_map_app?title=' + encodeURIComponent(data.name || '') +
+                '&latitude=' + data.latitude +
+                '&longitude=' + data.longitude;
+          break;
+
+        case 'openMatch':
+          // 协议: action://to_detail/?id=xxx&matchType=xxx
+          url = 'action://to_detail/?id=' + data.id +
+                '&matchType=' + (data.type || 1);
+          break;
+          
+        case 'openActivityList':
+           // 协议: action://to_activity_list/?id=xxx&mapName=xxx
+           url = 'action://to_activity_list/?id=' + data.id +
+                 '&mapName=' + encodeURIComponent(data.mapName || '');
+           break;
+
+        case 'shareWx':
+          // 旧版使用注入对象 share_wx
+          if (window.share_wx && window.share_wx.postMessage) {
+             window.share_wx.postMessage(JSON.stringify(data));
+          } else {
+             console.error('[Bridge] share_wx injection not found');
+             alert('微信分享功能不可用(环境不支持)');
+          }
+          return; // shareWx 不需要走 URL 拦截
+
+        case 'launchWxMini':
+           // 旧版使用注入对象 wx_launch_mini
+           if (window.wx_launch_mini && window.wx_launch_mini.postMessage) {
+               window.wx_launch_mini.postMessage(JSON.stringify(data));
+           } else {
+               console.error('[Bridge] wx_launch_mini injection not found');
+           }
+           return;
+
+        case 'saveImage':
+           // 旧版使用注入对象 save_base64
+           if (window.save_base64 && window.save_base64.postMessage) {
+               window.save_base64.postMessage(data.base64);
+           } else {
+               console.error('[Bridge] save_base64 injection not found');
+           }
+           return;
+
+        default:
+          console.warn('[Bridge] No legacy fallback for action:', action);
+      }
+
+      if (url) {
+        console.log('[Bridge] Legacy URL jump:', url);
+        // 触发 URL 拦截
+        window.location.href = url;
+      }
+    },
+
+    // ==============================
+    // 公开 API
+    // ==============================
+
+    back: function () {
+      this._post('back');
+    },
+    
+    toHome: function() {
+        this._post('toHome');
+    },
+    
+    toLogin: function() {
+        this._post('toLogin');
+    },
+
+    setTitle: function (title) {
+      this._post('setTitle', { title: title });
+    },
+
+    openMap: function (latitude, longitude, name) {
+      this._post('openMap', {
+        latitude: latitude,
+        longitude: longitude,
+        name: name
+      });
+    },
+
+    openMatch: function (id, type) {
+      this._post('openMatch', {
+        id: id,
+        type: type
+      });
+    },
+    
+    openActivityList: function(id, mapName) {
+        this._post('openActivityList', {
+            id: id,
+            mapName: mapName
+        });
+    },
+
+    shareWx: function (options) {
+      this._post('shareWx', options);
+    },
+
+    launchWxMini: function (username, path) {
+      this._post('launchWxMini', { username: username, path: path });
+    },
+    
+    saveImage: function (base64Str) {
+       this._post('saveImage', { base64: base64Str });
+    },
+    
+    getToken: function () {
+        this._post('getToken');
+    },
+    
+    _tokenCallback: null,
+    onToken: function(callback) {
+        this._tokenCallback = callback;
+    },
+    receiveToken: function(token) {
+        console.log('[Bridge] Received token:', token);
+        if (this._tokenCallback) {
+            this._tokenCallback(token);
+        }
+    }
+  };
+
+  window.Bridge = Bridge;
+
+})(window);

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

@@ -0,0 +1,219 @@
+<!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>

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

@@ -0,0 +1,88 @@
+<!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>

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

@@ -0,0 +1,113 @@
+<!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>

+ 102 - 0
card/sdk_delivery/demo_project/mock_flutter.js

@@ -0,0 +1,102 @@
+(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);

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

@@ -0,0 +1,120 @@
+<!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>

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

@@ -0,0 +1,176 @@
+<!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>

+ 102 - 0
card/sdk_delivery/mock_flutter.js

@@ -0,0 +1,102 @@
+(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);