blob: 69c0ec8d2d00e606b81aa7a5a952b18920c419af [file] [log] [blame]
guangchao.xu070005a2020-12-07 09:56:40 +08001<template>
2 <view v-if="visibleSync" :style="[customStyle, {
3 zIndex: uZindex - 1
4 }]" class="u-drawer" hover-stop-propagation>
5 <u-mask :duration="duration" :custom-style="maskCustomStyle" :maskClickAble="maskCloseAble" :z-index="uZindex - 2" :show="showDrawer && mask" @click="maskClick"></u-mask>
6 <view
7 class="u-drawer-content"
8 @tap="modeCenterClose(mode)"
9 :class="[
10 safeAreaInsetBottom ? 'safe-area-inset-bottom' : '',
11 'u-drawer-' + mode,
12 showDrawer ? 'u-drawer-content-visible' : '',
13 zoom && mode == 'center' ? 'u-animation-zoom' : ''
14 ]"
15 @touchmove.stop.prevent
16 @tap.stop.prevent
17 :style="[style]"
18 >
19 <view class="u-mode-center-box" @tap.stop.prevent @touchmove.stop.prevent v-if="mode == 'center'" :style="[centerStyle]">
20 <u-icon
21 @click="close"
22 v-if="closeable"
23 class="u-close"
24 :class="['u-close--' + closeIconPos]"
25 :name="closeIcon"
26 :color="closeIconColor"
27 :size="closeIconSize"
28 ></u-icon>
29 <scroll-view class="u-drawer__scroll-view" scroll-y="true">
30 <slot />
31 </scroll-view>
32 </view>
33 <scroll-view class="u-drawer__scroll-view" scroll-y="true" v-else>
34 <slot />
35 </scroll-view>
36 <view @tap="close" class="u-close" :class="['u-close--' + closeIconPos]">
37 <u-icon
38 v-if="mode != 'center' && closeable"
39 :name="closeIcon"
40 :color="closeIconColor"
41 :size="closeIconSize"
42 ></u-icon>
43 </view>
44 </view>
45 </view>
46</template>
47
48<script>
49/**
50 * popup 弹窗
51 * @description 弹出层容器,用于展示弹窗、信息提示等内容,支持上、下、左、右和中部弹出。组件只提供容器,内部内容由用户自定义
52 * @tutorial https://www.uviewui.com/components/popup.html
53 * @property {String} mode 弹出方向(默认left)
54 * @property {Boolean} mask 是否显示遮罩(默认true)
55 * @property {Stringr | Number} length mode=left | 见官网说明(默认auto)
56 * @property {Boolean} zoom 是否开启缩放动画,只在mode为center时有效(默认true)
57 * @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
58 * @property {Boolean} mask-close-able 点击遮罩是否可以关闭弹出层(默认true)
59 * @property {Object} custom-style 用户自定义样式
60 * @property {Stringr | Number} negative-top 中部弹出时,往上偏移的值
61 * @property {Numberr | String} border-radius 弹窗圆角值(默认0)
62 * @property {Numberr | String} z-index 弹出内容的z-index值(默认1075)
63 * @property {Boolean} closeable 是否显示关闭图标(默认false)
64 * @property {String} close-icon 关闭图标的名称,只能uView的内置图标
65 * @property {String} close-icon-pos 自定义关闭图标位置(默认top-right)
66 * @property {String} close-icon-color 关闭图标的颜色(默认#909399)
67 * @property {Number | String} close-icon-size 关闭图标的大小,单位rpx(默认30)
68 * @event {Function} open 弹出层打开
69 * @event {Function} close 弹出层收起
70 * @example <u-popup v-model="show"><view>出淤泥而不染,濯清涟而不妖</view></u-popup>
71 */
72export default {
73 name: 'u-popup',
74 props: {
75 /**
76 * 显示状态
77 */
78 show: {
79 type: Boolean,
80 default: false
81 },
82 /**
83 * 弹出方向,left|right|top|bottom|center
84 */
85 mode: {
86 type: String,
87 default: 'left'
88 },
89 /**
90 * 是否显示遮罩
91 */
92 mask: {
93 type: Boolean,
94 default: true
95 },
96 // 抽屉的宽度(mode=left|right),或者高度(mode=top|bottom),单位rpx,或者"auto"
97 // 或者百分比"50%",表示由内容撑开高度或者宽度
98 length: {
99 type: [Number, String],
100 default: 'auto'
101 },
102 // 是否开启缩放动画,只在mode=center时有效
103 zoom: {
104 type: Boolean,
105 default: true
106 },
107 // 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
108 safeAreaInsetBottom: {
109 type: Boolean,
110 default: false
111 },
112 // 是否可以通过点击遮罩进行关闭
113 maskCloseAble: {
114 type: Boolean,
115 default: true
116 },
117 // 用户自定义样式
118 customStyle: {
119 type: Object,
120 default() {
121 return {};
122 }
123 },
124 value: {
125 type: Boolean,
126 default: false
127 },
128 // 此为内部参数,不在文档对外使用,为了解决Picker和keyboard等融合了弹窗的组件
129 // 对v-model双向绑定多层调用造成报错不能修改props值的问题
130 popup: {
131 type: Boolean,
132 default: true
133 },
134 // 显示显示弹窗的圆角,单位rpx
135 borderRadius: {
136 type: [Number, String],
137 default: 0
138 },
139 zIndex: {
140 type: [Number, String],
141 default: ''
142 },
143 // 是否显示关闭图标
144 closeable: {
145 type: Boolean,
146 default: false
147 },
148 // 关闭图标的名称,只能uView的内置图标
149 closeIcon: {
150 type: String,
151 default: 'close'
152 },
153 // 自定义关闭图标位置,top-left为左上角,top-right为右上角,bottom-left为左下角,bottom-right为右下角
154 closeIconPos: {
155 type: String,
156 default: 'top-right'
157 },
158 // 关闭图标的颜色
159 closeIconColor: {
160 type: String,
161 default: '#909399'
162 },
163 // 关闭图标的大小,单位rpx
164 closeIconSize: {
165 type: [String, Number],
166 default: '30'
167 },
168 // 宽度,只对左,右,中部弹出时起作用,单位rpx,或者"auto"
169 // 或者百分比"50%",表示由内容撑开高度或者宽度,优先级高于length参数
170 width: {
171 type: String,
172 default: ''
173 },
174 // 高度,只对上,下,中部弹出时起作用,单位rpx,或者"auto"
175 // 或者百分比"50%",表示由内容撑开高度或者宽度,优先级高于length参数
176 height: {
177 type: String,
178 default: ''
179 },
180 // 给一个负的margin-top,往上偏移,避免和键盘重合的情况,仅在mode=center时有效
181 negativeTop: {
182 type: [String, Number],
183 default: 0
184 },
185 // 遮罩的样式,一般用于修改遮罩的透明度
186 maskCustomStyle: {
187 type: Object,
188 default() {
189 return {}
190 }
191 },
192 // 遮罩打开或收起的动画过渡时间,单位ms
193 duration: {
194 type: [String, Number],
195 default: 250
196 }
197 },
198 data() {
199 return {
200 visibleSync: false,
201 showDrawer: false,
202 timer: null,
203 closeFromInner: false, // value的值改变,是发生在内部还是外部
204 };
205 },
206 computed: {
207 // 根据mode的位置,设定其弹窗的宽度(mode = left|right),或者高度(mode = top|bottom)
208 style() {
209 let style = {};
210 // 如果是左边或者上边弹出时,需要给translate设置为负值,用于隐藏
211 if (this.mode == 'left' || this.mode == 'right') {
212 style = {
213 width: this.width ? this.getUnitValue(this.width) : this.getUnitValue(this.length),
214 height: '100%',
215 transform: `translate3D(${this.mode == 'left' ? '-100%' : '100%'},0px,0px)`
216 };
217 } else if (this.mode == 'top' || this.mode == 'bottom') {
218 style = {
219 width: '100%',
220 height: this.height ? this.getUnitValue(this.height) : this.getUnitValue(this.length),
221 transform: `translate3D(0px,${this.mode == 'top' ? '-100%' : '100%'},0px)`
222 };
223 }
224 style.zIndex = this.uZindex;
225 // 如果用户设置了borderRadius值,添加弹窗的圆角
226 if (this.borderRadius) {
227 switch (this.mode) {
228 case 'left':
229 style.borderRadius = `0 ${this.borderRadius}rpx ${this.borderRadius}rpx 0`;
230 break;
231 case 'top':
232 style.borderRadius = `0 0 ${this.borderRadius}rpx ${this.borderRadius}rpx`;
233 break;
234 case 'right':
235 style.borderRadius = `${this.borderRadius}rpx 0 0 ${this.borderRadius}rpx`;
236 break;
237 case 'bottom':
238 style.borderRadius = `${this.borderRadius}rpx ${this.borderRadius}rpx 0 0`;
239 break;
240 default:
241 }
242 // 不加可能圆角无效
243 style.overflow = 'hidden';
244 }
245 if(this.duration) style.transition = `all ${this.duration / 1000}s linear`;
246 return style;
247 },
248 // 中部弹窗的特有样式
249 centerStyle() {
250 let style = {};
251 style.width = this.width ? this.getUnitValue(this.width) : this.getUnitValue(this.length);
252 // 中部弹出的模式,如果没有设置高度,就用auto值,由内容撑开高度
253 style.height = this.height ? this.getUnitValue(this.height) : 'auto';
254 style.zIndex = this.uZindex;
255 style.marginTop = `-${this.$u.addUnit(this.negativeTop)}`;
256 if (this.borderRadius) {
257 style.borderRadius = `${this.borderRadius}rpx`;
258 // 不加可能圆角无效
259 style.overflow = 'hidden';
260 }
261 return style;
262 },
263 // 计算整理后的z-index值
264 uZindex() {
265 return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
266 }
267 },
268 watch: {
269 value(val) {
270 if (val) {
271 this.open();
272 } else if(!this.closeFromInner) {
273 this.close();
274 }
275 this.closeFromInner = false;
276 }
277 },
278 mounted() {
279 // 组件渲染完成时,检查value是否为true,如果是,弹出popup
280 this.value && this.open();
281 },
282 methods: {
283 // 判断传入的值,是否带有单位,如果没有,就默认用rpx单位
284 getUnitValue(val) {
285 if(/(%|px|rpx|auto)$/.test(val)) return val;
286 else return val + 'rpx'
287 },
288 // 遮罩被点击
289 maskClick() {
290 this.close();
291 },
292 close() {
293 // 标记关闭是内部发生的,否则修改了value值,导致watch中对value检测,导致再执行一遍close
294 // 造成@close事件触发两次
295 this.closeFromInner = true;
296 this.change('showDrawer', 'visibleSync', false);
297 },
298 // 中部弹出时,需要.u-drawer-content将居中内容,此元素会铺满屏幕,点击需要关闭弹窗
299 // 让其只在mode=center时起作用
300 modeCenterClose(mode) {
301 if (mode != 'center' || !this.maskCloseAble) return;
302 this.close();
303 },
304 open() {
305 this.change('visibleSync', 'showDrawer', true);
306 },
307 // 此处的原理是,关闭时先通过动画隐藏弹窗和遮罩,再移除整个组件
308 // 打开时,先渲染组件,延时一定时间再让遮罩和弹窗的动画起作用
309 change(param1, param2, status) {
310 // 如果this.popup为false,意味着为picker,actionsheet等组件调用了popup组件
311 if (this.popup == true) {
312 this.$emit('input', status);
313 }
314 this[param1] = status;
315 if(status) {
316 // #ifdef H5 || MP
317 this.timer = setTimeout(() => {
318 this[param2] = status;
319 this.$emit(status ? 'open' : 'close');
320 }, 50);
321 // #endif
322 // #ifndef H5 || MP
323 this.$nextTick(() => {
324 this[param2] = status;
325 this.$emit(status ? 'open' : 'close');
326 })
327 // #endif
328 } else {
329 this.timer = setTimeout(() => {
330 this[param2] = status;
331 this.$emit(status ? 'open' : 'close');
332 }, this.duration);
333 }
334 }
335 }
336};
337</script>
338
339<style scoped lang="scss">
340@import "../../libs/css/style.components.scss";
341
342.u-drawer {
343 /* #ifndef APP-NVUE */
344 display: block;
345 /* #endif */
346 position: fixed;
347 top: 0;
348 left: 0;
349 right: 0;
350 bottom: 0;
351 overflow: hidden;
352}
353
354.u-drawer-content {
355 /* #ifndef APP-NVUE */
356 display: block;
357 /* #endif */
358 position: absolute;
359 z-index: 1003;
360 transition: all 0.25s linear;
361}
362
363.u-drawer__scroll-view {
364 width: 100%;
365 height: 100%;
366}
367
368.u-drawer-left {
369 top: 0;
370 bottom: 0;
371 left: 0;
372 background-color: #ffffff;
373}
374
375.u-drawer-right {
376 right: 0;
377 top: 0;
378 bottom: 0;
379 background-color: #ffffff;
380}
381
382.u-drawer-top {
383 top: 0;
384 left: 0;
385 right: 0;
386 background-color: #ffffff;
387}
388
389.u-drawer-bottom {
390 bottom: 0;
391 left: 0;
392 right: 0;
393 background-color: #ffffff;
394}
395
396.u-drawer-center {
397 @include vue-flex;
398 flex-direction: column;
399 bottom: 0;
400 left: 0;
401 right: 0;
402 top: 0;
403 justify-content: center;
404 align-items: center;
405 opacity: 0;
406 z-index: 99999;
407}
408
409.u-mode-center-box {
410 min-width: 100rpx;
411 min-height: 100rpx;
412 /* #ifndef APP-NVUE */
413 display: block;
414 /* #endif */
415 position: relative;
416 background-color: #ffffff;
417}
418
419.u-drawer-content-visible.u-drawer-center {
420 transform: scale(1);
421 opacity: 1;
422}
423
424.u-animation-zoom {
425 transform: scale(1.15);
426}
427
428.u-drawer-content-visible {
429 transform: translate3D(0px, 0px, 0px) !important;
430}
431
432.u-close {
433 position: absolute;
434 z-index: 3;
435}
436
437.u-close--top-left {
438 top: 30rpx;
439 left: 30rpx;
440}
441
442.u-close--top-right {
443 top: 30rpx;
444 right: 30rpx;
445}
446
447.u-close--bottom-left {
448 bottom: 30rpx;
449 left: 30rpx;
450}
451
452.u-close--bottom-right {
453 right: 30rpx;
454 bottom: 30rpx;
455}
456</style>