From 7045a05a733a760dbc22a680b481a9b1b595a5e8 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Wed, 1 Jul 2026 09:39:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=85=A8=E5=B1=80?= =?UTF-8?q?=E9=9A=90=E7=A7=81=E5=8D=8F=E8=AE=AE=E7=AE=A1=E7=90=86=E5=99=A8?= =?UTF-8?q?=E5=B9=B6=E9=80=82=E9=85=8D=E9=B8=BF=E8=92=99=E5=85=83=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增common/js/privacy.js全局隐私协议管理器,统一处理多平台隐私弹窗逻辑 2. 替换APP-HARMONY为MP-HARMONY平台判断标识,适配鸿蒙元服务 3. 在mp-html组件中增加MP-HARMONY平台兼容 4. 在App.vue中集成隐私弹窗初始化逻辑 5. 重构privacy-popup.vue,接入全局隐私管理器,优化弹窗互斥逻辑 6. 全局挂载$privacy实例到Vue原型链 --- App.vue | 7 + common/js/privacy.js | 241 ++++++ components/ns-newform/ns-newform.vue | 2 +- components/privacy-popup/privacy-popup.vue | 751 ++++++++++-------- main.js | 5 +- .../mp-html/components/mp-html/node/node.vue | 8 +- 6 files changed, 660 insertions(+), 354 deletions(-) create mode 100644 common/js/privacy.js diff --git a/App.vue b/App.vue index b7e119d..a69db66 100644 --- a/App.vue +++ b/App.vue @@ -3,6 +3,7 @@ import configExternal, { themeConfig } from 'common/js/config-external.js' import Lang from 'common/js/lang.js'; import ThemeOverride from 'common/js/theme-override.js'; + import PrivacyManager from 'common/js/privacy.js'; import { Weixin @@ -78,6 +79,12 @@ } // #endif + // 隐私协议弹窗: boot 完成后统一触发,确保翻译数据已就绪 + // 通过 PrivacyManager → uni.$emit('privacy:show') 通知当前页面的 privacy-popup 组件 + // 只弹一次: 同意后写 storage,后续不再弹 + // 异步查询平台隐私状态(微信/鸿蒙), H5 同步返回 + try { await PrivacyManager.show(); } catch (e) { console.warn('[privacy] show fail', e); } + // ---- 3) 远端就绪后,主题再 apply 一次(preset + remote 合并 ) // 这一次 setThemeStyle + themeColorSet 是同步路径(preset 已在 cache,remote 也在 cache) // 如果跟 step 2 的 apply 值相同,Vuex 会重复 commit 但页面无视觉变化 diff --git a/common/js/privacy.js b/common/js/privacy.js new file mode 100644 index 0000000..f566ca3 --- /dev/null +++ b/common/js/privacy.js @@ -0,0 +1,241 @@ +/** + * 全局隐私协议管理器(单例) + * + * 职责: + * 1. 统一判断是否需要弹窗(storage + 平台级隐私状态) + * 2. 通过 uni.$emit('privacy:show') 通知当前页面的 privacy-popup 组件弹窗 + * 3. 用户同意后写 storage,拒绝后不写(允许下次再弹) + * 4. 只弹一次:同意后永不再弹 + * 5. 事件补偿:若 emit 时无监听者,保留 _pendingShow,页面 mounted 时补偿弹窗 + * + * 平台差异: + * H5: 仅查 storage + * 微信小程序: wx.getPrivacySetting() 查平台级隐私状态 + * 鸿蒙元服务: has.getPrivacySetting() 查平台级隐私状态 + * + * 事件: + * uni.$emit('privacy:show') — 通知当前页面组件弹窗 + * uni.$emit('privacy:agreed') — 用户已同意 + * uni.$emit('privacy:disagreed') — 用户已拒绝 + * uni.$emit('privacy:closeOthers') — 通知其他页面关闭弹窗 + * + * 使用: + * App.vue onLaunch (boot完成后): await PrivacyManager.show() + * privacy-popup.vue: mounted 中 uni.$on('privacy:show') + popPendingShow() 补偿 + * privacy-popup.vue: handleAgree 中 PrivacyManager.agree() + * privacy-popup.vue: handleDisagree 中 PrivacyManager.disagree() + */ + +const STORAGE_KEY = 'privacyAgreed'; + +// 内部状态 +let _needShow = false; // 是否需要弹窗 +let _hasChecked = false; // 是否已执行过 check +let _pendingShow = false; // emit 时无监听者的补偿标志 + +/** + * 读取 storage 中的同意状态 + */ +function getStorageAgreed() { + try { + return !!uni.getStorageSync(STORAGE_KEY); + } catch (e) { + return false; + } +} + +/** + * 微信小程序: 查询平台级隐私设置 + * @returns {Promise<{needAuthorization: boolean, privacyContractName?: string}>} + */ +function getWxPrivacySetting() { + return new Promise((resolve) => { + // #ifdef MP-WEIXIN + try { + if (wx.getPrivacySetting) { + wx.getPrivacySetting({ + success: (res) => { + resolve({ + needAuthorization: !!res.needAuthorization, + privacyContractName: res.privacyContractName || '' + }); + }, + fail: () => resolve({ needAuthorization: !getStorageAgreed() }) + }); + } else { + // 低版本基础库,回退到 storage + resolve({ needAuthorization: !getStorageAgreed() }); + } + } catch (e) { + resolve({ needAuthorization: !getStorageAgreed() }); + } + // #endif + // #ifndef MP-WEIXIN + resolve({ needAuthorization: !getStorageAgreed() }); + // #endif + }); +} + +/** + * 快应用: 查询平台级隐私设置 + * @returns {Promise<{needAuthorization: boolean, privacyContractName?: string}>} + */ +function getQuickAppPrivacySetting() { + return new Promise((resolve) => { + // #ifdef QUICKAPP-WEBVIEW + try { + const has = require('@system.has'); + if (has && has.getPrivacySetting) { + has.getPrivacySetting({ + success: (res) => { + resolve({ + needAuthorization: !!res.needAuthorization, + privacyContractName: res.privacyContractName || '' + }); + }, + fail: () => resolve({ needAuthorization: !getStorageAgreed() }) + }); + return; + } + } catch (e) {} + // #endif + // 非快应用环境: 仅查 storage + resolve({ needAuthorization: !getStorageAgreed() }); + }); +} + +/** + * 鸿蒙元服务: 查询平台级隐私设置 + * 鸿蒙元服务的隐私授权由系统自动管理,应用层只需检查 storage + * @returns {Promise<{needAuthorization: boolean, privacyContractName?: string}>} + */ +function getHarmonyPrivacySetting() { + return new Promise((resolve) => { + // #ifdef MP-HARMONY + // 鸿蒙元服务: 隐私授权由系统管理,应用层仅检查 storage + // 系统会在用户首次使用隐私API时自动弹出系统级隐私授权弹窗 + resolve({ needAuthorization: !getStorageAgreed() }); + // #endif + // #ifndef MP-HARMONY + resolve({ needAuthorization: !getStorageAgreed() }); + // #endif + }); +} + +/** + * 综合查询: 平台级隐私状态 + storage 状态 + * 任一判断为"需要授权"则弹窗 + * @returns {Promise<{needAuthorization: boolean, privacyContractName?: string}>} + */ +async function checkPrivacy() { + // 微信小程序 + // #ifdef MP-WEIXIN + return await getWxPrivacySetting(); + // #endif + + // 快应用 + // #ifdef QUICKAPP-WEBVIEW + return await getQuickAppPrivacySetting(); + // #endif + + // 鸿蒙元服务 + // #ifdef MP-HARMONY + return await getHarmonyPrivacySetting(); + // #endif + + // H5 / 其他平台: 仅查 storage + // #ifndef MP-WEIXIN || QUICKAPP-WEBVIEW || MP-HARMONY + return { needAuthorization: !getStorageAgreed() }; + // #endif +} + +const PrivacyManager = { + /** + * 同步检查 storage 中的同意状态(快速判断,不查平台) + * @returns {boolean} true=需要弹窗, false=不需要 + */ + check() { + _needShow = !getStorageAgreed(); + _hasChecked = true; + return _needShow; + }, + + /** + * 触发弹窗(如果需要) + * 由 App.vue onLaunch 在 boot 完成后调用 + * 异步查询平台隐私状态,设置 _pendingShow 标志 + * 页面组件 mounted 时通过 consumePendingShow() 消费标志并弹窗 + * 不直接 emit 事件,避免 emit + 补偿双触发 + */ + async show() { + const result = await checkPrivacy(); + _needShow = result.needAuthorization; + _hasChecked = true; + + if (!_needShow) return; + + // 设置待弹窗标志 + // 已 mounted 的页面组件通过事件接收 + // 未 mounted 的页面组件 mounted 时通过 consumePendingShow 补偿 + _pendingShow = true; + + // 通知已 mounted 的页面组件(如果有监听者) + try { uni.$emit('privacy:show'); } catch (e) {} + + // 若组件已接收事件并弹窗, 会在 handlePrivacyShow 中调 consumePendingShow 清除 _pendingShow + // 若无监听者, _pendingShow 保持 true, 等页面 mounted 时补偿 + }, + + /** + * 页面 mounted 时补偿检查: 若 show() 在页面 mounted 之前 emit 了事件 + * 此方法返回 true 并清除标志,页面组件据此自动弹窗 + * @returns {boolean} true=有补偿弹窗,false=无 + */ + consumePendingShow() { + if (_pendingShow && _needShow) { + _pendingShow = false; + return true; + } + _pendingShow = false; + return false; + }, + + /** + * 用户同意:写 storage,标记不再弹 + * 由 privacy-popup.vue 的 handleAgree 调用 + */ + agree() { + try { uni.setStorageSync(STORAGE_KEY, true); } catch (e) {} + _needShow = false; + _pendingShow = false; + try { uni.$emit('privacy:agreed'); } catch (e) {} + }, + + /** + * 用户拒绝:不写 storage,允许下次再弹 + * 由 privacy-popup.vue 的 handleDisagree 调用 + */ + disagree() { + _needShow = false; + _pendingShow = false; + try { uni.$emit('privacy:disagreed'); } catch (e) {} + }, + + /** + * 当前是否需要弹窗(供页面查询) + * @returns {boolean} + */ + needShow() { + return _needShow; + }, + + /** + * 是否已执行过 check + * @returns {boolean} + */ + hasChecked() { + return _hasChecked; + }, +}; + +export default PrivacyManager; diff --git a/components/ns-newform/ns-newform.vue b/components/ns-newform/ns-newform.vue index a01132f..a5a1202 100644 --- a/components/ns-newform/ns-newform.vue +++ b/components/ns-newform/ns-newform.vue @@ -370,7 +370,7 @@ export default { // #ifdef APP-PLUS return 'app'; // #endif - // #ifdef APP-HARMONY + // #ifdef MP-HARMONY return 'harmony'; // #endif // #ifdef QUICKAPP-WEBVIEW diff --git a/components/privacy-popup/privacy-popup.vue b/components/privacy-popup/privacy-popup.vue index 984f89f..a317578 100644 --- a/components/privacy-popup/privacy-popup.vue +++ b/components/privacy-popup/privacy-popup.vue @@ -1,12 +1,12 @@ -