在vue3项目中components文件夹创建message文件夹作为存放组件
准备工作
在message文件夹中创建四个文件分别是:
- AtjMessage.vue - message组件
- MessageGroup.vue - 用于创建message组件的父组件,用做message出场和退场动画
- type.ts(message.ts) - 是用存放message的props的文件
- utils.ts - 用作方法创建message组件的函数
创建type.ts(message.ts)文件
具体代码如下:
typescript代码:
import { VNode } from 'vue'
export interface MessageProps {
id?: number
message?: string | VNode
showClose?: boolean
type?: 'success' | 'error' | 'warning' | 'info'
duration?: number
}
等价于Javascript代码:
/**
* @typedef {Object} MessageProps
* @property {number} [id] - 消息ID
* @property {(string|import('vue').VNode)} [message] - 消息内容
* @property {boolean} [showClose] - 是否显示关闭按钮
* @property {('success'|'error'|'warning'|'info')} [type] - 消息类型
* @property {number} [duration] - 显示时长
*/
// 使用示例
const messageProps = {
id: 1,
message: '这是一条消息',
showClose: true,
type: 'success',
duration: 3000
}
创建Message.vue组件
具体代码如下:
<template>
<div class="atj-message" :class="type" v-if="visible">
<div class="atj-message-content">
<div class="atj-message-content-title">
<div class="atj-message-content-title-text">
<slot name="title"></slot>
<!-- 如果内容是VNode,则渲染VNode -->
<component :is="messageContent" v-if="isVNodes"/>
<!-- 如果内容是字符串,则渲染字符串 -->
<template v-else>{{ message }}</template>
<span
class="atj-message-content-title-close"
v-if="showClose"
@click.stop="handleClose"
>×</span>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { defineProps, defineOptions, computed, ref, onMounted, defineEmits } from 'vue'
import type { MessageProps } from './type'
// 定义props
const props = defineProps<MessageProps>()
// 定义emit
const emit = defineEmits(['close'])
defineOptions({
name: 'AtjMessage'
})
// 默认类型为info
const type = computed(() => props.type ?? 'info')
const visible = ref(true)
// 判断内容是否是VNode
const isVNodes = computed(() => props.message && typeof props.message !== 'string')
// 如果内容是VNode,则渲染VNode
const messageContent = computed(() => isVNodes.value ? props.message : null)
// 关闭消息
// 注意,这里的点击关闭不是设置visible 为 false,
// 因为我们是通过MessageGroup的数组渲染出来的,所以定义emit通知父组件删除数组中对应的元素,
// 这样子我们的退出进场动画都可以由效果,如果直接visible.value为false,退出动画就没有了
function handleClose () {
emit('close', props.id)
}
// 挂载后设置定时器
onMounted(() => {
if (props.duration !== 0) {
setTimeout(() => {
handleClose()
}, props.duration || 3000)
}
})
</script>
<style lang="less">
.atj-message {
cursor: pointer;
border-radius: 6px;
margin-top: 5px;
width: fit-content;
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.atj-message-content {
width: 100%;
height: 100%;
.atj-message-content-title {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.atj-message-content-title-text {
font-size: 16px;
font-weight: bold;
color: #ffffff;
text-align: center;
.atj-message-content-title-close{
width: 10px;
height: 10px;
}
}
}
}
}
.info {
background-color: #909399;
}
.success {
background-color: #67C23A;
}
.error {
background-color: #F56C6C;
}
.warning {
background-color: #E6A23C;
}
</style>
创建MessageGroup.vue组件
具体代码如下:
<template>
<TransitionGroup name="fade" tag="div" class="atj-message-group">
<AtjMessage
v-for="item in messages"
:key="item.id"
v-bind="item"
@close="removeMessage"
/>
</TransitionGroup>
</template>
<script lang="ts" setup>
import { defineOptions, ref, defineExpose } from 'vue'
import AtjMessage from './AtjMessage.vue'
import type { MessageProps } from './type'
defineOptions({
name: 'MessageGroup'
})
//数组中的每个元素都必须包含 MessageProps 的所有属性
//同时必须有一个 number 类型的 id 属性
//[] 表示这是一个数组
const messages = ref<(MessageProps & { id: number })[]>([])
//存储销毁回调函数的变量
let onDestroy: (() => void) | null = null
//添加函数
//在我们进行添加的时候用户是不需要输入id的,所以我们要自己创建id
//...options,这一段就是说:展开旧的数据把id分别加入对应的数据当中,这样有利于我们进行删除操作
const addMessage = (options: MessageProps) => {
const id = Date.now()
messages.value.push({
...options,
id
})
}
//进行删除操作
//在上述代码中我们是这样些的
/*<AtjMessage
v-for="item in messages"
:key="item.id"
v-bind="item"
@close="removeMessage"
/>*/
//我们在message定义了emit,我们在MessageGroup.vue使用以下函数进行监听
const removeMessage = (id: number) => {
const index = messages.value.findIndex(item => item.id === id)
if (index > -1) {
//删除对应的函数
messages.value.splice(index, 1)
//如果messages数组长度为0就通知uilt.ts文件将创建的div和MessageGroup.vue组件删除
if (messages.value.length === 0 && onDestroy) {
setTimeout(() => {
onDestroy?.()
}, 500) // 等待动画结束后再销毁
}
}
}
//将MessageGroup.vue组件的addMessage方法和onDestroy销毁方法暴露出去让uilt.ts文件可访问
defineExpose({
addMessage,
onDestroy: (callback: () => void) => {
onDestroy = callback
}
})
</script>
<style lang="less">
.atj-message-group {
position: relative;
min-width: 300px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
/* 1. 声明过渡效果 */
.fade-move,
.fade-enter-active,
.fade-leave-active {
transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
}
/* 2. 声明进入和离开的状态 */
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(-20px);
}
/* 3. 确保离开的项目被移除出了布局流
以便正确地计算移动时的动画效果。 */
.fade-leave-active {
position: absolute;
}
</style>
创建utils.ts文件
具体代码如下:
import { createVNode, render } from 'vue'
import MessageGroup from './MessageGroup.vue'
import type { MessageProps } from './type'
let messageInstance: any = null
let container: HTMLElement | null = null
export function createMessage () {
if (!messageInstance) {
// 创建容器
container = document.createElement('div')
container.style.position = 'fixed'
container.style.top = '20px'
container.style.left = '50%'
container.style.transform = 'translateX(-50%)'
container.style.zIndex = '9999'
document.body.appendChild(container)
// 创建 vnode
const vnode = createVNode(MessageGroup)
render(vnode, container)
messageInstance = vnode.component
// 设置销毁回调
messageInstance.exposed?.onDestroy(() => {
if (container && document.body.contains(container)) {
render(null, container)
document.body.removeChild(container)
messageInstance = null
container = null
}
})
}
return messageInstance
}
//在其他文件使用uilt.ts文件可以使用这个方法创建message
export function message (options: MessageProps) {
const instance = createMessage()
instance?.exposed?.addMessage(options)
}
使用
具体代码如下:
在homeview页面中:
<template>
<div class="home">
<button @click="messageclick">点击</button>
</div>
</template>
<script lang="ts" setup>
import { message } from '@/components/message/utils'
import { defineComponent, h } from 'vue'
defineComponent({
name: 'HomeView'
})
function messageclick () {
// 调用 message 组件的函数
// message({
// message: '你点击了我',
// type: 'success',
// showClose: true
// })
message({
message: h('p', null, [
h('span', null, '内容可以是 '),
h('i', { style: 'color: teal' }, 'VNode')
]),
type: 'success',
duration: 30000
})
}
</script>