234 lines
7.7 KiB
JavaScript
234 lines
7.7 KiB
JavaScript
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;
|
|
}
|