Popup 弹窗
17:06说明
弹出层容器,只提供容器本身,内部内容由使用方自定义。支持 top/bottom/left/right/center 五个弹出方向,mode='inline' 时直接作为普通内容嵌入页面流(无遮罩、无定位、无手势)。通过 v-model:show 控制显隐,可配置遮罩、圆角、背景色、关闭图标、安全区适配、过渡时长与居中缩放,并支持下拉关闭、关闭前拦截(before-close)以及只派发 close 的受控关闭(popup=false)。
兼容性
| 平台 | 支持情况 |
|---|---|
| App | ✅ 支持(内容层无 max-height/overflow 兜底,超长内容需自行处理滚动) |
| H5 | ✅ 支持(弹出期间锁定页面滚动,多层弹窗按层计数) |
| 微信小程序 | ✅ 支持 |
示例
弹出方向 mode
点按钮按该方向弹一次,弹窗内直接列出本例生效与未生效的属性。
查看源码
<template>
<view class="btn-wrap">
<ux-button v-for="(item, index) in modes" :key="index" :type="mode == item.value ? 'primary' : 'default'" custom-class="btn-item" size="small" shape="round" :text="item.label" @click="open(item.value)"></ux-button>
</view>
<ux-popup v-model:show="show" :mode="mode" :closeable="true" length="50%" @closed="onClosed()" @opened="onOpened()">
<view class="popup-body">
<text class="popup-body-title">{{ label }} 弹窗内容</text>
<view class="popup-params">
<text v-for="(line, index) in params" :key="index" class="popup-params-item">{{ line }}</text>
</view>
<text class="popup-body-tip">Popup 只是容器,您可以放置任意自定义内容在内部。</text>
<view class="popup-body-action">
<ux-button block type="primary" text="关闭弹窗" @click="show = false"></ux-button>
</view>
</view>
</ux-popup>
</template>
<script setup lang="uts">
import { useLocale, type PopupMode } from '@/uni_modules/uview-pro-x';
const { t } = useLocale('demo');
const show = ref<boolean>(false);
const mode = ref<PopupMode>('center');
const modes: { value: PopupMode; label: string }[] = [
{ label: '上', value: 'top' },
{ label: '下', value: 'bottom' },
{ label: '左', value: 'left' },
{ label: '右', value: 'right' },
{ label: '中', value: 'center' }
];
const label = computed<string>((): string => {
const item = modes.find(m => m.value === mode.value);
return item != null ? item.label : mode.value;
});
function open(val: PopupMode): void {
mode.value = val;
show.value = true;
}
function onOpened(): void {
// 进场动画播放完毕:演示页只做计数,不打日志
}
function onClosed(): void {
// 退场动画播放完毕且节点卸载:演示页只做计数,不打日志
}
const params = computed<string[]>(() => [
'mode = ' + mode.value,
'length = 50%',
'closeable = true'
]);
</script>
<style lang="scss">
.popup-body {
width: 100%;
}
.popup-body-title {
/* #ifndef APP */
display: block;
/* #endif */
font-size: 16px;
font-weight: bold;
color: $u-main-color;
text-align: center;
/* 左右内缩避开关闭图标热区(图标尺寸 + 16px 内缩),否则长标题会从图标底下穿过 */
padding: 16px 44px 8px 44px;
}
/* 档位与打开入口统一用 ux-button:.btn-wrap 横向流式排列,.btn-item 走间距,单行入口走 .btn-open */
.btn-wrap {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
.btn-item {
margin-right: 12px;
margin-bottom: 12px;
}
/* 单行入口:block 占满一行,并与上方 cell 组边框拉开间距 */
.btn-open {
margin-top: 12px;
}
/* 紧接在 ux-cell-group 后的按钮行:补出顶部间距 */
.btn-wrap--first {
margin-top: 12px;
}
.popup-level-label {
/* #ifndef APP */
display: block;
/* #endif */
font-size: 13px;
color: $u-content-color;
margin-top: 12px;
margin-bottom: 6px;
}
.popup-params {
margin: 0 16px;
padding: 8px 10px;
background-color: $u-bg-color;
border-radius: 6px;
}
.popup-params-item {
/* #ifndef APP */
display: block;
/* #endif */
font-size: 12px;
color: $u-content-color;
line-height: 1.7;
}
.popup-body-tip {
/* #ifndef APP */
display: block;
/* #endif */
font-size: 12px;
color: $u-tips-color;
line-height: 1.6;
padding: 10px 16px 0 16px;
}
.popup-body-action {
padding: 16px;
}
</style>内嵌面板 mode=inline
mode=inline 不渲染遮罩、不进浮层,直接作为普通内容嵌入页面流,适合无需遮挡的内联面板。
查看源码
<template>
<ux-popup mode="inline" width="100%" :border-radius="8">
<view class="popup-body">
<text class="popup-body-title">inline 嵌入模式</text>
<view class="popup-params">
<text v-for="(line, index) in params" :key="index" class="popup-params-item">{{ line }}</text>
</view>
<text class="popup-body-tip">不渲染遮罩、不进页面栈,直接作为普通内容参与页面布局。</text>
</view>
</ux-popup>
</template>
<script setup lang="uts">
const params = computed<string[]>(() => [
'mode = inline',
'width = 100%',
'border-radius = 8'
]);
</script>
<style lang="scss">
/* 样式同 basic 片段:popup-body / popup-body-title / popup-params / popup-params-item / popup-body-tip */
</style>尺寸 length / width / height
三个入口分别演示 bottom 高度、center 定宽高、left 宽度;弹窗内回显生效项与未生效项。
查看源码
<template>
<view class="btn-wrap">
<ux-button custom-class="btn-item" shape="round" type="primary" text="底部抽屉 length=45%" @click="open('bottom', '45%', '', '')"></ux-button>
<ux-button custom-class="btn-item" shape="round" type="primary" text="居中定宽高 70% × 45%" @click="open('center', 'auto', '70%', '45%')"></ux-button>
<ux-button custom-class="btn-item" shape="round" type="primary" text="左侧抽屉 length=60%" @click="open('left', '60%', '', '')"></ux-button>
</view>
<ux-popup v-model:show="show" :mode="mode" :length="length" :width="width" :height="height" :border-radius="12" :closeable="true" @closed="onClosed()" @opened="onOpened()">
<view class="popup-body">
<text class="popup-body-title">尺寸演示</text>
<view class="popup-params">
<text v-for="(line, index) in params" :key="index" class="popup-params-item">{{ line }}</text>
</view>
<text class="popup-body-tip">bottom/top 用 length 控制高度,left/right 用 length 控制宽度,center 用 width + height。</text>
<view class="popup-body-action">
<ux-button block type="primary" text="关闭弹窗" @click="show = false"></ux-button>
</view>
</view>
</ux-popup>
</template>
<script setup lang="uts">
import type { PopupMode } from '@/uni_modules/uview-pro-x';
const show = ref<boolean>(false);
const mode = ref<PopupMode>('bottom');
const length = ref<string>('45%');
const width = ref<string>('');
const height = ref<string>('');
function open(val: PopupMode, len: string, w: string, h: string): void {
mode.value = val;
length.value = len;
width.value = w;
height.value = h;
show.value = true;
}
function onOpened(): void {
// 进场动画播放完毕:演示页只做计数,不打日志
}
function onClosed(): void {
// 退场动画播放完毕且节点卸载:演示页只做计数,不打日志
}
/** 留空即未设置,回显成「未设置(默认)」,让默认行为也可观察 */
const params = computed<string[]>(() => [
'mode = ' + mode.value,
'length = ' + (length.value != '' ? length.value : '未设置(默认)'),
'width = ' + (width.value != '' ? width.value : '未设置(默认)'),
'height = ' + (height.value != '' ? height.value : '未设置(默认)')
]);
</script>
<style lang="scss">
/* 样式同 basic 片段:popup-body / popup-body-title / popup-params / popup-params-item / popup-body-tip / popup-body-action */
</style>圆角、背景色与居中偏移
border-radius 大于 0 时组件加 overflow:hidden 裁角;bg-color 作用于弹窗根节点;margin-top 仅 center 生效。
查看源码
<template>
<ux-cell-group>
<ux-cell-item :title="bgOn ? '自定义背景色:开启' : '自定义背景色:关闭'" :arrow="false" :border-bottom="false">
<ux-switch v-model="bgOn"></ux-switch>
</ux-cell-item>
</ux-cell-group>
<text class="popup-level-label">圆角档位(px)</text>
<view class="btn-wrap">
<ux-button v-for="(item, index) in radiusList" :key="index" :type="radius == item.value ? 'primary' : 'default'" custom-class="btn-item" size="small" shape="round" :text="item.label" @click="radius = item.value"></ux-button>
</view>
<text class="popup-level-label">居中偏移档位(px)</text>
<view class="btn-wrap">
<ux-button v-for="(item, index) in marginTopList" :key="index" :type="marginTop == item.value ? 'primary' : 'default'" custom-class="btn-item" size="small" shape="round" :text="item.label" @click="marginTop = item.value"></ux-button>
</view>
<ux-button block custom-class="btn-open" shape="round" type="primary" text="打开外观弹窗" @click="show = true"></ux-button>
<!-- 内容区不设背景色,bg-color 作用于弹窗根节点才能整块透出 -->
<ux-popup v-model:show="show" mode="center" width="70%" :border-radius="radius" :bg-color="bgColor" :margin-top="marginTop" :closeable="true" @closed="onClosed()" @opened="onOpened()">
<view class="popup-body">
<text class="popup-body-title">圆角 / 背景 / 偏移</text>
<view class="popup-params">
<text v-for="(line, index) in params" :key="index" class="popup-params-item">{{ line }}</text>
</view>
<text class="popup-body-tip">圆角大于 0 时组件会加 overflow:hidden 裁掉直角内容;margin-top 只对 center 生效。</text>
<view class="popup-body-action">
<ux-button block type="primary" text="关闭弹窗" @click="show = false"></ux-button>
</view>
</view>
</ux-popup>
</template>
<script setup lang="uts">
const show = ref<boolean>(false);
const radius = ref<string>('12');
const marginTop = ref<string>('0');
const bgOn = ref<boolean>(true);
const bgColorValue = '#eaf2ff';
const radiusList: { value: string; label: string }[] = [
{ label: '0', value: '0' },
{ label: '12', value: '12' },
{ label: '24', value: '24' }
];
const marginTopList: { value: string; label: string }[] = [
{ label: '-80', value: '-80' },
{ label: '0', value: '0' },
{ label: '40', value: '40' }
];
/** 传给组件的背景色:关闭时为 '' 表示未设置,走主题白底 */
const bgColor = computed<string>((): string => bgOn.value ? bgColorValue : '');
function onOpened(): void {
// 进场动画播放完毕:演示页只做计数,不打日志
}
function onClosed(): void {
// 退场动画播放完毕且节点卸载:演示页只做计数,不打日志
}
const params = computed<string[]>(() => [
'border-radius = ' + radius.value,
'bg-color = ' + (bgColor.value != '' ? bgColor.value : '未设置(默认)'),
'margin-top = ' + marginTop.value
]);
</script>
<style lang="scss">
/* 样式同 basic 片段:popup-body / popup-body-title / popup-params / popup-body-tip / popup-body-action / btn-wrap / btn-item / popup-level-label */
</style>遮罩显隐与点击关闭 mask / mask-close-able
关掉 mask 后弹窗下方不再压暗;关掉 mask-close-able 后点遮罩不再关闭,只能靠按钮或代码。
查看源码
<template>
<ux-cell-group>
<ux-cell-item :title="maskOn ? '显示遮罩' : '无遮罩'" :arrow="false">
<ux-switch v-model="maskOn"></ux-switch>
</ux-cell-item>
<ux-cell-item :title="maskClickClose ? '点击遮罩关闭' : '点击遮罩不关闭'" :arrow="false" :border-bottom="false">
<ux-switch v-model="maskClickClose"></ux-switch>
</ux-cell-item>
</ux-cell-group>
<ux-button block custom-class="btn-open" shape="round" type="primary" text="打开顶部弹窗" @click="show = true"></ux-button>
<ux-popup v-model:show="show" mode="top" length="45%" :mask="maskOn" :mask-close-able="maskClickClose" :border-radius="12" @closed="onClosed()" @opened="onOpened()">
<view class="popup-body">
<text class="popup-body-title">遮罩配置</text>
<view class="popup-params">
<text v-for="(line, index) in params" :key="index" class="popup-params-item">{{ line }}</text>
</view>
<text class="popup-body-tip">mask=false 时弹窗下方不再压暗;mask-close-able=false 时点遮罩不再关闭。</text>
<view class="popup-body-action">
<ux-button block type="primary" text="关闭弹窗" @click="show = false"></ux-button>
</view>
</view>
</ux-popup>
</template>
<script setup lang="uts">
const show = ref<boolean>(false);
const maskOn = ref<boolean>(true);
const maskClickClose = ref<boolean>(true);
function onOpened(): void {
// 进场动画播放完毕:演示页只做计数,不打日志
}
function onClosed(): void {
// 退场动画播放完毕且节点卸载:演示页只做计数,不打日志
}
const params = computed<string[]>(() => [
'mask = ' + (maskOn.value ? 'true' : 'false'),
'mask-close-able = ' + (maskClickClose.value ? 'true' : 'false'),
'mode = top',
'length = 45%'
]);
</script>
<style lang="scss">
/* 样式同 basic 片段 */
</style>遮罩样式与层级 mask-custom-style / z-index
mask-custom-style 用连字符键定制遮罩,z-index 同时抬高弹窗与遮罩层级,height 直接指定弹窗高度。
查看源码
<template>
<ux-button block custom-class="btn-open" shape="round" type="primary" text="打开自定义遮罩弹窗" @click="show = true"></ux-button>
<ux-popup v-model:show="show" mode="center" width="70%" height="38%" :border-radius="12" :closeable="true" :mask-custom-style="maskCustomStyle" :z-index="2000" @closed="onClosed()" @opened="onOpened()">
<view class="popup-body">
<text class="popup-body-title">遮罩样式与层级</text>
<view class="popup-params">
<text v-for="(line, index) in params" :key="index" class="popup-params-item">{{ line }}</text>
</view>
</view>
</ux-popup>
</template>
<script setup lang="uts">
const show = ref<boolean>(false);
/** 遮罩自定义样式:键统一连字符 */
const maskCustomStyle = ref<UTSJSONObject>({
'background-color': 'rgba(24, 144, 255, 0.35)'
});
function onOpened(): void {
// 进场动画播放完毕:演示页只做计数,不打日志
}
function onClosed(): void {
// 退场动画播放完毕且节点卸载:演示页只做计数,不打日志
}
const params = computed<string[]>(() => [
'mask-custom-style = background-color: rgba(24,144,255,.35)',
'z-index = 2000',
'width = 70%',
'height = 38%'
]);
</script>
<style lang="scss">
/* 样式同 basic 片段 */
</style>关闭图标 closeable / close-icon-*
图标自带 padding 命中区(能从图标一路铺到弹窗边角),层级 z-index 9 压住内容区。
查看源码
<template>
<ux-cell-group>
<ux-cell-item :title="closeable ? '显示关闭图标' : '隐藏关闭图标'" :arrow="false">
<ux-switch v-model="closeable"></ux-switch>
</ux-cell-item>
<ux-cell-item :title="iconCircle ? '图标 close-circle' : '图标 close'" :arrow="false">
<ux-switch v-model="iconCircle"></ux-switch>
</ux-cell-item>
<ux-cell-item :title="iconPrimary ? '图标颜色:主题蓝' : '图标颜色:错误红'" :arrow="false" :border-bottom="false">
<ux-switch v-model="iconPrimary"></ux-switch>
</ux-cell-item>
</ux-cell-group>
<text class="popup-level-label">关闭图标位置(点按钮按该位置弹一次)</text>
<view class="btn-wrap">
<ux-button v-for="(item, index) in positions" :key="index" :type="pos == item.value ? 'primary' : 'default'" custom-class="btn-item" size="small" shape="round" :text="item.label" @click="open(item.value)"></ux-button>
</view>
<!-- 关闭图标自身就是点击入口,内容区不再重复放关闭按钮 -->
<ux-popup v-model:show="show" mode="bottom" length="35%" :border-radius="12" :closeable="closeable" :close-icon="iconName" :close-icon-position="pos" :close-icon-color="iconColor" :close-icon-size="22" :mask-close-able="true" @closed="onClosed()" @opened="onOpened()">
<view class="popup-body">
<text class="popup-body-title">关闭图标</text>
<view class="popup-params">
<text v-for="(line, index) in params" :key="index" class="popup-params-item">{{ line }}</text>
</view>
<text class="popup-body-tip">图标带 padding 命中区(可从图标一路铺到弹窗边角),层级 z-index 9 压住内容;closeable=false 时只能靠遮罩或代码关闭。</text>
</view>
</ux-popup>
</template>
<script setup lang="uts">
import { $u, type PopupCloseIconPos } from '@/uni_modules/uview-pro-x';
const show = ref<boolean>(false);
const closeable = ref<boolean>(true);
const iconCircle = ref<boolean>(true);
const iconPrimary = ref<boolean>(false);
const pos = ref<PopupCloseIconPos>('top-right');
const positions: { value: PopupCloseIconPos; label: string }[] = [
{ label: '左上', value: 'top-left' },
{ label: '右上', value: 'top-right' },
{ label: '左下', value: 'bottom-left' },
{ label: '右下', value: 'bottom-right' }
];
const iconName = computed<string>((): string => iconCircle.value ? 'close-circle' : 'close');
const iconColor = computed<string>((): string => iconPrimary.value ? $u.strOr($u.color.primary) : $u.strOr($u.color.error));
function open(val: PopupCloseIconPos): void {
pos.value = val;
show.value = true;
}
function onOpened(): void {
// 进场动画播放完毕:演示页只做计数,不打日志
}
function onClosed(): void {
// 退场动画播放完毕且节点卸载:演示页只做计数,不打日志
}
const params = computed<string[]>(() => [
'closeable = ' + (closeable.value ? 'true' : 'false'),
'close-icon = ' + iconName.value,
'close-icon-position = ' + pos.value,
'close-icon-color / size = ' + iconColor.value + ' / 22'
]);
</script>
<style lang="scss">
/* 样式同 basic 片段 */
</style>安全区适配 safe-area-inset-bottom / -top
两项组件默认已开启(默认 true):下沿贴屏幕底时加 padding-bottom(mode=bottom|left|right),上沿贴屏幕顶时加 padding-top 避让状态栏(mode=top|left|right),center 两端都不贴。本例把按钮与蓝条贴到了内容末端,用开关对比差异。
查看源码
<template>
<ux-cell-group>
<ux-cell-item :title="insetBottom ? '底部安全区:开启' : '底部安全区:关闭'" :arrow="false">
<ux-switch v-model="insetBottom"></ux-switch>
</ux-cell-item>
<ux-cell-item :title="insetTop ? '顶部安全区:开启' : '顶部安全区:关闭'" :arrow="false" :border-bottom="false">
<ux-switch v-model="insetTop"></ux-switch>
</ux-cell-item>
</ux-cell-group>
<view class="btn-wrap btn-wrap--first">
<ux-button custom-class="btn-item" shape="round" type="primary" text="打开底部弹窗(贴底锚点)" @click="bottomShow = true"></ux-button>
<ux-button custom-class="btn-item" shape="round" type="primary" text="打开顶部弹窗(贴顶锚点)" @click="topShow = true"></ux-button>
<ux-button custom-class="btn-item" shape="round" type="primary" text="打开左侧抽屉(两端贴边)" @click="leftShow = true"></ux-button>
</view>
<!-- 底部:内容贴到根节点下边缘(贴底按钮 + 标尺),padding-bottom 的差值才看得见 -->
<ux-popup v-model:show="bottomShow" mode="bottom" length="45%" :border-radius="12" :safe-area-inset-bottom="insetBottom" @closed="onClosed()" @opened="onOpened()">
<view class="popup-body popup-body--fill">
<text class="popup-body-title">底部安全区</text>
<view class="popup-params">
<text v-for="(line, index) in bottomParams" :key="index" class="popup-params-item">{{ line }}</text>
</view>
<text class="popup-body-tip">开关切换时,按钮与下方蓝条整体上下平移一个安全区高度。</text>
<view class="popup-body-spacer"></view>
<view class="popup-body-action popup-body-action--flush">
<ux-button block type="primary" shape="square" :border-radius="0" text="贴底主操作按钮" @click="bottomShow = false"></ux-button>
<view class="popup-safe-edge"></view>
</view>
</view>
</ux-popup>
<!-- 顶部:贴顶标尺随 padding-top 一起下移,让出状态栏 -->
<ux-popup v-model:show="topShow" mode="top" length="45%" :border-radius="12" :safe-area-inset-top="insetTop" @closed="onClosed()" @opened="onOpened()">
<view class="popup-body">
<view class="popup-safe-edge"></view>
<text class="popup-body-title">顶部安全区</text>
<view class="popup-params">
<text v-for="(line, index) in topParams" :key="index" class="popup-params-item">{{ line }}</text>
</view>
<text class="popup-body-tip">开启后蓝条从状态栏下方开始,关闭时顶进状态栏。</text>
<view class="popup-body-action">
<ux-button block type="primary" text="关闭弹窗" @click="topShow = false"></ux-button>
</view>
</view>
</ux-popup>
<!-- 左侧抽屉:高度 100%,上下两端都贴屏幕边,两项避让同时生效(开关共用) -->
<ux-popup v-model:show="leftShow" mode="left" length="70%" :border-radius="12" :safe-area-inset-bottom="insetBottom" :safe-area-inset-top="insetTop" @closed="onClosed()" @opened="onOpened()">
<view class="popup-body popup-body--fill">
<view class="popup-safe-edge"></view>
<text class="popup-body-title">左侧抽屉内容</text>
<view class="popup-params">
<text v-for="(line, index) in leftParams" :key="index" class="popup-params-item">{{ line }}</text>
</view>
<text class="popup-body-tip">抽屉高度 100%,上下两端都贴屏幕边,两项避让同时生效:顶部让出状态栏、底部让出安全区。</text>
<view class="popup-body-spacer"></view>
<view class="popup-body-action popup-body-action--flush">
<ux-button block type="primary" shape="square" :border-radius="0" text="关闭弹窗" @click="leftShow = false"></ux-button>
<view class="popup-safe-edge"></view>
</view>
</view>
</ux-popup>
</template>
<script setup lang="uts">
import { useSafeArea, useWindowInfo } from '@/uni_modules/uview-pro-x';
/** 组件内部同样用这两个 hook 计算根节点 padding,这里取真实数值用于回显 */
const { bottom } = useSafeArea();
const { statusBarHeight } = useWindowInfo();
const bottomShow = ref<boolean>(false);
const topShow = ref<boolean>(false);
const leftShow = ref<boolean>(false);
const insetBottom = ref<boolean>(true);
const insetTop = ref<boolean>(true);
function onOpened(): void {
// 进场动画播放完毕:演示页只做计数,不打日志
}
function onClosed(): void {
// 退场动画播放完毕且节点卸载:演示页只做计数,不打日志
}
/** 无安全区的设备(PC / 部分模拟器)看不出差异,回显里直说,避免被当成组件失效 */
function insetText(px: number): string {
return px > 0 ? px.toString() + 'px' : '0px(本机无安全区,看不出差异)';
}
const bottomParams = computed<string[]>(() => [
'safe-area-inset-bottom = ' + (insetBottom.value ? 'true' : 'false'),
'useSafeArea().bottom = ' + insetText(bottom.value),
'mode = bottom',
'length = 45%'
]);
const topParams = computed<string[]>(() => [
'safe-area-inset-top = ' + (insetTop.value ? 'true' : 'false'),
'statusBarHeight = ' + insetText(statusBarHeight.value),
'mode = top',
'length = 45%'
]);
const leftParams = computed<string[]>(() => [
'safe-area-inset-top = ' + (insetTop.value ? 'true' : 'false'),
'safe-area-inset-bottom = ' + (insetBottom.value ? 'true' : 'false'),
'mode = left',
'length = 70%'
]);
</script>
<style lang="scss">
/* 样式同 basic 片段;以下为贴边锚点与填充布局 */
.popup-body--fill {
flex: 1;
min-height: 0;
}
.popup-body-spacer {
flex: 1;
min-height: 0;
}
/* 贴边形态:按钮与标尺直接坐到内容区末端,安全区 padding 的差异才可见 */
.popup-body-action--flush {
padding: 16px 0 0 0;
}
.popup-safe-edge {
width: 100%;
height: 3px;
background-color: $u-type-primary;
}
</style>过渡时长与缩放 duration / zoom
duration 控制显隐动画时长(单位 ms),zoom 只在 mode=center 时生效。
查看源码
<template>
<ux-cell-group>
<ux-cell-item :title="zoom ? '居中缩放动画:开启' : '居中缩放动画:关闭'" :arrow="false" :border-bottom="false">
<ux-switch v-model="zoom"></ux-switch>
</ux-cell-item>
</ux-cell-group>
<text class="popup-level-label">过渡时长档位</text>
<view class="btn-wrap">
<ux-button v-for="(item, index) in durationList" :key="index" :type="duration == item.value ? 'primary' : 'default'" custom-class="btn-item" size="small" shape="round" :text="item.label" @click="duration = item.value"></ux-button>
</view>
<ux-button block custom-class="btn-open" shape="round" type="primary" text="打开动画弹窗" @click="show = true"></ux-button>
<!-- zoom 只在 mode=center 时生效,故本弹窗固定 center -->
<ux-popup v-model:show="show" mode="center" width="70%" :border-radius="12" :duration="duration" :zoom="zoom" :closeable="true" @closed="onClosed()" @opened="onOpened()">
<view class="popup-body">
<text class="popup-body-title">过渡时长与缩放</text>
<view class="popup-params">
<text v-for="(line, index) in params" :key="index" class="popup-params-item">{{ line }}</text>
</view>
<text class="popup-body-tip">关闭 zoom 后只剩位移不带缩放;打开时从 0.85 缩放弹到 1。</text>
<view class="popup-body-action">
<ux-button block type="primary" text="关闭弹窗" @click="show = false"></ux-button>
</view>
</view>
</ux-popup>
</template>
<script setup lang="uts">
const show = ref<boolean>(false);
const duration = ref<number>(300);
const zoom = ref<boolean>(true);
const durationList: { value: number; label: string }[] = [
{ label: '100ms', value: 100 },
{ label: '300ms', value: 300 },
{ label: '600ms', value: 600 }
];
function onOpened(): void {
// 进场动画播放完毕:演示页只做计数,不打日志
}
function onClosed(): void {
// 退场动画播放完毕且节点卸载:演示页只做计数,不打日志
}
const params = computed<string[]>(() => [
'duration = ' + duration.value.toString() + 'ms',
'zoom = ' + (zoom.value ? 'true' : 'false'),
'mode = center'
]);
</script>
<style lang="scss">
/* 样式同 basic 片段;档位小标签样式见 appearance 片段 */
</style>下拉关闭 show-drag-bar / close-on-slide-down / scroll-id
把手只要渲染出来就恒可下拉关闭;close-on-slide-down 只管内容区那一条路径。
查看源码
<template>
<ux-cell-group>
<ux-cell-item :title="dragBar ? '显示拖拽条把手' : '隐藏拖拽条把手'" :arrow="false">
<ux-switch v-model="dragBar"></ux-switch>
</ux-cell-item>
<ux-cell-item :title="slideClose ? '内容区下拉关闭:开启' : '内容区下拉关闭:关闭'" :arrow="false" :border-bottom="false">
<ux-switch v-model="slideClose"></ux-switch>
</ux-cell-item>
</ux-cell-group>
<text class="popup-level-label">下拉阈值档位</text>
<view class="btn-wrap">
<ux-button v-for="(item, index) in thresholdList" :key="index" :type="threshold == item.value ? 'primary' : 'default'" custom-class="btn-item" size="small" shape="round" :text="item.label" @click="threshold = item.value"></ux-button>
</view>
<ux-button block custom-class="btn-open" shape="round" type="primary" text="打开下拉关闭弹窗" @click="show = true"></ux-button>
<!-- 滚动区靠 flex:1 拿确定高度,内部 10 行编号内容确保真的可滚 -->
<ux-popup v-model:show="show" mode="bottom" length="65%" :border-radius="12" :show-drag-bar="dragBar" :close-on-slide-down="slideClose" :slide-down-threshold="threshold" scroll-id="popup-scroll-content" @closed="onClosed()" @opened="onOpened()">
<view class="popup-body popup-body--fill">
<text class="popup-body-title">下拉关闭手势</text>
<view class="popup-params">
<text v-for="(line, index) in params" :key="index" class="popup-params-item">{{ line }}</text>
</view>
<scroll-view id="popup-scroll-content" class="popup-scroll">
<view v-for="(row, index) in rows" :key="index" class="popup-scroll-row" :class="index == rows.length - 1 ? 'popup-scroll-row--last' : ''">
<text class="popup-scroll-row-no">{{ row }}</text>
<text class="popup-scroll-row-text">向上滑动可滚动;滚到顶部后继续下拉才触发关闭</text>
</view>
</scroll-view>
<view class="popup-body-action popup-body-action--flush">
<ux-button block type="primary" shape="square" :border-radius="0" text="关闭弹窗" @click="show = false"></ux-button>
</view>
</view>
</ux-popup>
</template>
<script setup lang="uts">
const show = ref<boolean>(false);
const dragBar = ref<boolean>(true);
const slideClose = ref<boolean>(true);
const threshold = ref<number>(80);
const thresholdList: { value: number; label: string }[] = [
{ label: '40px', value: 40 },
{ label: '80px', value: 80 },
{ label: '120px', value: 120 }
];
/** 行号列表:10 行 44px 明显高于可视区,滚动位移才看得见 */
const rows = computed<string[]>(() => {
const list: string[] = [];
for (let i = 1; i <= 10; i++) {
list.push(i.toString());
}
return list;
});
function onOpened(): void {
// 进场动画播放完毕:演示页只做计数,不打日志
}
function onClosed(): void {
// 退场动画播放完毕且节点卸载:演示页只做计数,不打日志
}
const params = computed<string[]>(() => [
'show-drag-bar = ' + (dragBar.value ? 'true' : 'false'),
'close-on-slide-down = ' + (slideClose.value ? 'true' : 'false'),
'slide-down-threshold = ' + threshold.value.toString() + 'px',
'scroll-id = popup-scroll-content'
]);
</script>
<style lang="scss">
/* 样式同 basic 片段(含 popup-body--fill / popup-body-action--flush) */
/* 滚动区必须拿到确定高度才能滚:父级已受限时用 flex:1,不能再叠 height(两者并存相当于
没有高度约束,滚动区会被内容撑开,反而滚不动) */
.popup-scroll {
flex: 1;
min-height: 0;
margin: 10px 16px 0 16px;
border: 1px solid $u-border-color;
border-radius: 6px;
}
.popup-scroll-row {
display: flex;
flex-direction: row;
align-items: center;
height: 44px;
padding: 0 10px;
border-bottom: 1px solid $u-border-color;
}
.popup-scroll-row--last {
border-bottom: none;
}
.popup-scroll-row-no {
font-size: 13px;
font-weight: bold;
color: $u-type-primary;
width: 26px;
}
.popup-scroll-row-text {
flex: 1;
min-width: 0;
font-size: 13px;
color: $u-content-color;
}
</style>关闭前拦截 before-close
before-close 返回 false(或 Promise<false>)时取消本次关闭:下面演示的第一次关闭被拦下,第二次才真正关闭。
查看源码
<template>
<ux-button block custom-class="btn-open" shape="round" type="primary" text="打开可拦截弹窗" @click="open()"></ux-button>
<!-- 只保留组件内部的关闭路径(关闭图标 / 遮罩点击 / 下拉关闭),直接回写 v-model 不经过拦截 -->
<ux-popup v-model:show="show" mode="bottom" length="35%" :border-radius="12" :before-close="guard" :closeable="true" @closed="onClosed()" @opened="onOpened()">
<view class="popup-body">
<text class="popup-body-title">关闭前拦截 before-close</text>
<view class="popup-params">
<text v-for="(line, index) in params" :key="index" class="popup-params-item">{{ line }}</text>
</view>
<text class="popup-body-tip">请点右上角关闭图标或遮罩来关闭:第一次会被拦下,第二次才真正关闭。</text>
</view>
</ux-popup>
</template>
<script setup lang="uts">
import type { BeforeChangeInterceptor } from '@/uni_modules/uview-pro-x';
const show = ref<boolean>(false);
const count = ref<number>(0);
function open(): void {
count.value = 0;
show.value = true;
}
/** 返回 false(或 Promise<false>)即取消本次关闭 */
const guard: BeforeChangeInterceptor = (): boolean => {
count.value = count.value + 1;
if (count.value == 1) {
uni.showToast({ title: '第一次关闭会被拦截', icon: 'none' });
return false;
}
count.value = 0;
return true;
};
function onOpened(): void {
// 进场动画播放完毕:演示页只做计数,不打日志
}
function onClosed(): void {
// 退场动画播放完毕且节点卸载:演示页只做计数,不打日志
}
const params = computed<string[]>(() => [
'before-close = interceptor()',
'已拦截次数 = ' + count.value.toString(),
'closeable = true'
]);
</script>
<style lang="scss">
/* 样式同 basic 片段 */
</style>受控关闭 popup=false
popup=false 时组件只派发 close、不回写 v-model,由父层自行关闭(ux-select / ux-picker 等嵌套消费方沿用此契约)。
查看源码
<template>
<ux-button block custom-class="btn-open" shape="round" type="primary" text="打开受控弹窗" @click="show = true"></ux-button>
<!-- popup=false 时组件只派发 close、不回写 v-model,由父层自行关闭(多层嵌套消费方沿用此契约) -->
<ux-popup v-model:show="show" mode="top" length="35%" :border-radius="12" :closeable="true" :popup="false" @close="onClose()" @closed="onClosed()" @opened="onOpened()">
<view class="popup-body">
<text class="popup-body-title">受控关闭 popup=false</text>
<view class="popup-params">
<text v-for="(line, index) in params" :key="index" class="popup-params-item">{{ line }}</text>
</view>
<text class="popup-body-tip">点关闭图标或遮罩只会派发 close 事件,页面回写 false 才会真正退场。</text>
</view>
</ux-popup>
</template>
<script setup lang="uts">
const show = ref<boolean>(false);
/** 组件只派发 close,本页负责回写 false 驱动退场动画 */
function onClose(): void {
show.value = false;
}
function onOpened(): void {
// 进场动画播放完毕:演示页只做计数,不打日志
}
function onClosed(): void {
// 退场动画播放完毕且节点卸载:演示页只做计数,不打日志
}
const params = computed<string[]>(() => [
'popup = false',
'mode = top',
'close 事件回写 = close → show=false'
]);
</script>
<style lang="scss">
/* 样式同 basic 片段 */
</style>事件计数 opened / closed
opened = 进场动画播完,closed = 退场动画播完且节点卸载,全部弹窗实例共用一组计数。计数在涨但看不见内容层,说明事件通道通、显隐通道断。
API
Props
| 属性 | 说明 | 类型 | 默认值 | 版本 |
|---|---|---|---|---|
customStyle | 自定义根节点样式 | string | UTSJSONObject | - | - |
customClass | 自定义根节点样式类 | string | '' | - |
mode | 弹出方向,left|right|top|bottom|center,inline 模式直接插入页面 | PopupMode | 'inline' | 'bottom' | - |
mask | 是否显示遮罩 | boolean | true | - |
length | 抽屉的宽度(mode=left|right),或者高度(mode=top|bottom),单位px,或"auto"/百分比 | string | number | 'auto' | - |
zoom | 是否开启缩放动画,只在 mode=center 时有效 | boolean | true | - |
safeAreaInsetBottom | 是否开启底部安全区适配(默认 true)。只在弹窗下沿贴屏幕底时生效:mode=bottom|left|right | boolean | true | - |
safeAreaInsetTop | 是否开启顶部安全区适配(默认 true)。只在弹窗上沿贴屏幕顶时生效:mode=top|left|right; 自定义导航栏隐藏原生导航栏时需避让状态栏(避让量取 statusBarHeight,不是 safeAreaInsets.top) | boolean | true | - |
maskCloseAble | 是否可以通过点击遮罩进行关闭 | boolean | true | - |
popup | 内部参数:true 时关闭路径由本组件回写 v-model(show 由 defineModel('show') 声明,不列入本类型); false 时只派发 close,由父层自行回写(多层嵌套消费方沿用旧契约) | boolean | true | - |
borderRadius | 圆角值 | string | number | 0 | - |
zIndex | 弹窗 z-index | string | number | '' | - |
closeable | 是否显示关闭图标 | boolean | false | - |
closeIcon | 关闭图标的名称 | string | 'close' | - |
closeIconPosition | 自定义关闭图标位置 | PopupCloseIconPos | 'top-right' | - |
closeIconColor | 关闭图标的颜色 | string | '' | - |
closeIconSize | 关闭图标的大小,单位px | string | number | '16' | - |
width | 弹窗宽度 | string | '' | - |
height | 弹窗高度 | string | '' | - |
marginTop | 中部弹出时的纵向偏移,正值下移、负值上移,支持 px/百分比 | string | number | 0 | - |
bgColor | 弹窗背景色,留空走主题变量 $u-bg-white | string | '' | - |
maskCustomStyle | 遮罩自定义样式 | UTSJSONObject | - | - |
duration | 动画时长,单位 ms | string | number | 300 | - |
closeOnSlideDown | 是否开启「内容区」下拉关闭(仅 mode=bottom 有效,默认 false)。 ⚠️ 只管内容区这一条路径,管不到拖拽条把手:把手只要渲染出来就恒可下拉关闭 (showDragBar=true 且 closeOnSlideDown=false 时,用户仍能拖把手关掉弹窗)。 推荐组合:showDragBar=true + closeOnSlideDown=true;想彻底关掉把手下拉,目前只能 showDragBar=false(不渲染)。 该重叠属既有行为,改动会影响已接入方的使用预期,2026-09-28 决策本轮不调整 | boolean | false | - |
slideDownThreshold | 触发下拉关闭的纵向位移阈值,单位 px(把手与内容区两条路径共用同一阈值)。 只有位移判定、没有速度判定,故快速轻扫若未达该位移不会关闭(手感偏「黏」),同上属既有行为 | number | 40 | - |
scrollId | 内容区滚动容器 id,下拉关闭时用于判断是否已滚到顶部。 ⚠️ 不传时视为「已在顶部」,内容区下拉手势会被接管成关闭动作,长列表滚不动 —— 内容区自带 scroll-view 时必须传本项。 另:本项只管手势归属,scroll-view 自身还必须有确定高度(父级高度受限 + flex:1), 否则它被内容撑开、没有可滚的溢出,同样滚不动 | string | '' | - |
showDragBar | 是否显示顶部拖拽条(仅 mode=bottom 有效),同时作为下拉关闭手势的把手。 本项只是「渲染」开关,把手的可拖性不受 closeOnSlideDown 约束(见该字段说明); 命中区是整条 32px 全宽容器,不是 25x4 的横条本体 | boolean | false | - |
beforeClose | 关闭前拦截器:返回 false 或 Promise<false> 时取消关闭 | BeforeChangeInterceptor | - | - |
Events
| 事件名 | 说明 | 回调参数 | 版本 |
|---|---|---|---|
open | 开始打开(起始态已绘制、终态写入前) | - | - |
opened | 打开动画结束 | - | - |
close | 开始收起(终态已写回起始值) | - | - |
closed | 收起动画结束、节点卸载 | - | - |
clickMask | 点击遮罩 | - | - |
Slots
| 插槽名 | 说明 | 版本 |
|---|---|---|
default | 弹窗内容 | - |
drag | 自定义顶部拖拽条(默认渲染横条把手) | - |
注意事项
- 显隐模型:内容层节点由
v-if创建/卸载,真实显示态(realShow)与受控show相差一段动画时长;打开时先绘制起始态一帧(nextTick+ 50ms)再命令式写终态,关闭时反向写回起始值并播完时长才卸载节点。 - 遮罩由
ux-mask承载,mask=true时常驻不卸载,显隐只由show驱动。 zoom与淡入淡出都只在mode='center'生效,其余方向只做位移。- 安全区:
safe-area-inset-bottom仅mode=bottom|left|right生效,safe-area-inset-top仅mode=top|left|right生效,且仅当对应安全区/状态栏高度大于 0 时才加 padding;顶部避让取statusBarHeight(自定义导航栏场景)。 - 下拉关闭仅在
mode='bottom'有效:把手(show-drag-bar=true)一经渲染即恒可下拉关闭,不受close-on-slide-down约束;close-on-slide-down只管内容区那条路径。 - 内容区自带
scroll-view时必须传scroll-id,否则手势会被接管成关闭动作、长列表滚不动;且滚动容器需有确定高度(父级受限 +flex:1),否则被内容撑开同样滚不动。 slide-down-threshold只判位移不判速度,快速轻扫若未达阈值不会关闭。before-close返回false或Promise<false>取消本次关闭;直接回写v-model不经过拦截。popup=false时组件只派发close、不回写v-model,需由父层自行关闭(ux-select/ux-picker等嵌套消费方沿用此契约)。- H5 端弹出期间会锁定页面滚动,多层弹窗按层计数,最后一层关闭时还原滚动位置;App 端内容层没有
max-height/overflow兜底,超出屏幕的内容需自行处理滚动。