推广小程序前端代码
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

539 lines
12 KiB

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