use-popup.vue 6.9 KB

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