su-popup.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. <template>
  2. <view
  3. v-if="showPopup"
  4. class="uni-popup"
  5. :class="[popupstyle, isDesktop ? 'fixforpc-z-index' : '']"
  6. :style="[{ zIndex: zIndex }]"
  7. @touchmove.stop.prevent="clear"
  8. >
  9. <view @touchstart="touchstart">
  10. <uni-transition
  11. key="1"
  12. v-if="maskShow"
  13. name="mask"
  14. mode-class="fade"
  15. :styles="maskClass"
  16. :duration="duration"
  17. :show="showTrans"
  18. @click="onTap"
  19. />
  20. <uni-transition
  21. key="2"
  22. :mode-class="ani"
  23. name="content"
  24. :styles="{ ...transClass, ...borderRadius }"
  25. :duration="duration"
  26. :show="showTrans"
  27. @click="onTap"
  28. >
  29. <view
  30. v-if="showPopup"
  31. class="uni-popup__wrapper"
  32. :style="[{ backgroundColor: bg }, borderRadius]"
  33. :class="[popupstyle]"
  34. @click="clear"
  35. >
  36. <image v-if="showClose" class="close-icon" src="/static/popup_close_icon.png" @click="close"></image>
  37. <slot />
  38. </view>
  39. </uni-transition>
  40. </view>
  41. <!-- #ifdef H5 -->
  42. <keypress v-if="maskShow" @esc="onTap" />
  43. <!-- #endif -->
  44. </view>
  45. <!-- #ifdef MP -->
  46. <view v-else style="display: none">
  47. <slot></slot>
  48. </view>
  49. <!-- #endif -->
  50. </template>
  51. <script>
  52. // #ifdef H5
  53. import keypress from './keypress.js';
  54. // #endif
  55. /**
  56. * PopUp 弹出层
  57. * @description 弹出层组件,为了解决遮罩弹层的问题
  58. * @tutorial https://ext.dcloud.net.cn/plugin?id=329
  59. * @property {String} type = [top|center|bottom|left|right|message|dialog|share] 弹出方式
  60. * @value top 顶部弹出
  61. * @value center 中间弹出
  62. * @value bottom 底部弹出
  63. * @value left 左侧弹出
  64. * @value right 右侧弹出
  65. * @value message 消息提示
  66. * @value dialog 对话框
  67. * @value share 底部分享示例
  68. * @property {Boolean} animation = [true|false] 是否开启动画
  69. * @property {Boolean} maskClick = [true|false] 蒙版点击是否关闭弹窗(废弃)
  70. * @property {Boolean} isMaskClick = [true|false] 蒙版点击是否关闭弹窗
  71. * @property {String} backgroundColor 主窗口背景色
  72. * @property {String} maskBackgroundColor 蒙版颜色
  73. * @property {Boolean} safeArea 是否适配底部安全区
  74. * @event {Function} change 打开关闭弹窗触发,e={show: false}
  75. * @event {Function} maskClick 点击遮罩触发
  76. */
  77. import sheep from '@/common';
  78. export default {
  79. name: 'SuPopup',
  80. components: {
  81. // #ifdef H5
  82. keypress,
  83. // #endif
  84. },
  85. emits: ['change', 'maskClick', 'close'],
  86. props: {
  87. // 开启状态
  88. show: {
  89. type: Boolean,
  90. default: false,
  91. },
  92. // 顶部,底部时有效
  93. space: {
  94. type: Number,
  95. default: 0,
  96. },
  97. // 默认圆角
  98. round: {
  99. type: [String, Number],
  100. default: 0,
  101. },
  102. // 是否显示关闭
  103. showClose: {
  104. type: Boolean,
  105. default: false,
  106. },
  107. // 开启动画
  108. animation: {
  109. type: Boolean,
  110. default: true,
  111. },
  112. // 弹出层类型,可选值,top: 顶部弹出层;bottom:底部弹出层;center:全屏弹出层
  113. // message: 消息提示 ; dialog : 对话框
  114. type: {
  115. type: String,
  116. default: 'bottom',
  117. },
  118. // maskClick
  119. isMaskClick: {
  120. type: Boolean,
  121. default: null,
  122. },
  123. // TODO 2 个版本后废弃属性 ,使用 isMaskClick
  124. maskClick: {
  125. type: Boolean,
  126. default: null,
  127. },
  128. // 可设置none
  129. backgroundColor: {
  130. type: String,
  131. default: '#FAFAFA',
  132. },
  133. backgroundImage: {
  134. type: String,
  135. default: '',
  136. },
  137. safeArea: {
  138. type: Boolean,
  139. default: true,
  140. },
  141. maskBackgroundColor: {
  142. type: String,
  143. default: 'rgba(0, 0, 0, 0.3)',
  144. },
  145. zIndex: {
  146. type: [String, Number],
  147. default: 10075,
  148. },
  149. },
  150. watch: {
  151. show: {
  152. handler: function (newValue, oldValue) {
  153. if (typeof oldValue === 'undefined' && !newValue) {
  154. return;
  155. }
  156. if (newValue) {
  157. this.open();
  158. } else {
  159. this.close();
  160. }
  161. },
  162. immediate: true,
  163. },
  164. /**
  165. * 监听type类型
  166. */
  167. type: {
  168. handler: function (type) {
  169. if (!this.config[type]) return;
  170. this[this.config[type]](true);
  171. },
  172. immediate: true,
  173. },
  174. isDesktop: {
  175. handler: function (newVal) {
  176. if (!this.config[newVal]) return;
  177. this[this.config[this.type]](true);
  178. },
  179. immediate: true,
  180. },
  181. /**
  182. * 监听遮罩是否可点击
  183. * @param {Object} val
  184. */
  185. maskClick: {
  186. handler: function (val) {
  187. this.mkclick = val;
  188. },
  189. immediate: true,
  190. },
  191. isMaskClick: {
  192. handler: function (val) {
  193. this.mkclick = val;
  194. },
  195. immediate: true,
  196. },
  197. // H5 下禁止底部滚动
  198. showPopup(show) {
  199. // #ifdef H5
  200. // fix by mehaotian 处理 h5 滚动穿透的问题
  201. document.getElementsByTagName('body')[0].style.overflow = show ? 'hidden' : 'visible';
  202. // #endif
  203. },
  204. },
  205. data() {
  206. return {
  207. sheep,
  208. duration: 300,
  209. ani: [],
  210. showPopup: false,
  211. showTrans: false,
  212. popupWidth: 0,
  213. popupHeight: 0,
  214. config: {
  215. top: 'top',
  216. bottom: 'bottom',
  217. center: 'center',
  218. left: 'left',
  219. right: 'right',
  220. message: 'top',
  221. dialog: 'center',
  222. share: 'bottom',
  223. },
  224. maskClass: {
  225. position: 'fixed',
  226. bottom: 0,
  227. top: 0,
  228. left: 0,
  229. right: 0,
  230. backgroundColor: 'rgba(0, 0, 0, 0.3)',
  231. },
  232. transClass: {
  233. position: 'fixed',
  234. left: 0,
  235. right: 0,
  236. },
  237. maskShow: true,
  238. mkclick: true,
  239. popupstyle: this.isDesktop ? 'fixforpc-top' : 'top',
  240. };
  241. },
  242. computed: {
  243. isDesktop() {
  244. return this.popupWidth >= 500 && this.popupHeight >= 500;
  245. },
  246. bg() {
  247. if (this.backgroundColor === '' || this.backgroundColor === 'none') {
  248. return 'transparent';
  249. }
  250. return this.backgroundColor;
  251. },
  252. borderRadius() {
  253. if (this.round) {
  254. if (this.type === 'bottom') {
  255. return {
  256. 'border-top-left-radius': parseFloat(this.round) + 'px',
  257. 'border-top-right-radius': parseFloat(this.round) + 'px',
  258. };
  259. }
  260. if (this.type === 'center') {
  261. return {
  262. 'border-top-left-radius': parseFloat(this.round) + 'px',
  263. 'border-top-right-radius': parseFloat(this.round) + 'px',
  264. 'border-bottom-left-radius': parseFloat(this.round) + 'px',
  265. 'border-bottom-right-radius': parseFloat(this.round) + 'px',
  266. };
  267. }
  268. if (this.type === 'top') {
  269. return {
  270. 'border-bottom-left-radius': parseFloat(this.round) + 'px',
  271. 'border-bottom-right-radius': parseFloat(this.round) + 'px',
  272. };
  273. }
  274. }
  275. },
  276. },
  277. mounted() {
  278. const fixSize = () => {
  279. const { windowWidth, windowHeight, windowTop, safeArea, screenHeight, safeAreaInsets } =
  280. sheep.$platform.device;
  281. this.popupWidth = windowWidth;
  282. this.popupHeight = windowHeight + (windowTop || 0);
  283. // TODO fix by mehaotian 是否适配底部安全区 ,目前微信ios 、和 app ios 计算有差异,需要框架修复
  284. if (safeArea && this.safeArea) {
  285. // #ifdef MP-WEIXIN
  286. this.safeAreaInsets = screenHeight - safeArea.bottom;
  287. // #endif
  288. // #ifndef MP-WEIXIN
  289. this.safeAreaInsets = safeAreaInsets.bottom;
  290. // #endif
  291. } else {
  292. this.safeAreaInsets = 0;
  293. }
  294. };
  295. fixSize();
  296. // #ifdef H5
  297. // window.addEventListener('resize', fixSize)
  298. // this.$once('hook:beforeDestroy', () => {
  299. // window.removeEventListener('resize', fixSize)
  300. // })
  301. // #endif
  302. },
  303. // #ifndef VUE3
  304. // TODO vue2
  305. destroyed() {
  306. this.setH5Visible();
  307. },
  308. // #endif
  309. // #ifdef VUE3
  310. // TODO vue3
  311. unmounted() {
  312. this.setH5Visible();
  313. },
  314. // #endif
  315. created() {
  316. // this.mkclick = this.isMaskClick || this.maskClick
  317. if (this.isMaskClick === null && this.maskClick === null) {
  318. this.mkclick = true;
  319. } else {
  320. this.mkclick = this.isMaskClick !== null ? this.isMaskClick : this.maskClick;
  321. }
  322. if (this.animation) {
  323. this.duration = 300;
  324. } else {
  325. this.duration = 0;
  326. }
  327. // TODO 处理 message 组件生命周期异常的问题
  328. this.messageChild = null;
  329. // TODO 解决头条冒泡的问题
  330. this.clearPropagation = false;
  331. this.maskClass.backgroundColor = this.maskBackgroundColor;
  332. },
  333. methods: {
  334. setH5Visible() {
  335. // #ifdef H5
  336. // fix by mehaotian 处理 h5 滚动穿透的问题
  337. document.getElementsByTagName('body')[0].style.overflow = 'visible';
  338. // #endif
  339. },
  340. /**
  341. * 公用方法,不显示遮罩层
  342. */
  343. closeMask() {
  344. this.maskShow = false;
  345. },
  346. /**
  347. * 公用方法,遮罩层禁止点击
  348. */
  349. disableMask() {
  350. this.mkclick = false;
  351. },
  352. // TODO nvue 取消冒泡
  353. clear(e) {
  354. // #ifndef APP-NVUE
  355. e.stopPropagation();
  356. // #endif
  357. this.clearPropagation = true;
  358. },
  359. open(direction) {
  360. // fix by mehaotian 处理快速打开关闭的情况
  361. if (this.showPopup) {
  362. clearTimeout(this.timer);
  363. this.showPopup = false;
  364. }
  365. let innerType = ['top', 'center', 'bottom', 'left', 'right', 'message', 'dialog', 'share'];
  366. if (!(direction && innerType.indexOf(direction) !== -1)) {
  367. direction = this.type;
  368. }
  369. if (!this.config[direction]) {
  370. console.error('缺少类型:', direction);
  371. return;
  372. }
  373. this[this.config[direction]]();
  374. this.$emit('change', {
  375. show: true,
  376. type: direction,
  377. });
  378. },
  379. close(type) {
  380. this.showTrans = false;
  381. this.$emit('change', {
  382. show: false,
  383. type: this.type,
  384. });
  385. this.$emit('close');
  386. clearTimeout(this.timer);
  387. // // 自定义关闭事件
  388. // this.customOpen && this.customClose()
  389. this.timer = setTimeout(() => {
  390. this.showPopup = false;
  391. }, 300);
  392. },
  393. // TODO 处理冒泡事件,头条的冒泡事件有问题 ,先这样兼容
  394. touchstart() {
  395. this.clearPropagation = false;
  396. },
  397. onTap() {
  398. if (this.clearPropagation) {
  399. // fix by mehaotian 兼容 nvue
  400. this.clearPropagation = false;
  401. return;
  402. }
  403. this.$emit('maskClick');
  404. if (!this.mkclick) return;
  405. this.close();
  406. },
  407. /**
  408. * 顶部弹出样式处理
  409. */
  410. top(type) {
  411. this.popupstyle = this.isDesktop ? 'fixforpc-top' : 'top';
  412. this.ani = ['slide-top'];
  413. this.transClass = {
  414. position: 'fixed',
  415. left: 0,
  416. right: 0,
  417. top: this.space + 'px',
  418. backgroundColor: this.bg,
  419. };
  420. // TODO 兼容 type 属性 ,后续会废弃
  421. if (type) return;
  422. this.showPopup = true;
  423. this.showTrans = true;
  424. this.$nextTick(() => {
  425. if (this.messageChild && this.type === 'message') {
  426. this.messageChild.timerClose();
  427. }
  428. });
  429. },
  430. /**
  431. * 底部弹出样式处理
  432. */
  433. bottom(type) {
  434. this.popupstyle = 'bottom';
  435. this.ani = ['slide-bottom'];
  436. this.transClass = {
  437. position: 'fixed',
  438. left: 0,
  439. right: 0,
  440. bottom: 0,
  441. paddingBottom: this.safeAreaInsets + this.space + 'px',
  442. backgroundColor: this.bg,
  443. };
  444. // TODO 兼容 type 属性 ,后续会废弃
  445. if (type) return;
  446. this.showPopup = true;
  447. this.showTrans = true;
  448. },
  449. /**
  450. * 中间弹出样式处理
  451. */
  452. center(type) {
  453. this.popupstyle = 'center';
  454. this.ani = ['zoom-out', 'fade'];
  455. this.transClass = {
  456. position: 'fixed',
  457. /* #ifndef APP-NVUE */
  458. display: 'flex',
  459. flexDirection: 'column',
  460. /* #endif */
  461. bottom: 0,
  462. left: 0,
  463. right: 0,
  464. top: 0,
  465. justifyContent: 'center',
  466. alignItems: 'center',
  467. };
  468. // TODO 兼容 type 属性 ,后续会废弃
  469. if (type) return;
  470. this.showPopup = true;
  471. this.showTrans = true;
  472. },
  473. left(type) {
  474. this.popupstyle = 'left';
  475. this.ani = ['slide-left'];
  476. this.transClass = {
  477. position: 'fixed',
  478. left: 0,
  479. bottom: 0,
  480. top: 0,
  481. backgroundColor: this.bg,
  482. /* #ifndef APP-NVUE */
  483. display: 'flex',
  484. flexDirection: 'column',
  485. /* #endif */
  486. };
  487. // TODO 兼容 type 属性 ,后续会废弃
  488. if (type) return;
  489. this.showPopup = true;
  490. this.showTrans = true;
  491. },
  492. right(type) {
  493. this.popupstyle = 'right';
  494. this.ani = ['slide-right'];
  495. this.transClass = {
  496. position: 'fixed',
  497. bottom: 0,
  498. right: 0,
  499. top: 0,
  500. backgroundColor: this.bg,
  501. /* #ifndef APP-NVUE */
  502. display: 'flex',
  503. flexDirection: 'column',
  504. /* #endif */
  505. };
  506. // TODO 兼容 type 属性 ,后续会废弃
  507. if (type) return;
  508. this.showPopup = true;
  509. this.showTrans = true;
  510. },
  511. },
  512. };
  513. </script>
  514. <style lang="scss">
  515. // 关闭icon
  516. .close-icon {
  517. position: absolute;
  518. right: 30rpx;
  519. top: 30rpx;
  520. z-index: 100;
  521. width: 40rpx;
  522. height: 40rpx;
  523. }
  524. .uni-popup {
  525. position: fixed;
  526. /* #ifndef APP-NVUE */
  527. z-index: 99;
  528. /* #endif */
  529. &.top,
  530. &.left,
  531. &.right {
  532. /* #ifdef H5 */
  533. top: var(--window-top);
  534. /* #endif */
  535. /* #ifndef H5 */
  536. top: 0;
  537. /* #endif */
  538. }
  539. .uni-popup__wrapper {
  540. overflow: hidden;
  541. /* #ifndef APP-NVUE */
  542. display: block;
  543. /* #endif */
  544. position: relative;
  545. background: v-bind(backgroundImage) no-repeat;
  546. background-size: 100% 100%;
  547. /* iphonex 等安全区设置,底部安全区适配 */
  548. /* #ifndef APP-NVUE */
  549. // padding-bottom: constant(safe-area-inset-bottom);
  550. // padding-bottom: env(safe-area-inset-bottom);
  551. /* #endif */
  552. &.left,
  553. &.right {
  554. /* #ifdef H5 */
  555. padding-top: var(--window-top);
  556. /* #endif */
  557. /* #ifndef H5 */
  558. padding-top: 0;
  559. /* #endif */
  560. flex: 1;
  561. }
  562. }
  563. }
  564. .fixforpc-z-index {
  565. /* #ifndef APP-NVUE */
  566. z-index: 999;
  567. /* #endif */
  568. }
  569. .fixforpc-top {
  570. top: 0;
  571. }
  572. </style>