feat(h5): 我的页改 H5(mine 页 + shared bridge/api) (#89)
- h5/mine/index.html: 个人中心「我的页」H5 实现 - h5/shared/bridge.js, api.js: H5↔客户端桥与 API 封装(mine 依赖) Reviewed-on: #89 Co-authored-by: xiebing <xiebing@wonderable.ai> Co-committed-by: xiebing <xiebing@wonderable.ai>
This commit was merged in pull request #89.
This commit is contained in:
+7771
File diff suppressed because it is too large
Load Diff
@@ -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>。path 以 / 开头,如 '/savings/battle'。 */
|
||||
function apiGet(path) {
|
||||
return fetch(BASE + path, { method: 'GET', headers: authHeaders() }).then(handle);
|
||||
}
|
||||
|
||||
/** POST /api/v1<path>,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);
|
||||
@@ -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('<event>', '<json>')")。
|
||||
* 事件: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);
|
||||
Reference in New Issue
Block a user