tn-popup.vue 13 KB

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