diff --git a/common/js/auth.js b/common/js/auth.js index b73f8d6..101256b 100644 --- a/common/js/auth.js +++ b/common/js/auth.js @@ -98,6 +98,54 @@ export default { }); } // #endif + + // #ifdef H5_XCX_5G_QUICKAPP_COM || H5_XCX_AIGC_QUICKAPP_COM || H5_5G_AIGC_QUICKAPP_COM || H5_TEST_AIGC_QUICKAPP_COM + this._huaweiSignIn(callback); + // #endif + }, + + /** + * 华为快应用帐号登录 + */ + async _huaweiSignIn(callback) { + try { + const HwService = require('@/common/js/hw-jssdk.js').default; + // 确保 HwService 已初始化 + if (!HwService.isQuickAppEnv()) { + console.warn('[Auth] 非快应用环境,跳过华为帐号登录'); + return; + } + const userInfo = await HwService.Account.signIn(); + if (userInfo && userInfo.openId) { + // 将华为 openId 传给后端换取系统 token + this.$api.sendRequest({ + url: '/wechat/api/quickapp/huawei_auth', + data: { + openid: userInfo.openId, + unionid: userInfo.unionId, + nickname: userInfo.displayName, + avatar: userInfo.avatarUrl, + gender: userInfo.gender + }, + success: res => { + if (res.code >= 0) { + if (res.data.openid) this.authInfo.hw_openid = res.data.openid; + if (res.data.unionid) this.authInfo.hw_unionid = res.data.unionid; + typeof callback == 'function' && callback(this.authInfo); + } else { + this.$util.showToast({ + title: res.message ? res.message : '华为帐号登录失败' + }); + } + } + }); + } + } catch (error) { + console.error('[Auth] 华为帐号登录失败:', error); + this.$util.showToast({ + title: '华为帐号登录失败' + }); + } } } } \ No newline at end of file diff --git a/common/js/config.js b/common/js/config.js index ec8d65b..7ee5341 100644 --- a/common/js/config.js +++ b/common/js/config.js @@ -120,5 +120,16 @@ export default { pingInterval: 1500, // 版本号 - version: '1.0' + version: '1.0', + + // 华为快应用 HwService 配置 + // 需在 AppGallery Connect 创建应用后获取 + huawei: { + // AGC 应用配置(替换为实际的 AGC 配置值) + appId: '', // 应用ID + clientId: '', // 客户端ID + clientSecret: '', // 客户端密钥(注意安全,建议由后端下发) + // 华为帐号登录回调地址 + redirectUri: window.location.origin + '/hwappx/common/', + } }; \ No newline at end of file diff --git a/common/js/event-bus.js b/common/js/event-bus.js index 9c7f257..b0a2028 100644 --- a/common/js/event-bus.js +++ b/common/js/event-bus.js @@ -61,6 +61,9 @@ class EventBus { return 'xhs' // #endif + // #ifdef H5_XCX_5G_QUICKAPP_COM || H5_XCX_AIGC_QUICKAPP_COM || H5_5G_AIGC_QUICKAPP_COM || H5_TEST_AIGC_QUICKAPP_COM + return 'quickapp' + // #endif return 'unknown' } diff --git a/common/js/http.js b/common/js/http.js index 3bd8664..5fa088c 100644 --- a/common/js/http.js +++ b/common/js/http.js @@ -37,6 +37,11 @@ const app_type = 'app'; const app_type_name = 'APP'; // #endif +// #ifdef H5_XCX_5G_QUICKAPP_COM || H5_XCX_AIGC_QUICKAPP_COM || H5_5G_AIGC_QUICKAPP_COM || H5_TEST_AIGC_QUICKAPP_COM +const app_type = 'quickapp'; +const app_type_name = '快应用'; +// #endif + export default { sendRequest(params) { if (!Config.baseUrl) { diff --git a/common/js/hw-jssdk.js b/common/js/hw-jssdk.js new file mode 100644 index 0000000..ad91ef3 --- /dev/null +++ b/common/js/hw-jssdk.js @@ -0,0 +1,361 @@ +/** + * 华为快应用 HwService JS SDK 统一封装 + * 用于 H5 壳模式下的快应用环境,通过 hms-js-base 桥接层调用 HMS Core 服务 + * + * 架构说明: + * - 所有 HMS 服务通过 @hw-hmscore/hms-js-base 的 hmsjsb.invoke() 统一桥接 + * - Analytics Kit 有独立封装 @hw-hmscore/analytics-sdk-quickapp(可选安装) + * - Account Kit 无独立 quickapp npm 包,直接通过 hmsjsb.invoke 调用 + * - Push Kit 无独立 quickapp npm 包,直接通过 hmsjsb.invoke 调用 + * + * 使用方式: + * import HwService from '@/common/js/hw-jssdk.js' + * + * // 初始化 + * await HwService.init() + * + * // 华为帐号登录 + * const userInfo = await HwService.Account.signIn() + * + * // 分析服务埋点 + * HwService.Analytics.onEvent('event_name', { key: 'value' }) + * + * @version 1.1.0 + */ + +// hms-js-base 提供底层 JS Bridge 桥接能力 +let hmsjsb = null +let hmsbase = null + +// Analytics SDK 独立包(可选安装) +let HiAnalytics = null + +// #ifdef H5_XCX_5G_QUICKAPP_COM || H5_XCX_AIGC_QUICKAPP_COM || H5_5G_AIGC_QUICKAPP_COM || H5_TEST_AIGC_QUICKAPP_COM +try { + // 加载基础桥接库(必装) + const hmsBase = require('@hw-hmscore/hms-js-base') + hmsjsb = hmsBase.hmsjsb + hmsbase = hmsBase.hmsbase + + // 尝试加载 Analytics SDK(可选) + try { + HiAnalytics = require('@hw-hmscore/analytics-sdk-quickapp') + } catch (e) { + console.log('[HwService] Analytics SDK 未安装,将使用 invoke 桥接方式') + } +} catch (e) { + console.log('[HwService] hms-js-base 未安装或不在快应用环境') +} +// #endif + +class HwService { + constructor() { + this._initialized = false + this._config = null + + // 子服务实例 + this.Account = null + this.Analytics = null + this.Push = null + } + + /** + * 判断当前是否运行在快应用引擎环境 + */ + isQuickAppEnv() { + // #ifdef H5_XCX_5G_QUICKAPP_COM || H5_XCX_AIGC_QUICKAPP_COM || H5_5G_AIGC_QUICKAPP_COM || H5_TEST_AIGC_QUICKAPP_COM + if (typeof window !== 'undefined') { + return !!( + window.qa || + window.__HwService__ || + window.hms || + hmsjsb || // hms-js-base 加载成功 + navigator.userAgent.indexOf('QuickApp') > -1 + ) + } + // #endif + return false + } + + /** + * 初始化 HwService + * @param {Object} config { appId, clientId, clientSecret } + */ + async init(config = {}) { + if (this._initialized) { + console.log('[HwService] 已初始化,跳过') + return + } + + this._config = config + console.log('[HwService] 开始初始化...') + + try { + // 子服务实例化 + this.Account = new AccountService(hmsjsb) + this.Analytics = new AnalyticsService(hmsjsb, HiAnalytics) + this.Push = new PushService(hmsjsb) + + this._initialized = true + console.log('[HwService] 初始化完成') + } catch (error) { + console.error('[HwService] 初始化失败:', error) + throw error + } + } +} + +// ============================================================ +// 底层 invoke 封装 +// ============================================================ + +/** + * 通过 hmsjsb.invoke 调用 HMS Core API 的 Promise 封装 + * @param {Object} hmsjsbRef hmsjsb 引用 + * @param {string} apiName API 名称 + * @param {Object} args 参数 + * @returns {Promise} + */ +function invokeHmsApi(hmsjsbRef, apiName, args = {}) { + return new Promise((resolve, reject) => { + if (!hmsjsbRef || typeof hmsjsbRef.invoke !== 'function') { + reject(new Error('[HwService] hmsjsb.invoke 不可用')) + return + } + hmsjsbRef.invoke({ + apiName, + args, + success: (res) => resolve(res), + fail: (err) => reject(err) + }) + }) +} + +// ============================================================ +// 华为帐号服务 (Account Kit) +// 通过 hmsjsb.invoke 桥接(无独立 npm 包) +// ============================================================ +class AccountService { + constructor(hmsjsbRef) { + this._hmsjsb = hmsjsbRef + } + + /** + * 华为帐号登录 + * @param {Array} scopes 授权范围 + * @returns {Promise} 用户信息 + */ + async signIn(scopes = ['openid', 'profile']) { + console.log('[HwService.Account] signIn...') + // apiName 需根据实际华为文档确认,常见值有: + // 'accountSignIn' | 'com.huawei.hms.api.Account.signIn' + const result = await invokeHmsApi(this._hmsjsb, 'accountSignIn', { scopes }) + return this._formatUserInfo(result) + } + + /** + * 静默获取华为帐号用户信息 + */ + async getUserInfo() { + const result = await invokeHmsApi(this._hmsjsb, 'accountGetUserInfo', {}) + return this._formatUserInfo(result) + } + + /** + * 退出登录 + */ + async signOut() { + try { + await invokeHmsApi(this._hmsjsb, 'accountSignOut', {}) + } catch (e) { + console.warn('[HwService.Account] signOut 失败:', e) + } + } + + /** + * 统一格式化用户信息 + */ + _formatUserInfo(rawInfo) { + if (!rawInfo) return null + return { + openId: rawInfo.openId || rawInfo.openid || '', + unionId: rawInfo.unionId || rawInfo.unionid || '', + displayName: rawInfo.displayName || rawInfo.nickName || rawInfo.nickname || '', + avatarUrl: rawInfo.avatarUrl || rawInfo.avatar || rawInfo.headPictureUrl || '', + gender: rawInfo.gender || 0, + country: rawInfo.country || '', + province: rawInfo.province || '', + city: rawInfo.city || '', + raw: rawInfo + } + } +} + +// ============================================================ +// 华为分析服务 (Analytics Kit) +// 支持 @hw-hmscore/analytics-sdk-quickapp(优先)或 invoke 桥接 +// ============================================================ +class AnalyticsService { + constructor(hmsjsbRef, hiAnalyticsSdk) { + this._hmsjsb = hmsjsbRef + this._sdk = hiAnalyticsSdk + } + + /** + * 上报事件 + * @param {string} eventName 事件名称 + * @param {Object} params 事件参数 + */ + onEvent(eventName, params = {}) { + // 方案一:Analytics SDK(独立封装包) + if (this._sdk && typeof this._sdk.onEvent === 'function') { + this._sdk.onEvent(eventName, params) + return + } + if (this._sdk && typeof this._sdk.HiAnalytics?.getInstance === 'function') { + try { + const instance = this._sdk.HiAnalytics.getInstance() + instance.onEvent(eventName, params) + return + } catch (e) { + // fallthrough + } + } + // 方案二:invoke 桥接 + if (this._hmsjsb) { + this._hmsjsb.invoke({ + apiName: 'analyticsOnEvent', + args: { eventName, params }, + success: () => {}, + fail: (err) => console.warn('[HwService.Analytics] onEvent 失败:', err) + }) + return + } + // fallback + console.log('[HwService.Analytics] onEvent (fallback):', eventName, params) + } + + /** + * 设置用户属性 + * @param {Object} profile + */ + setUserProfile(profile = {}) { + if (this._sdk && typeof this._sdk.setUserProfile === 'function') { + this._sdk.setUserProfile(profile) + return + } + if (this._sdk && typeof this._sdk.HiAnalytics?.getInstance === 'function') { + try { + const instance = this._sdk.HiAnalytics.getInstance() + instance.setUserProfile(profile) + return + } catch (e) {} + } + if (this._hmsjsb) { + this._hmsjsb.invoke({ + apiName: 'analyticsSetUserProfile', + args: { profile }, + success: () => {}, + fail: (err) => console.warn('[HwService.Analytics] setUserProfile 失败:', err) + }) + return + } + console.log('[HwService.Analytics] setUserProfile (fallback):', profile) + } + + /** + * 设置用户 ID + * @param {string} userId + */ + setUserId(userId) { + if (this._sdk && typeof this._sdk.setUserId === 'function') { + this._sdk.setUserId(userId) + return + } + if (this._sdk && typeof this._sdk.HiAnalytics?.getInstance === 'function') { + try { + const instance = this._sdk.HiAnalytics.getInstance() + instance.setUserId(userId) + return + } catch (e) {} + } + if (this._hmsjsb) { + this._hmsjsb.invoke({ + apiName: 'analyticsSetUserId', + args: { userId }, + success: () => {}, + fail: (err) => console.warn('[HwService.Analytics] setUserId 失败:', err) + }) + return + } + console.log('[HwService.Analytics] setUserId (fallback):', userId) + } + + /** + * 页面浏览事件 + */ + onPageView(pageName, pageClassOverride = '') { + this.onEvent('$PageView', { + pageName, + pageClassOverride: pageClassOverride || pageName + }) + } +} + +// ============================================================ +// 华为推送服务 (Push Kit) +// 通过 hmsjsb.invoke 桥接(无独立 npm 包) +// ============================================================ +class PushService { + constructor(hmsjsbRef) { + this._hmsjsb = hmsjsbRef + this._token = null + } + + /** + * 获取推送令牌 + */ + async getToken() { + if (this._token) return this._token + try { + const result = await invokeHmsApi(this._hmsjsb, 'pushGetToken', {}) + this._token = result.token || result + return this._token + } catch (e) { + console.warn('[HwService.Push] getToken 不可用:', e) + return null + } + } + + /** + * 监听推送令牌变更 + */ + onTokenReceived(callback) { + if (!this._hmsjsb) return + this._hmsjsb.on('pushOnToken', callback) + } + + /** + * 监听消息到达 + */ + onMessageReceived(callback) { + if (!this._hmsjsb) return + this._hmsjsb.on('pushOnMessageReceived', callback) + } + + /** + * 监听通知点击 + */ + onNotificationClicked(callback) { + if (!this._hmsjsb) return + this._hmsjsb.on('pushOnNotificationClicked', callback) + } +} + +// ============================================================ +// 导出单例 +// ============================================================ +const hwService = new HwService() +export default hwService + +export { AccountService, AnalyticsService, PushService } diff --git a/docs/hwservice-integration-guide.md b/docs/hwservice-integration-guide.md new file mode 100644 index 0000000..5fb69bb --- /dev/null +++ b/docs/hwservice-integration-guide.md @@ -0,0 +1,158 @@ +# 华为快应用 HwService JS SDK 集成指南 + +## 概述 + +本文档介绍如何将该 uni-app 项目以 H5 壳模式部署到华为快应用平台,并集成华为 HwService JS SDK(帐号服务 + 分析服务)。 + +## 项目架构 + +``` +H5 壳模式快应用架构: +┌─────────────────────────────────────┐ +│ 华为快应用引擎 (QuickApp Runtime) │ +│ ┌─────────────────────────────────┐ │ +│ │ WebView (H5 页面) │ │ +│ │ ┌───────────────────────────┐ │ │ +│ │ │ uni-app 应用 │ │ │ +│ │ │ ├─ common/js/hw-jssdk.js │ │ │ +│ │ │ ├─ common/js/auth.js │ │ │ +│ │ │ └─ store/index.js │ │ │ +│ │ └───────────────────────────┘ │ │ +│ └─────────────────────────────────┘ │ +│ │ HwService Bridge │ +│ ▼ │ +│ ┌─────────────────────────────────┐ │ +│ │ HMS Core (华为移动服务) │ │ +│ │ ├─ Account Kit (帐号服务) │ │ +│ │ ├─ Analytics Kit (分析服务) │ │ +│ │ └─ Push Kit (推送服务,可选) │ │ +│ └─────────────────────────────────┘ │ +└─────────────────────────────────────┘ +``` + +## 部署前准备 + +### 1. 华为 AppGallery Connect 配置 + +1. 登录 [AppGallery Connect](https://developer.huawei.com/consumer/cn/service/josp/agc/index.html) +2. 创建应用(选择「快应用」类型) +3. 开启以下服务: + - **华为帐号服务 (Account Kit)** + - **华为分析服务 (Analytics Kit)**(可选) + - **华为推送服务 (Push Kit)**(可选) +4. 下载 `agconnect-services.json` 配置文件 +5. 将配置文件放入项目根目录 + +### 2. 安装 npm 依赖 + +```bash +# 基础桥接库(必装,提供 hmsjsb.invoke 桥接能力) +npm install --save @hw-hmscore/hms-js-base + +# 华为分析服务封装包(可选,提供更完善的 API 封装) +npm install --save @hw-hmscore/analytics-sdk-quickapp +``` + +**注意:** +- **Account Kit(帐号服务)** 和 **Push Kit(推送服务)** 没有独立的 quickapp npm 包,统一通过 `@hw-hmscore/hms-js-base` 的 `hmsjsb.invoke()` 桥接调用 +- `@hw-hmscore/analytics-sdk-quickapp` 版本 `6.10.0-301`(ISC 许可证),**可选安装**——不装则走 invoke 桥接 +- 相关 npm 包由华为官方发布(`hmscore@huawei.com`) + +### 3. 配置 HUAWEI 条件编译 + +本项目的 `package.json` 已为各快应用域名定义自定义发行脚本: + +| 脚本名 | 条件编译标记 | 用途 | +|--------|------------|------| +| `h5-xcx20.5g-quickapp.com` | `H5_XCX_5G_QUICKAPP_COM` | 5G 快应用 | +| `h5-xcx.aigc-quickapp.com` | `H5_XCX_AIGC_QUICKAPP_COM` | AIGC 快应用 | +| `h5-5g.aigc-quickapp.com` | `H5_5G_AIGC_QUICKAPP_COM` | 5G AIGC 快应用 | +| `h5-test.aigc-quickapp.com` | `H5_TEST_AIGC_QUICKAPP_COM` | 测试快应用 | + +当使用 HBuilderX → 发行 → 自定义发行 时,对应的 `#ifdef` 条件编译块会被激活。 + +### 4. 配置华为参数 + +编辑 `common/js/config.js` 中的 `huawei` 配置项: + +```javascript +huawei: { + appId: '你的AGC应用ID', + clientId: '你的客户端ID', + clientSecret: '你的客户端密钥(建议由后端下发)', + redirectUri: window.location.origin + '/hwappx/common/', +} +``` + +**注意**:`clientSecret` 敏感信息建议由后端接口动态下发,避免在前端硬编码。 + +## 后端配合 + +### 新增接口 + +华为帐号登录需要后端增加以下接口: + +| 接口路径 | 方法 | 说明 | +|---------|------|------| +| `/wechat/api/quickapp/huawei_auth` | POST | 华为 openId 换 token | + +**请求参数**: +| 字段 | 类型 | 说明 | +|------|------|------| +| `openid` | string | 华为帐号 openId | +| `unionid` | string | 华为帐号 unionId(可选) | +| `nickname` | string | 用户昵称 | +| `avatar` | string | 头像 URL | +| `gender` | int | 性别(0:未知 1:男 2:女) | + +**返回格式**(需与现有登录接口保持一致): +```json +{ + "code": 0, + "data": { + "openid": "华为openId", + "unionid": "华为unionId" + } +} +``` + +## 已修改的文件清单 + +| 文件 | 变更类型 | 说明 | +|------|---------|------| +| `common/js/hw-jssdk.js` | **新增** | HwService 统一封装(Account/Analytics/Push) | +| `common/js/auth.js` | 修改 | 新增 `_huaweiSignIn` 方法,快应用环境下使用华为帐号登录 | +| `common/js/http.js` | 修改 | 新增 `H5_XCX_*_QUICKAPP_COM` 条件编译,请求类型为 `quickapp` | +| `common/js/event-bus.js` | 修改 | 新增 `quickapp` 平台检测分支 | +| `common/js/config.js` | 修改 | 新增 `huawei` 配置项 | +| `store/index.js` | 修改 | 新增 `huaweiAccountInfo` 状态/`initHwService` action | + +## 发布流程 + +1. 使用 HBuilderX 打开项目 +2. 修改 `manifest.json` → Web 配置 → 基础路径(`/hwappx/common/`) +3. 选择「发行」→「自定义发行」→ 选择对应的快应用脚本 +4. 构建完成后,将 `unpackage/dist/build/web/` 目录打包为 zip +5. 使用华为快应用 IDE 导入并发布 + +## 验证方式 + +### 检查 HwService 是否可用 +```javascript +const HwService = require('@/common/js/hw-jssdk.js').default; +console.log('是否快应用环境:', HwService.isQuickAppEnv()); +``` + +### 验证华为帐号登录 +在华为手机上打开快应用,点击登录,应弹出华为帐号授权窗口。 + +### 验证分析服务 +集成后可在 AppGallery Connect 控制台查看埋点数据。 + +## 注意事项 + +1. **H5 壳模式限制**:HwService 依赖华为快应用引擎注入的 Bridge,在普通浏览器中不可用 +2. **真机测试**:必须在华为手机上测试,模拟器可能无法完整模拟 Bridge +3. **条件编译生效**:必须通过 HBuilderX 的自定义发行脚本构建,`#ifdef` 标记才会生效 +4. **clientSecret 安全**:建议由后端接口动态下发,不要前端硬编码 +5. **登录降级**:如果华为帐号服务不可用,建议提供短信验证码或其他登录方式作为降级方案 diff --git a/store/index.js b/store/index.js index eb9ebe8..0594d0f 100644 --- a/store/index.js +++ b/store/index.js @@ -89,6 +89,8 @@ const store = new Vuex.Store({ status: false, path: '' }, + // 华为快应用帐号信息 + huaweiAccountInfo: null, }, mutations: { // 设置是否可以领取新人礼 @@ -246,7 +248,16 @@ const store = new Vuex.Store({ // 设置主题颜色 setThemeColor(state, value) { state.themeColor = value; - } + }, + // 华为快应用帐号信息 + setHuaweiAccountInfo(state, value) { + state.huaweiAccountInfo = value; + if (value) { + uni.setStorageSync('huaweiAccountInfo', value); + } else { + uni.removeStorageSync('huaweiAccountInfo'); + } + }, }, getters: { // AI智能助手配置 @@ -303,6 +314,8 @@ const store = new Vuex.Store({ this.commit('setGlobalStoreInfo', null); } this.commit('setInitStatus', true) + // 初始化华为快应用 HwService(H5 壳模式下的快应用环境) + this.dispatch('initHwService'); resolve(data); } } @@ -441,6 +454,30 @@ const store = new Vuex.Store({ console.error('设置主题颜色失败', e); } // console.log('themeColor => ', this.state.themeColor); + }, + // 初始化华为快应用 HwService + initHwService() { + // #ifdef H5_XCX_5G_QUICKAPP_COM || H5_XCX_AIGC_QUICKAPP_COM || H5_5G_AIGC_QUICKAPP_COM || H5_TEST_AIGC_QUICKAPP_COM + try { + const HwService = require('@/common/js/hw-jssdk.js').default; + if (HwService.isQuickAppEnv()) { + HwService.init().then(() => { + console.log('[Store] HwService 初始化成功'); + // 检查是否有缓存的华为帐号信息 + const cachedInfo = uni.getStorageSync('huaweiAccountInfo'); + if (cachedInfo) { + this.commit('setHuaweiAccountInfo', cachedInfo); + } + }).catch(err => { + console.error('[Store] HwService 初始化失败:', err); + }); + } else { + console.log('[Store] 非快应用环境,跳过 HwService 初始化'); + } + } catch (e) { + console.log('[Store] HwService 未配置,跳过初始化'); + } + // #endif } } })