From 479cc3ba05796af14dbfebae3d99b5cfaebf9a02 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Wed, 9 Sep 2026 10:39:54 +0800 Subject: [PATCH] =?UTF-8?q?fix(mobile):=20=E6=89=8B=E6=9C=BA=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E9=AB=98=E5=BA=A6=E8=B6=85=E5=87=BA=E5=B1=8F=E5=B9=95?= =?UTF-8?q?=E2=80=94=E2=80=94=E9=AB=98=E5=BA=A6=E9=92=B3=E5=88=B6=E5=88=B0?= =?UTF-8?q?=E5=8F=AF=E8=A7=86=E5=B1=8F=E9=AB=98(2026-09-08=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=8A=A5=E9=9A=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因(两层): 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 只是首帧静态兜底。 --- bricks/popup.js | 74 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/bricks/popup.js b/bricks/popup.js index ce2edea..5682628 100644 --- a/bricks/popup.js +++ b/bricks/popup.js @@ -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 优先(软键盘会触发它的 resize,window.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;