/** * 终端判断:手机 / 微信优先进 H5 客户中心 * * 覆盖: * - `?desktop=1` / `?forcePc=1`:强制网页端(调试用) * - `?h5=1`:强制 H5 * - sessionStorage `oms_force_desktop` / `oms_force_h5`:同会话记忆 */ const FORCE_DESKTOP_KEY = 'oms_force_desktop'; const FORCE_H5_KEY = 'oms_force_h5'; const MOBILE_UA_RE = /Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|Windows Phone|HarmonyOS|OpenHarmony/i; /** 微信内这些路径用 replace,避免历史栈叠高触发底部 ← → */ const WECHAT_SPA_PATH_RE = /^\/(h5|wechat|register|track\/query)(\/|$)/; type WeixinJSBridgeLike = { call: (name: string, ...args: unknown[]) => void; }; declare global { interface Window { WeixinJSBridge?: WeixinJSBridgeLike; } } /** iPadOS 13+ 桌面 UA 伪装,用触控点辅助识别 */ function isIpadDesktopUa(ua: string): boolean { return /Macintosh/i.test(ua) && typeof navigator !== 'undefined' && navigator.maxTouchPoints > 1; } export function isWeChatBrowser(ua = typeof navigator !== 'undefined' ? navigator.userAgent : ''): boolean { return /MicroMessenger/i.test(ua); } export function isMobileUserAgent(ua = typeof navigator !== 'undefined' ? navigator.userAgent : ''): boolean { if (!ua) return false; if (MOBILE_UA_RE.test(ua)) return true; if (isIpadDesktopUa(ua)) return true; return false; } function readQueryFlag(key: string): string | null { if (typeof window === 'undefined') return null; try { return new URLSearchParams(window.location.search).get(key); } catch { return null; } } function persistForceFlagsFromQuery() { if (typeof sessionStorage === 'undefined') return; const desktop = readQueryFlag('desktop') ?? readQueryFlag('forcePc'); const h5 = readQueryFlag('h5'); if (desktop === '1') { sessionStorage.setItem(FORCE_DESKTOP_KEY, '1'); sessionStorage.removeItem(FORCE_H5_KEY); } if (h5 === '1') { sessionStorage.setItem(FORCE_H5_KEY, '1'); sessionStorage.removeItem(FORCE_DESKTOP_KEY); } } /** * 客户门户是否优先走 H5(手机 / 微信) * 管理端登录不受此影响 */ export function preferH5Portal(): boolean { if (typeof window === 'undefined') return false; persistForceFlagsFromQuery(); try { if (sessionStorage.getItem(FORCE_DESKTOP_KEY) === '1') return false; if (sessionStorage.getItem(FORCE_H5_KEY) === '1') return true; } catch { // ignore } const qDesktop = readQueryFlag('desktop') ?? readQueryFlag('forcePc'); if (qDesktop === '1') return false; if (readQueryFlag('h5') === '1') return true; const ua = navigator.userAgent || ''; if (isWeChatBrowser(ua) || isMobileUserAgent(ua)) return true; // 窄屏兜底(部分内嵌 WebView UA 不标准) try { if (window.matchMedia('(max-width: 768px)').matches) return true; } catch { // ignore } return false; } function resolveRoutePath(to: unknown): string { if (typeof to === 'string') { return to.split('?')[0].split('#')[0] || ''; } if (to && typeof to === 'object' && 'path' in to) { const path = String((to as { path?: unknown }).path || ''); return path.split('?')[0].split('#')[0] || ''; } return ''; } export function isWeChatSpaPath(path: string): boolean { return WECHAT_SPA_PATH_RE.test(path || ''); } /** * 尝试隐藏微信内置工具栏(老版本微信 / 部分安卓、旧 iOS 仍会显示底部 ← →)。 * 新版微信多数已无此栏,调用无副作用。 */ export function hideWeChatBrowserNav(): void { if (typeof window === 'undefined' || !isWeChatBrowser()) return; const invoke = () => { try { window.WeixinJSBridge?.call('hideToolbar'); } catch { // ignore } }; if (window.WeixinJSBridge) { invoke(); return; } document.addEventListener('WeixinJSBridgeReady', invoke, { once: true }); } /** * H5 站内跳转:微信内用 replace,避免历史栈变长触发底部左右箭头栏。 * 非微信仍可用 push,保留系统返回。 */ export function h5Navigate( router: { push: (to: unknown) => unknown; replace: (to: unknown) => unknown }, to: unknown, ): unknown { if (isWeChatBrowser()) { return router.replace(to); } return router.push(to); } let weChatHistoryGuardInstalled = false; /** * 微信内将 H5/注册相关 router.push 统一改为 replace。 * 覆盖漏网的 push(顾问跳转、登录后跳转等),从根上压住历史栈。 */ export function installWeChatH5HistoryGuard(router: { push: (to: unknown) => unknown; replace: (to: unknown) => unknown; }): void { if (weChatHistoryGuardInstalled || typeof window === 'undefined' || !isWeChatBrowser()) { return; } weChatHistoryGuardInstalled = true; const originalPush = router.push.bind(router); router.push = ((to: unknown) => { const path = resolveRoutePath(to); if (isWeChatSpaPath(path) || isWeChatSpaPath(window.location.pathname)) { return router.replace(to); } return originalPush(to); }) as typeof router.push; hideWeChatBrowserNav(); // 部分机型 Bridge 就绪较晚,再补一次 window.setTimeout(hideWeChatBrowserNav, 800); window.setTimeout(hideWeChatBrowserNav, 2000); }