/** * 顶部安全区几何(状态栏 + 右上角胶囊按钮)——全站唯一事实源 * * 为什么需要它(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()) } }) * * * 单位一律 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, };