1. add cache for privacy check result to reduce repeated calls 2. refactor lang bundle boot process with integrity checks 3. add timeout recovery for late fetch responses 4. improve error handling and fallback logic 5. add comments for better maintainability
477 lines
12 KiB
Vue
477 lines
12 KiB
Vue
<template>
|
|
<view>
|
|
<view class="privacy-mask" v-if="showPop">
|
|
<view class="privacy-wrap">
|
|
<view class="privacy-title">{{ i18n.title }}</view>
|
|
<view class="privacy-desc">
|
|
{{ i18n.descPrefix }}
|
|
<text class="privacy-link" @tap="openPrivacyContract">{{ i18n.privacyContractName }}</text>
|
|
<!-- #ifdef QUICKAPP-WEBVIEW || H5 || MP-HARMONY -->
|
|
<text class="privacy-link" @tap="openPrivacyService">{{ i18n.privacyServiceName }}</text>
|
|
<!-- #endif -->
|
|
{{ i18n.descSuffix }}
|
|
</view>
|
|
<view class="privacy-button-flex">
|
|
<button class="privacy-button-btn bg-disagree" @tap="handleDisagree">{{ i18n.disagreeBtn }}</button>
|
|
<!-- #ifdef QUICKAPP-WEBVIEW || MP-WEIXIN -->
|
|
<button id="agree-btn" class="privacy-button-btn bg-agree" open-type="agreePrivacyAuthorization"
|
|
@agreeprivacyauthorization="handleAgree">{{ i18n.agreeBtn }}</button>
|
|
<!-- #endif -->
|
|
<!-- #ifdef H5 || MP-HARMONY -->
|
|
<button id="agree-btn" class="privacy-button-btn bg-agree" @tap="handleAgree">{{ i18n.agreeBtn }}</button>
|
|
<!-- #endif -->
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
// #ifdef QUICKAPP-WEBVIEW
|
|
import has from '@system.has'
|
|
// #endif
|
|
import PrivacyManager from '@/common/js/privacy.js';
|
|
import i18nLazyMixin from '@/common/mixin/i18n-lazy.js';
|
|
|
|
const privacyContractPage = '/pages_tool/agreement/contenr?type=0';
|
|
const privacyServicePage = '/pages_tool/agreement/contenr?type=1';
|
|
|
|
export default {
|
|
name: 'PrivacyPopup',
|
|
mixins: [i18nLazyMixin],
|
|
data() {
|
|
return {
|
|
agree: false,
|
|
showPop: false,
|
|
privacyAuthorization: null,
|
|
privacyResolves: new Set(),
|
|
// 平台提供的隐私协议名称(由 wx.getPrivacySetting 等异步设置)
|
|
// 弹窗前才参与 i18n.privacyContractName 的填充
|
|
platformContractName: '',
|
|
// 弹窗文案: 模板只读 i18n.xxx
|
|
// 初值为空串, 弹窗前在 popUp() 中**同步**填充
|
|
// 此时 App.vue 已 await boot() + await PrivacyManager.show() 完成,
|
|
// Lang.boot() 必然已完成, $lang() 返回真实翻译
|
|
i18n: {
|
|
title: '',
|
|
descPrefix: '',
|
|
descSuffix: '',
|
|
privacyContractName: '',
|
|
privacyServiceName: '',
|
|
disagreeBtn: '',
|
|
agreeBtn: '',
|
|
disagreeTip: '',
|
|
},
|
|
}
|
|
},
|
|
mounted() {
|
|
// 调试: 记录本实例的 ID 和当前总数
|
|
this.listenPrivacySettingChange()
|
|
this.curPageShow()
|
|
|
|
// 监听全局隐私弹窗事件(由 App.vue boot 完成后通过 PrivacyManager.show() 触发)
|
|
uni.$on('privacy:show', this.handlePrivacyShow)
|
|
|
|
// 监听关闭事件: 其他实例弹窗时通知本实例关闭
|
|
uni.$on('privacy:forceClose', this.handleForceClose)
|
|
|
|
// 补偿: 若 PrivacyManager.show() 在本组件 mounted 之前 emit 了事件
|
|
// 事件已丢失,此处通过 consumePendingShow 补偿弹窗
|
|
if (PrivacyManager.consumePendingShow()) {
|
|
this.$nextTick(() => {
|
|
this.showPrivacyPopup()
|
|
})
|
|
}
|
|
},
|
|
created() {
|
|
try {
|
|
// #ifdef MP-WEIXIN
|
|
wx.getPrivacySetting({
|
|
success: (res) => {
|
|
this.platformContractName = res.privacyContractName || ''
|
|
}
|
|
});
|
|
// #endif
|
|
|
|
// #ifdef QUICKAPP-WEBVIEW
|
|
if (typeof has != 'undefined' && has?.getPrivacySetting) {
|
|
has.getPrivacySetting({
|
|
success: (res) => {
|
|
if (res.privacyContractName) {
|
|
this.platformContractName = res.privacyContractName
|
|
}
|
|
},
|
|
})
|
|
}
|
|
// #endif
|
|
|
|
// #ifdef MP-HARMONY
|
|
// 鸿蒙元服务: 隐私授权由系统管理, 无需应用层查询 privacyContractName
|
|
// #endif
|
|
} catch (e) {
|
|
console.error("=========低版本基础库==========", e)
|
|
}
|
|
},
|
|
methods: {
|
|
/**
|
|
* 接收全局隐私弹窗事件
|
|
* 由 App.vue → PrivacyManager.show() → uni.$emit('privacy:show') 触发
|
|
* 多个实例同时监听时, 只有第一个调用 consumePendingShow 成功的实例才弹窗
|
|
*/
|
|
handlePrivacyShow() {
|
|
// 互斥: 只有一个实例能消费成功
|
|
if (!PrivacyManager.consumePendingShow()) {
|
|
return;
|
|
}
|
|
this.showPrivacyPopup()
|
|
},
|
|
|
|
/**
|
|
* 接收其他实例的关闭通知
|
|
* 当另一个 privacy-popup 实例弹窗时, 本实例关闭自己的弹窗
|
|
* 避免多个实例同时弹窗导致需要点击两次
|
|
*/
|
|
handleForceClose() {
|
|
this.disPopUp()
|
|
},
|
|
|
|
/**
|
|
* 统一弹窗 + 关闭其他页面的弹窗
|
|
* 通过 uni.$emit('privacy:forceClose') 通知其他实例关闭
|
|
*/
|
|
showPrivacyPopup() {
|
|
// 先通知其他实例关闭
|
|
try { uni.$emit('privacy:forceClose'); } catch (e) {}
|
|
// 直接弹窗: boot 阶段已经保证 bundle 完整
|
|
// 完整性校验在 lang.js _bootImpl() 中完成(检查 defaultAppNameMp 等关键 key)
|
|
// 不完整时 force 重试, 仍不完整则报错不静默回退
|
|
// 所以这里无需再检测/自愈, 信任 boot 已就绪
|
|
this.popUp()
|
|
},
|
|
|
|
/**
|
|
* 根据是否需要授权决定弹窗或 emit agree
|
|
*/
|
|
checkAndShow(needAuthorization) {
|
|
if (uni.getStorageSync('skipPrivacyCheck')) {
|
|
uni.removeStorageSync('skipPrivacyCheck');
|
|
this.$emit('agree');
|
|
return;
|
|
}
|
|
|
|
if (needAuthorization) {
|
|
const privacyAgreed = uni.getStorageSync('privacyAgreed');
|
|
if (privacyAgreed) {
|
|
this.$emit('agree')
|
|
} else {
|
|
this.showPrivacyPopup()
|
|
}
|
|
} else {
|
|
this.$emit('agree')
|
|
}
|
|
},
|
|
|
|
getPrivacySettingByQuickApp() {
|
|
// #ifdef QUICKAPP-WEBVIEW
|
|
if (typeof has != 'undefined' && has?.getPrivacySetting) {
|
|
has.getPrivacySetting({
|
|
success: (res) => {
|
|
this.checkAndShow(res.needAuthorization)
|
|
},
|
|
})
|
|
} else {
|
|
this.checkAndShow(!uni.getStorageSync('privacyAgreed'))
|
|
}
|
|
// #endif
|
|
},
|
|
|
|
/**
|
|
* 监听平台侧的隐私授权事件
|
|
* 微信小程序: wx.onNeedPrivacyAuthorization 由当前页面实例注册
|
|
* 鸿蒙元服务: has.onPrivacySettingChange
|
|
* 与 PrivacyManager 协同:
|
|
* - 若 PrivacyManager 判断需要弹窗 → 弹自定义弹窗, resolve 存入 privacyResolves
|
|
* - 若已同意 → 直接 resolve 放行
|
|
*/
|
|
listenPrivacySettingChange() {
|
|
// #ifdef MP-WEIXIN
|
|
if (wx.onNeedPrivacyAuthorization) {
|
|
wx.onNeedPrivacyAuthorization((resolve) => {
|
|
// 检查是否已同意
|
|
if (!PrivacyManager.needShow()) {
|
|
// 已同意, 直接放行
|
|
resolve({ event: 'agree' })
|
|
} else {
|
|
// 需要弹窗, 存 resolve 等用户操作
|
|
if (typeof this.privacyAuthorization === 'function') {
|
|
this.privacyAuthorization(resolve)
|
|
} else {
|
|
// 兜底: 组件未就绪, 直接放行避免 API 卡死
|
|
resolve({ event: 'agree' })
|
|
}
|
|
}
|
|
})
|
|
}
|
|
// #endif
|
|
|
|
// #ifdef QUICKAPP-WEBVIEW
|
|
if (typeof has != 'undefined' && has?.onPrivacySettingChange) {
|
|
has.onPrivacySettingChange((res) => {
|
|
if (res.needAuthorization && PrivacyManager.needShow()) {
|
|
this.showPrivacyPopup()
|
|
}
|
|
})
|
|
}
|
|
// #endif
|
|
|
|
// #ifdef MP-HARMONY
|
|
// 鸿蒙元服务: 隐私授权由系统自动管理, 无需应用层监听
|
|
// #endif
|
|
},
|
|
|
|
/**
|
|
* 初始化监听程序(页面级)
|
|
*/
|
|
curPageShow() {
|
|
this.privacyAuthorization = resolve => {
|
|
this.privacyResolves.add(resolve)
|
|
this.showPrivacyPopup()
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 弹窗时关掉其他页面的隐私弹窗,避免重叠
|
|
*/
|
|
closeOtherPagePopUp(closePopUp) {
|
|
// 通过全局事件通知所有页面关闭弹窗
|
|
try { uni.$emit('privacy:closeOthers', this._uid); } catch (e) {}
|
|
},
|
|
|
|
openPrivacyContract() {
|
|
// #ifdef MP-WEIXIN
|
|
wx.openPrivacyContract({
|
|
success(res) {},
|
|
fail(err) {}
|
|
});
|
|
// #endif
|
|
|
|
// #ifdef QUICKAPP-WEBVIEW
|
|
if (typeof has != 'undefined' && has?.openPrivacySetting) {
|
|
has.openPrivacySetting({
|
|
success: (res) => {},
|
|
fail: (err) => {}
|
|
});
|
|
} else {
|
|
this.$util.redirectTo(privacyContractPage);
|
|
}
|
|
// #endif
|
|
|
|
// #ifdef MP-HARMONY
|
|
// 鸿蒙元服务: 使用应用内协议页面
|
|
this.$util.redirectTo(privacyContractPage);
|
|
// #endif
|
|
|
|
// #ifdef H5
|
|
this.$util.redirectTo(privacyContractPage);
|
|
// #endif
|
|
},
|
|
|
|
openPrivacyService() {
|
|
// #ifdef QUICKAPP-WEBVIEW
|
|
if (typeof has != 'undefined' && has?.openPrivacySetting) {
|
|
has.openPrivacySetting({
|
|
success: (res) => {},
|
|
fail: (err) => {}
|
|
});
|
|
} else {
|
|
this.$util.redirectTo(privacyServicePage);
|
|
}
|
|
// #endif
|
|
|
|
// #ifdef MP-HARMONY
|
|
this.$util.redirectTo(privacyServicePage);
|
|
// #endif
|
|
|
|
// #ifdef H5
|
|
this.$util.redirectTo(privacyServicePage);
|
|
// #endif
|
|
},
|
|
|
|
handleDisagree() {
|
|
this.privacyResolves.forEach(resolve => {
|
|
resolve({ event: 'disagree' })
|
|
})
|
|
this.privacyResolves.clear()
|
|
|
|
// 先通知其他实例关闭, 再关闭自己, 避免叠层
|
|
try { uni.$emit('privacy:forceClose'); } catch (e) {}
|
|
this.disPopUp()
|
|
|
|
// 修复: 拒绝时不写 privacyAgreed, 允许下次再弹
|
|
// 由 PrivacyManager 管理 storage
|
|
PrivacyManager.disagree()
|
|
|
|
uni.showModal({
|
|
content: this.i18n.disagreeTip,
|
|
success: () => {
|
|
this.$emit('disagree')
|
|
}
|
|
})
|
|
},
|
|
|
|
handleAgree() {
|
|
this.privacyResolves.forEach(resolve => {
|
|
resolve({
|
|
event: 'agree',
|
|
buttonId: 'agree-btn'
|
|
})
|
|
})
|
|
this.privacyResolves.clear()
|
|
|
|
// 先通知其他实例关闭, 再关闭自己
|
|
try { uni.$emit('privacy:forceClose'); } catch (e) {}
|
|
this.disPopUp()
|
|
|
|
// 由 PrivacyManager 管理 storage
|
|
PrivacyManager.agree()
|
|
this.$emit('agree')
|
|
},
|
|
|
|
/**
|
|
* mixin i18n-lazy 要求的方法
|
|
* 返回一个对象, key 与 data().i18n 字段一一对应
|
|
* 调用时机在 popUp() 中(此时 boot 已完成, $lang() 返回真实翻译)
|
|
*
|
|
* 这里**不做任何兜底**:
|
|
* - 无 hardcode fallback
|
|
* - 不依赖本地 lang
|
|
* - 直接信任 boot 阶段的完整性校验结果
|
|
* - 若 boot 阶段校验失败, $lang() 会返回空串, UI 显示空文案(按钮可点)
|
|
*/
|
|
i18nPrepareFn() {
|
|
let appName, privacyContractName, privacyServiceName;
|
|
|
|
// 关键: appName 必须先解析为字符串, 才能作为 descPrefix/descSuffix 的参数
|
|
// #ifdef H5 || MP-HARMONY
|
|
appName = this.$lang('components.privacyPopup.defaultAppNameH5');
|
|
privacyContractName = this.platformContractName || this.$lang('components.privacyPopup.defaultContractNameH5');
|
|
privacyServiceName = this.$lang('components.privacyPopup.defaultServiceNameH5');
|
|
// #endif
|
|
// #ifdef MP-WEIXIN
|
|
appName = this.$lang('components.privacyPopup.defaultAppNameMp');
|
|
privacyContractName = this.platformContractName || this.$lang('components.privacyPopup.defaultContractNameMp');
|
|
privacyServiceName = '';
|
|
// #endif
|
|
|
|
const result = {
|
|
title: this.$lang('components.privacyPopup.title'),
|
|
descPrefix: this.$lang('components.privacyPopup.descPrefix', { appName }),
|
|
descSuffix: this.$lang('components.privacyPopup.descSuffix', { appName }),
|
|
privacyContractName,
|
|
privacyServiceName,
|
|
disagreeBtn: this.$lang('components.privacyPopup.disagreeBtn'),
|
|
agreeBtn: this.$lang('components.privacyPopup.agreeBtn'),
|
|
disagreeTip: this.$lang('components.privacyPopup.disagreeTip'),
|
|
};
|
|
return result;
|
|
},
|
|
|
|
popUp() {
|
|
// 弹窗前**一次性**计算所有 i18n 文案
|
|
// boot 阶段已用 isBundleComplete() 保证 bundle 完整
|
|
// $lang() 返回真实翻译(效果与 H5 一致)
|
|
this.prepareI18n();
|
|
this.showPop = true;
|
|
},
|
|
|
|
disPopUp() {
|
|
this.showPop = false;
|
|
},
|
|
},
|
|
beforeDestroy() {
|
|
this.privacyResolves.clear()
|
|
// 移除全局事件监听
|
|
uni.$off('privacy:show', this.handlePrivacyShow)
|
|
uni.$off('privacy:forceClose', this.handleForceClose)
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.privacy-mask {
|
|
position: fixed;
|
|
z-index: 5000;
|
|
top: 0;
|
|
right: 0;
|
|
left: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.2);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.privacy-wrap {
|
|
width: 632rpx;
|
|
padding: 48rpx 30rpx;
|
|
box-sizing: border-box;
|
|
background: #fff;
|
|
border-radius: 16rpx;
|
|
}
|
|
|
|
.privacy-title {
|
|
padding: 0rpx 30rpx 40rpx 30rpx;
|
|
font-weight: 700;
|
|
font-size: 36rpx;
|
|
text-align: center;
|
|
}
|
|
|
|
.privacy-desc {
|
|
font-size: 30rpx;
|
|
color: #555;
|
|
line-height: 2;
|
|
text-align: left;
|
|
padding: 0 40rpx;
|
|
}
|
|
|
|
.privacy-link {
|
|
color: #2f80ed;
|
|
}
|
|
|
|
.privacy-button-flex {
|
|
display: flex;
|
|
padding: 20rpx 40rpx;
|
|
}
|
|
|
|
.privacy-button-btn {
|
|
color: #FFF;
|
|
font-size: 30rpx;
|
|
/* #ifdef QUICKAPP-WEBVIEW || H5 || MP-HARMONY */
|
|
font-size: 28rpx;
|
|
/* #endif */
|
|
font-weight: 500;
|
|
line-height: 100rpx;
|
|
text-align: center;
|
|
height: 100rpx;
|
|
border-radius: 20rpx;
|
|
border: none;
|
|
background: #07c160;
|
|
flex: 1;
|
|
margin-right: 30rpx;
|
|
justify-content: center;
|
|
}
|
|
|
|
.privacy-button-btn::after {
|
|
border: none;
|
|
}
|
|
|
|
.bg-disagree {
|
|
color: #07c160;
|
|
background: #f2f2f2;
|
|
}
|
|
|
|
.bg-agree {
|
|
margin-right: 0rpx;
|
|
}
|
|
</style>
|