uv-popup.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. <template>
  2. <view
  3. v-if="showPopup"
  4. class="uv-popup"
  5. :class="[popupClass, isDesktop ? 'fixforpc-z-index' : '']"
  6. >
  7. <view @touchstart="touchstart">
  8. <!-- 遮罩层 -->
  9. <uv-overlay
  10. key="1"
  11. v-if="maskShow && overlay"
  12. :show="showTrans"
  13. :duration="duration"
  14. :custom-style="overlayStyle"
  15. :opacity="overlayOpacity"
  16. :zIndex="zIndex"
  17. @click="onTap"
  18. ></uv-overlay>
  19. <uv-transition
  20. key="2"
  21. :mode="ani"
  22. name="content"
  23. :custom-style="transitionStyle"
  24. :duration="duration"
  25. :show="showTrans"
  26. @click="onTap"
  27. >
  28. <view
  29. class="uv-popup__content"
  30. :style="[contentStyle]"
  31. :class="[popupClass]"
  32. @click="clear"
  33. >
  34. <uv-status-bar v-if="safeAreaInsetTop"></uv-status-bar>
  35. <slot />
  36. <uv-safe-bottom v-if="safeAreaInsetBottom"></uv-safe-bottom>
  37. <view
  38. v-if="closeable"
  39. @tap.stop="close"
  40. class="uv-popup__content__close"
  41. :class="['uv-popup__content__close--' + closeIconPos]"
  42. hover-class="uv-popup__content__close--hover"
  43. hover-stay-time="150"
  44. >
  45. <uv-icon
  46. name="close"
  47. color="#909399"
  48. size="18"
  49. bold
  50. ></uv-icon>
  51. </view>
  52. </view>
  53. </uv-transition>
  54. </view>
  55. <!-- #ifdef H5 -->
  56. <keypress v-if="maskShow" @esc="onTap" />
  57. <!-- #endif -->
  58. </view>
  59. </template>
  60. <script>
  61. // #ifdef H5
  62. import keypress from './keypress.js'
  63. // #endif
  64. import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
  65. import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
  66. /**
  67. * PopUp 弹出层
  68. * @description 弹出层组件,为了解决遮罩弹层的问题
  69. * @tutorial https://www.uvui.cn/components/popup.html
  70. * @property {String} mode = [top|center|bottom|left|right] 弹出方式
  71. * @value top 顶部弹出
  72. * @value center 中间弹出
  73. * @value bottom 底部弹出
  74. * @value left 左侧弹出
  75. * @value right 右侧弹出
  76. * @property {Number} duration 动画时长,默认300
  77. * @property {Boolean} overlay 是否显示遮罩,默认true
  78. * @property {Boolean} overlayOpacity 遮罩透明度,默认0.5
  79. * @property {Object} overlayStyle 遮罩自定义样式
  80. * @property {Boolean} closeOnClickOverlay = [true|false] 蒙版点击是否关闭弹窗,默认true
  81. * @property {Number | String} zIndex 弹出层的层级
  82. * @property {Boolean} safeAreaInsetTop 是否留出顶部安全区(状态栏高度),默认false
  83. * @property {Boolean} safeAreaInsetBottom 是否为留出底部安全区适配,默认true
  84. * @property {Boolean} closeable 是否显示关闭图标,默认false
  85. * @property {Boolean} closeIconPos 自定义关闭图标位置,`top-left`-左上角,`top-right`-右上角,`bottom-left`-左下角,`bottom-right`-右下角,默认top-right
  86. * @property {String} bgColor 主窗口背景色
  87. * @property {String} maskBackgroundColor 蒙版颜色
  88. * @property {Boolean} customStyle 自定义样式
  89. * @event {Function} change 打开关闭弹窗触发,e={show: false}
  90. * @event {Function} maskClick 点击遮罩触发
  91. */
  92. export default {
  93. name: 'uv-popup',
  94. components: {
  95. // #ifdef H5
  96. keypress
  97. // #endif
  98. },
  99. mixins: [mpMixin, mixin],
  100. emits: ['change', 'maskClick'],
  101. props: {
  102. // 弹出层类型,可选值,top: 顶部弹出层;bottom:底部弹出层;center:全屏弹出层
  103. // message: 消息提示 ; dialog : 对话框
  104. mode: {
  105. type: String,
  106. default: 'center'
  107. },
  108. // 动画时长,单位ms
  109. duration: {
  110. type: [String, Number],
  111. default: 300
  112. },
  113. // 层级
  114. zIndex: {
  115. type: [String, Number],
  116. default: 10075
  117. },
  118. bgColor: {
  119. type: String,
  120. default: '#ffffff'
  121. },
  122. safeArea: {
  123. type: Boolean,
  124. default: true
  125. },
  126. // 是否显示遮罩
  127. overlay: {
  128. type: Boolean,
  129. default: true
  130. },
  131. // 点击遮罩是否关闭弹窗
  132. closeOnClickOverlay: {
  133. type: Boolean,
  134. default: true
  135. },
  136. // 遮罩的透明度,0-1之间
  137. overlayOpacity: {
  138. type: [Number, String],
  139. default: 0.4
  140. },
  141. // 自定义遮罩的样式
  142. overlayStyle: {
  143. type: [Object, String],
  144. default: ''
  145. },
  146. // 是否为iPhoneX留出底部安全距离
  147. safeAreaInsetBottom: {
  148. type: Boolean,
  149. default: true
  150. },
  151. // 是否留出顶部安全距离(状态栏高度)
  152. safeAreaInsetTop: {
  153. type: Boolean,
  154. default: false
  155. },
  156. // 是否显示关闭图标
  157. closeable: {
  158. type: Boolean,
  159. default: false
  160. },
  161. // 自定义关闭图标位置,top-left为左上角,top-right为右上角,bottom-left为左下角,bottom-right为右下角
  162. closeIconPos: {
  163. type: String,
  164. default: 'top-right'
  165. },
  166. // mode=center,也即中部弹出时,是否使用缩放模式
  167. zoom: {
  168. type: Boolean,
  169. default: true
  170. },
  171. round: {
  172. type: [Number, String],
  173. default: 0
  174. }
  175. },
  176. watch: {
  177. /**
  178. * 监听type类型
  179. */
  180. type: {
  181. handler: function(type) {
  182. if (!this.config[type]) return
  183. this[this.config[type]](true)
  184. },
  185. immediate: true
  186. },
  187. isDesktop: {
  188. handler: function(newVal) {
  189. if (!this.config[newVal]) return
  190. this[this.config[this.mode]](true)
  191. },
  192. immediate: true
  193. },
  194. // H5 下禁止底部滚动
  195. showPopup(show) {
  196. // #ifdef H5
  197. // fix by mehaotian 处理 h5 滚动穿透的问题
  198. document.getElementsByTagName('body')[0].style.overflow = show ? 'hidden' : 'visible'
  199. // #endif
  200. }
  201. },
  202. data() {
  203. return {
  204. ani: [],
  205. showPopup: false,
  206. showTrans: false,
  207. popupWidth: 0,
  208. popupHeight: 0,
  209. config: {
  210. top: 'top',
  211. bottom: 'bottom',
  212. center: 'center',
  213. left: 'left',
  214. right: 'right',
  215. message: 'top',
  216. dialog: 'center',
  217. share: 'bottom'
  218. },
  219. transitionStyle: {
  220. position: 'fixed',
  221. left: 0,
  222. right: 0
  223. },
  224. maskShow: true,
  225. mkclick: true,
  226. popupClass: this.isDesktop ? 'fixforpc-top' : 'top'
  227. }
  228. },
  229. computed: {
  230. isDesktop() {
  231. return this.popupWidth >= 500 && this.popupHeight >= 500
  232. },
  233. bg() {
  234. if (this.bgColor === '' || this.bgColor === 'none' || this.$uv.getPx(this.round)>0) {
  235. return 'transparent'
  236. }
  237. return this.bgColor
  238. },
  239. contentStyle() {
  240. const style = {};
  241. if (this.bgColor) {
  242. style.backgroundColor = this.bg
  243. }
  244. if(this.round) {
  245. const value = this.$uv.addUnit(this.round)
  246. style.backgroundColor = this.bgColor
  247. if(this.mode === 'top') {
  248. style.borderBottomLeftRadius = value
  249. style.borderBottomRightRadius = value
  250. } else if(this.mode === 'bottom') {
  251. style.borderTopLeftRadius = value
  252. style.borderTopRightRadius = value
  253. } else if(this.mode === 'center') {
  254. style.borderRadius = value
  255. }
  256. }
  257. return this.$uv.deepMerge(style, this.$uv.addStyle(this.customStyle))
  258. }
  259. },
  260. // #ifndef VUE3
  261. // TODO vue2
  262. destroyed() {
  263. this.setH5Visible()
  264. },
  265. // #endif
  266. // #ifdef VUE3
  267. // TODO vue3
  268. unmounted() {
  269. this.setH5Visible()
  270. },
  271. // #endif
  272. created() {
  273. // TODO 处理 message 组件生命周期异常的问题
  274. this.messageChild = null
  275. // TODO 解决头条冒泡的问题
  276. this.clearPropagation = false
  277. },
  278. methods: {
  279. setH5Visible() {
  280. // #ifdef H5
  281. // fix by mehaotian 处理 h5 滚动穿透的问题
  282. document.getElementsByTagName('body')[0].style.overflow = 'visible'
  283. // #endif
  284. },
  285. /**
  286. * 公用方法,不显示遮罩层
  287. */
  288. closeMask() {
  289. this.maskShow = false
  290. },
  291. // TODO nvue 取消冒泡
  292. clear(e) {
  293. // #ifndef APP-NVUE
  294. e.stopPropagation()
  295. // #endif
  296. this.clearPropagation = true
  297. },
  298. open(direction) {
  299. // fix by mehaotian 处理快速打开关闭的情况
  300. if (this.showPopup) {
  301. return
  302. }
  303. let innerType = ['top', 'center', 'bottom', 'left', 'right', 'message', 'dialog', 'share']
  304. if (!(direction && innerType.indexOf(direction) !== -1)) {
  305. direction = this.mode
  306. }
  307. if (!this.config[direction]) {
  308. return this.$uv.error(`缺少类型:${direction}`);
  309. }
  310. this[this.config[direction]]()
  311. this.$emit('change', {
  312. show: true,
  313. type: direction
  314. })
  315. },
  316. close(type) {
  317. this.showTrans = false
  318. this.$emit('change', {
  319. show: false,
  320. type: this.mode
  321. })
  322. clearTimeout(this.timer)
  323. // // 自定义关闭事件
  324. this.timer = setTimeout(() => {
  325. this.showPopup = false
  326. }, 300)
  327. },
  328. // TODO 处理冒泡事件,头条的冒泡事件有问题 ,先这样兼容
  329. touchstart() {
  330. this.clearPropagation = false
  331. },
  332. onTap() {
  333. if (this.clearPropagation) {
  334. // fix by mehaotian 兼容 nvue
  335. this.clearPropagation = false
  336. return
  337. }
  338. this.$emit('maskClick')
  339. if (!this.closeOnClickOverlay) return
  340. this.close()
  341. },
  342. /**
  343. * 顶部弹出样式处理
  344. */
  345. top(type) {
  346. this.popupClass = this.isDesktop ? 'fixforpc-top' : 'top'
  347. this.ani = ['slide-top']
  348. this.transitionStyle = {
  349. position: 'fixed',
  350. zIndex: this.zIndex,
  351. left: 0,
  352. right: 0,
  353. backgroundColor: this.bg
  354. }
  355. // TODO 兼容 type 属性 ,后续会废弃
  356. if (type) return
  357. this.showPopup = true
  358. this.showTrans = true
  359. this.$nextTick(() => {
  360. if (this.messageChild && this.mode === 'message') {
  361. this.messageChild.timerClose()
  362. }
  363. })
  364. },
  365. /**
  366. * 底部弹出样式处理
  367. */
  368. bottom(type) {
  369. this.popupClass = 'bottom'
  370. this.ani = ['slide-bottom']
  371. this.transitionStyle = {
  372. position: 'fixed',
  373. zIndex: this.zIndex,
  374. left: 0,
  375. right: 0,
  376. bottom: 0,
  377. backgroundColor: this.bg
  378. }
  379. // TODO 兼容 type 属性 ,后续会废弃
  380. if (type) return
  381. this.showPopup = true
  382. this.showTrans = true
  383. },
  384. /**
  385. * 中间弹出样式处理
  386. */
  387. center(type) {
  388. this.popupClass = 'center'
  389. this.ani = this.zoom?['zoom-in', 'fade']:['fade'];
  390. this.transitionStyle = {
  391. position: 'fixed',
  392. zIndex: this.zIndex,
  393. /* #ifndef APP-NVUE */
  394. display: 'flex',
  395. flexDirection: 'column',
  396. /* #endif */
  397. bottom: 0,
  398. left: 0,
  399. right: 0,
  400. top: 0,
  401. justifyContent: 'center',
  402. alignItems: 'center'
  403. }
  404. // TODO 兼容 type 属性 ,后续会废弃
  405. if (type) return
  406. this.showPopup = true
  407. this.showTrans = true
  408. },
  409. left(type) {
  410. this.popupClass = 'left'
  411. this.ani = ['slide-left']
  412. this.transitionStyle = {
  413. position: 'fixed',
  414. zIndex: this.zIndex,
  415. left: 0,
  416. bottom: 0,
  417. top: 0,
  418. backgroundColor: this.bg,
  419. /* #ifndef APP-NVUE */
  420. display: 'flex',
  421. flexDirection: 'column'
  422. /* #endif */
  423. }
  424. // TODO 兼容 type 属性 ,后续会废弃
  425. if (type) return
  426. this.showPopup = true
  427. this.showTrans = true
  428. },
  429. right(type) {
  430. this.popupClass = 'right'
  431. this.ani = ['slide-right']
  432. this.transitionStyle = {
  433. position: 'fixed',
  434. zIndex: this.zIndex,
  435. bottom: 0,
  436. right: 0,
  437. top: 0,
  438. backgroundColor: this.bg,
  439. /* #ifndef APP-NVUE */
  440. display: 'flex',
  441. flexDirection: 'column'
  442. /* #endif */
  443. }
  444. // TODO 兼容 type 属性 ,后续会废弃
  445. if (type) return
  446. this.showPopup = true
  447. this.showTrans = true
  448. }
  449. }
  450. }
  451. </script>
  452. <style lang="scss" scoped>
  453. .uv-popup {
  454. position: fixed;
  455. /* #ifndef APP-NVUE */
  456. z-index: 99;
  457. /* #endif */
  458. &.top,
  459. &.left,
  460. &.right {
  461. /* #ifdef H5 */
  462. top: var(--window-top);
  463. /* #endif */
  464. /* #ifndef H5 */
  465. top: 0;
  466. /* #endif */
  467. }
  468. .uv-popup__content {
  469. /* #ifndef APP-NVUE */
  470. display: block;
  471. /* #endif */
  472. position: relative;
  473. &.left,
  474. &.right {
  475. /* #ifdef H5 */
  476. padding-top: var(--window-top);
  477. /* #endif */
  478. /* #ifndef H5 */
  479. padding-top: 0;
  480. /* #endif */
  481. flex: 1;
  482. }
  483. &__close {
  484. position: absolute;
  485. &--hover {
  486. opacity: 0.4;
  487. }
  488. }
  489. &__close--top-left {
  490. top: 15px;
  491. left: 15px;
  492. }
  493. &__close--top-right {
  494. top: 15px;
  495. right: 15px;
  496. }
  497. &__close--bottom-left {
  498. bottom: 15px;
  499. left: 15px;
  500. }
  501. &__close--bottom-right {
  502. right: 15px;
  503. bottom: 15px;
  504. }
  505. }
  506. }
  507. .fixforpc-z-index {
  508. /* #ifndef APP-NVUE */
  509. z-index: 999;
  510. /* #endif */
  511. }
  512. .fixforpc-top {
  513. top: 0;
  514. }
  515. </style>