封装一个消息推送弹窗

24 阅读2分钟

ToastContainer.vue

  <div class="toast-container">
    <transition-group name="van-slide-left" tag="div">
      <div
        v-for="(toast, index) in toasts"
        :key="toast.id"
        class="toast-wrap"
        :style="{ top: `${index * 80}px` }"
      >
        <UseToast v-bind="toast.props" :on-close="() => removeToast(toast.id)" />
      </div>
    </transition-group>
  </div>
</template>

<script setup lang="ts">
import { toasts, removeToast } from '@/hooks/toastManager'
import UseToast from '@/components/Dialog/Toast.vue'
</script>

<style scoped>
.toast-container {
  position: fixed;
  left: 0;
  width: 300px;
  top: 50px;
  display: flex;
  flex-direction: column; /* 弹窗从上到下排列 */
}

.toast-wrap {
  position: absolute; /* 改为 absolute 以便更好地控制位置 */
  width: 100%;
  transition:
    top 0.5s ease-in-out,
    opacity 0.5s ease-in-out; /* 过渡效果 */
}
.van-slide-left-enter-active {
  animation: slide-in-left 0.5s forwards;
}

.van-slide-left-leave-active {
  animation: fade-out 0.5s forwards;
}

@keyframes slide-in-left {
  from {
    transform: translateX(-100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

@keyframes fade-out {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}
</style>

Toast.vue

<template>
  <transition name="notification-fade" @leave="handleLeave">
    <div class="toast-wrap" :style="{ bottom: `${index * 60}px` }">
      <div class="toast">
        <img v-if="type == 1" src="@/assets/images/grift.png" alt="" />
        <div class="content">
          <div class="span">
            {{ lang == 'english' ? 'Congratulations on winning' : 'Parabéns pela vitória' }}
            <strong>{{ currencySymbol }}{{ content }}</strong>
          </div>

          <div class="next">
            o WinGo 1Min. <img src="@/assets/images/next.png" alt="" srcset="" class="next" />
          </div>
        </div>
        <button class="close-btn" @click="closeToast">
          <img src="@/assets/images/closeToast.png" alt="" srcset="" />
        </button>
      </div>
    </div>
  </transition>
</template>
<script setup lang="ts">
import { getStorage } from '@/utils/local'
import { ref, onMounted } from 'vue'
const currencySymbol = getStorage('currencySymbol')
const lang = getStorage('lang')
const props = defineProps({
  title: {
    type: String
  },
  type: {
    type: Number
  },
  content: {
    type: String
  },
  duration: {
    type: Number,
    required: true
  },
  index: {
    type: Number,
    required: true
  },
  onClose: {
    type: Function,
    required: true
  }
})
const currentRate = ref(100)
const rate = ref(100)
const speed = ref(100)

onMounted(() => {
  rate.value = 0
  speed.value = (100 / props.duration) * 1000
})
const closeToast = () => props.onClose()
const handleLeave = () => {
  // 触发自定义事件通知父组件
  const event = new CustomEvent('toast-leave', { detail: props.index })
  window.dispatchEvent(event)
}
</script>
<style lang="less" scoped>
.notification-fade-enter-active,
.notification-fade-leave-active {
  transition: opacity 0.5s;
}
.close-btn {
  position: absolute;
  top: 5px;
  right: 10px;
  background: none;
  border: none;
  font-size: 16px;
  cursor: pointer;
  color: #535353;
}
.notification-fade-enter,
.notification-fade-leave-to .notification-fade-leave-active {
  opacity: 0;
}
.toast-wrap {
  z-index: 999;
  width: 100%;
  height: 100%;
  position: fixed;
  top: 0;
  left: 0;
  margin: 0 auto;
  right: 0;
}

.toast {
  position: relative;
  width: 325px;
  height: 61px;
  background: #ffffff;
  box-shadow: 0px 0px 10px 1px #999;
  border-radius: 4px;
  margin: 10px auto 0px;
  display: flex;
  padding: 9px;
  overflow: hidden;
  transition: opacity 0.5s ease-in-out;
  .content {
    margin-left: 10px;
    height: 61px;
    .span {
      font-size: 14px;
      color: #535353;
    }
    strong {
      font-weight: bold;
      font-size: 15px;
      line-height: 20px;
      color: #ffa235;
      display: inline-block;
      margin-left: 10px;
    }
    .next {
      img {
        width: 14px;
        height: 14px;
        vertical-align: middle;
        object-fit: scale-down;
      }
      font-size: 14px;
      color: #000;
    }
  }
  img {
    width: 20px;
    height: 25px;
    object-fit: scale-down;
  }
  p {
    font-weight: 400;
    font-size: 14px;
    color: #d6d6d6;
    line-height: 38px;
  }
}
</style>

useToast.ts


type ToastOptions =
  | string
  | {
      duration?: number
      text: string
      type?: number
      content?: string
    }

export function useToast(options: ToastOptions) {
  let title: string, duration: number, type: number, content: string
  if (typeof options === 'string') {
    title = options
    duration = 1500
    type = 1
    content = ''
  } else {
    title = options.text
    duration = options.duration || 1500
    type = options.type || 1
    content = options.content || ''
  }

  const id = Date.now() + Math.random() // 生成唯一ID
  const toast = {
    id,
    props: { title, type, duration, content, index: 0, onClose: () => removeToast(id) }
  }

  addToast(toast)

  setTimeout(() => {
    removeToast(id)
  }, duration)
}


使用  在app。vue中引入<ToastContainer /> 

![image.png](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/67654ee6a1db4dfba5e8834550d16dac~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LiA5aSpZWU=:q75.awebp?rk3s=f64ab15b&x-expires=1743991748&x-signature=9av5prRR8rsHb5DilYuKNwHAh68%3D)

使用方式
useToast({
          text: '',
          duration: 3000,
          type: 1,
          content: _data.winAmount
        })