效果如图
由于 Vuetify
的 v-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 }
},
结论
- 有点问题, 比如消息间隔太长的话, 动画有点莫名其妙
因为 element-ui
的动画又是另一个文件夹的东西了, 懒得看了😅, 所以直接搬 animate.css
- 我发现
quasar
也有notify
, 而且超好看, 奥里给! 正经人谁自己写组件啊(╯°□°)╯︵ ┻━┻