// #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 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')}} 重新求值 Vue.mixin({ watch: { componentRefresh() { this.$forceUpdate(); } } }); // 鸿蒙元服务环境: 全局接管 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); } }