接到的需求很普通底部弹层占屏幕六成高度顶部一行标题中间一长串可滚动的选项列表底部钉一个确定按钮。我在 u-popup 上写了height: 60%弹层纹丝不动还是被内容撑成一条长短不一的长条改成height: 400px高度是出来了可里面的 scroll-view 死活不滚底部按钮还被 iPhone 的小黑条盖掉一半。如果你也在 uView 的 uni-popup 上折腾过这件事那这篇就是把我踩过的坑按顺序摊开讲一遍。先把结论放前面u-popup 给不了你高度不是因为它不支持高度而是因为它的高度由内部三层节点里最内层那一层决定而那一层默认是内容撑开的自动高度。你要做的不是给组件找一个不存在的属性而是让插槽里那个属于你自己的容器拿到一个确定高度。后面几节我会把三层结构拆开把三种落地写法的取舍摆出来再把滚动、安全区、键盘这三个跟着高度一起冒出来的问题挨个收拾一遍。适合正在用 uView 2.x 做小程序或 App、并且已经被弹层高度卡住的同学。1. 先分清是高度没生效还是生效了但塌了这两种情况看起来都是高度不对但根因完全不同排查方向也完全相反。分不清就乱改最后只会把代码改成一堆!important。1.1 百分比高度失效最常见的那一种在插槽里写height: 100%、height: 60%完全没反应弹层还是被内容撑开——这是九成以上的人遇到的第一种。它的原因跟 u-popup 本身关系不大是 CSS 的一条基础规则百分比高度需要一个高度确定的父元素作为参照父元素高度是auto由内容撑开子元素的百分比高度就无从计算浏览器和小程序的渲染引擎都会直接忽略这一条声明退回自动高度。而 u-popup 的内容层恰好就是auto高度于是就形成了循环内容层等你的容器撑开你的容器等内容层给参照。这个循环在 CSS 里永远解不开唯一的出路是打断循环——给你的容器一个不依赖父级的确定高度比如固定像素、vh或者由 JS 算好之后透传进去的 px 值。搞清楚这一点后面所有方案其实都是在做同一件事把高度从相对父级换成绝对可得。1.2 固定高度生效了但滚动、圆角、动画一起崩第二种症状更隐蔽你写了height: 400px高度确实出来了但紧接着冒出三个新问题。scroll-view 不滚因为它的高度还是auto内容超出后被父级的overflow: hidden直接裁掉顶部的圆角没了因为内容层本身有圆角和裁剪你的容器又套了一层背景色把圆角盖住了弹出动画从下往上滑的时候会闪一下因为 u-popup 的入场动画用的是translateY(100%)这个100%是相对内容层自己的高度算的你临时改高了动画的起始位置就偏了。这三个问题我在三个项目里都单独踩过一遍。它们的共同点是它们不是高度设置失败而是高度设置成功之后的副作用。所以排查的第一件事就是确认你现在处在哪一种如果改高度完全没反应去看 1.1如果高度变了但布局乱了就直接跳到第 4 节。1.3 一个三十秒的定位方法不确定是不是百分比参照系的问题有个特别快的验证办法把 u-popup 整段临时替换成一个普通view把同样的 CSS 一模一样的抄进去。!-- 仅用于排查验证完记得换回来 -- view classdebug-wrap view classsheet :style{ height: sheetHeight } !-- 原来的插槽内容 -- /view /view如果换成普通 view 之后高度正常了说明你的 CSS 逻辑没问题问题出在父级高度不确定上如果换成普通 view 之后还是不对那就是你的高度值本身写错了比如60%写成60、vh写成了vw。这个方法的价值在于把组件因素一次性排除掉比在 u-popup 里一层层试属性快得多。我现在的习惯是遇到任何组件里样式不生效的问题第一反应都是先换成原生节点试一次。注意标题里说的 uni-popup在 uView 里对应的组件名是u-popup插件市场里还有另一个同名的官方组件uni-popup。两者内部节点名不一样但内容层自动高度、没有尺寸属性这一点是一样的下面的思路对两者通用。2. 拆开弹层的节点结构看清楚 height 该写在谁身上想要一次改对就得知道 u-popup 渲染出来到底有几层。这个小节的内容你不需要背知道去哪看就行。2.1 三层结构遮罩层、定位层、内容层u-popup 展开之后大致是三层嵌套。最外层是遮罩负责半透明背景和点击关闭中间层是定位层根据mode决定内容贴哪一边——bottom贴底、center居中、left贴左这一层通常用position: fixed加上 flex 对齐最内层是内容层组件给它挂了类似u-popup__content这样的类名圆角、背景色、内边距、裁剪都在这一层上而你的插槽内容就渲染在它的内部。关键点在于内容层的尺寸是auto。bottom模式下它的宽度由定位层撑满、高度由你的内容决定center模式下宽高都由内容决定。所以给弹层设高度这件事本质上只有两种做法——要么改内容层的尺寸改组件内部节点要么在内容层内部再放一个自己完全掌控尺寸的容器改自己的节点。2.2 为什么改组件内部节点这条路不保险改内部节点用的是深度选择器/* Vue 2 的写法 */ /deep/ .u-popup__content { height: 60vh; } /* Vue 3 的写法 */ :deep(.u-popup__content) { height: 60vh; }这段代码在 H5 和 App 端大概率能跑通在小程序端要打一个问号。原因是小程序的自定义组件存在样式隔离父级页面或父组件的样式能不能穿透到子组件内部取决于编译配置里的styleIsolation设置以及编译器的具体表现。我遇到过的实际情况就是——H5 调试完全正常微信小程序真机上那段深度选择器像没写一样弹层还是被内容撑开。所以这条路我把它放在备选位置能用但不能当作唯一方案更不能是唯一方案。还有一个更长远的问题u-popup__content这类内部类名是组件作者定的你升级一次 uView 小版本类名或层级就可能变。把样式绑在别人的内部实现上升级日就是返工日。2.3 直接翻源码比翻文档快查不清楚某个版本支持哪些属性时我的做法是直接打开node_modules/uview-ui/components/u-popup/u-popup.vue不同安装方式路径略有差异uview-ui也可能叫uview-plus之类。在这个文件里能看到两样东西一是props里到底有没有尺寸相关的属性二是template里的真实层级和类名。看源码最大的好处是不会被文档版本差坑到——网上搜到的教程可能是 1.x 的那时候圆角属性叫borderRadius而你项目里是 2.x叫round属性名写错不会报错只是安安静静地不生效这种错最耗时间。我自己就曾经因为radius和round这两个写法排查了半个下午。小技巧不确定某个属性在当前版本存不存在可以在组件上随便写一个绝对不存在的属性比如:round16 :round-xxx1然后去小程序调试器的组件面板里看 props 列表。存在的属性会出现不存在的不会。3. 三种能落地的方案按和组件的耦合程度排序同一个问题有三种解法我不打算说都可以因为它们的维护成本差得很远。下面的顺序就是我的推荐顺序。3.1 方案一不碰组件把高度写在插槽内的容器上首选这是我现在所有项目的标准做法。核心思想是把 u-popup 只当成一层滑动容器 遮罩尺寸控制全部交给自己在插槽里写的容器。template u-popup :showshow modebottom :round16 :safe-area-inset-bottomtrue closeable closeonClose !-- 这个 view 完全属于我高度我说了算 -- view classsheet :style{ height: sheetHeight } view classsheet__header text classsheet__title选择城市/text /view scroll-view classsheet__body scroll-y :style{ height: bodyHeight } view v-foritem in list :keyitem.id classsheet__item {{ item.name }} /view /scroll-view view classsheet__footer view classsheet__btn clickconfirm确定/view /view /view /u-popup /template高度值的算法放在 JS 里理由是vh在小程序、App、nvue 三端表现不一致而 JS 拿到的窗口高度是可靠的computed: { sheetHeight() { const { windowHeight } uni.getSystemInfoSync() // 占窗口高度的 60%取整避免出现 0.5px 的毛边 return Math.round(windowHeight * 0.6) px }, bodyHeight() { const { windowHeight } uni.getSystemInfoSync() const total Math.round(windowHeight * 0.6) const headerH 48 // 标题栏与 CSS 中保持一致 const footerH 60 // 底部按钮区与 CSS 中保持一致 return Math.max(total - headerH - footerH, 0) px } }为什么内容区高度要单独算、而不是用 CSS 的calc(100% - 108px)因为100%只有在父级高度确定时才有效——这一点前面讲过。在这里.sheet已经有确定 px 高度了所以100%其实能用。但calc()里的单位混用是个坑小程序原生支持 rpx 参与 calc而 H5 和 App 端在编译期把 rpx 转成 px 或 vw早期版本里calc(100% - 108rpx)编译出来是坏的。为了三端一致我宁可把所有高度都在 JS 里算成 px 一次性透传CSS 里一个高度都不写。3.2 方案二深度选择器覆写内部内容层当你确实需要改的是组件的定位层比如想让center模式的弹窗整体带一个最大高度上限可以在页面样式里加/* #ifdef H5 || APP-PLUS */ /deep/ .u-popup__content { max-height: 80vh; overflow: hidden; } /* #endif */这条方案我用得很少只在两种场景开一是这个弹层的结构必须由组件层统一控制比如一个封装了十几种弹层的通用业务组件二是时间紧、方案一改动面太大。用的时候有两个必须做的动作第一用条件编译把平台差异隔开别让一段只在 H5 生效的样式污染小程序第二在代码里留一行注释写清楚依赖 u-popup 内部类名升级 uView 时需回归验证。不留注释的话半年后接手的人只会觉得这里莫名其妙。3.3 方案三绕开 popup自己用 fixed 定位写一层如果你的弹层结构很复杂比如多层嵌套弹窗、弹窗里还要弹窗我的建议是干脆不用 popup自己写一层template view v-ifvisible classself-sheet-mask clickonMaskClick view classself-sheet :style{ height: sheetHeight } click.stop slot / /view /view /template style scoped .self-sheet-mask { position: fixed; left: 0; top: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 999; display: flex; flex-direction: column; justify-content: flex-end; } .self-sheet { width: 100%; background: #ffffff; border-radius: 32rpx 32rpx 0 0; overflow: hidden; } /style代价是入场动画、遮罩点击、安全区适配、z-index管理全部要自己写。收益是结构完全透明高度、滚动、层级问题一次解决。我给它的定位是当你在 u-popup 上花的调试时间超过两小时就该考虑这条路了。方案侵入性小程序端可靠性适合场景主要风险方案一插槽内容器控高低只改自己的代码高绝大多数业务弹层需自己算内容区高度方案二深度选择器覆写中依赖组件内部类名取决于样式隔离配置通用弹层封装升级组件后失效方案三自写 fixed 弹层高全部自己实现高复杂嵌套、结构特殊动画与安全区要自己兜4. 高度定住了滚动和安全区才是真正的坑这一节是全文里我最想让你看完的部分。高度问题本身五分钟能解决但高度解决之后冒出来的三个问题每一个都能让你多花一小时。4.1 scroll-view 必须拿到确定高度否则它只是看起来能滚scroll-view 的滚动条是靠内容高度超过容器高度触发的如果容器高度是auto容器会跟着内容一起长高永远不会出现超出自然也就不滚。所以给 scroll-view 设高度是必要条件。给高度的方式有两种。第一种是上面代码里的显式 px优点是确定缺点是标题栏或底部按钮高度变了要同步改 JS 里的数值。第二种是用 flex.sheet { display: flex; flex-direction: column; } .sheet__body { flex: 1; min-height: 0; /* 关键不加这行在部分平台上会被内容撑开 */ }min-height: 0这行最容易被漏掉。flex 子项的默认min-height是auto意味着它不能被压缩到比内容更小于是flex: 1就失效了scroll-view 又变成被撑开的状态。这个坑我在 H5 和 App 上各踩过一次表现都是flex 写了但高度没受控。两种方式我都用过现在的取舍标准是标题栏和底部按钮高度固定且简单用 flex内容结构复杂、高度会动态变化用 JS 显式算。如果标题栏里要放实时变化的东西比如一个搜索框 一行筛选标签那就用createSelectorQuery在渲染后测量async measureBody() { const info uni.getSystemInfoSync() const total Math.round(info.windowHeight * 0.6) const query uni.createSelectorQuery().in(this) // 组件内必须加 in(this) query.select(.sheet__header).boundingClientRect() query.select(.sheet__footer).boundingClientRect() query.exec((res) { const [header, footer] res const used (header ? header.height : 0) (footer ? footer.height : 0) this.bodyHeight Math.max(total - used, 0) px }) }这里有个必须记住的细节在组件内部调用createSelectorQuery()一定要接.in(this)否则查询的是页面作用域节点选不到res全是null而且不报错。我第一次遇到这个现象时以为是选择器写错了把.sheet__header改成.sheet .sheet__header、加折腾了很久才发现是作用域的问题。4.2 iPhone 底部安全区把内容顶出屏幕u-popup 的safe-area-inset-bottom属性默认值为true会在内容层底部补一段内边距用来避开 iPhone 底部的小横条。这本意是好的但如果你给弹层设的是固定总高这段内边距会从内部挤压你的内容区而不是把整个弹层往上抬。表现就是底部按钮被压扁、或者被推到屏幕外一点点。处理方式取决于你把高度设在哪一层。如果你用的是方案一把安全区适配交给自己更省心关掉组件的safe-area-inset-bottom在底部按钮区自己加内边距。.sheet__footer { height: 60px; padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom); box-sizing: content-box; /* 让 padding 加在高度之外 */ }constant()和env()两行都写是历史兼容做法前者给老版本 iOS后者给新版本顺序不能反。另外box-sizing要设成content-box不然那 34px 的内边距会被算进 60px 里按钮区的实际可用高度被吃掉一半这是底部按钮看起来被压扁最常见的原因。4.3 键盘弹起导致的跳动弹层里如果有输入框键盘弹起会改变窗口可视高度。这时候有两件事会发生一是uni.getSystemInfoSync()拿到的windowHeight会变小部分平台如果你在onLoad时就固定了高度值那个值不会跟着变表现是弹层被键盘顶上去或者藏在键盘后面二是你重新算了一次高度弹层突然跳一下。我的处理原则是弹层高度只算一次绑定在打开时键盘弹起时不去重算弹层本身只让内容区滚动到底部。做法是在打开弹层的时机重新执行一次高度计算而不是在computed里依赖可能变化的系统信息openSheet() { // 每次打开都重新取一次避免横竖屏切换或键盘改变可视高度后取值过期 this.sheetHeight Math.round(uni.getSystemInfoSync().windowHeight * 0.6) px this.show true }如果确实需要键盘弹起时上推弹层用组件自带的modebottom加上页面的adjust-position行为处理别去手动监听键盘高度再做动画手写的键盘动画在三端上几乎不可能调得一样顺。5. 一次H5 正常、真机高度不生效的完整排查过程把前面几节的理论收一下讲一个我上个月真实处理的工单。整个过程写下来是因为我觉得排查路径比结论更有参考价值。5.1 现象与第一反应项目是一个城市选择弹层modebottom要求占屏幕 55%。开发同学反馈H5 预览一切正常高度刚刚好打包成小程序在真机上打开弹层完全没高度限制被 30 个城市的列表撑到几乎满屏只有最上面一小截能看见列表的第一项。我拿到的第一份代码是这样/* 页面样式scoped */ /deep/ .u-popup__content { height: 55vh; overflow: hidden; }看到这段我的第一反应是这不是高度没生效这是H5 生效了、小程序没生效典型的样式穿透问题。5.2 逐层排除的三个步骤第一步确认样式有没有到。在小程序调试器的 Wxml 面板里选中内容层那个节点看右边样式面板里有没有那条height: 55vh。结果是没有说明深度选择器根本没穿透进去不是优先级问题。第二步确认是不是样式隔离。翻编译配置页面和组件都没有做styleIsolation的特殊设置。这里要说明一句不同平台、不同 uni-app 版本对默认样式隔离的处理不一样所以深度选择器能不能穿透这件事本身就没有统一答案。结论是这条路在当前项目的当前平台组合下不可靠靠它就是在赌。第三步验证替代方案。把高度从页面样式里删掉改成方案一——在插槽里加一层自己的容器高度由 JS 算好透传。改完之后真机高度正常了滚动的 scroll-view 也正常了。5.3 修完之后又冒出来的第二个问题高度对了底部按钮却贴着屏幕最下沿被 iPhone 的小横条压住了一半。这就是 4.2 节讲的那个坑组件的safe-area-inset-bottom本来在处理这件事但因为我们在方案一里给容器设了固定总高那段内边距是从内部挤压内容区的把 60px 的按钮区挤成了 26px。最终的处理是关掉组件的安全区属性由底部按钮区自己承担内边距并且把box-sizing设成content-box。改完之后真机、H5、开发者工具三处表现一致。现象根因修复动作H5 高度正常真机被内容撑开深度选择器未穿透样式隔离拦截改为插槽内容器控高滚动区域高度设了但不滚容器高度仍是 auto显式 px 或flex: 1min-height: 0底部按钮被压扁安全区内边距从内部挤压内容区关组件安全区自己加padding-bottom选择器查不到节点、返回 null组件内没加.in(this)补上作用域参数6. 弹层里要塞日历什么时候该放弃弹窗搜 uview 弹层高度的人里有一批需求其实是想在弹窗里放日历这也是uview 日历直接展示这个说法最近被频繁提起的原因。我的观点比较直接如果日历本身就占满了一屏那弹窗只会给你添麻烦。6.1 日历的两种放法第一种是弹窗里放日历日历跟着弹层一起滑上来。好处是不离开当前页面坏处是你要同时解决弹层高度、日历内部高度、日历滚动区域高度三个尺寸问题而且日历一年滚到底的时候弹层和日历两套滚动会打架。第二种是不用弹窗直接在页面里展开。页面里放一个普通容器用v-if或者v-show控制展开收起容器高度由日历自己决定。这样完全绕开了弹层高度问题——容器高度不用设因为它不需要滚动页面本身会滚。template view classpage view classfield clicktoggleCalendar text{{ picked || 请选择日期 }}/text text classfield__arrow{{ visible ? 收起 : 展开 }}/text /view view v-ifvisible classcalendar-host !-- 具体组件名与所需属性以你项目的版本为准 -- u-calendar modedate :default-datepicked confirmonConfirm closevisible false / /view /view /template这里有个版本差异必须提醒有一部分 uView 版本的日历组件是自带一层弹窗的靠show属性控制显隐另一些版本要求你自己用 u-popup 包一层。判断方法很简单还是那招——去node_modules里打开这个组件的源码看它的template里有没有引用弹窗组件。这一步花两分钟能省下后面两小时。6.2 直接展示时的高度账日历不用弹窗之后唯一还要注意的高度问题是展开收起时的动画。直接v-if是硬切视觉上比较生硬想要顺滑一点可以用max-height做过渡.calendar-host { overflow: hidden; transition: max-height 0.28s ease; max-height: 900rpx; /* 比日历实际高度略大即可 */ }max-height的值不用精确只要比日历最高的情况比如月视图 农历 六行日期大一点就行。用max-height而不用height的原因也很简单日历的实际高度会随着有没有副标题、有没有农历、不同月份行数不同而变化写死height一定会在某个月份上露馅——要么裁掉最后一行要么留一大块空白。在高度会变化的场景里max-height永远比height安全这条经验不限于日历。6.3 选择的判据我自己的判据是这样的需要聚焦单一操作、且离开当前上下文无所谓的场景用弹窗比如选择城市、选择规格需要边看页面边操作、或者内容本身就是一屏的开场的场景直接内联展开日历就是后者。所有日历相关的需求在弹窗里做都会比在页面里做多花至少一倍时间而且维护成本更高。7. 一份可以直接抄的模板和自测清单最后把前面所有内容落成一个可以直接复制粘贴的成品再补一份上线前的检查清单。7.1 完整组件模板template u-popup :showshow modebottom :round16 :safe-area-inset-bottomfalse :closeabletrue closeonClose view classsheet :style{ height: sheetHeight } view classsheet__header text classsheet__title{{ title }}/text /view scroll-view classsheet__body scroll-y :style{ height: bodyHeight } :show-scrollbarfalse slot / /scroll-view view classsheet__footer view classsheet__btn clickonConfirm确定/view /view /view /u-popup /template script export default { name: HeightSheet, props: { show: { type: Boolean, default: false }, title: { type: String, default: 请选择 }, ratio: { type: Number, default: 0.6 } }, data() { return { sheetHeight: 0px, bodyHeight: 0px } }, watch: { // 每次打开时重新计算避免窗口尺寸变化后取值过期 show(val) { if (val) { this.$nextTick(this.calcHeight) } } }, methods: { calcHeight() { const info uni.getSystemInfoSync() const total Math.round(info.windowHeight * this.ratio) const query uni.createSelectorQuery().in(this) query.select(.sheet__header).boundingClientRect() query.select(.sheet__footer).boundingClientRect() query.exec((res) { const [header, footer] res const used (header header.height ? header.height : 0) (footer footer.height ? footer.height : 0) this.sheetHeight total px this.bodyHeight Math.max(total - used, 0) px }) }, onClose() { this.$emit(close) }, onConfirm() { this.$emit(confirm) } } } /script style scoped .sheet { display: flex; flex-direction: column; background-color: #ffffff; border-radius: 32rpx 32rpx 0 0; overflow: hidden; } .sheet__header { flex-shrink: 0; padding: 32rpx 32rpx 16rpx; } .sheet__title { font-size: 32rpx; font-weight: 600; color: #303133; } .sheet__body { /* 高度由 JS 透传这里只负责视觉 */ } .sheet__footer { flex-shrink: 0; padding: 16rpx 32rpx; /* 安全区自己处理注意 content-box */ box-sizing: content-box; padding-bottom: calc(16rpx constant(safe-area-inset-bottom)); padding-bottom: calc(16rpx env(safe-area-inset-bottom)); } .sheet__btn { height: 88rpx; line-height: 88rpx; text-align: center; border-radius: 44rpx; background-color: #2979ff; color: #ffffff; font-size: 30rpx; } /style这份模板里有几个刻意的设计选择值得说明一下。用watch而不是mounted触发测量是因为弹层是v-if控制的父组件传进来show之前节点根本不存在mounted里查必然是null。overflow: hidden加在最外层容器上是为了让圆角把内部内容裁掉否则滚动内容会盖过圆角。flex-shrink: 0加在头尾两栏上是为了防止内容太长时它们被 flex 压缩——不加的话bodyHeight明明算对了头尾两栏也会变形。7.2 参数速查表关键项推荐写法说明弹层总高JS 算windowHeight * ratio透传 px避免 vh 在 nvue 下失效内容区高总高减去实测头尾高度用createSelectorQuery().in(this)内容区滚动scroll-view 显式高度容器 auto 高度时不会滚安全区关组件属性自己加env()记得box-sizing: content-box圆角外层容器overflow: hidden否则滚动内容盖过圆角头尾栏flex-shrink: 0防止被内容压缩动画跳动高度在打开时确定不在过程中变化避免translateY起始位置偏移日历类内容优先页面内联展开绕开弹层高度问题7.3 上线前的自测清单这份清单是我每次交付弹层类需求前都会过一遍的你可以按项目情况增减。三端一致性H5、App、目标小程序各开一次确认高度一致、底部按钮都没有被遮挡。超长内容列表塞 100 条确认滚动正常、头尾两栏没有变形。空内容列表为 0 条时确认弹层高度依然固定不会塌成一条线。键盘弹层里放输入框确认键盘弹起时不会把确定按钮顶出可视区。机型差异至少在一台带底部横条的 iPhone 和一台安卓上各看一次安全区表现。横竖屏如果应用支持横屏旋转之后重开弹层确认高度重新计算。样式隔离如果用了深度选择器在小程序真机上再验证一次别只看 H5 预览。我自己在几个项目里反复验证下来最容易在最后关头翻车的两项是空内容和真机样式隔离——前者在开发时几乎不会遇到后者在 H5 上根本看不出来。
阅读完成 · 觉得有帮助?