利用vue3仿苹果系统侧边消息提示效果实例

2022-04-15 0 1,098
目录
  • 动效预览
  • 其他UI库
  • 开始
  • 组件目录结构
  • toasts.vue
    • 大概的DOM结构
  • index.js
    • 注册组件 & 定义全局变量
  • instance.js
    • 手动挂载实例
  • 完整代码
    • 总结

      动效预览

      最近在做毕业设计, 想给毕设系统加上一个仿苹果系统的侧边消息提示框, 让我们先来看看效果.

      利用vue3仿苹果系统侧边消息提示效果实例

      其他UI库

      熟悉前端开发的同学可能发现了, 在 Element UI 中这个组件叫 Notification 通知; 在Bootstrap 中这个组件叫 Toasts.

      开始

      当初看到这个组件就觉得很酷炫, 今天就带大家看一下我是怎么一步一步实现的, 有不对或者可以优化的地方请各位大佬点评. ? (本次组件基于 Vue3 实现)

      组件目录结构

      Toasts

      |

      | — index.js         // 注册组件, 定义全局变量以便调用

      |

      | — instance.js      // 手动实例创建前后的逻辑

      | — toasts.vue       // 消息提示 HTMl 部分

      |

      | — toastsBus.js     // 解决 vue3 去除 $on和$emit 的解决方案

      toasts.vue

      大概的DOM结构

      利用vue3仿苹果系统侧边消息提示效果实例

      <!-- 弹窗 -->
      <div class="toast-container">
          <!-- icon图标 -->
          <template>
              ...
          </template>
          <!-- 主要内容 -->
          <div class="toast-content">
              <!-- 标题及其倒计时 -->
              <div class="toast-head">
                  ...
              </div>
              <!-- body -->
              <div class="toast-body">...</div>
              <!-- 操作按钮 -->
              <div class="toast-operate">
                  ...
              </div>
          </div>
          <!-- 关闭 -->
          <div class="toast-close">
              <i class="fi fi-rr-cross-small"></i>
          </div>
      </div>
      

      index.js

      注册组件 & 定义全局变量

      在这里我们注册组件, 定义全局变量以便调用

      import toast from './instance'
      import Toast from './toasts.vue'
      
      export default (app) => {
          // 注册组件
          app.component(Toast.name, Toast);
          // 注册全局变量, 后续只需调用 $Toast({}) 即可
          app.config.globalProperties.$Toast = toast;
      }
      

      instance.js

      手动挂载实例

      这里是全文的重点

      首先我们学习如何将组件手动挂载至页面

      import { createApp } from 'vue';
      import Toasts from './toasts'
      
      const toasts = (options) => {
          // 创建父容器
          let root = document.createElement('div');
          document.body.appendChild(root)
          // 创建Toasts实例
          let ToastsConstructor = createApp(Toasts, options)
          // 挂载父亲元素
          let instance = ToastsConstructor.mount(root)
          // 抛出实例本身给vue
          return instance
      }
      export default toasts;
      

      给每一个创建的 toasts 正确的定位

      利用vue3仿苹果系统侧边消息提示效果实例

      如图所示, 每创建一个 toasts 将会排列到上一个 toasts 的下方(这里的间隙为16px). 想要做到这种效果我们需要知道 已存在 的toasts 的高度.

      // instance.js
      
      // 这里我们需要定义一个数组来存放当前存活的 toasts
      let instances = []
      
      const toasts = (options) => {
          ...
          // 创建后将实例加入数组
          instances.push(instance)
          
          // 重制高度
          let verticalOffset = 0
          // 遍历获取当前已存活的 toasts 高度及其间隙 累加
          instances.forEach(item => {
              verticalOffset += item.$el.offsetHeight + 16
          })
          // 累加本身需要的间隙
          verticalOffset += 16
          // 赋值当前实例y轴方向便宜长度
          instance.toastPosition.y = verticalOffset
          ...
      }
      export default toasts;
      

      加入 主动&定时 关闭功能

      让我们先来分析一下这里的业务:

      • 定时关闭: 在 toast 创建时给一个自动关闭时间, 当计时器结束后自动关闭.
      • 主动关闭: 点击关闭按钮关闭 toast.

      在这个基础上我们可以加上一些人性化的操作, 例如鼠标移入某个 toast 时停止它的自动关闭(其他 toast 不受影响), 当鼠标移开时重新启用它的自动关闭.

      <!-- toasts.vue -->
      <template>
          <transition name="toast" @after-leave="afterLeave" @after-enter="afterEnter">
              <div ref="container" class="toast-container" :style="toastStyle" v-show="visible" @mouseenter="clearTimer" @mouseleave="createTimer">
                  ...
                  <!-- 关闭 -->
                  <div class="toast-close"  @click="destruction">
                      <i class="fi fi-rr-cross-small"></i>
                  </div>
              </div>
          </transition>
      </template>
      
      <script>
      import Bus from './toastsBus'
      import {ref, computed, onMounted, onBeforeUnmount} from 'vue'
      export default {
          props: {
              // 自动关闭时间 (单位毫秒)
              autoClose: {
                  type: Number,
                  default: 4500
              }
          },
          setup(props){
              // 是否显示
              const visible = ref(false);  
              
              // toast容器实例
              const container = ref(null);
              // toast本身高度
              const height = ref(0);
              
              // toast位置
              const toastPosition = ref({
                  x: 16,
                  y: 16
              })
              const toastStyle = computed(()=>{
                  return {
                      top: `${toastPosition.value.y}px`,
                      right: `${toastPosition.value.x}px`,
                  }
              })
              
              // toast的id
              const id = ref('')
              
              // toast离开动画结束后
              function afterLeave(){
                  // 告诉 instance.js 需要进行关闭操作 ()
                  Bus.$emit('closed',id.value);
              }
              // toast进入动画结束后
              function afterEnter(){
                  height.value = container.value.offsetHeight
              }
      
              // 定时器
              const timer = ref(null);
      
              // 鼠标进入toast
              function clearTimer(){
                   if(timer.value)
                      clearTimeout(timer.value)
              }
              // 鼠标移出toast
              function createTimer(){
                 if(props.autoClose){
                      timer.value = setTimeout(() => {
                          visible.value = false
                      }, props.autoClose)
                  }
              }
      
              // 销毁
              function destruction(){
                  visible.value = false
              }
              
              onMounted(()=>{
                  createTimer();
              })
      
              onBeforeUnmount(()=>{
                  if(timer.value)
                      clearTimeout(timer.value)
              })
              
              
              return {
                  visible,
                  container,
                  height,
                  toastPosition,
                  toastStyle,
                  id,
                  afterLeave,
                  afterEnter,
                  timer,
                  clearTimer,
                  createTimer,
                  destruction
              }
          }
      }
      </script>
      

      我们来分析一下 instance.js 中 toast 关闭时的逻辑

      1. 将此 toast 从存活数组中删除, 并且遍历数组将从此条开始后面的 toast 位置向上位移.
      2. 从 <body> 中删除Dom元素.
      3. 调用 unmount() 销毁实例.

      利用vue3仿苹果系统侧边消息提示效果实例

      // instance.js
      import { createApp } from 'vue';
      import Toasts from './toasts'
      import Bus from './toastsBus'
      
      let instances = []
      let seed = 1
      
      const toasts = (options) => {
          // 手动挂载实例
          let ToastsConstructor = createApp(Toasts, options)
          let instance = ToastsConstructor.mount(root)
          // 给实例加入唯一标识符
          instance.id = id
          // 显示实例
          instance.visible = true
          
          ...
          
          // 监听 toasts.vue 传来关闭事件
          Bus.$on('closed', (id) => {
              // 因为这里会监听到所有的 ‘closed' 事件, 所以要匹配 id 确保
              if (instance.id == id) {
                  // 调用删除逻辑
                  removeInstance(instance)
                  // 在 <body> 上删除dom元素
                  document.body.removeChild(root)
                  // 销毁实例
                  ToastsConstructor.unmount();
              }
          })
          
          instances.push(instance)
          return instance
      }
      
      export default toasts;
      
      // 删除逻辑
      const removeInstance = (instance) => {
          if (!instance) return
          let len = instances.length
          // 找出当前需要销毁的下标
          const index = instances.findIndex(item => {
              return item.id === instance.id
          })
          // 从数组中删除
          instances.splice(index, 1)
          // 如果当前数组中还存在存活 Toasts, 需要遍历将下面的Toasts上移, 重新计算位移
          if (len <= 1) return
          // 获取被删除实例的高度
          const h = instance.height
          // 遍历被删除实例以后下标的 Toasts
          for (let i = index; i < len - 1; i++) {
              // 公式: 存活的实例将本身的 y 轴偏移量减去被删除高度及其间隙高度
              instances[i].toastPosition.y = parseInt(instances[i].toastPosition.y - h - 16)
          }
      }
      

      完整代码

      index.js

      import toast from './instance'
      import Toast from './toasts.vue'
      
      export default (app) => {
          app.component(Toast.name, Toast);
          app.config.globalProperties.$Toast = toast;
      }
      

      toastsBus.js

      import emitter from 'tiny-emitter/instance'
      
      export default {
          $on: (...args) => emitter.on(...args),
          $once: (...args) => emitter.once(...args),
          $off: (...args) => emitter.off(...args),
          $emit: (...args) => emitter.emit(...args)
      }
      

      instance.js

      import { createApp } from 'vue';
      import Toasts from './toasts'
      import Bus from './toastsBus'
      
      let instances = []
      let seed = 1
      
      const toasts = (options) => {
          // 创建父容器
          const id = `toasts_${seed++}`
          let root = document.createElement('div');
          root.setAttribute('data-id', id)
          document.body.appendChild(root)
          let ToastsConstructor = createApp(Toasts, options)
          let instance = ToastsConstructor.mount(root)
          instance.id = id
          instance.visible = true
              // 重制高度
          let verticalOffset = 0
          instances.forEach(item => {
              verticalOffset += item.$el.offsetHeight + 16
          })
          verticalOffset += 16
      
          instance.toastPosition.y = verticalOffset
      
          Bus.$on('closed', (id) => {
              if (instance.id == id) {
                  removeInstance(instance)
                  document.body.removeChild(root)
                  ToastsConstructor.unmount();
              }
          })
          instances.push(instance)
          return instance
      }
      
      export default toasts;
      
      const removeInstance = (instance) => {
          if (!instance) return
          let len = instances.length
          const index = instances.findIndex(item => {
              return item.id === instance.id
          })
          instances.splice(index, 1)
          if (len <= 1) return
          const h = instance.height
          for (let i = index; i < len - 1; i++) {
              instances[i].toastPosition.y = parseInt(instances[i].toastPosition.y - h - 16)
          }
      }
      

      toast.vue

      加入亿点点细节, 例如icon可以自定义或者是图片, 可以取消关闭按钮, 设置自动关闭时长, 或者停用自动关闭功能.

      <template>
      <transition name="toast" @after-leave="afterLeave" @after-enter="afterEnter">
        <!-- 弹窗 -->
        <div ref="container" class="toast-container" :style="toastStyle" v-show="visible" @mouseenter="clearTimer" @mouseleave="createTimer">
          <!-- icon -->
          <template v-if="type || type != 'custom' || type != 'img'">
              <div class="toast-icon success" v-if="type==='success'">
                  <i class="fi fi-br-check"></i>
              </div>
              <div class="toast-icon warning" v-if="type==='warning'">
                  ?
              </div>
              <div class="toast-icon info" v-if="type==='info'">
                  <i class="fi fi-sr-bell-ring"></i>
              </div>
              <div class="toast-icon error" v-if="type==='error'">
                  <i class="fi fi-br-cross-small"></i>
              </div>
          </template>
          <div :style="{'backgroundColor': customIconBackground}" class="toast-icon" v-if="type==='custom'" v-html="customIcon"></div>
          <img class="toast-custom-img" :src="customImg" v-if="type==='img'"/>
          <!-- content -->
          <div class="toast-content">
              <!-- head -->
              <div class="toast-head" v-if="title">
                  <!-- title -->
                  <span class="toast-title">{{title}}</span>
                  <!-- time -->
                  <span class="toast-countdown">{{countDown}}</span>
              </div>
              <!-- body -->
              <div class="toast-body" v-if="message" v-html="message"></div>
              <!-- operate -->
              <div class="toast-operate">
                  <a class="toast-button-confirm" 
                     :class="[{'success':type==='success'},
                              {'warning':type==='warning'},
                              {'info':type==='info'},
                              {'error':type==='error'}]">{{confirmText}}</a>
              </div>
          </div>
          <!-- 关闭 -->
          <div v-if="closeIcon" class="toast-close"  @click="destruction">
              <i class="fi fi-rr-cross-small"></i>
          </div>
        </div>
        </transition>
      </template>
      
      <script>
      import Bus from './toastsBus'
      import {ref, computed, onMounted, onBeforeUnmount} from 'vue'
      export default {
          props: {
              title: String,
              closeIcon: {
                  type: Boolean,
                  default: true
              },
              message: String,
              type: {
                  type: String,
                  validator: function(val) {
                      return ['success', 'warning', 'info', 'error', 'custom', 'img'].includes(val);
                  }
              },
              confirmText: String,
              customIcon: String,
              customIconBackground: String,
              customImg: String,
              autoClose: {
                  type: Number,
                  default: 4500
              }
          },
          setup(props){
              // 显示
              const visible = ref(false);
      
              // 容器实例
              const container = ref(null);
      
              // 高度
              const height = ref(0);
      
              // 位置
              const toastPosition = ref({
                  x: 16,
                  y: 16
              })
              const toastStyle = computed(()=>{
                  return {
                      top: `${toastPosition.value.y}px`,
                      right: `${toastPosition.value.x}px`,
                  }
              })
      
              // 倒计时
              const countDown = computed(()=>{
                  return '2 seconds ago'
              })
      
              const id = ref('')
      
              // 离开以后
              function afterLeave(){
                  Bus.$emit('closed',id.value);
              }
              // 进入以后
              function afterEnter(){
                  height.value = container.value.offsetHeight
              }
      
              // 定时器
              const timer = ref(null);
      
              // 鼠标进入
              function clearTimer(){
                   if(timer.value)
                      clearTimeout(timer.value)
              }
              // 鼠标移出
              function createTimer(){
                 if(props.autoClose){
                      timer.value = setTimeout(() => {
                          visible.value = false
                      }, props.autoClose)
                  }
              }
      
              // 销毁
              function destruction(){
                  visible.value = false
              }
      
              onMounted(()=>{
                  createTimer();
              })
      
              onBeforeUnmount(()=>{
                  if(timer.value)
                      clearTimeout(timer.value)
              })
      
              return {
                  visible,
                  toastPosition,
                  toastStyle,
                  countDown,
                  afterLeave,
                  afterEnter,
                  clearTimer,
                  createTimer,
                  timer,
                  destruction,
                  container,
                  height,
                  id
              }
          }
      }
      </script>
      
      <style lang="scss" scoped>
      // 外部容器
      .toast-container{
          width: 330px;
          box-shadow: rgba(0, 0, 0, 0.1) 0px 2px 12px 0px;
          background-color: rgba(#F7F7F7, .6);
          border: 1px solid #E5E5E5;
          padding: 14px 13px;
          z-index: 1001;
          position: fixed;
          top: 0;
          right: 0;
          border-radius: 10px;
          backdrop-filter: blur(15px);
          display: flex;
          align-items: stretch;
          transition: all .3s ease;
          will-change: top,left;
      }
      // -------------- icon --------------
      .toast-icon, .toast-close{
          flex-shrink: 0;
      }
      .toast-icon{
          width: 30px;
          height: 30px;
          border-radius: 100%;
          display: inline-flex;
          align-items: center;
          justify-content: center;
      }
      // 正确
      .toast-icon.success{
          background-color: rgba(#2BB44A, .15);
          color: #2BB44A;
      }
      // 异常
      .toast-icon.warning{
          background-color: rgba(#ffcc00, .15);
          color: #F89E23;
          font-weight: 600;
          font-size: 18px;
      }
      // 错误
      .toast-icon.error{
          font-size: 18px;
          background-color: rgba(#EB2833, .1);
          color: #EB2833;
      }
      // 信息
      .toast-icon.info{
          background-color: rgba(#3E71F3, .1);
          color: #3E71F3;
      }
      // 自定义图片
      .toast-custom-img{
          width: 40px;
          height: 40px;
          border-radius: 10px;
          overflow: hidden;
          flex-shrink: 0;
      }
      // ------------- content -----------
      .toast-content{
          padding: 0 8px 0 13px;
          flex: 1;
      }
      // -------------- head --------------
      .toast-head{
          display: flex;
          align-items: center;
          justify-content: space-between;
      }
      // title
      .toast-title{
          font-size: 16px;
          line-height: 24px;
          color: #191919;
          font-weight: 600;
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
      }
      // time
      .toast-countdown{
          font-size: 12px;
          color: #929292;
          line-height: 18.375px;
      }
      // --------------- body -----------
      .toast-body{
          color: #191919;
          line-height: 21px;
          padding-top: 5px;
      }
      // ---------- close -------
      .toast-close{
          padding: 3px;
          cursor: pointer;
          font-size: 18px;
          width: 24px;
          height: 24px;
          border-radius: 8px;
          display: inline-flex;
          align-items: center;
          justify-content: center;
      }
      .toast-close:hover{
          background-color: rgba(#E4E4E4, .5);
      }
      // --------- operate ----------
      .toast-button-confirm{
          font-weight: 600;
          color: #3E71F3;
      }
      .toast-button-confirm:hover{
          color: #345ec9;
      }
      // 成功
      .toast-button-confirm.success{
          color: #2BB44A;
      }
      .toast-button-confirm.success:hover{
          color: #218a3a;
      }
      // 异常
      .toast-button-confirm.warning{
          color: #F89E23;
      }
      .toast-button-confirm.warning:hover{
          color: #df8f1f;
      }
      // 信息
      .toast-button-confirm.info{
          color: #3E71F3;
      }
      .toast-button-confirm.info:hover{
          color: #345ec9;
      }
      // 错误
      .toast-button-confirm.error{
          color: #EB2833;
      }
      .toast-button-confirm.error:hover{
          color: #c9101a;
      }
      
      
      /*动画*/
      .toast-enter-from,
      .toast-leave-to{
        transform: translateX(120%);
      }
      .v-leave-from,
      .toast-enter-to{
        transform: translateX(00%);
      }
      </style>
      

      main.js

      import { createApp } from 'vue'
      import App from './App.vue'
      
      const app = createApp(App)
      
      import '@/assets/font/UIcons/font.css'
      
      // 安装toasts
      import toasts from './components/toasts'
      
      app.use(toasts).mount('#app')
      

      使用

      <template>
          <button @click="clickHandle">发送</button>
      </template>
      
      <script>
      import { getCurrentInstance } from 'vue'
      export default {
        setup(){
          const instance = getCurrentInstance()
          function clickHandle(){
            // 这里调用 vue3 的全局变量时比较羞耻, 不知道各位大佬有没有其他好办法
            instance.appContext.config.globalProperties.$Toast({
              type: 'info',
              title: '这是一句标题',
              message: '本文就是梳理mount函数的主要逻辑,旨在理清基本的处理流程(Vue 3.1.1版本)。'
            })
          }
          return {
            clickHandle
          }
        }
      }
      </script>
      

      icon图标字体获取

      www.flaticon.com/

      总结

      到此这篇关于利用vue3仿苹果系统侧边消息提示效果的文章就介绍到这了,更多相关vue3仿苹果侧边消息提示内容请搜索NICE源码以前的文章或继续浏览下面的相关文章希望大家以后多多支持NICE源码!

      免责声明:
      1、本网站所有发布的源码、软件和资料均为收集各大资源网站整理而来;仅限用于学习和研究目的,您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。 不得使用于非法商业用途,不得违反国家法律。否则后果自负!

      2、本站信息来自网络,版权争议与本站无关。一切关于该资源商业行为与www.niceym.com无关。
      如果您喜欢该程序,请支持正版源码、软件,购买注册,得到更好的正版服务。
      如有侵犯你版权的,请邮件与我们联系处理(邮箱:skknet@qq.com),本站将立即改正。

      NICE源码网 JavaScript 利用vue3仿苹果系统侧边消息提示效果实例 https://www.niceym.com/21598.html