/** * 全局隐私协议管理器(单例) * * 职责: * 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'; // 缓存上一次 getPrivacySetting 的结果, 避免每个组件 mounted 都重新检查 let _lastCheckResult = null; let _lastCheckTs = 0; // 内部状态 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 + 补偿双触发 * * @param {Object} [options] * @param {boolean} [options.skip=false] true=本次跳过弹窗(不写 storage,下次正常访问其他页面还会弹) */ async show(options = {}) { // 主动跳过: 不查 storage、不写 storage、不弹窗 // 用于 H5 浏览器直接访问特定 URL(子串匹配)时不让隐私弹窗挡住 if (options && options.skip) { _needShow = false; _hasChecked = true; _pendingShow = false; return; } const _storageAgreed = getStorageAgreed(); 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 并清除标志,页面组件据此自动弹窗 * * 鸿蒙元服务兼容: * Lang.boot() 可能 hang, PrivacyManager.show() 可能从未被调用过; * 此时 _pendingShow 仍是默认 false, 但用户从未同意过隐私, 必须强制弹一次。 * 所以增加 storage 检测: 用户未同意也返回 true (补偿弹窗)。 * @returns {boolean} true=需要弹窗,false=不需要 */ consumePendingShow() { if (_pendingShow && _needShow) { _pendingShow = false; return true; } // 鸿蒙兜底: PrivacyManager.show() 没跑过 + 用户没同意过 storage → 必须弹 // 同一次进程内只补一次,避免隐私弹窗被反复触发 if (!_hasChecked && !_pendingShow && !getStorageAgreed()) { _pendingShow = false; _needShow = true; _hasChecked = true; 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;