160 lines
6.2 KiB
JavaScript
160 lines
6.2 KiB
JavaScript
/**
|
||||
|
|
* 顶部安全区几何(状态栏 + 右上角胶囊按钮)——全站唯一事实源
|
|||
|
|
*
|
|||
|
|
* 为什么需要它(2026-09-23 真实缺陷):
|
|||
|
|
* 五个自定义导航页(index / discover / messages / activity / mine)此前用
|
|||
|
|
* `padding-top: env(safe-area-inset-top)` 顶开状态栏。这个值在微信里**只反映设备的
|
|||
|
|
* 物理安全区**(刘海/挖孔),且安卓上常为 0,更关键的是它**完全不反映右上角胶囊按钮
|
|||
|
|
* (「••• ⊙」)的位置**。于是出现两类重合:
|
|||
|
|
* ① 竖向:导航行落在胶囊所在的横带里(消息页四个快捷入口、活动页分类胶囊被压住);
|
|||
|
|
* ② 横向:右对齐元素(首页「消息」、找对象页搜索、活动页日历、我的页「编辑资料」、
|
|||
|
|
* 聊天页「⋯」)直接压在胶囊下面,点不到。
|
|||
|
|
* 正确做法是用 `wx.getMenuButtonBoundingClientRect()` 实测胶囊矩形,反推:
|
|||
|
|
* - navBarHeight:与胶囊同高并垂直居中的导航行高度 = 胶囊高 + 上下间距 ×2
|
|||
|
|
* - reserve :屏幕右边缘到胶囊左边缘的距离 + 余量,即右对齐元素必须让出的宽度
|
|||
|
|
* - navTop :状态栏 + 导航行高度,即页面内容「可以开始」的 y 坐标
|
|||
|
|
*
|
|||
|
|
* 用法(页面):
|
|||
|
|
* const { navSafeData } = require('../../utils/nav-metrics')
|
|||
|
|
* Page({ data: { navVars: '' }, onLoad() { this.setData(navSafeData()) } })
|
|||
|
|
* <view class="page" style="{{navVars}}"> <!-- wxss 里用 var(--nav-top) 等 -->
|
|||
|
|
*
|
|||
|
|
* 单位一律 px(inline style 与 rpx 混用会算错),rpx 换算交给 wxss 侧的 var()。
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
/** 胶囊左侧再留出的余量,避免右对齐文字与胶囊贴边(px) */
|
|||
|
|
var CAPSULE_GAP_PX = 8;
|
|||
|
|
/** 导航行高度合理区间:过小会压字,过大会把内容推太远(px) */
|
|||
|
|
var MIN_NAV_BAR_HEIGHT = 40;
|
|||
|
|
var MAX_NAV_BAR_HEIGHT = 56;
|
|||
|
|
/** 拿不到胶囊信息时的兜底(按带刘海的 iPhone 取,宁可多留不可少留) */
|
|||
|
|
var FALLBACK = {
|
|||
|
|
windowWidth: 375,
|
|||
|
|
statusBarHeight: 20,
|
|||
|
|
navBarHeight: 44,
|
|||
|
|
reserve: 102,
|
|||
|
|
navTop: 64,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
var cache = null;
|
|||
|
|
|
|||
|
|
function toPositiveNumber(value, fallback) {
|
|||
|
|
var n = Number(value);
|
|||
|
|
if (!isFinite(n) || n <= 0) return fallback;
|
|||
|
|
return n;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function clamp(value, min, max) {
|
|||
|
|
if (value < min) return min;
|
|||
|
|
if (value > max) return max;
|
|||
|
|
return value;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 纯函数:只做几何计算,不碰 wx。便于在 Node 侧用多机型样本做断言。
|
|||
|
|
* @param {{windowWidth?:number, statusBarHeight?:number, capsule?:{top:number,left:number,width:number,height:number,right?:number}}} input
|
|||
|
|
*/
|
|||
|
|
function computeNavMetrics(input) {
|
|||
|
|
var source = input || {};
|
|||
|
|
var windowWidth = toPositiveNumber(source.windowWidth, FALLBACK.windowWidth);
|
|||
|
|
var statusBarHeight = toPositiveNumber(source.statusBarHeight, FALLBACK.statusBarHeight);
|
|||
|
|
var capsule = source.capsule;
|
|||
|
|
var hasCapsule = !!(capsule && Number(capsule.width) > 0 && Number(capsule.height) > 0);
|
|||
|
|
var navBarHeight = FALLBACK.navBarHeight;
|
|||
|
|
var reserve = FALLBACK.reserve;
|
|||
|
|
if (hasCapsule) {
|
|||
|
|
// 胶囊在状态栏下方通常还有一点间距,导航行取「胶囊高 + 上下对称间距」,视觉上正好居中对齐
|
|||
|
|
var gap = Math.max(Number(capsule.top || 0) - statusBarHeight, 0);
|
|||
|
|
navBarHeight = clamp(
|
|||
|
|
Number(capsule.height) + gap * 2,
|
|||
|
|
MIN_NAV_BAR_HEIGHT,
|
|||
|
|
MAX_NAV_BAR_HEIGHT,
|
|||
|
|
);
|
|||
|
|
// 右对齐元素要整体避开胶囊:从右边缘到胶囊左边 + 余量,且不超过半屏(避免极端机型把内容挤没)
|
|||
|
|
reserve = clamp(windowWidth - Number(capsule.left || 0) + CAPSULE_GAP_PX, 0, windowWidth * 0.5);
|
|||
|
|
}
|
|||
|
|
navBarHeight = Math.round(navBarHeight);
|
|||
|
|
reserve = Math.round(reserve);
|
|||
|
|
return {
|
|||
|
|
windowWidth: windowWidth,
|
|||
|
|
statusBarHeight: Math.round(statusBarHeight),
|
|||
|
|
navBarHeight: navBarHeight,
|
|||
|
|
reserve: reserve,
|
|||
|
|
navTop: Math.round(statusBarHeight) + navBarHeight,
|
|||
|
|
source: hasCapsule ? 'capsule' : 'fallback',
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function readWindowInfo() {
|
|||
|
|
try {
|
|||
|
|
if (typeof wx !== 'undefined' && typeof wx.getWindowInfo === 'function') {
|
|||
|
|
return wx.getWindowInfo() || {};
|
|||
|
|
}
|
|||
|
|
} catch (e) { /* 低版本基础库无此接口 */ }
|
|||
|
|
try {
|
|||
|
|
if (typeof wx !== 'undefined' && typeof wx.getSystemInfoSync === 'function') {
|
|||
|
|
return wx.getSystemInfoSync() || {};
|
|||
|
|
}
|
|||
|
|
} catch (e) { /* 接口异常时走兜底 */ }
|
|||
|
|
return {};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function readCapsuleRect() {
|
|||
|
|
try {
|
|||
|
|
if (typeof wx !== 'undefined' && typeof wx.getMenuButtonBoundingClientRect === 'function') {
|
|||
|
|
return wx.getMenuButtonBoundingClientRect();
|
|||
|
|
}
|
|||
|
|
} catch (e) { /* 同上 */ }
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 带缓存的取值:同一进程内胶囊几何不会变,避免每页 onLoad 重复调用系统接口。
|
|||
|
|
* @param {boolean} [force] 强制重新读取(机型/窗口变化时可用)
|
|||
|
|
*/
|
|||
|
|
function getNavMetrics(force) {
|
|||
|
|
if (cache && !force) return cache;
|
|||
|
|
var win = readWindowInfo();
|
|||
|
|
cache = computeNavMetrics({
|
|||
|
|
windowWidth: win.windowWidth,
|
|||
|
|
statusBarHeight: win.statusBarHeight,
|
|||
|
|
capsule: readCapsuleRect(),
|
|||
|
|
});
|
|||
|
|
return cache;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 页面 data 片段:同时给出 CSS 变量串(给 wxml 根节点 inline style)与 px 数值。
|
|||
|
|
* wxss 侧约定(四个变量各司其职,别混用):
|
|||
|
|
* --nav-status 状态栏高度:把「导航行」顶到胶囊所在横带的起点
|
|||
|
|
* --nav-h 导航行高度:与胶囊垂直居中对齐,导航行高就写它
|
|||
|
|
* --nav-right 右对齐元素必须让出的宽度(胶囊安全区)
|
|||
|
|
* --nav-top 状态栏 + 导航行 = 内容真正可以开始的 y(导航行之后的第一个区块用)
|
|||
|
|
*/
|
|||
|
|
function navSafeData(force) {
|
|||
|
|
var m = getNavMetrics(force);
|
|||
|
|
return {
|
|||
|
|
navVars: '--nav-status:' + m.statusBarHeight + 'px;'
|
|||
|
|
+ '--nav-h:' + m.navBarHeight + 'px;'
|
|||
|
|
+ '--nav-right:' + m.reserve + 'px;'
|
|||
|
|
+ '--nav-top:' + m.navTop + 'px;',
|
|||
|
|
navTopPx: m.navTop,
|
|||
|
|
navStatusPx: m.statusBarHeight,
|
|||
|
|
navBarHeightPx: m.navBarHeight,
|
|||
|
|
capsuleReservePx: m.reserve,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 仅测试用:清空缓存 */
|
|||
|
|
function clearNavMetricsCache() {
|
|||
|
|
cache = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = {
|
|||
|
|
computeNavMetrics: computeNavMetrics,
|
|||
|
|
getNavMetrics: getNavMetrics,
|
|||
|
|
navSafeData: navSafeData,
|
|||
|
|
clearNavMetricsCache: clearNavMetricsCache,
|
|||
|
|
FALLBACK: FALLBACK,
|
|||
|
|
};
|