Files
lucky_shop/main.js
ZF sun 94142268a0 refactor(i18n): 完成全量硬编码中文文案替换为多语言方案
1.  替换所有页面/组件中的硬编码中文提示文案为$lang调用,包括支付、帮助中心、公告等页面
2.  重构diy.js加载失败文案处理逻辑,支持参数化替换
3.  新增多语言混入逻辑,实现导航标题随语种动态切换
4.  重构支付组件初始化逻辑,优化平台适配代码
5.  添加多语言单元测试和硬编码扫描脚本
6.  更新语言包,新增缺失的翻译词条
2026-07-23 09:46:16 +08:00

245 lines
9.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// #ifdef H5
import './common/js/pc'
// #endif
import Vue from 'vue'
import App from './App'
import store from './store'
import Util from './common/js/util.js'
import Http from './common/js/http.js'
import Lang from './common/js/lang.js'
import Config from './common/js/config.js'
import EventBus from './common/js/event-bus.js'
import DomEventBridge from './common/js/dom-event-bridge.js'
import globalConfig from './common/js/golbalConfig.js';
import {
uniStorage
} from './common/js/storage.js'
import configExternal from './common/js/config-external.js'
import PrivacyManager from './common/js/privacy.js'
import splashState from './common/js/splash-state.js'
import ThemeOverride from './common/js/theme-override.js'
Vue.prototype.$store = store //挂在vue
Vue.config.productionTip = false
Vue.prototype.$util = Util;
Vue.prototype.$api = Http;
Vue.prototype.$langConfig = Lang; //语言包对象
Vue.prototype.$lang = Lang.lang; //解析语言包
Vue.prototype.$config = Config;
Vue.prototype.$privacy = PrivacyManager; //全局隐私管理器
// 调试: 暴露 PrivacyManager 到 window, 便于 e2e 测试 / 调试 / 控制台排查
// 仅 H5 + dev mode 暴露, 生产构建会因条件编译 + webpack DefinePlugin 消除
// #ifdef H5
if (typeof window !== 'undefined') {
window.__privacy__ = PrivacyManager
// 隐私调试日志开关 (H5 dev only)
// 设为 false 关闭 privacy 内部 console.log
// 设 true: 走 verbose 日志 (reEvaluate 触发/调用, rules-updated 等)
// 注: 不在条件编译 H5 内, 因为微信/鸿蒙也可启用
try {
if (typeof globalThis !== 'undefined' && globalThis.__PRIVACY_DEBUG__ === undefined) {
globalThis.__PRIVACY_DEBUG__ = true // dev 默认开, 生产可通过 DefinePlugin 关闭
}
} catch (e) { }
}
// #endif
// #ifdef H5
EventBus.setDomBridge(DomEventBridge)
// #endif
Vue.prototype.$eventBus = EventBus;
Vue.mixin(globalConfig);
// #ifdef H5
// 全局接管下拉刷新:原生快应用宿主内通过 notifyNativeHost('reload') 转发,
// 其余平台只走 uni.stopPullDownRefresh 兜底,不会影响各页面原 onPullDownRefresh 的其他逻辑
// 全局 mixin:下拉刷新 → 在快应用宿主内转发原生 webview.reload()
import quickappPullReload from './common/js/quickapp-pull-refresh.js'
Vue.mixin(quickappPullReload);
// #endif
// #ifdef MP-WEIXIN
// 全局兜底分享 mixin:
// 为所有未显式声明 onShareAppMessage / onShareTimeline 的页面提供默认实现,
// 保证"全页面都支持转发 / 朋友圈 / 复制路径"。
// - 已显式声明的页面优先级更高(uni-app 规范),不受影响
// - 后端 /weapp/api/weapp/share 提供标题 / 封面,失败时退化到站点名 + 当前路径
import MpShareDefault from './common/js/mp-share-default.js';
Vue.mixin(MpShareDefault);
// #endif
// 远端 i18n 翻译热更新:复用项目原有"componentRefresh"机制(同 diy.js 下拉刷新)
// lang.js 在翻译覆写更新时 commit('setComponentRefresh'),
// golbalConfig.js 已把 componentRefresh 注册为全局 computed,
// 在此 watch 它并 $forceUpdate,触发所有页面内 {{$lang('xxx')}} 重新求值
// 同时调用 lang.refresh() 让当前页面的 navigationBarTitleText 跟随语种变化
// (pages.json 里的硬编码中文标题需要被 $lang('title') 覆盖)
Vue.mixin({
watch: {
componentRefresh() {
this.$forceUpdate();
try {
if (this.$langConfig && typeof this.$langConfig.refresh === 'function') {
// emit=false: lang.js 已 emit 过,这里避免重复 store.commit
this.$langConfig.refresh(false);
}
} catch (e) { /* 容错:refresh 失败不影响主流程 */ }
}
}
});
// 鸿蒙元服务环境: 全局接管 onTabItemTap, 兜底系统 TabBar 点击
// 背景: 鸿蒙下 uni.switchTab 对系统 TabBar 点击静默失败
// 通过全局 mixin 覆盖所有未显式定义 onTabItemTap 的页面(子包动态 tab 页等),
// 显式定义 onTabItemTap 的页面(如 pages/empty/index.vue、index.js)优先级高于此 mixin
// #ifdef MP-HARMONY
Vue.mixin({
onTabItemTap(e) {
console.log('[TabBar][HARMONY] global onTabItemTap fired:', e);
if (!e || !e.pagePath) return;
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const currentPath = currentPage ? '/' + currentPage.route : '';
const targetPath = e.pagePath.startsWith('/') ? e.pagePath : '/' + e.pagePath;
console.log('[TabBar][HARMONY] onTabItemTap currentPath=', currentPath, 'targetPath=', targetPath);
// 已经在目标页则不重复跳转
if (currentPath === targetPath) return;
// 用干净路径(不带 query), 避免 HarmonyOS 对带 query 的 tabBar 跳转有兼容性问题
uni.reLaunch({
url: targetPath,
success: () => console.log('[TabBar][HARMONY] global reLaunch success:', targetPath),
fail: (err) => console.error('[TabBar][HARMONY] global reLaunch fail:', targetPath, err)
});
}
});
// #endif
App.mpType = 'app';
// 重写存储,增加前缀
uniStorage();
// 组件已改为按需引入,不再全局注册
// ============================================================
// 平台差异处理:
// H5: 可在 $mount() 前 await boot, 首屏直接有翻译
// 小程序/鸿蒙: App() 必须同步注册, $mount() 不能推迟
// → 同步 $mount() + lang() 短路返回空串 + boot 完成后 $forceUpdate
//
// 以下代码所有平台共享:
// - lang.js: bootCompleted 标志, boot 未完成时 lang() 返回空串
// - http.js: sendRequest 门控, boot 未完成时业务请求排队
// - splash-state.js: 统一启动屏状态 (H5 降级 uni.showLoading, 小程序/鸿蒙走 splash 组件)
// - App.vue: onLaunch 中 splashState.show() + await boot + splashState.hide()
// ============================================================
// #ifdef H5
// H5: 先 await boot 再 $mount(), 首屏直接有翻译数据
// splashState.show() 内部 H5 降级为 uni.showLoading (await 期间 Vue 还没挂载,组件层无法渲染)
// 关键: 无条件 show (不依赖 Lang.isRemoteOnly), 任何模式下都让 uni.showLoading 兜底
// - local 模式: Lang.boot() sync, splash 立即 hide, 用户几乎看不到
// - remote-only 模式: Lang.boot() 远端拉取, splash 显示整个 boot 期
async function bootstrap() {
if (Lang.isRemoteOnly && Lang.isRemoteOnly()) {
splashState.show();
try {
// 1) i18n: 拉远端语言包 (langConfig.source === 'remote-only')
await Lang.boot();
// 2) theme: 阻塞式预热 brand 桶覆写 (themeConfig.overrideRemote.source)
await ThemeOverride.boot();
// 3) theme(remote-only): 异步拉全量 8 个 preset 替换 stub
// 失败静默, 兜底用 webpack stub(只含 default),首屏不白屏
ThemeOverride.loadAllPresets().catch((e) => {
console.warn('[theme-presets] load fail', e);
});
} catch (e) {
console.warn('[i18n] boot fail', e);
} finally {
splashState.hide({ reason: 'boot-complete' });
}
}
const app = new Vue({
...App,
store
});
app.$mount();
setupResizeListener();
}
bootstrap()
// #endif
// #ifndef H5
// 小程序/鸿蒙: 同步 $mount(), boot 在 App.vue onLaunch 中异步执行
// lang() 在 boot 未完成时返回空串, boot 完成后 emitUpdated 触发 $forceUpdate
//
// 关键: splashState.show() 必须**最早期**同步触发 (这里, 不在 App.vue onLaunch)
// 原因: onLaunch 到 step 1.5 splashState.show() 之前, 用户看到的是白屏
// splash 状态机只需要 'visible=true', 实际渲染等组件 mount
splashState.show();
const app = new Vue({
...App,
store
});
app.$mount();
// #endif
// ============================================================
// 窗口尺寸变化监听
// ------------------------------------------------------------
// 背景: themeColorSet 会把 token 中的 rpx 转 px,转比例基于 window.innerWidth/750
// 缓存 key 已含 window.innerWidth,所以 cacheKey 不匹配时会自动重算;
// 但需要 resize 事件触发一次 themeColorSet 才会让 cacheKey 重新计算。
//
// 兼容点: 不同发布产物的 resize 来源不一样
// 1. 优先用 uni.onWindowResize — uni-app 跨端 API, H5 / App-Plus 支持
// 2. 兜底用 window.addEventListener('resize') — 纯 H5 产物中直接可用
// 3. 全部 try-catch,某条路径失败不影响其他路径
// 4. 用 200ms 去抖,避免拖动窗口时频繁触发(实测连续 resize 事件可达 60fps)
//
// 调用时机: 仅 H5 端在 $mount() 之后调用;小程序/鸿蒙端
// - 微信小程序: 屏幕尺寸由设备决定,运行时不会变化,无需监听
// - 鸿蒙: 待验证,目前不在这里注册以避免在不支持的环境下抛错
// ============================================================
function setupResizeListener() {
let resizeTimer = null;
const fire = () => {
if (resizeTimer) clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
resizeTimer = null;
try {
store.dispatch('themeColorSet');
} catch (e) {
console.warn('[theme] resize -> themeColorSet fail', e);
}
}, 200);
};
// 路径 1: uni 跨端 API(H5 / App-Plus 上有效)
try {
if (typeof uni !== 'undefined' && typeof uni.onWindowResize === 'function') {
uni.onWindowResize(fire);
}
} catch (e) {
console.warn('[theme] uni.onWindowResize bind fail', e);
}
// 路径 2: 原生 window resize 事件(H5 兜底,某些自定义 H5 产物可能没装 uni)
try {
if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
window.addEventListener('resize', fire);
}
} catch (e) {
console.warn('[theme] window.resize bind fail', e);
}
}