Files
lucky_shop/App.vue
ZF sun c86e688e47 refactor(privacy&lang): optimize privacy check and language bundle loading
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
2026-07-02 16:03:04 +08:00

420 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 splashState from 'common/js/splash-state.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 完成, 这里秒回 (splash 也由 main.js 全权负责)
// 小程序/鸿蒙: main.js 同步 $mount(), boot 在这里异步执行
// → splashState.show() 同步设置 visible 标志, 框架推页面后 splash 组件挂载时读取 isShow() 渲染
const langBoot = Lang.boot();
this.$langReady = langBoot;
// #ifndef H5
const _needSplash = Lang.isRemoteOnly && Lang.isRemoteOnly();
if (_needSplash) {
splashState.show();
}
// #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 (_needSplash) {
splashState.hide({ reason: 'boot-complete' });
}
// #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>