use-popup.vue 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. <template>
  2. <view v-if="visibleSync" class="pos-f pos-full overflow-hidden use-popup" :class="{ 'use-popup-visible': showPopup }" :style="[customStyle]">
  3. <use-mask :show="showPopup && mask" :maskClickAble="maskCloseAble" @click="maskClick"></use-mask>
  4. <view class="pos-a use-popup-content"
  5. :class="[
  6. bgclass,
  7. safeAreaInsetBottom ? 'safe-area-inset-bottom' : '',
  8. 'use-popup-' + mode,
  9. showPopup ? 'use-popup-content-visible' : '',
  10. zoom && mode == 'center' ? 'use-animation-zoom' : ''
  11. ]" :style="[style]" @tap.stop.prevent @touchmove.stop.prevent @tap="modeCenterClose(mode)">
  12. <view v-if="mode == 'center'" class="bg-main pos-r use-mode-center-box" :style="[centerStyle]" @tap.stop.prevent @touchmove.stop.prevent>
  13. <slot />
  14. </view>
  15. <block v-else>
  16. <slot />
  17. </block>
  18. </view>
  19. </view>
  20. </template>
  21. <script>
  22. import useMask from '../use-mask/use-mask.vue'
  23. export default {
  24. components:{
  25. useMask
  26. },
  27. props: {
  28. /**
  29. * 显示状态
  30. */
  31. show: {
  32. type: Boolean,
  33. default: false
  34. },
  35. /**
  36. * 弹出方向,left|right|top|bottom|center
  37. */
  38. mode: {
  39. type: String,
  40. default: 'left'
  41. },
  42. /**
  43. * 是否显示遮罩
  44. */
  45. mask: {
  46. type: Boolean,
  47. default: true
  48. },
  49. /**
  50. * 背景 class 样式
  51. * */
  52. bgclass: {
  53. type: String,
  54. default: 'bg-main'
  55. },
  56. // 抽屉的宽度(mode=left|right),或者高度(mode=top|bottom),单位rpx,或者"auto"
  57. // 或者百分比"50%",表示由内容撑开高度或者宽度
  58. length: {
  59. type: [Number, String],
  60. default: 'auto'
  61. },
  62. // 是否开启缩放动画,只在mode=center时有效
  63. zoom: {
  64. type: Boolean,
  65. default: true
  66. },
  67. // 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
  68. safeAreaInsetBottom: {
  69. type: Boolean,
  70. default: false
  71. },
  72. // 是否可以通过点击遮罩进行关闭
  73. maskCloseAble: {
  74. type: Boolean,
  75. default: true
  76. },
  77. // 用户自定义样式
  78. customStyle: {
  79. type: Object,
  80. default () {
  81. return {};
  82. }
  83. },
  84. value: {
  85. type: Boolean,
  86. default: false
  87. },
  88. // 此为内部参数,不在文档对外使用,为了解决Picker和keyboard等融合了弹窗的组件
  89. // 对v-model双向绑定多层调用造成报错不能修改props值的问题
  90. popup: {
  91. type: Boolean,
  92. default: true
  93. },
  94. // 显示显示弹窗的圆角,单位rpx
  95. borderRadius: {
  96. type: [Number, String],
  97. default: 0
  98. },
  99. zIndex: {
  100. type: [Number, String],
  101. default: '10020'
  102. }
  103. },
  104. data() {
  105. return {
  106. visibleSync: false,
  107. showPopup: false,
  108. };
  109. },
  110. watch: {
  111. value(val) {
  112. if (val) {
  113. this.open();
  114. } else {
  115. if (this.showPopup) this.close();
  116. }
  117. }
  118. },
  119. computed: {
  120. // 根据mode的位置,设定其弹窗的宽度(mode = left|right),或者高度(mode = top|bottom)
  121. style() {
  122. let style = {};
  123. let translate = '100%';
  124. // 判断是否是否百分比或者auto值,是的话,直接使用该值,否则默认为rpx单位的数值
  125. let length = (/%$/.test(this.length) || this.length == 'auto') ? this.length : uni.upx2px(this.length) + 'px';
  126. // 如果是左边或者上边弹出时,需要给translate设置为负值,用于隐藏
  127. if (this.mode == 'left' || this.mode == 'top') {
  128. translate = length == 'auto' ? '-100%' : '-' + length;
  129. }
  130. if (this.mode == 'left' || this.mode == 'right') {
  131. style = {
  132. width: length,
  133. height: '100%',
  134. transform: `translate3D(${translate},0px,0px)`
  135. };
  136. } else if (this.mode == 'top' || this.mode == 'bottom') {
  137. style = {
  138. width: '100%',
  139. height: length,
  140. transform: `translate3D(0px,${translate},0px)`
  141. };
  142. }
  143. style.zIndex = this.zIndex;
  144. // 如果用户设置了borderRadius值,添加弹窗的圆角
  145. if (this.borderRadius) {
  146. switch (this.mode) {
  147. case 'top':
  148. style.borderRadius = `0 0 ${this.borderRadius}rpx ${this.borderRadius}rpx`;
  149. break;
  150. case 'right':
  151. style.borderRadius = `${this.borderRadius}rpx 0 0 ${this.borderRadius}rpx`;
  152. break;
  153. case 'bottom':
  154. style.borderRadius = `${this.borderRadius}rpx ${this.borderRadius}rpx 0 0`;
  155. break;
  156. case 'left':
  157. style.borderRadius = `0 ${this.borderRadius}rpx ${this.borderRadius}rpx 0`;
  158. break;
  159. default:
  160. break;
  161. }
  162. // 不加可能圆角无效
  163. style.overflow = 'hidden';
  164. }
  165. return style;
  166. },
  167. // 中部弹窗的特有样式
  168. centerStyle() {
  169. let style = {};
  170. let length = (/%$/.test(this.length) || this.length == 'auto') ? this.length : uni.upx2px(this.length) + 'px';
  171. style.width = length;
  172. style.zIndex = this.zIndex;
  173. if (this.borderRadius) {
  174. style.borderRadius = `${this.borderRadius}rpx`;
  175. // 不加可能圆角无效
  176. style.overflow = 'hidden';
  177. }
  178. return style;
  179. }
  180. },
  181. created() {
  182. // 先让弹窗组件渲染,再改变遮罩和抽屉元素的样式,让其动画其起作用(必须要有延时,才会有效果)
  183. this.visibleSync = this.value;
  184. this.$api.timerout(() => {
  185. this.showPopup = this.value;
  186. }, 30);
  187. },
  188. methods: {
  189. open() {
  190. this.change('visibleSync', 'showPopup', true);
  191. },
  192. close() {
  193. this.change('showPopup', 'visibleSync', false);
  194. },
  195. // 遮罩被点击
  196. maskClick() {
  197. this.close();
  198. },
  199. // 中部弹出时,需要.use-popup-content将居中内容,此元素会铺满屏幕,点击需要关闭弹窗
  200. // 让其只在mode=center时起作用
  201. modeCenterClose(mode) {
  202. if (mode != 'center' || !this.maskCloseAble) return;
  203. this.close();
  204. },
  205. // 此处的原理是,关闭时先通过动画隐藏弹窗和遮罩,再移除整个组件
  206. // 打开时,先渲染组件,延时一定时间再让遮罩和弹窗的动画起作用
  207. change(param1, param2, state) {
  208. // 如果this.popup为false,以为着为picker,actionsheet等组件调用了popup组件
  209. if (this.popup) this.$emit('input', state);
  210. this[param1] = state;
  211. this.$api.timerout(() => {
  212. this[param2] = state;
  213. this.$emit(state ? 'open' : 'close');
  214. }, state ? 30 : 100);
  215. }
  216. }
  217. };
  218. </script>
  219. <style lang="scss">
  220. .use-popup {
  221. z-index: 10010;
  222. }
  223. .use-popup-content {
  224. z-index: 10020;
  225. transition: all 0.25s ease-in-out;
  226. }
  227. .use-popup-top {
  228. top: 0;
  229. right: 0;
  230. left: 0;
  231. }
  232. .use-popup-right {
  233. top: 0;
  234. right: 0;
  235. bottom: 0;
  236. }
  237. .use-popup-bottom {
  238. right: 0;
  239. bottom: 0;
  240. left: 0;
  241. }
  242. .use-popup-left {
  243. top: 0;
  244. bottom: 0;
  245. left: 0;
  246. }
  247. .use-popup-center {
  248. display: flex;
  249. align-items: center;
  250. justify-content: center;
  251. flex-direction: column;
  252. top: 0;
  253. right: 0;
  254. bottom: 0;
  255. left: 0;
  256. opacity: 0;
  257. z-index: 99999;
  258. }
  259. .use-mode-center-box {
  260. min-width: 100rpx;
  261. min-height: 100rpx;
  262. }
  263. .use-popup-content-visible.use-popup-center {
  264. transform: scale(1);
  265. opacity: 1;
  266. }
  267. .use-animation-zoom {
  268. transform: scale(1.15);
  269. }
  270. .use-popup-content-visible {
  271. transform: translate3D(0px, 0px, 0px) !important;
  272. }
  273. </style>