front/src/utils/device.ts
zzh bd97ed76fd feat: ?????????????????/???????
??????????????????????????????? H5/?????

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-30 15:16:55 +08:00

182 lines
5.1 KiB
TypeScript
Raw 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.

/**
* 终端判断:手机 / 微信优先进 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);
}