??????????????????????????????? H5/????? Co-authored-by: Cursor <cursoragent@cursor.com>
182 lines
5.1 KiB
TypeScript
182 lines
5.1 KiB
TypeScript
/**
|
||
* 终端判断:手机 / 微信优先进 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);
|
||
}
|