From d304057e62c224fc0f93fc281274f110a709791d Mon Sep 17 00:00:00 2001 From: xiebing Date: Sun, 28 Jun 2026 10:43:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(h5):=20=E6=88=91=E7=9A=84=E9=A1=B5?= =?UTF-8?q?=E6=94=B9=20H5=EF=BC=88mine=20=E9=A1=B5=20+=20shared=20bridge/a?= =?UTF-8?q?pi=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - h5/mine/index.html: 个人中心「我的页」H5 实现 - h5/shared/bridge.js, api.js: H5↔客户端桥与 API 封装(mine 依赖) --- h5/mine/index.html | 7771 +++++++++++++++++++++++++++++++++++++++++++ h5/shared/api.js | 52 + h5/shared/bridge.js | 162 + 3 files changed, 7985 insertions(+) create mode 100644 h5/mine/index.html create mode 100644 h5/shared/api.js create mode 100644 h5/shared/bridge.js diff --git a/h5/mine/index.html b/h5/mine/index.html new file mode 100644 index 0000000..936b725 --- /dev/null +++ b/h5/mine/index.html @@ -0,0 +1,7771 @@ + + + + + + + + +比价APP 高保真原型 (外卖版) + + + + + + + + + + + +
+ +
+

方案切换

+
暂无多方案模块
在 variants 配置中添加模块即可
+
+
+ +
+ +
返回
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
9:41
+
+ + + +
+
+ +
+
+ +
+
未登录
+
登录后查看你的省钱战绩
+ +
+
+
+
上报更低价赚金币
+
比价记录
+
意见反馈
+
设置
+
+
+
+ + +
+
首页
+
福利
+
记录
+
我的
+
+ +
+
+
+
🍱
+
请选择您常用的平台
+
进入平台选好商品后,AI 一键替你比多家
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
连续签到14天 最高可领2888金币
+

签到领取今日金币

+
已连续签到 2 天,今天可领 220 金币
+ +
+
+
已领取
+
180
+
+
+
已领取
+
380
+
+
+
+
220
+
+
+
+
260
+
+
+
+
888
+
+
+ +
+
+
第1天
+
第2天
+
今天
+
明天
+
第7天
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
🎉🎊✨🎊🎉
+
新手大礼包
+
专享新客福利,开启领取
+
+
+
+
+
+
+
+
+
+
+
+
🎁
+
+
+5,000金币
+
已存入「福利」,随时可提现
+ +
+
+ + +
+5,000 金币
+ +
+ + + + + + +
+ +

「开启省钱之旅」按钮样式

+
点击切换,预览不同方案
+
+
红橙渐变当前
+
黄底黑字品牌主色
+
黑底黄字高对比
+
金色光泽动态扫光
+
玻璃质感立体黄
+
描边偏移克制款
+
红包喜庆红+金
+
呼吸跳动动效款
+
蜜糖渐变黄→琥珀
+
金币描边深金细边
+
+ +
+

领券弹窗副标题样式

+
主标题下那行小字,强化"收益"信号
+
+
基线灰当前最弱
+
金币胶囊品牌黄徽章
+
关键词高亮黄色荧光笔
+
优惠券小票齿边+朱红
+
数字徽章预计立省¥
+
双行节奏预告+承诺
+
V2b 金币翻转动效活力
+
V2c 双色分段黑黄拼接
+
V2d 闪光描边轻盈款
+
V2e 数字嵌入¥18+ 内嵌
+
V2f 双图标金币+闪电
+
V2g 立体浮起金属高光
+
流程植入领券→比价→底价
+
+ +
+

「完成设置」弹窗版式

