feat: 添加全局隐私协议管理器并适配鸿蒙元服务

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原型链
This commit is contained in:
2026-07-01 09:39:04 +08:00
parent 4adaa945a1
commit 7045a05a73
6 changed files with 660 additions and 354 deletions

View File

@@ -3,6 +3,7 @@
import configExternal, { themeConfig } from 'common/js/config-external.js' import configExternal, { themeConfig } from 'common/js/config-external.js'
import Lang from 'common/js/lang.js'; import Lang from 'common/js/lang.js';
import ThemeOverride from 'common/js/theme-override.js'; import ThemeOverride from 'common/js/theme-override.js';
import PrivacyManager from 'common/js/privacy.js';
import { import {
Weixin Weixin
@@ -78,6 +79,12 @@
} }
// #endif // #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 合并 ) // ---- 3) 远端就绪后,主题再 apply 一次(preset + remote 合并 )
// 这一次 setThemeStyle + themeColorSet 是同步路径(preset 已在 cache,remote 也在 cache) // 这一次 setThemeStyle + themeColorSet 是同步路径(preset 已在 cache,remote 也在 cache)
// 如果跟 step 2 的 apply 值相同,Vuex 会重复 commit 但页面无视觉变化 // 如果跟 step 2 的 apply 值相同,Vuex 会重复 commit 但页面无视觉变化

241
common/js/privacy.js Normal file
View File

@@ -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;

View File

