1. add global i18n fallback toast on lang:fallback event, only show once per process 2. add getCachedBundleAny to retrieve expired cache for boot fallback 3. add cache fallback logic when remote i18n bundle load fails
440 lines
14 KiB
Vue
440 lines
14 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) {}
|
|
});
|
|
// 监听国际化远端拉取失败: 给用户英文 toast 提示
|
|
// 触发时机: lang.js setRemoteStatus('fail', ...) 时
|
|
// 设计: 用 setRemoteStatus 内部归一化后的 reason 决定文案
|
|
// - 'timeout' / 'network' / 'http' / 'parse' / 'exception' 都弹同一句英文说明
|
|
// - 'unknown' 不弹(可能是业务主动设置,不是真失败)
|
|
// 防刷: 同一进程内只弹一次(避免重试时反复刷屏)
|
|
if (!globalThis.__i18nFallbackToastShown) {
|
|
uni.$on && uni.$on('lang:fallback', (detail) => {
|
|
const r = detail && detail.reason;
|
|
if (r === 'unknown' || r === undefined || r === null) return;
|
|
if (globalThis.__i18nFallbackToastShown) return;
|
|
globalThis.__i18nFallbackToastShown = true;
|
|
try {
|
|
uni.showToast({
|
|
title: 'Failed to load i18n. Please check your network and try again.',
|
|
icon: 'none',
|
|
duration: 4000,
|
|
});
|
|
} catch (e) {}
|
|
});
|
|
}
|
|
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> |