fix(mobile): 手机弹窗高度超出屏幕——高度钳制到可视屏高(2026-09-08用户报障)

根因(两层):
1. popup.js positify_tl 用 body rect 高算内联 px 高:手机上文档滚动后
   body 高于可视屏,百分比换算随之超屏;
2. CSS 100vh 在手机浏览器是 large viewport(含地址栏遮挡区)>innerHeight,
   且只有 .popupwindow 被钳制,普通 .popup(消息/下拉等弹出控件)无钳制。

修法(框架层,应用零改动):
- popup.js 新增 _mobile_clamp():手机视口下以 window.innerHeight 实测
  钳制——PopupWindow 恒等于可视高-TabBar实测高(setProperty important
  覆盖CSS !important,不依赖dvh支持,老微信X5兜底);普通 Popup 仅超屏时
  钳高并回拉 top。positify_tl/popup_from_widget/stop_moving 各出口接入;
- visualViewport+window resize 监听:转屏/软键盘弹出收起时重钳(100ms
  防抖),destroy 时清理防泄漏;
- bricks.css 手机媒体查询:.popup:not(.popupwindow) 补 max-height
  100vh/dvh 钳制+overflow-y:auto;@supports dvh 优先(随地址栏收缩),
  JS 实测内联 important 是权威,CSS 只是首帧静态兜底。
This commit is contained in:
yumoqing 2026-09-09 10:39:54 +08:00
parent 5648da13c7
commit 479cc3ba05

View File

@ -67,6 +67,21 @@ bricks.Popup = class extends bricks.VBox {
this.set_style('display', 'none');
bricks.Body.add_widget(this);
this.bind('click', this.bring_to_top.bind(this));
// 可视高度变化(转屏/软键盘弹出收起/桌面窗口缩进手机视口)时重钳弹窗高度。
// visualViewport 优先(软键盘会触发它的 resizewindow.resize 不一定触发)。
// 无条件注册:构造时可能是桌面视口,之后缩窄进手机模式也要生效;
// _mobile_clamp 内部自带 is_mobile() 判断,桌面模式是空操作。
{
var vv = window.visualViewport;
var on_vv_resize = () => {
if (!this.opened || !this.dom_element || !this.dom_element.isConnected) return;
if (this._clamp_task) clearTimeout(this._clamp_task);
this._clamp_task = setTimeout(() => { this._clamp_task = null; this._mobile_clamp(); }, 100);
};
if (vv && vv.addEventListener) vv.addEventListener('resize', on_vv_resize);
window.addEventListener('resize', on_vv_resize);
this._vv_resize_bound = on_vv_resize;
}
if (this.auto_open){
this.open();
}
@ -127,6 +142,7 @@ bricks.Popup = class extends bricks.VBox {
}
this.set_style('top', y + 'px');
this.set_style('left', x + 'px');
this._mobile_clamp();
}
setup_resizable(){
console.log('============= setup_resizable() called ================')
@ -218,11 +234,13 @@ bricks.Popup = class extends bricks.VBox {
var rect, w, h, t, l;
if (this.opts.eventpos && this.opts.origin_event){
bricks.relocate_by_eventpos(this.opts.origin_event, this);
this._mobile_clamp();
return;
}
if (this.top && this.left){
this.set_style('top', this.top);
this.set_style('left', this.left);
this._mobile_clamp();
return;
}
rect = bricks.app.showRectage();
@ -279,11 +297,54 @@ bricks.Popup = class extends bricks.VBox {
}
this.set_style('top', t + 'px');
this.set_style('left', l + 'px');
this._mobile_clamp();
return {
top:t,
left:l
}
}
/* 2026-09-08 =
* 根因 positify_tl body rect 高度算内联 px 手机上文档滚动后 body
* 可高于可视屏百分比换算随之超屏 CSS 100vh 在手机浏览器是
* large viewport含被地址栏/工具栏遮挡部分大于 window.innerHeight
* 修法手机视口下统一以 window.innerHeight真实可视高钳制
* - PopupWindow全屏语义高度恒等于可视高 - TabBar 实测高CSS 里写死
* 56px这里用 offsetHeight 实测更准setProperty important 覆盖
* CSS !important不依赖 dvh 的浏览器支持微信 X5 等老内核无 dvh
* - 普通 Popup下拉/小提示等锚定控件只在超出可视屏时钳高并回拉 top
* resize/键盘弹出visualViewport resize时对打开中的弹窗重钳 */
_mobile_clamp(){
if (!bricks.is_mobile || !bricks.is_mobile()) return;
var el = this.dom_element;
if (!el || !el.isConnected) return;
var vh = window.innerHeight;
var vw = window.innerWidth;
var is_win = (this instanceof bricks.PopupWindow);
var reserve = 0;
if (is_win){
var tabbar = document.querySelector('.tabbar');
if (tabbar && tabbar.style.display !== 'none') reserve = tabbar.offsetHeight || 0;
}
var maxh = Math.max(vh - reserve, 100);
if (is_win || el.offsetHeight > maxh || this._mobile_clamped){
el.style.setProperty('height', maxh + 'px', 'important');
el.style.setProperty('max-height', maxh + 'px', 'important');
this._mobile_clamped = true;
}
if (el.offsetWidth > vw){
el.style.setProperty('width', vw + 'px', 'important');
el.style.setProperty('max-width', vw + 'px', 'important');
}
if (!is_win){
var h = el.offsetHeight;
var t = parseFloat(el.style.top);
if (isNaN(t)) t = 0;
if (t < 0) t = 0;
if (t + h > vh){
el.style.setProperty('top', Math.max(vh - h, 0) + 'px', 'important');
}
}
}
setup_movable(){
this.moving_w.bind('mousedown', this.rec_start_pos.bind(this));
@ -332,6 +393,8 @@ bricks.Popup = class extends bricks.VBox {
this.moving_w.unbind('touchmove', this.moving.bind(this));
bricks.Body.unbind('mouseup', this.stop_moving.bind(this));
bricks.Body.unbind('touchend', this.stop_moving.bind(this));
// 手机端拖拽结束重钳:防止拖出可视屏底部(高度超屏回拉)
this._mobile_clamp();
}
click_outside(event){
if (event.target != this.dom_element){
@ -392,6 +455,17 @@ bricks.Popup = class extends bricks.VBox {
if (this.opened){
this.dismiss();
}
// 清掉手机视口 resize 监听,防止弹窗销毁后监听器泄漏
if (this._vv_resize_bound){
var vv = window.visualViewport;
if (vv && vv.removeEventListener) vv.removeEventListener('resize', this._vv_resize_bound);
window.removeEventListener('resize', this._vv_resize_bound);
this._vv_resize_bound = null;
}
if (this._clamp_task){
clearTimeout(this._clamp_task);
this._clamp_task = null;
}
if (this.parent){
this.parent.remove_widget(this);
this.parent = null;