vuetify3 + Vue3 封装 Message 组件

2,412 阅读1分钟

效果如图

yjsts-rjgtt.gif

由于 Vuetifyv-alert 组件是静态的, 所以参考 element-ui 封装一个动态组件, 并且支持指令调用

没啥可讲的, 直接上代码

代码

目录结构
└─src
    ├─plugins
    │  └─message
          └─index.ts
          └─message.vue
<!-- message.vue -->
<template>
  <Transition
    @after-leave="onClose"
    enter-active-class="animate__animated animate__fadeInUp"
    leave-active-class="animate__animated animate__fadeOutUp"
  >
    <div
      :id="id"
      class="message"
      :style="{ top: top + 'px' }"
      v-show="visibled"
    >
      <v-alert
        class="alert"
        outlined
        :type="type"
        max-width="300"
        variant="contained-text"
      >
       {{ message }}
      </v-alert>
    </div>
  </Transition>
</template>

<script lang="ts">
import { defineComponent, onMounted, PropType, ref } from 'vue'
import { useTimeoutFn } from '@vueuse/core'
import 'animate.css'

export default defineComponent({
  name: 'message',
  props: {
    id: String,
    type: {
      validator: (value: string) => {
        return ['success', 'warning', 'error', 'info'].includes(value)
      },
      default: 'info',
      type: String as PropType<'error' | 'success' | 'warning' | 'info'>,
    },
    top: {
      type: Number,
      default: 56,
    },
    message: {
      type: String,
      default: '',
    },
    duration: {
      type: Number,
      default: 3000,
    },
    onClose: {
      type: Function,
      default: () => {},
    },
  },
  setup(props) {
    const visibled = ref(false)

    let stopTimer: (() => void) | undefined = undefined

    // 开启定时器
    const startTimer = () => {
      if (props.duration > 0) {
        ;({ stop: stopTimer } = useTimeoutFn(() => {
          if (visibled.value) close() // 取消展示
        }, props.duration))
      }
    }

    const clearTimer = () => {
      stopTimer?.()
    }

    // 为了重新开始计时
    const reTime = () => {
      clearTimer()
      startTimer()
    }

    const close = () => {
      visibled.value = false
    }

    onMounted(() => {
      startTimer()
      visibled.value = true
    })

    return {
      visibled,
      close,
      reTime,
    }
  },
})
</script>

<style scoped lang="scss">
.message {
  position: fixed;
  pointer-events: none;
  left: 0;
  right: 0;
  z-index: 9999;
  transition: top 0.7s linear;

  .alert {
    margin: auto;
  }
}
</style>
// index.ts
import { createApp, render, getCurrentInstance } from 'vue'
import type { App } from 'vue'
import vuetify from 'plugins/vuetify'
import MessageConstructor from './message.vue'

// hooks
export function useMessage() {
  const {
    // @ts-ignore
    appContext: {
      app: {
        config: {
          globalProperties: { $message },
        },
      },
    },
  } = getCurrentInstance()

  return $message
}

type MessageQueue = App[]

const instances: MessageQueue = [] // 消息队列

const offset = 60 // 单个消息框偏移

let seed = 1

const message = function (options: Object | string) {
  if (typeof options === 'string') {
    options = {
      message: options,
    }
  }

  const id = `message_${seed++}`

  const container = document.createElement('div')

  // createVNode 不行, 不能使用 use
  const app = createApp(MessageConstructor, {
    id,
    top: (instances.length + 1) * offset,
    onClose: () => {
      render(null, container)
      close(id)
    },
    ...options,
  })
    .use(vuetify) // vuetify 的组件貌似必须挂载在具有 symbol(vuetify) 的节点上...
    .mount(container)

  instances.push(app as any)

  // 把虚拟节点加进 dom 树里(不要把 container 加进去)
  document.body.appendChild(container.firstElementChild!)
}

;['success', 'info', 'warning', 'error'].forEach((type: string) => {
  message[type] = (options: Object | string) => {
    if (typeof options === 'string') {
      return message({
        type,
        message: options,
      })
    } else if (typeof options === 'object') {
      return message({
        type,
        ...options,
      })
    }
  }
})

// 消息关闭时的相关处理函数...
// 例如把其他消息的 top 缩小
function close(id: string): void {
  const idx = instances.findIndex((app: any) => id === app.id)

  if (idx === -1) return

  const app = instances[idx]

  // 如果没有找到虚拟节点就什么都不做
  if (!app) return

  // 从 idx 位置开始删除一个节点
  instances.splice(idx, 1)

  const len = instances.length

  if (len < 1) return // 删除一个虚拟节点后消息队列内没有元素, 什么都不做了

  for (let i = idx; i < len; i++) {
    instances[i]['reTime']() // 重新开始定时

    // style 的 top 是 ..px 的形式, 因此需要 parseInt 解析出数字
    const pos: number =
      Number.parseInt((instances[i] as any).$el.style.top, 10) - offset
    instances[i]['$el'].style.top = `${pos}px`
  }
}

// 消息提示框插件
export default {
  install(app: App) {
    app.config.globalProperties.$message = message
  },
}

使用

setup() {
    const $message = useMessage() // 获取函数

    function msg() {
      $message.error('hello')
      
      $message('hello') // 默认 type='info'
      
      $message.success({
          message: 'hello',
          duration: 1000,
      })
    }

    return { msg }
},

结论

  1. 有点问题, 比如消息间隔太长的话, 动画有点莫名其妙

QQ录屏20220527103420_.gif

因为 element-ui 的动画又是另一个文件夹的东西了, 懒得看了😅, 所以直接搬 animate.css

  1. 我发现 quasar 也有 notify, 而且超好看, 奥里给! 正经人谁自己写组件啊(╯°□°)╯︵ ┻━┻