HyPopup.vue 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. <template>
  2. <transition :name="transitionName">
  3. <view v-if="visible" class="hy-popup-wrapper" :class="positionClass" @click="handleOverlayClick">
  4. <view class="hy-popup-overlay" :style="{ backgroundColor: overlayBackgroundColor }"></view>
  5. <view
  6. class="hy-popup-content"
  7. :class="[customClass, { 'hy-popup-no-padding': noPadding }]"
  8. @click.stop
  9. :style="{
  10. width: width,
  11. maxWidth: maxWidth,
  12. backgroundColor: backgroundColor,
  13. borderRadius: borderRadius + 'rpx'
  14. }"
  15. >
  16. <!-- 自定义头部插槽 -->
  17. <slot name="header"></slot>
  18. <!-- 自定义内容插槽 -->
  19. <slot></slot>
  20. <!-- 关闭按钮 -->
  21. <view class="close-wrap" v-if="showClose" @click="handleClose">
  22. <u-icon name="close-circle" size="40" color="#999"></u-icon>
  23. <text>关闭</text>
  24. </view>
  25. </view>
  26. </view>
  27. </transition>
  28. </template>
  29. <script lang="ts" setup>
  30. import { computed } from 'vue'
  31. // 定义动画类型
  32. const AnimationType = ['scale', 'slide-up', 'slide-down', 'slide-left', 'slide-right', 'fade'] as const
  33. // eslint-disable-next-line no-redeclare
  34. type AnimationType = (typeof AnimationType)[number]
  35. // 定义弹窗位置
  36. const PositionType = ['center', 'top', 'bottom', 'left', 'right'] as const
  37. // eslint-disable-next-line no-redeclare
  38. type PositionType = (typeof PositionType)[number]
  39. // 定义组件的Props
  40. interface Props {
  41. // v-model绑定值
  42. modelValue: boolean
  43. // 是否显示关闭按钮
  44. showClose?: boolean
  45. // 点击遮罩层是否可以关闭
  46. closeOnClickOverlay?: boolean
  47. // 按ESC键是否可以关闭(仅Web环境)
  48. closeOnPressEscape?: boolean
  49. // 自定义class
  50. customClass?: string
  51. // 动画类型
  52. animationType?: AnimationType
  53. // 弹窗位置
  54. position?: PositionType
  55. // 宽度
  56. width?: string | number
  57. // 最大宽度
  58. maxWidth?: string | number
  59. // 背景色
  60. backgroundColor?: string
  61. // 遮罩背景色
  62. overlayBackgroundColor?: string
  63. // 圆角大小
  64. borderRadius?: number
  65. // 关闭按钮颜色
  66. closeIconColor?: string
  67. // 关闭按钮样式
  68. closeButtonStyle?: object
  69. // 是否无内边距
  70. noPadding?: boolean
  71. // 是否显示遮罩
  72. showOverlay?: boolean
  73. // 是否显示已领取遮罩
  74. showUsedReceived?: boolean
  75. }
  76. // 使用默认值定义Props
  77. const props = withDefaults(defineProps<Props>(), {
  78. showClose: false,
  79. closeOnClickOverlay: true,
  80. closeOnPressEscape: true,
  81. customClass: '',
  82. animationType: 'scale',
  83. position: 'center',
  84. width: '80%',
  85. maxWidth: '600rpx',
  86. backgroundColor: 'transparent',
  87. overlayBackgroundColor: 'rgba(0, 0, 0, 0.8)',
  88. borderRadius: 24,
  89. closeIconColor: '#fff',
  90. closeButtonStyle: () => ({}),
  91. noPadding: false,
  92. showOverlay: true,
  93. showUsedReceived: false
  94. })
  95. // 定义组件的Events
  96. const emit = defineEmits<{
  97. 'update:modelValue': [value: boolean]
  98. close: []
  99. opened: []
  100. closed: []
  101. }>()
  102. // 处理v-model的双向绑定
  103. const visible = computed({
  104. get: () => props.modelValue,
  105. set: (value) => {
  106. emit('update:modelValue', value)
  107. if (value) {
  108. emit('opened')
  109. } else {
  110. emit('closed')
  111. }
  112. }
  113. })
  114. // 计算动画名称
  115. const transitionName = computed(() => {
  116. return `hy-popup-${props.animationType}`
  117. })
  118. // 计算位置类名
  119. const positionClass = computed(() => {
  120. return `hy-popup-position-${props.position}`
  121. })
  122. // 点击关闭按钮
  123. const handleClose = () => {
  124. visible.value = false
  125. emit('close')
  126. }
  127. // 点击遮罩层
  128. const handleOverlayClick = () => {
  129. if (props.closeOnClickOverlay) {
  130. handleClose()
  131. }
  132. }
  133. </script>
  134. <style lang="scss" scoped>
  135. .hy-popup-wrapper {
  136. position: fixed;
  137. top: 0;
  138. left: 0;
  139. right: 0;
  140. bottom: 0;
  141. z-index: 999;
  142. display: flex;
  143. align-items: center;
  144. justify-content: center;
  145. box-sizing: border-box;
  146. }
  147. .close-wrap {
  148. display: flex;
  149. flex-direction: column;
  150. align-items: center;
  151. margin-top: 20rpx;
  152. image {
  153. width: 62rpx;
  154. height: 62rpx;
  155. margin: 46rpx 0 8rpx 0;
  156. }
  157. text {
  158. font-size: 30rpx;
  159. color: rgba(255, 255, 255, 0.76);
  160. }
  161. }
  162. // 位置控制
  163. .hy-popup-position-center {
  164. align-items: center;
  165. justify-content: center;
  166. }
  167. .hy-popup-position-top {
  168. align-items: flex-start;
  169. justify-content: center;
  170. }
  171. .hy-popup-position-bottom {
  172. align-items: flex-end;
  173. justify-content: center;
  174. }
  175. .hy-popup-position-left {
  176. align-items: center;
  177. justify-content: flex-start;
  178. }
  179. .hy-popup-position-right {
  180. align-items: center;
  181. justify-content: flex-end;
  182. }
  183. .hy-popup-overlay {
  184. position: absolute;
  185. top: 0;
  186. left: 0;
  187. right: 0;
  188. bottom: 0;
  189. }
  190. .hy-popup-content {
  191. position: relative;
  192. box-sizing: border-box;
  193. overflow: hidden;
  194. padding: 30rpx;
  195. }
  196. .hy-popup-no-padding {
  197. padding: 0;
  198. }
  199. .hy-popup-close {
  200. position: absolute;
  201. top: 20rpx;
  202. right: 20rpx;
  203. width: 60rpx;
  204. height: 60rpx;
  205. display: flex;
  206. align-items: center;
  207. justify-content: center;
  208. border-radius: 50%;
  209. background-color: rgba(0, 0, 0, 0.1);
  210. z-index: 10;
  211. }
  212. .hy-popup-close-icon {
  213. font-size: 40rpx;
  214. font-weight: bold;
  215. }
  216. // 通用过渡设置
  217. $transition-duration: 0.3s;
  218. $transition-timing: ease;
  219. // Scale 动画
  220. .hy-popup-scale-enter-active,
  221. .hy-popup-scale-leave-active {
  222. transition: all $transition-duration $transition-timing;
  223. }
  224. .hy-popup-scale-enter-from .hy-popup-overlay,
  225. .hy-popup-scale-leave-to .hy-popup-overlay {
  226. opacity: 0;
  227. }
  228. .hy-popup-scale-enter-from .hy-popup-content,
  229. .hy-popup-scale-leave-to .hy-popup-content {
  230. transform: scale(0.8);
  231. opacity: 0;
  232. }
  233. // Slide-up 动画
  234. .hy-popup-slide-up-enter-active,
  235. .hy-popup-slide-up-leave-active {
  236. transition: all $transition-duration $transition-timing;
  237. }
  238. .hy-popup-slide-up-enter-from .hy-popup-overlay,
  239. .hy-popup-slide-up-leave-to .hy-popup-overlay {
  240. opacity: 0;
  241. }
  242. .hy-popup-slide-up-enter-from .hy-popup-content {
  243. transform: translateY(100%);
  244. opacity: 0;
  245. }
  246. .hy-popup-slide-up-leave-to .hy-popup-content {
  247. transform: translateY(100%);
  248. opacity: 0;
  249. }
  250. // Slide-down 动画
  251. .hy-popup-slide-down-enter-active,
  252. .hy-popup-slide-down-leave-active {
  253. transition: all $transition-duration $transition-timing;
  254. }
  255. .hy-popup-slide-down-enter-from .hy-popup-overlay,
  256. .hy-popup-slide-down-leave-to .hy-popup-overlay {
  257. opacity: 0;
  258. }
  259. .hy-popup-slide-down-enter-from .hy-popup-content {
  260. transform: translateY(-100%);
  261. opacity: 0;
  262. }
  263. .hy-popup-slide-down-leave-to .hy-popup-content {
  264. transform: translateY(-100%);
  265. opacity: 0;
  266. }
  267. // Slide-left 动画
  268. .hy-popup-slide-left-enter-active,
  269. .hy-popup-slide-left-leave-active {
  270. transition: all $transition-duration $transition-timing;
  271. }
  272. .hy-popup-slide-left-enter-from .hy-popup-overlay,
  273. .hy-popup-slide-left-leave-to .hy-popup-overlay {
  274. opacity: 0;
  275. }
  276. .hy-popup-slide-left-enter-from .hy-popup-content {
  277. transform: translateX(-100%);
  278. opacity: 0;
  279. }
  280. .hy-popup-slide-left-leave-to .hy-popup-content {
  281. transform: translateX(-100%);
  282. opacity: 0;
  283. }
  284. // Slide-right 动画
  285. .hy-popup-slide-right-enter-active,
  286. .hy-popup-slide-right-leave-active {
  287. transition: all $transition-duration $transition-timing;
  288. }
  289. .hy-popup-slide-right-enter-from .hy-popup-overlay,
  290. .hy-popup-slide-right-leave-to .hy-popup-overlay {
  291. opacity: 0;
  292. }
  293. .hy-popup-slide-right-enter-from .hy-popup-content {
  294. transform: translateX(100%);
  295. opacity: 0;
  296. }
  297. .hy-popup-slide-right-leave-to .hy-popup-content {
  298. transform: translateX(100%);
  299. opacity: 0;
  300. }
  301. // Fade 动画
  302. .hy-popup-fade-enter-active,
  303. .hy-popup-fade-leave-active {
  304. transition: all $transition-duration $transition-timing;
  305. }
  306. .hy-popup-fade-enter-from .hy-popup-overlay,
  307. .hy-popup-fade-leave-to .hy-popup-overlay {
  308. opacity: 0;
  309. }
  310. .hy-popup-fade-enter-from .hy-popup-content,
  311. .hy-popup-fade-leave-to .hy-popup-content {
  312. opacity: 0;
  313. }
  314. </style>