# Flutter H5 排行榜对接文档 本文说明 Flutter App 如何打开 H5 排行榜、向 H5 传递登录态,以及接收 H5 通过 JSBridge 发回的导航事件。 ## 入口地址 本地开发: ```text http://127.0.0.1:30000/rank/index.html?env=local&token={access_token}&app_code=lalu ``` 测试环境: ```text https://{h5-test-domain}/rank/index.html?env=test&token={access_token}&app_code=lalu ``` 线上环境: ```text https://{h5-domain}/rank/index.html?token={access_token}&app_code=lalu ``` `env` 规则: | 参数 | 说明 | | --- | --- | | 不传 | H5 请求 `https://api.global-interaction.com/` | | `env=test` | H5 请求 `https://api-test.global-interaction.com/` | | `env=local` | H5 请求 `http://localhost:13000/`,用于本机 gateway 联调 | ## URL 参数 | 参数 | 必填 | 说明 | | --- | --- | --- | | `token` | 是 | App 登录后拿到的 access token。H5 会作为 `Authorization: Bearer ` 调后端。 | | `access_token` | 否 | `token` 的兼容别名。 | | `accessToken` | 否 | `token` 的兼容别名。 | | `app_code` | 是 | App 编码,当前传 `lalu`。H5 会作为 `X-App-Code` 请求头。 | | `appCode` | 否 | `app_code` 的兼容别名。 | | `env` | 否 | API 环境切换,见上表。 | H5 启动后会调用: ```http GET /api/v1/users/me/overview Authorization: Bearer X-App-Code: lalu ``` 这个接口用于确认登录态并缓存当前用户信息。排行榜数据使用同一个 token 和 app code 请求。 ## H5 页面能力 排行榜页面地址: ```text /rank/index.html ``` 页面内包含三个榜单,通过 tab 切换: | H5 Tab | 后端 `board_type` | 说明 | | --- | --- | --- | | `Wealth` | `sent` | 财富榜,按用户送礼总金币排行 | | `Room` | `room` | 房间榜,按房间收礼总金币排行 | | `Charm` | `received` | 魅力榜,按用户收礼总金币排行 | 时间维度: | H5 Tab | 后端 `period` | | --- | --- | | `Daily` | `today` | | `Weekly` | `week` | | `Monthly` | `month` | 排行榜接口: ```http GET /api/v1/activities/user-leaderboards?board_type=sent&period=today&page=1&page_size=20 Authorization: Bearer X-App-Code: lalu ``` H5 会分页加载,`page_size=20`。如果无 token 或接口失败,页面会展示 Figma 默认占位数据,不阻塞页面打开。 ## Flutter JSBridge H5 会按以下优先级尝试发送消息: 1. `window.flutter_inappwebview.callHandler("HyAppBridge", action, payload)` 2. `window.HyAppBridgeChannel.postMessage(jsonString)` 3. `window.FlutterBridge.postMessage(jsonString)` 4. `window.webkit.messageHandlers.HyAppBridge.postMessage({ action, payload })` Flutter 只需要实现其中一种即可。推荐统一注册 `HyAppBridge`。 ### 消息格式 除 `flutter_inappwebview.callHandler` 外,postMessage 的消息是 JSON 字符串: ```json { "action": "openUser", "payload": { "user_id": "10001" } } ``` ### H5 发出的 action | action | payload | 触发场景 | Flutter 行为 | | --- | --- | --- | --- | | `ready` | `{ "page": "rank" }` | H5 初始化完成 | 可关闭加载态 | | `back` | `{}` | 用户点击左上角返回 | 关闭 WebView 或返回上一页 | | `openUser` | `{ "user_id": "10001" }` | 点击财富榜/魅力榜用户头像、榜单项 | 打开用户详情页 | | `openRoom` | `{ "room_id": "room_id" }` | 点击房间榜房间头像、榜单项 | 进入或打开房间详情 | | `openRankNote` | `{ "board_type": "sent" }` | 点击右上角问号 | 当前 H5 已内置说明页;Flutter 可只打点,不需要跳转 | `board_type` 取值: | 值 | 说明 | | --- | --- | | `sent` | 财富榜 | | `room` | 房间榜 | | `received` | 魅力榜 | ## Flutter 示例 ### flutter_inappwebview ```dart final url = Uri.parse( 'http://127.0.0.1:30000/rank/index.html' '?env=local' '&token=${Uri.encodeComponent(accessToken)}' '&app_code=lalu', ); InAppWebView( initialUrlRequest: URLRequest(url: WebUri(url.toString())), onWebViewCreated: (controller) { controller.addJavaScriptHandler( handlerName: 'HyAppBridge', callback: (args) async { final action = args.isNotEmpty ? args[0] as String : ''; final payload = args.length > 1 && args[1] is Map ? Map.from(args[1] as Map) : {}; switch (action) { case 'ready': // 关闭 loading break; case 'back': Navigator.of(context).maybePop(); break; case 'openUser': final userId = payload['user_id']?.toString() ?? ''; if (userId.isNotEmpty) { // TODO: 打开用户详情页 } break; case 'openRoom': final roomId = payload['room_id']?.toString() ?? ''; if (roomId.isNotEmpty) { // TODO: 打开房间或房间详情页 } break; case 'openRankNote': // H5 内部已展示说明页,这里可做埋点 break; } return {'ok': true}; }, ); }, ) ``` ### webview_flutter JavaScriptChannel 如果使用 `webview_flutter`,可以注入 `HyAppBridgeChannel`: ```dart final controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..addJavaScriptChannel( 'HyAppBridgeChannel', onMessageReceived: (message) { final data = jsonDecode(message.message) as Map; final action = data['action']?.toString() ?? ''; final payload = data['payload'] is Map ? Map.from(data['payload'] as Map) : {}; switch (action) { case 'ready': break; case 'back': Navigator.of(context).maybePop(); break; case 'openUser': final userId = payload['user_id']?.toString() ?? ''; break; case 'openRoom': final roomId = payload['room_id']?.toString() ?? ''; break; } }, ) ..loadRequest(Uri.parse(rankUrl)); ``` ## 后端依赖 H5 依赖以下 Gateway HTTP 接口: | 接口 | 说明 | | --- | --- | | `GET /api/v1/users/me/overview` | 解析 token 后获取当前用户信息 | | `GET /api/v1/activities/user-leaderboards` | 获取排行榜 | 业务接口返回 envelope: ```json { "code": "OK", "message": "ok", "request_id": "req_xxx", "data": {} } ``` H5 也兼容旧的 `code: 0` 成功格式。 ## 本地联调 在 `hyapp-h5` 目录运行: ```bash make run ``` 默认打开: ```text http://127.0.0.1:30000/rank/index.html?env=local ``` Flutter 真机调本机服务时,`env=local` 对 H5 来说会请求 `http://localhost:13000/`。如果 WebView 运行在真机上,`localhost` 指向手机自身,不是开发机。真机联调建议: 1. 使用测试环境 `env=test`。 2. 或把 H5/API 部署到同一局域网可访问地址。 3. 或后续给 H5 增加可配置 `api_base` 参数。 ## 验收点 1. Flutter 打开 H5 时带上 `token` 和 `app_code`。 2. H5 首次加载后发出 `ready`。 3. 点击左上角返回,Flutter 收到 `back` 并关闭 WebView。 4. 点击财富榜/魅力榜用户,Flutter 收到 `openUser`。 5. 点击房间榜房间,Flutter 收到 `openRoom`。 6. 切换 `Wealth / Room / Charm` 和 `Daily / Weekly / Monthly` 时,H5 正确请求排行榜接口。 7. 点击右上角问号,H5 展示说明页,返回后仍停留在排行榜页。