Files
lucky_shop/App.vue
ZF sun 7045a05a73 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原型链
2026-07-01 09:39:04 +08:00

419 lines
13 KiB
Vue

<script>
import auth from 'common/js/auth.js';
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
} from 'common/js/wx-jssdk.js';
export default {
mixins: [auth],
data() {
return {
// remote-only 模式下,业务可通过 await getApp().$langReady 等待语言包就绪
$langReady: Promise.resolve({ ok: true, source: 'local' })
};
},
onLaunch: async function(options) {
// ============================================================
// 主题提前加载(关键):让首屏渲染前 CSS 变量已挂上,避免闪一下
// ============================================================
// ---- 0) 同步预热: 立即读 storage 拿 preset,触发 require(不 await)
// 目的: 让 loadTheme 的 require 提前进入 microtask,后续 await 几乎瞬时
const cached = uni.getStorageSync('themeStyle');
let presetName = themeConfig.defaultTheme;
if (cached) {
presetName = (typeof cached === 'object') ? cached.name : cached;
}
// 触发 preset require(不 await,缓存 promise 到 configExternal.loadedConfigs)
configExternal.loadTheme(presetName);
// ---- 1) uniacid 立即写入(后续所有 HTTP 都要用到)
const uniacidChanged = options.query && options.query.uniacid && String(uni.getStorageSync('uniacid') || '') !== String(options.query.uniacid);
if (options.query && options.query.uniacid) {
uni.setStorageSync('uniacid', options.query.uniacid);
}
if (uniacidChanged) {
try { Lang.onUniacidChange(); } catch (e) {}
try { ThemeOverride.onUniacidChange(); } catch (e) {}
try { uni.$emit && uni.$emit('uniacid:changed'); } catch (e) {}
}
// remote-only 模式:
// H5: main.js 已 await boot 完成, 这里秒回
// 小程序/鸿蒙: main.js 同步 $mount(), boot 在这里异步执行
// → boot 期间 showLoading 遮罩, 完成后 hideLoading
const langBoot = Lang.boot();
this.$langReady = langBoot;
let _hideLangLoading = false;
// #ifndef H5
if (Lang.isRemoteOnly && Lang.isRemoteOnly()) {
try {
uni.showLoading({ title: '加载中...', mask: true });
_hideLangLoading = true;
} catch (e) {}
}
// #endif
// ---- 2) 主题应用(同步 await) + 远端拉取(并行 await )
// 关键: 这一步必须 await,否则 page 会在主题挂上之前就渲染
// 内部行为: 第一次 apply 只用 preset(此时 remote 还没拉),远端就绪后第二次 apply 叠加
// 并行把 Lang.boot / ThemeOverride.boot 也拉了,节省总时间
await Promise.all([
// A) 主题第一次 apply — 走 preset,失败静默走默认
this.$store.dispatch('applyThemeWithRemote', presetName)
.catch(e => console.warn('[theme] pre-apply fail', e)),
// B) Lang boot
langBoot.catch(e => console.warn('[i18n] boot fail', e)),
// C) ThemeOverride boot(brand/light/dark 三桶,bootTimeout=2000ms)
ThemeOverride.boot().catch(e => console.warn('[theme] boot fail', e)),
]);
// #ifndef H5
if (_hideLangLoading) {
try { uni.hideLoading(); } catch (e) {}
}
// #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 但页面无视觉变化
await this.$store.dispatch('applyThemeWithRemote', presetName);
// ============================================================
// 后续: 监听器 / 平台条件 / 其它 storage commit(全部同步/轻量)
// ============================================================
// 监听远端翻译更新事件(管理后台改了词条后由后端推送 / 主动 reload)
// 注意:lang.js 内 emitUpdated 已经 commit 了 setComponentRefresh,这里
// 只做副作用(navigation title 等),不重复 forceUpdate
uni.$on && uni.$on('lang:updated', (info) => {
if (info && info.lang) {
Lang.refresh(true); // true = 显式推送 emitUpdated(虽然 loadOverrides 内部已发,这里保险起见)
}
});
// 监听远端主题覆写更新(管理后台改了 token 后,本地 Vuex 重新合并并应用)
// 关键: 走 applyThemeWithRemote action,保留原 setThemeStyle/themeColorSet 链路不动
uni.$on && uni.$on('theme:updated', () => {
// 重新从 storage 读 preset(可能用户在管理页切了预设)
const cur = uni.getStorageSync('themeStyle');
const curName = (cur && typeof cur === 'object') ? cur.name : (cur || 'default');
this.$store.dispatch('applyThemeWithRemote', curName);
});
// 监听 uniacid 切换: 通知主题运行时清旧租户缓存,下次 refresh 拿到新值
uni.$on && uni.$on('uniacid:changed', () => {
try { ThemeOverride.onUniacidChange(); } catch (e) {}
});
// lang:fallback / lang:remote-status / theme:fallback / theme:remote-status 不在 App.vue 监听,留给业务按需订阅
uni.hideTabBar();
// #ifdef MP
if (uni.getUpdateManager) {
const updateManager = uni.getUpdateManager();
updateManager.onCheckForUpdate(function(res) {
// 请求完新版本信息的回调
});
updateManager.onUpdateReady(function(res) {
uni.showModal({
title: Lang.lang('common.updateTip'),
content: Lang.lang('common.updateReady'),
success(res) {
if (res.confirm) {
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
updateManager.applyUpdate();
}
}
});
});
updateManager.onUpdateFailed(function(res) {
// 新的版本下载失败
});
}
// #endif
// #ifdef H5
if (this.$util.getDeviceInfo().platform == 'ios') {
uni.setStorageSync('initUrl', location.href);
}
// #endif
if (uni.onNetworkStatusChange) {
uni.onNetworkStatusChange(function(res) {
if (!res.isConnected) {
uni.showModal({
title: Lang.lang('common.networkLost'),
content: Lang.lang('common.networkCheck'),
showCancel: false
});
}
});
}
// /api/config/init 拉服务器侧数据(包含真正该用的 preset name)
// 内部: success 回调里再次 dispatch('applyThemeWithRemote', serverPreset)
// fire-and-forget: 不阻塞后续同步 commit
this.$store.dispatch('init');
// 插件是否存在
if (uni.getStorageSync('addonIsExist')) {
this.$store.commit('setAddonIsExist', uni.getStorageSync('addonIsExist'));
}
// 默认图
if (uni.getStorageSync('defaultImg')) {
this.$store.commit('setDefaultImg', uni.getStorageSync('defaultImg'));
}
// 站点信息
if (uni.getStorageSync('siteInfo')) {
this.$store.commit('setSiteInfo', uni.getStorageSync('siteInfo'));
}
// 门店配置
if (uni.getStorageSync('globalStoreConfig')) {
this.$store.commit('setGlobalStoreConfig', uni.getStorageSync('globalStoreConfig'));
}
// 门店信息
if (uni.getStorageSync('globalStoreInfo')) {
this.$store.commit('setGlobalStoreInfo', uni.getStorageSync('globalStoreInfo'));
}
// 默认门店信息
if (uni.getStorageSync('defaultStoreInfo')) {
this.$store.commit('setDefaultStoreInfo', uni.getStorageSync('defaultStoreInfo'));
}
// 客服配置
if (uni.getStorageSync('servicerConfig')) {
this.$store.commit('setServicerConfig', uni.getStorageSync('servicerConfig'));
}
// 企业微信配置
if (uni.getStorageSync('wxworkConfig')) {
this.$store.commit('setWxworkConfig', uni.getStorageSync('wxworkConfig'));
}
// 版权信息
if (uni.getStorageSync('copyright')) {
this.$store.commit('setCopyright', uni.getStorageSync('copyright'));
}
// 地址配置
if (uni.getStorageSync('mapConfig')) {
this.$store.commit('setMapConfig', uni.getStorageSync('mapConfig'));
}
if (uni.getStorageSync('token')) {
this.$store.commit('setToken', uni.getStorageSync('token'));
}
// 会员信息
if (uni.getStorageSync('memberInfo')) {
this.$store.commit('setMemberInfo', uni.getStorageSync('memberInfo'));
// 查询购物车信息
this.$store.dispatch('getCartNumber');
}
// #ifdef H5
// 自动授权登录
// 未登录情况下
if (!uni.getStorageSync('memberInfo')) {
this.getAuthInfo();
}
if (this.$store.state.token) {
this.$api.sendRequest({
url: '/api/member/info',
success: (res) => {
if (res.code >= 0) {
this.$store.commit('setMemberInfo', res.data);
}
}
});
}
// #endif
// #ifdef MP-ALIPAY
if (options.query && options.query.m) uni.setStorageSync('source_member', options.query.m);
// #endif
},
onShow: function(options) {
// #ifdef MP
// 自动授权登录
this.getAuthInfo();
if (this.$store.state.token) {
this.$api.sendRequest({
url: '/api/member/info',
success: (res) => {
if (res.code >= 0) {
this.$store.commit('setMemberInfo', res.data);
}
}
});
}
// #endif
// #ifdef MP-ALIPAY
if (options.query && options.query.m) uni.setStorageSync('source_member', options.query.m);
// #endif
},
onHide: function() {},
methods: {
/**
* 获取授权信息
*/
getAuthInfo() {
// #ifdef H5
if (this.$util.isWeiXin()) {
this.$util.getUrlCode(urlParams => {
if (urlParams.source_member) uni.setStorageSync('source_member', urlParams
.source_member);
if (urlParams.code == undefined) {
this.$api.sendRequest({
url: '/wechat/api/wechat/authcode',
data: {
redirect_url: location.href,
scopes: 'snsapi_userinfo'
},
success: res => {
if (res.code >= 0) {
location.href = res.data;
}
}
});
} else {
this.$api.sendRequest({
url: '/wechat/api/wechat/authcodetoopenid',
data: {
code: urlParams.code
},
success: res => {
if (res.code >= 0) {
let data = {};
if (res.data.openid) data.wx_openid = res.data.openid;
if (res.data.unionid) data.wx_unionid = res.data.unionid;
if (res.data.userinfo) Object.assign(data, res.data
.userinfo);
this.authLogin(data);
}
}
});
}
});
}
// #endif
// #ifdef MP
this.getCode(data => {
this.authLogin(data, 'authOnlyLogin');
});
// #endif
},
/**
* 授权登录
*/
authLogin(data, type = 'authLogin') {
if (uni.getStorageSync('source_member')) data.source_member = uni.getStorageSync('source_member');
uni.setStorageSync('authInfo', data);
this.$api.sendRequest({
url: type == 'authLogin' ? '/api/login/auth' : '/api/login/authonlylogin',
data,
success: res => {
if (res.code >= 0) {
this.$store.commit('setToken', res.data.token);
this.getMemberInfo()
this.$store.dispatch('getCartNumber');
}
}
});
},
/**
* 公众号分享设置
*/
shareConfig() {
this.$api.sendRequest({
url: '/wechat/api/wechat/share',
data: {
url: window.location.href
},
success: res => {
if (res.code == 0) {
var wxJS = new Weixin();
wxJS.init(res.data.jssdk_config);
let share_data = JSON.parse(JSON.stringify(res.data.share_config.data));
if (share_data) {
wxJS.setShareData({
title: share_data.title,
desc: share_data.desc,
link: share_data.link,
imgUrl: this.$util.img(share_data.imgUrl)
},
res => {
}
);
}
let hideOptionMenu = res.data.share_config.permission.hideOptionMenu;
let hideMenuItems = res.data.share_config.permission.hideMenuItems;
if (hideOptionMenu) {
wxJS.weixin.hideOptionMenu(); //屏蔽分享好友等按钮
} else {
wxJS.weixin.showOptionMenu(); //放开分享好友等按钮
}
}
},
fail: err => {}
});
},
getMemberInfo() {
this.$api.sendRequest({
url: '/api/member/info',
success: (res) => {
if (res.code >= 0) {
// 登录成功,存储会员信息
this.$store.commit('setMemberInfo', res.data);
}
}
});
}
},
watch: {
$route: {
handler(newName, oldName) {
if (this.$util.isWeiXin()) {
this.shareConfig();
}
},
// 代表在watch里声明了firstName这个方法之后立即先去执行handler方法
immediate: true
}
}
};
</script>
<style lang="scss">
@import './common/css/main.scss';
@import './common/css/iconfont.css';
@import './common/css/icondiy.css'; // 自定义图标库
@import './common/css/icon/extend.css'; // 扩展图标库
page {
background: #f4f6fa;
}
</style>