+
点击切换,下次进入弹窗即生效
+
+
轻收尾·重期待承上小,起下大
+
分割式三段节奏
+
叙事流递进式
+
行为优先主舞台让给行为
+
徽章+行动卡chip 强调最后一步
+
进度条推进脉动黄推进感
+
数字步骤① ✓ → ② 当前
+
极简专注大字承担引导
+
+ +
+ + + + + + + + + + + + diff --git a/h5/shared/api.js b/h5/shared/api.js new file mode 100644 index 0000000..adea2dd --- /dev/null +++ b/h5/shared/api.js @@ -0,0 +1,52 @@ +/** + * SGApi —— H5 调 app-server 后端的薄封装。 + * + * 同源:H5 由 app-server 的 /media/h5/ 托管,后端在 /api/v1,同 host:port → 用相对路径、无 CORS。 + * 鉴权:JWT Bearer。token 经 SGBridge.getToken() 从原生取(原生持登录态);浏览器调试走 bridge 的 mock token。 + * 401:交原生拉登录(requestLogin)兜底,本次请求按失败 reject;正式的 refresh 重试策略阶段2 再补。 + * + * 依赖 shared/bridge.js 先加载(取 token)。 + */ +(function (global) { + 'use strict'; + + var BASE = '/api/v1'; + + function authHeaders() { + var t = (global.SGBridge && global.SGBridge.getToken()) || ''; + var h = { 'Content-Type': 'application/json' }; + if (t) h['Authorization'] = 'Bearer ' + t; + return h; + } + + function handle(res) { + if (res.status === 401) { + // 未授权:拉原生登录(异步),本次请求按失败处理,调用方自行决定是否重试 + if (global.SGBridge) global.SGBridge.requestLogin(); + return Promise.reject(new Error('unauthorized')); + } + if (!res.ok) { + return res.text().then(function (t) { + return Promise.reject(new Error('http ' + res.status + ' ' + t)); + }); + } + // 204 / 空体兜底 + return res.text().then(function (t) { return t ? JSON.parse(t) : null; }); + } + + /** GET /api/v1。path 以 / 开头,如 '/savings/battle'。 */ + function apiGet(path) { + return fetch(BASE + path, { method: 'GET', headers: authHeaders() }).then(handle); + } + + /** POST /api/v1,body 自动 JSON 序列化。 */ + function apiPost(path, body) { + return fetch(BASE + path, { + method: 'POST', + headers: authHeaders(), + body: JSON.stringify(body || {}), + }).then(handle); + } + + global.SGApi = { base: BASE, get: apiGet, post: apiPost }; +})(window); diff --git a/h5/shared/bridge.js b/h5/shared/bridge.js new file mode 100644 index 0000000..ed70e0f --- /dev/null +++ b/h5/shared/bridge.js @@ -0,0 +1,162 @@ +/** + * SGBridge —— 傻瓜比价 H5 ↔ Android 原生 的桥。 + * + * 背景:四个主 tab(首页/福利/记录/我的)由原生 Compose 改造为 WebView 加载本工程 H5。 + * H5 只负责"画 + 取后端数据";凡需要原生能力(登录态 / 跳转 / 跳外卖 App / 比价领券 / + * 权限 / 定位 / Toast / 激励视频),一律经本桥调用原生。 + * + * 协议两个方向: + * ① H5 → 原生:Android 端 WebView.addJavascriptInterface(obj, "SGBridgeNative")。 + * obj 方法都是【同步】:查询类返回 String(JSON 或纯串),动作类无返回。 + * ② 原生 → H5:原生执行 evaluateJavascript("window.SGBridge._emit('', '')")。 + * 事件:onAuthChange(登录态变) / onBalanceChange(余额变) / onSigninChange(签到态变) / onResume(回前台刷新)。 + * + * 离线兜底:浏览器里(无 SGBridgeNative)走 MOCK,便于不装 App 直接在本地 server 调样式 / 渲染。 + * 与原生实现对应:见 shaguabijia-app-android 的 SGBridge.kt(方法名逐个对齐本文件)。 + */ +(function (global) { + 'use strict'; + + var native = global.SGBridgeNative || null; + var hasNative = !!native; + + // ---- 离线 MOCK(仅无原生时生效,便于浏览器调试渲染;真机一律走 native) ---- + var MOCK = { + authState: { loggedIn: true, userId: 1, nickname: '冰', avatarUrl: '', phone: '188****8888' }, + token: 'mock-token-for-browser-debug', + deviceId: 'browser-debug-device', + appVersion: '0.0.0-debug', + }; + + function safeParse(s, fallback) { + try { return s ? JSON.parse(s) : fallback; } catch (e) { return fallback; } + } + + // ====== 查询类(同步返回) ====== + + /** 当前登录态 + 用户基本信息 → {loggedIn, userId, nickname, avatarUrl, phone}。 */ + function getAuthState() { + if (hasNative && native.getAuthState) return safeParse(native.getAuthState(), { loggedIn: false }); + return MOCK.authState; + } + + /** 后端鉴权用的 access token(空串=未登录)。原生持登录态,H5 调后端前取它拼 Bearer。 */ + function getToken() { + if (hasNative && native.getToken) return native.getToken() || ''; + return MOCK.token; + } + + /** 设备唯一标识(心跳 / 领券状态查询等用)。 */ + function getDeviceId() { + if (hasNative && native.getDeviceId) return native.getDeviceId() || ''; + return MOCK.deviceId; + } + + /** App 版本号。 */ + function getAppVersion() { + if (hasNative && native.getAppVersion) return native.getAppVersion() || ''; + return MOCK.appVersion; + } + + /** 已安装的目标电商/外卖 App 包名数组(原生 InstalledApps 探测)。H5 选平台弹窗据此判真实装机态。 */ + function getInstalledApps() { + if (hasNative && native.getInstalledApps) return safeParse(native.getInstalledApps(), []); + // MOCK(浏览器无原生):给主流已装,便于本地预览选平台弹窗正常显示"有"。 + return ['com.sankuai.meituan', 'com.taobao.taobao', 'com.jingdong.app.mall', 'me.ele']; + } + + /** 今日是否已领券(置灰「去领取」→「去查看」)。原生读 CompareButtonState(SP 按天);无桥默认 false。 */ + function getCouponClaimedToday() { + if (hasNative && native.getCouponClaimedToday) return !!native.getCouponClaimedToday(); + return false; + } + + // ====== 动作类(无返回;异步结果走事件) ====== + + /** 跳原生页。route 取值对齐安卓 Routes(invite / settings / feedback / withdrawal / compareRecords / reportFlow / guideVideo / compareResult / coinHistory / cashHistory / welfareRules ...)。 */ + function navigate(route) { + if (hasNative && native.navigate) native.navigate(route); + else console.log('[SGBridge mock] navigate →', route); + } + + /** 拉起极光一键登录。结果异步经 onAuthChange 事件回来(不在此函数返回)。 */ + function requestLogin() { + if (hasNative && native.requestLogin) native.requestLogin(); + else console.log('[SGBridge mock] requestLogin'); + } + + /** 原生居中 Toast。 */ + function toast(msg) { + if (hasNative && native.toast) native.toast(String(msg)); + else console.log('[SGBridge mock] toast →', msg); + } + + /** 跳美团/外卖 App(deeplink 优先;空则原生按包名启动,未装可跳应用商店)。 */ + function openMeituan(deeplink) { + if (hasNative && native.openMeituan) native.openMeituan(deeplink || ''); + else console.log('[SGBridge mock] openMeituan →', deeplink); + } + + /** 触发 agent 比价流程(原生起无障碍引擎)。 */ + function startCompare() { + if (hasNative && native.startCompare) native.startCompare(); + else console.log('[SGBridge mock] startCompare'); + } + + /** 触发一键领券(原生先校验悬浮窗/无障碍权限,再起前台服务)。platforms: string[]。 */ + function startCouponClaim(platforms) { + var json = JSON.stringify(platforms || []); + if (hasNative && native.startCouponClaim) native.startCouponClaim(json); + else console.log('[SGBridge mock] startCouponClaim →', json); + } + + /** 按候选包名拉起对应平台 App。照原生 HomePicker 点击跳转:原生在该平台一组候选包里取首个可 + * getLaunchIntentForPackage 的拉起(NEW_TASK|CLEAR_TASK 冷启到平台首页)。packages: string[](一个平台一组候选包,任一可拉即拉)。 */ + function launchApp(packages) { + var json = JSON.stringify(packages || []); + if (hasNative && native.launchApp) native.launchApp(json); + else console.log('[SGBridge mock] launchApp →', json); + } + + // ====== 原生 → H5 事件总线 ====== + + var listeners = {}; // event → [fn] + + /** 订阅原生事件。返回取消订阅函数。 */ + function on(event, fn) { + (listeners[event] || (listeners[event] = [])).push(fn); + return function off() { + listeners[event] = (listeners[event] || []).filter(function (f) { return f !== fn; }); + }; + } + + /** 供原生回调:window.SGBridge._emit('onAuthChange', '{...}')。payload 可为 JSON 串或对象。 */ + function _emit(event, payload) { + var data = typeof payload === 'string' ? safeParse(payload, payload) : payload; + (listeners[event] || []).forEach(function (fn) { + try { fn(data); } catch (e) { console.error('[SGBridge] listener error', event, e); } + }); + } + + global.SGBridge = { + hasNative: hasNative, + // 查询 + getAuthState: getAuthState, + getToken: getToken, + getDeviceId: getDeviceId, + getAppVersion: getAppVersion, + getInstalledApps: getInstalledApps, + getCouponClaimedToday: getCouponClaimedToday, + // 动作 + navigate: navigate, + requestLogin: requestLogin, + toast: toast, + openMeituan: openMeituan, + startCompare: startCompare, + startCouponClaim: startCouponClaim, + launchApp: launchApp, + // 事件 + on: on, + _emit: _emit, + }; +})(window); -- 2.52.0