问题 1:五个导航页顶部与右上角胶囊按钮(··· ⊙)重合 - 根因:自定义导航用 env(safe-area-inset-top) 取的是设备物理安全区, 在 Android 上常为 0,完全不反映胶囊位置;且右对齐元素从未为胶囊让出宽度。 - 新增 miniapp/utils/nav-metrics.js 作为唯一事实源:以 wx.getMenuButtonBoundingClientRect() 实测胶囊矩形,反推并导出四个 CSS 变量 --nav-status / --nav-h / --nav-right / --nav-top,并提供无接口时的兜底值。 - nav-bar 组件改为自行获取导航度量(attached + observer 双触发), 标题按胶囊垂直居中并收窄 max-width;外部传参降级为兜底。 - index / discover / messages / activity / mine 五页根结点注入同一组变量, WXSS 里的 env(safe-area-inset-top)、硬编码 88rpx 与右对齐 right:0 全量替换。 问题 2:首页「善缘公益服务」「善缘安全提醒」两个栏目过宽 - 根因:封面 <image> 只声明 width 未声明 height,小程序默认按 240px 计算, 实际把卡片撑到约 480rpx 高。 - 按「整体等比缩到 1/3」重构:两栏改为 mini-card 小卡,宽度 calc((100% - 2*gap)/3)、高度写死 168rpx,封面 height:100%; 公益服务一行三卡横向排布,安全提醒为「标题小卡 + 右侧文案条」, 长文案收进详情页,安全提示原文保留。 守卫与验证 - 新增 ui-tests/miniapp-capsule-safe-contracts.mjs: A 层多机型断言导航行与胶囊垂直居中、右对齐让位、navTop 与兜底值; B 层纯函数审计五个自带导航行页面(页面清单由文件系统推导,失配直接报红); C 层断言首页小卡宽为 1/3 且显式声明高度,并钉住旧 service-/safety- 选择器回归。 审计前剥离注释,避免守卫被自身示例文本误伤。 - 新增 scripts/audit-image-height.mjs:按属性名精确比对,审计全仓 「有 width 无 height」的 <image>,当前 30 个类 0 命中。 - 负向注入 N1~N7(含 nav-safe 未接入、旧选择器回归、极小屏幕、无胶囊兜底等) 全部被守卫咬住并 exit 1,还原后复跑通过。 - 真编译(wcc/wcsc):WXSS 88 / WXML 86,失败 0; tsc --noEmit 通过;UI 契约门控 21/21 PASS(新增第 21 个守卫并抬高 MIN_GUARDS)。 - 渲染探针实测:单卡 213rpx x 168rpx;五页右对齐元素右缘 423~528px, 均在胶囊右缘 365px 之外。 未纳入本次提交:miniapp/project.config.json(微信开发者工具自动改写, 仅追加若干布尔开关并删掉文件末换行)与生成的预览页 artifacts preview.html (约 7900 行生成产物),以免淹没代码评审视线。
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,
|
||
};
|