su-popup.vue 15 KB

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