@@ -370,7 +370,7 @@ export default {
// #ifdef APP-PLUS // #ifdef APP-PLUS
return 'app'; return 'app';
// #endif // #endif
// #ifdef APP-HARMONY // #ifdef MP-HARMONY
return 'harmony'; return 'harmony';
// #endif // #endif
// #ifdef QUICKAPP-WEBVIEW // #ifdef QUICKAPP-WEBVIEW

View File

@@ -1,12 +1,12 @@
<template> <template>
<view> <view>
<view class="privacy-mask" v-if="showPop"> <view class="privacy-mask" v-if="showPop">
<view class="privacy-wrap"> <view class="privacy-wrap">
<view class="privacy-title">{{ $lang('components.privacyPopup.title') }}</view> <view class="privacy-title">{{ $lang('components.privacyPopup.title') }}</view>
<view class="privacy-desc"> <view class="privacy-desc">
{{ $lang('components.privacyPopup.descPrefix', { appName: appName }) }} {{ $lang('components.privacyPopup.descPrefix', { appName: appName }) }}
<text class="privacy-link" @tap="openPrivacyContract">{{ privacyContractName }}</text> <text class="privacy-link" @tap="openPrivacyContract">{{ privacyContractName }}</text>
<!-- #ifdef QUICKAPP-WEBVIEW || H5 --> <!-- #ifdef QUICKAPP-WEBVIEW || H5 || MP-HARMONY -->
<text class="privacy-link" @tap="openPrivacyService">{{ privacyServiceName }}</text> <text class="privacy-link" @tap="openPrivacyService">{{ privacyServiceName }}</text>
<!-- #endif --> <!-- #endif -->
{{ $lang('components.privacyPopup.descSuffix', { appName: appName }) }} {{ $lang('components.privacyPopup.descSuffix', { appName: appName }) }}
@@ -17,349 +17,404 @@
<button id="agree-btn" class="privacy-button-btn bg-agree" open-type="agreePrivacyAuthorization" <button id="agree-btn" class="privacy-button-btn bg-agree" open-type="agreePrivacyAuthorization"
@agreeprivacyauthorization="handleAgree">{{ $lang('components.privacyPopup.agreeBtn') }}</button> @agreeprivacyauthorization="handleAgree">{{ $lang('components.privacyPopup.agreeBtn') }}</button>
<!-- #endif --> <!-- #endif -->
<!-- #ifdef WEB || H5 --> <!-- #ifdef H5 || MP-HARMONY -->
<button id="agree-btn" class="privacy-button-btn bg-agree" @tap="handleAgree">{{ $lang('components.privacyPopup.agreeBtn') }}</button> <button id="agree-btn" class="privacy-button-btn bg-agree" @tap="handleAgree">{{ $lang('components.privacyPopup.agreeBtn') }}</button>
<!-- #endif --> <!-- #endif -->
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</template> </template>
<script> <script>
// #ifdef QUICKAPP-WEBVIEW // #ifdef QUICKAPP-WEBVIEW
import has from '@system.has' import has from '@system.has'
// #endif // #endif
import PrivacyManager from '@/common/js/privacy.js';
const closeOtherPagePopUpHooks = new Set();
const privacyContractPage = '/pages_tool/agreement/contenr?type=0'; const privacyContractPage = '/pages_tool/agreement/contenr?type=0';
const privacyServicePage = '/pages_tool/agreement/contenr?type=1'; const privacyServicePage = '/pages_tool/agreement/contenr?type=1';
export default { export default {
name: 'PrivacyPopup', name: 'PrivacyPopup',
data() { data() {
return { return {
agree: false, agree: false,
showPop: false, showPop: false,
privacyAuthorization: null, privacyAuthorization: null,
privacyResolves: new Set(), privacyResolves: new Set(),
privacyContractName: this.$lang('components.privacyPopup.defaultContractNameMp'), privacyContractName: this.$lang('components.privacyPopup.defaultContractNameMp'),
appName: this.$lang('components.privacyPopup.defaultAppNameMp'), appName: this.$lang('components.privacyPopup.defaultAppNameMp'),
// #ifdef WEB || H5 // #ifdef H5
appName: this.$lang('components.privacyPopup.defaultAppNameH5'), appName: this.$lang('components.privacyPopup.defaultAppNameH5'),
privacyContractName: this.$lang('components.privacyPopup.defaultContractNameH5'), privacyContractName: this.$lang('components.privacyPopup.defaultContractNameH5'),
privacyServiceName: this.$lang('components.privacyPopup.defaultServiceNameH5'), privacyServiceName: this.$lang('components.privacyPopup.defaultServiceNameH5'),
// #endif // #endif
} // #ifdef MP-HARMONY
}, appName: this.$lang('components.privacyPopup.defaultAppNameH5'),
mounted() { privacyContractName: this.$lang('components.privacyPopup.defaultContractNameH5'),
this.listenPrivacySettingChange() privacyServiceName: this.$lang('components.privacyPopup.defaultServiceNameH5'),
this.curPageShow() // #endif
this.proactive() }
}, },
created() { mounted() {
try { this.listenPrivacySettingChange()
// #ifdef MP-WEIXIN this.curPageShow()
wx.getPrivacySetting({
success: (res) => { // 监听全局隐私弹窗事件(由 App.vue boot 完成后通过 PrivacyManager.show() 触发)
this.privacyContractName = res.privacyContractName uni.$on('privacy:show', this.handlePrivacyShow)
}
}); // 监听关闭事件: 其他实例弹窗时通知本实例关闭
// #endif uni.$on('privacy:forceClose', this.handleForceClose)
// #ifdef QUICKAPP-WEBVIEW || H5 // 补偿: 若 PrivacyManager.show() 在本组件 mounted 之前 emit 了事件
if (this.$util.isQuickApp() && typeof has != 'undefined' && has?.getPrivacySetting) { // 事件已丢失,此处通过 consumePendingShow 补偿弹窗
has.getPrivacySetting({ if (PrivacyManager.consumePendingShow()) {
success: (res) => { this.$nextTick(() => {
if (res.privacyContractName) { this.showPrivacyPopup()
this.privacyContractName = res.privacyContractName })
} }
}, },
}) created() {
} try {
// #endif // #ifdef MP-WEIXIN
} catch (e) { wx.getPrivacySetting({
console.error("=========低版本基础库==========", e) success: (res) => {
} this.privacyContractName = res.privacyContractName
}, }
methods: { });
/** // #endif
* 统一弹窗 + 关闭其他页面的弹窗
*/ // #ifdef QUICKAPP-WEBVIEW
showPrivacyPopup() { if (typeof has != 'undefined' && has?.getPrivacySetting) {
this.popUp() has.getPrivacySetting({
this.closeOtherPagePopUp(this.disPopUp) success: (res) => {
}, if (res.privacyContractName) {
this.privacyContractName = res.privacyContractName
/** }
* 根据是否需要授权决定弹窗或 emit agree },
*/ })
checkAndShow(needAuthorization) { }
if (uni.getStorageSync('skipPrivacyCheck')) { // #endif
uni.removeStorageSync('skipPrivacyCheck');
this.$emit('agree'); // #ifdef MP-HARMONY
return; // 鸿蒙元服务: 隐私授权由系统管理, 无需应用层查询 privacyContractName
} // #endif
} catch (e) {
if (needAuthorization) { console.error("=========低版本基础库==========", e)
const privacyAgreed = uni.getStorageSync('privacyAgreed'); }
if (privacyAgreed) { },
this.$emit('agree') methods: {
} else { /**
this.showPrivacyPopup() * 接收全局隐私弹窗事件
} * 由 App.vue → PrivacyManager.show() → uni.$emit('privacy:show') 触发
} else { * 多个实例同时监听时, 只有第一个调用 consumePendingShow 成功的实例才弹窗
this.$emit('agree') */
} handlePrivacyShow() {
}, // 互斥: 只有一个实例能消费成功
if (!PrivacyManager.consumePendingShow()) return
getPrivacySettingByQuickApp() { this.showPrivacyPopup()
// #ifdef QUICKAPP-WEBVIEW || H5 || WEB },
if (!this.$util.isQuickApp()) return;
if (typeof has != 'undefined' && has?.getPrivacySetting) { /**
has.getPrivacySetting({ * 接收其他实例的关闭通知
success: (res) => { * 当另一个 privacy-popup 实例弹窗时, 本实例关闭自己的弹窗
this.checkAndShow(res.needAuthorization) * 避免多个实例同时弹窗导致需要点击两次
}, */
}) handleForceClose() {
} else { this.disPopUp()
this.checkAndShow(!uni.getStorageSync('privacyAgreed')) },
}
// #endif /**
}, * 统一弹窗 + 关闭其他页面的弹窗
* 通过 uni.$emit('privacy:forceClose') 通知其他实例关闭
/** */
* 监听平台侧的隐私授权事件 showPrivacyPopup() {
*/ // 先通知其他实例关闭
listenPrivacySettingChange() { try { uni.$emit('privacy:forceClose'); } catch (e) {}
// #ifdef MP-WEIXIN // 再弹出本实例
if (wx.onNeedPrivacyAuthorization) { this.popUp()
wx.onNeedPrivacyAuthorization((resolve) => { },
if (typeof this.privacyAuthorization === 'function') {
this.privacyAuthorization(resolve) /**
} * 根据是否需要授权决定弹窗或 emit agree
}) */
} checkAndShow(needAuthorization) {
// #endif if (uni.getStorageSync('skipPrivacyCheck')) {
uni.removeStorageSync('skipPrivacyCheck');
// #ifdef QUICKAPP-WEBVIEW || H5 || WEB this.$emit('agree');
if (this.$util.isQuickApp() && typeof has != 'undefined' && has?.onPrivacySettingChange) { return;
has.onPrivacySettingChange((res) => { }
this.checkAndShow(res.needAuthorization)
}) if (needAuthorization) {
} const privacyAgreed = uni.getStorageSync('privacyAgreed');
// #endif if (privacyAgreed) {
}, this.$emit('agree')
} else {
/** this.showPrivacyPopup()
* 主动查询隐私授权状态并弹窗 }
*/ } else {
proactive() { this.$emit('agree')
// #ifdef MP-WEIXIN }
if (wx.getPrivacySetting) { },
wx.getPrivacySetting({
success: (res) => { getPrivacySettingByQuickApp() {
this.checkAndShow(res.needAuthorization) // #ifdef QUICKAPP-WEBVIEW
}, if (typeof has != 'undefined' && has?.getPrivacySetting) {
}); has.getPrivacySetting({
} else { success: (res) => {
// 低版本基础库直接检查本地存储 this.checkAndShow(res.needAuthorization)
this.checkAndShow(!uni.getStorageSync('privacyAgreed')) },
} })
// #endif } else {
this.checkAndShow(!uni.getStorageSync('privacyAgreed'))
// #ifdef QUICKAPP-WEBVIEW }
this.getPrivacySettingByQuickApp(); // #endif
// #endif },
// #ifdef H5 || WEB /**
this.checkAndShow(!uni.getStorageSync('privacyAgreed')) * 监听平台侧的隐私授权事件
// #endif * 微信小程序: wx.onNeedPrivacyAuthorization 由当前页面实例注册
}, * 鸿蒙元服务: has.onPrivacySettingChange
* 与 PrivacyManager 协同:
/** * - 若 PrivacyManager 判断需要弹窗 → 弹自定义弹窗, resolve 存入 privacyResolves
* 初始化监听程序(页面级) * - 若已同意 → 直接 resolve 放行
*/ */
curPageShow() { listenPrivacySettingChange() {
closeOtherPagePopUpHooks.add(this.disPopUp) // #ifdef MP-WEIXIN
this.privacyAuthorization = resolve => { if (wx.onNeedPrivacyAuthorization) {
this.privacyResolves.add(resolve) wx.onNeedPrivacyAuthorization((resolve) => {
this.showPrivacyPopup() // 检查是否已同意
} if (!PrivacyManager.needShow()) {
}, // 已同意, 直接放行
resolve({ event: 'agree' })
/** } else {
* 弹窗时关掉其他页面的隐私弹窗,避免重叠 // 需要弹窗, 存 resolve 等用户操作
*/ if (typeof this.privacyAuthorization === 'function') {
closeOtherPagePopUp(closePopUp) { this.privacyAuthorization(resolve)
closeOtherPagePopUpHooks.forEach(hook => { } else {
if (closePopUp !== hook) { // 兜底: 组件未就绪, 直接放行避免 API 卡死
hook() resolve({ event: 'agree' })
} }
}) }
}, })
}
openPrivacyContract() { // #endif
// #ifdef MP-WEIXIN
wx.openPrivacyContract({ // #ifdef QUICKAPP-WEBVIEW
success(res) {}, if (typeof has != 'undefined' && has?.onPrivacySettingChange) {
fail(err) {} has.onPrivacySettingChange((res) => {
}); if (res.needAuthorization && PrivacyManager.needShow()) {
// #endif this.showPrivacyPopup()
}
// #ifdef QUICKAPP-WEBVIEW || H5 || WEB })
if (this.$util.isQuickApp()) { }
if (typeof has != 'undefined' && has?.openPrivacySetting) { // #endif
has.openPrivacySetting({
success: (res) => {}, // #ifdef MP-HARMONY
fail: (err) => {} // 鸿蒙元服务: 隐私授权由系统自动管理, 无需应用层监听
}); // #endif
} else { },
this.$util.redirectTo(privacyContractPage);
} /**
} else { * 初始化监听程序(页面级)
this.$util.redirectTo(privacyContractPage); */
} curPageShow() {
// #endif this.privacyAuthorization = resolve => {
}, this.privacyResolves.add(resolve)
this.showPrivacyPopup()
openPrivacyService() { }
// #ifdef QUICKAPP-WEBVIEW || H5 || WEB },
if (this.$util.isQuickApp()) {
if (typeof has != 'undefined' && has?.openPrivacySetting) { /**
has.openPrivacySetting({ * 弹窗时关掉其他页面的隐私弹窗,避免重叠
success: (res) => {}, */
fail: (err) => {} closeOtherPagePopUp(closePopUp) {
}); // 通过全局事件通知所有页面关闭弹窗
} else { try { uni.$emit('privacy:closeOthers', this._uid); } catch (e) {}
this.$util.redirectTo(privacyServicePage); },
}
} else { openPrivacyContract() {
this.$util.redirectTo(privacyServicePage); // #ifdef MP-WEIXIN
} wx.openPrivacyContract({
// #endif success(res) {},
}, fail(err) {}
});
handleDisagree() { // #endif
this.privacyResolves.forEach(resolve => {
resolve({ event: 'disagree' }) // #ifdef QUICKAPP-WEBVIEW
}) if (typeof has != 'undefined' && has?.openPrivacySetting) {
this.privacyResolves.clear() has.openPrivacySetting({
this.disPopUp() success: (res) => {},
fail: (err) => {}
uni.setStorageSync('privacyAgreed', true); });
} else {
uni.showModal({ this.$util.redirectTo(privacyContractPage);
content: this.$lang('components.privacyPopup.disagreeTip'), }
success: () => { // #endif
this.$emit('disagree')
} // #ifdef MP-HARMONY
}) // 鸿蒙元服务: 使用应用内协议页面
}, this.$util.redirectTo(privacyContractPage);
// #endif
handleAgree() {
this.privacyResolves.forEach(resolve => { // #ifdef H5
resolve({ this.$util.redirectTo(privacyContractPage);
event: 'agree', // #endif
buttonId: 'agree-btn' },
})
}) openPrivacyService() {
this.privacyResolves.clear() // #ifdef QUICKAPP-WEBVIEW
this.disPopUp() if (typeof has != 'undefined' && has?.openPrivacySetting) {
has.openPrivacySetting({
uni.setStorageSync('privacyAgreed', true); success: (res) => {},
this.$emit('agree') fail: (err) => {}
}, });
} else {
popUp() { this.$util.redirectTo(privacyServicePage);
this.showPop = true; }
}, // #endif
disPopUp() { // #ifdef MP-HARMONY
this.showPop = false; this.$util.redirectTo(privacyServicePage);
}, // #endif
},
beforeDestroy() { // #ifdef H5
this.privacyResolves.clear() this.$util.redirectTo(privacyServicePage);
closeOtherPagePopUpHooks.delete(this.disPopUp) // #endif
}, },
}
</script> handleDisagree() {
this.privacyResolves.forEach(resolve => {
<style lang="scss" scoped> resolve({ event: 'disagree' })
.privacy-mask { })
position: fixed; this.privacyResolves.clear()
z-index: 5000;
top: 0; // 先通知其他实例关闭, 再关闭自己, 避免叠层
right: 0; try { uni.$emit('privacy:forceClose'); } catch (e) {}
left: 0; this.disPopUp()
bottom: 0;
background: rgba(0, 0, 0, 0.2); // 修复: 拒绝时不写 privacyAgreed, 允许下次再弹
display: flex; // 由 PrivacyManager 管理 storage
align-items: center; PrivacyManager.disagree()
justify-content: center;
} uni.showModal({
content: this.$lang('components.privacyPopup.disagreeTip'),
.privacy-wrap { success: () => {
width: 632rpx; this.$emit('disagree')
padding: 48rpx 30rpx; }
box-sizing: border-box; })
background: #fff; },
border-radius: 16rpx;
} handleAgree() {
this.privacyResolves.forEach(resolve => {
.privacy-title { resolve({
padding: 0rpx 30rpx 40rpx 30rpx; event: 'agree',
font-weight: 700; buttonId: 'agree-btn'
font-size: 36rpx; })
text-align: center; })
} this.privacyResolves.clear()
.privacy-desc { // 先通知其他实例关闭, 再关闭自己
font-size: 30rpx; try { uni.$emit('privacy:forceClose'); } catch (e) {}
color: #555; this.disPopUp()
line-height: 2;
text-align: left; // 由 PrivacyManager 管理 storage
padding: 0 40rpx; PrivacyManager.agree()
} this.$emit('agree')
},
.privacy-link {
color: #2f80ed; popUp() {
} this.showPop = true;
},
.privacy-button-flex {
display: flex; disPopUp() {
padding: 20rpx 40rpx; this.showPop = false;
} },
},
.privacy-button-btn { beforeDestroy() {
color: #FFF; this.privacyResolves.clear()
font-size: 30rpx; // 移除全局事件监听
// #ifdef QUICKAPP-WEBVIEW || H5 || WEB uni.$off('privacy:show', this.handlePrivacyShow)
font-size: 28rpx; uni.$off('privacy:forceClose', this.handleForceClose)
// #endif },
font-weight: 500; }
line-height: 100rpx; </script>
text-align: center;
height: 100rpx; <style lang="scss" scoped>
border-radius: 20rpx; .privacy-mask {
border: none; position: fixed;
background: #07c160; z-index: 5000;
flex: 1; top: 0;
margin-right: 30rpx; right: 0;
justify-content: center; left: 0;
} bottom: 0;
background: rgba(0, 0, 0, 0.2);
.privacy-button-btn::after { display: flex;
border: none; align-items: center;
} justify-content: center;
}
.bg-disagree {
color: #07c160; .privacy-wrap {
background: #f2f2f2; width: 632rpx;
} padding: 48rpx 30rpx;
box-sizing: border-box;
.bg-agree { background: #fff;
margin-right: 0rpx; 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> </style>

View File

@@ -15,6 +15,7 @@ import {
uniStorage uniStorage
} from './common/js/storage.js' } from './common/js/storage.js'
import configExternal from './common/js/config-external.js' import configExternal from './common/js/config-external.js'
import PrivacyManager from './common/js/privacy.js'
Vue.prototype.$store = store //挂在vue Vue.prototype.$store = store //挂在vue
@@ -26,7 +27,9 @@ Vue.prototype.$api = Http;
Vue.prototype.$langConfig = Lang; //语言包对象 Vue.prototype.$langConfig = Lang; //语言包对象
Vue.prototype.$lang = Lang.lang; //解析语言包 Vue.prototype.$lang = Lang.lang; //解析语言包
Vue.prototype.$config = Config; Vue.prototype.$config = Config;
Vue.prototype.$privacy = PrivacyManager; //全局隐私管理器
// #ifdef H5 // #ifdef H5

View File

@@ -12,7 +12,7 @@
<!-- 表格中的图片,使用 rich-text 防止大小不正确 --> <!-- 表格中的图片,使用 rich-text 防止大小不正确 -->
<rich-text v-if="n.name==='img'&&n.t" :style="'display:'+n.t" :nodes="[{attrs:{style:n.attrs.style||'',src:n.attrs.src},name:'img'}]" :data-i="i" @tap.stop="imgTap" /> <rich-text v-if="n.name==='img'&&n.t" :style="'display:'+n.t" :nodes="[{attrs:{style:n.attrs.style||'',src:n.attrs.src},name:'img'}]" :data-i="i" @tap.stop="imgTap" />
<!-- #endif --> <!-- #endif -->
<!-- #ifdef APP-HARMONY --> <!-- #ifdef MP-HARMONY || APP-HARMONY -->
<image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+ctrl[i]+'px;'+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||'scaleToFill'))" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" show-menu-by-longpress/> <image v-else-if="n.name==='img'" :id="n.attrs.id" :class="'_img '+n.attrs.class" :style="(ctrl[i]===-1?'display:none;':'')+'width:'+ctrl[i]+'px;'+n.attrs.style" :src="n.attrs.src||(ctrl.load?n.attrs['data-src']:'')" :mode="!n.h?'widthFix':(!n.w?'heightFix':(n.m||'scaleToFill'))" :data-i="i" @load="imgLoad" @error="mediaError" @tap.stop="imgTap" @longpress="imgLongTap" show-menu-by-longpress/>
<!-- #endif --> <!-- #endif -->
<!-- #ifndef H5 || APP-PLUS || MP-KUAISHOU --> <!-- #ifndef H5 || APP-PLUS || MP-KUAISHOU -->
@@ -156,7 +156,7 @@ export default {
} }
}, },
components: { components: {
// #ifndef ((H5 || APP-PLUS) && VUE3) || APP-HARMONY // #ifndef ((H5 || APP-PLUS) && VUE3) || MP-HARMONY
node node
// #endif // #endif
}, },
@@ -268,10 +268,10 @@ export default {
// #ifdef H5 || APP-PLUS // #ifdef H5 || APP-PLUS
node.attrs.src = node.attrs.src || node.attrs['data-src'] node.attrs.src = node.attrs.src || node.attrs['data-src']
// #endif // #endif
// #ifndef APP-HARMONY // #ifndef MP-HARMONY
this.root.$emit('imgtap', node.attrs) this.root.$emit('imgtap', node.attrs)
// #endif // #endif
// #ifdef APP-HARMONY // #ifdef MP-HARMONY
this.root.$emit('imgtap', { this.root.$emit('imgtap', {
...node.attrs ...node.attrs
}) })