const H5_STATUS_BAR_QUERY_KEYS = ['statusBarHeight', 'safeTop', 'safeAreaTop', 'appStatusBarHeight']; const H5_MIN_STATUS_BAR_HEIGHT = 54; const H5_MAX_STATUS_BAR_HEIGHT = 200; let currentStatusBarHeight = H5_MIN_STATUS_BAR_HEIGHT; let statusBarStyleObserver = null; // H5 产物运行在 5+ WebView 中时优先交给 App 打开,普通浏览器则在当前页跳转。 export function openExternalUrl(url, source = 'unknown') { const logPrefix = `[openExternalUrl][${source}]`; if (!url) { console.error(logPrefix, '跳转失败:URL 为空'); return false; } const hasGlobalThis = typeof globalThis !== 'undefined'; const plusObject = hasGlobalThis ? globalThis.plus : null; const runtime = plusObject?.runtime; const currentUrl = typeof window !== 'undefined' ? window.location.href : ''; console.log(logPrefix, '开始跳转', { targetUrl: url, currentUrl, userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : '', documentReadyState: typeof document !== 'undefined' ? document.readyState : '', hasWindow: typeof window !== 'undefined', hasGlobalThis, hasPlus: Boolean(plusObject), hasPlusRuntime: Boolean(runtime), hasPlusOpenURL: typeof runtime?.openURL === 'function', }); if (runtime?.openURL) { try { console.log(logPrefix, '命中 5+ WebView,准备调用 plus.runtime.openURL'); runtime.openURL(url, (error) => { console.error(logPrefix, 'plus.runtime.openURL 错误回调', error); }); console.log(logPrefix, 'plus.runtime.openURL 已调用(该 API 无成功回调)'); return true; } catch (error) { console.error(logPrefix, 'plus.runtime.openURL 抛出异常', error); return false; } } if (typeof window !== 'undefined') { try { console.warn(logPrefix, '未检测到 plus.runtime.openURL,回退到 window.location.assign'); window.location.assign(url); console.log(logPrefix, 'window.location.assign 已调用'); window.setTimeout(() => { if (window.location.href === currentUrl) { console.warn(logPrefix, '1.5 秒后页面 URL 未变化,跳转可能被 WebView 拦截'); } }, 1500); return true; } catch (error) { console.error(logPrefix, 'window.location.assign 抛出异常', error); return false; } } console.error(logPrefix, '跳转失败:window 和 plus.runtime 均不可用'); return false; } // 将 URL 参数、宿主全局变量和 postMessage 消息中的高度统一转换为数字。 function normalizeStatusBarHeight(value) { if (value === undefined || value === null || value === '') { return null; } const normalizedValue = typeof value === 'string' ? value.replace(/px$/i, '').trim() : value; const height = Number.parseFloat(normalizedValue); if (!Number.isFinite(height)) { return null; } return Math.max(0, Math.min(height, H5_MAX_STATUS_BAR_HEIGHT)); } // 宿主未提供高度或提供的高度过小时,统一使用 H5 最小状态栏高度。 function resolveStatusBarHeight(value) { const height = normalizeStatusBarHeight(value); return height === null ? H5_MIN_STATUS_BAR_HEIGHT : Math.max(height, H5_MIN_STATUS_BAR_HEIGHT); } function parseMessageData(data) { if (!data) { return null; } if (typeof data === 'string') { try { return JSON.parse(data); } catch (error) { return null; } } return data; } // URL 参数优先级最高,同时兼容普通查询参数和 hash 查询参数。 function readStatusBarHeightFromQuery() { const searchParamsList = []; searchParamsList.push(new URLSearchParams(window.location.search)); const hashQuery = window.location.hash.includes('?') ? window.location.hash.split('?')[1] : ''; if (hashQuery) { searchParamsList.push(new URLSearchParams(hashQuery)); } for (const params of searchParamsList) { for (const key of H5_STATUS_BAR_QUERY_KEYS) { const height = normalizeStatusBarHeight(params.get(key)); if (height !== null) { return height; } } } return null; } // 宿主可以在 H5 应用初始化前将状态栏高度注入 window。 function readStatusBarHeightFromWindow() { const candidates = [ window.__APP_STATUS_BAR_HEIGHT__, window.__UNI_STATUS_BAR_HEIGHT__, window.__APP_SHELL_CONFIG__?.statusBarHeight, window.__UNI_WEBVIEW_CONFIG__?.statusBarHeight, ]; for (const candidate of candidates) { const height = normalizeStatusBarHeight(candidate); if (height !== null) { return height; } } return null; } // 读取页面当前实际使用的 --status-bar-height。 export function getH5StatusBarHeight() { if (typeof window === 'undefined' || typeof document === 'undefined') { return 0; } const statusBarHeight = window .getComputedStyle(document.documentElement) .getPropertyValue('--status-bar-height') .replace(/px$/i, '') .trim(); return resolveStatusBarHeight(statusBarHeight); } // 依次从 URL、宿主全局变量中获取初始高度,均未提供时使用默认最小高度。 export function getH5HostStatusBarHeight() { if (typeof window === 'undefined') { return 0; } return resolveStatusBarHeight(readStatusBarHeightFromQuery() ?? readStatusBarHeightFromWindow()); } // 只在 html 上设置一个状态栏变量,body 和其他子元素会自动继承。 export function applyH5HostStatusBarHeight(height) { if (typeof window === 'undefined' || typeof document === 'undefined') { return 0; } const statusBarHeight = resolveStatusBarHeight(height); currentStatusBarHeight = statusBarHeight; document.documentElement.style.setProperty('--status-bar-height', `${statusBarHeight}px`, 'important'); // 通知使用数值高度参与 JavaScript 布局计算的页面重新计算。 window.dispatchEvent( new CustomEvent('app-shell-status-bar-change', { detail: { height: statusBarHeight }, }), ); return statusBarHeight; } // uni-app 重写 html 样式后,如果状态栏高度小于 22px,则立即恢复当前有效高度。 function observeStatusBarHeight() { if (statusBarStyleObserver || typeof MutationObserver === 'undefined') { return; } statusBarStyleObserver = new MutationObserver(() => { const statusBarHeight = normalizeStatusBarHeight( document.documentElement.style.getPropertyValue('--status-bar-height'), ); if (statusBarHeight === null || statusBarHeight < H5_MIN_STATUS_BAR_HEIGHT) { applyH5HostStatusBarHeight(currentStatusBarHeight); } }); statusBarStyleObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['style'], }); } // 解析宿主运行期间通过 postMessage 发送的状态栏高度。 function resolveMessageStatusBarHeight(message) { const data = parseMessageData(message?.data); if (!data) { return null; } return normalizeStatusBarHeight( data.statusBarHeight ?? data.safeTop ?? data.safeAreaTop ?? data.payload?.statusBarHeight ?? data.payload?.safeTop, ); } // 初始化状态栏高度,并支持宿主通过全局方法或 postMessage 动态更新。 export function initH5HostShell() { if (typeof window === 'undefined') { return 0; } const safeTop = applyH5HostStatusBarHeight(getH5HostStatusBarHeight()); if (window.__FRIENDSHIP_H5_SHELL_READY__) { return safeTop; } window.__FRIENDSHIP_H5_SHELL_READY__ = true; observeStatusBarHeight(); window.setAppShellStatusBarHeight = (nextHeight) => applyH5HostStatusBarHeight(nextHeight); window.addEventListener('message', (message) => { const nextHeight = resolveMessageStatusBarHeight(message); if (nextHeight !== null) { applyH5HostStatusBarHeight(nextHeight); } }); return safeTop; }