Message 消息提示组件

Message 消息提示组件


Message.vue

script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<script setup lang="ts">
import type { MessageProps } from './types';
import { computed, onMounted, ref, watch } from 'vue';
import { useOffset } from './methods';
import { typeIconMap } from '@veyra/utils';
import VrIcon from '../Icon/Icon.vue';

defineOptions({
name: 'VrMessage', // 定义组件名称
});

const props = withDefaults(defineProps<MessageProps>(), {
type: 'info', // 默认类型
duration: 3000, // 默认自动关闭时间
offset: 10, // 消息间距
transitionName: 'fade-up', // 默认过渡动画
});

const visible = ref(false); // 控制消息可见性
const messageRef = ref<HTMLDivElement>(); // 消息 DOM 引用

// 图标映射(根据 type 映射图标)
const iconName = computed(() => typeIconMap.get(props.type) ?? 'circle-info');

// 计算消息垂直位置
const { topOffset, bottomOffset } = useOffset({
getLastBottomOffset: getLastBottomOffset.bind(props),
offset: props.offset,
boxHeight: computed(() => messageRef.value?.offsetHeight ?? 0),
});

// 动态样式绑定
const cssStyle = computed(() => ({
top: `${topOffset.value}px`,
zIndex: props.zIndex,
}));

let timer: number;
// 启动自动关闭定时器
function startTimer() {
if (props.duration === 0) return;
timer = setTimeout(close, props.duration);
}

// 清除定时器
function clearTimer() {
clearTimeout(timer);
}

// 关闭消息
function close() {
visible.value = false;
}

// 组件挂载时初始化
onMounted(() => {
visible.value = true;
startTimer();
});

// 监听 visible 变化触发销毁逻辑
watch(visible, (val) => {
if (!val) props.onDestory(); // 触发销毁回调
});

// 暴露方法给外部
defineExpose({
bottomOffset,
close,
});
</script>

关键解释

defineOptions

定义组件名称为 VrMessage,在全局注册时需通过 VrMessage 调用。

1
2
3
defineOptions({
name: 'VrMessage',
});

withDefaults

props 设置默认值:

1
2
3
4
5
6
const props = withDefaults(defineProps<MessageProps>(), {
type: 'info',
duration: 3000,
offset: 10,
transitionName: 'fade-up',
});

useOffset

动态计算消息垂直位置:

  • topOffset:根据前一个消息的 bottomOffsetoffset 累加计算当前消息的 top 值。
  • boxHeight:通过 messageRef 获取当前消息的高度,用于计算后续消息的偏移。

Transition

通过 transitionName 控制入场/离开动画:

  • @after-leave:动画结束后触发 onDestory 移除 DOM。

defineExpose

暴露以下属性和方法给外部:

  • bottomOffset:当前消息底部位置,供后续消息计算偏移。
  • close:手动关闭消息的方法。

template

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<template>
<Transition
:name="transitionName"
@after-leave="props.onDestory()"
>
<div
v-show="visible"
:style="cssStyle"
class="vr-message"
:class="{
[`vr-message--${type}`]: type, // 类型样式(如 vr-message--success)
'is-close': showClose, // 是否显示关闭按钮
'text-center': center, // 文字居中
}"
role="alert"
@mouseenter="clearTimer" // 鼠标进入暂停定时器
@mouseleave="startTimer" // 鼠标离开重启定时器
>
<!-- 图标 -->
<vr-icon
v-if="type !== 'text'"
class="vr-message__icon"
:icon="iconName"
/>
<!-- 内容 -->
<div class="vr-message__content">
<slot>
<render-vnode v-if="message" :vNode="message" />
</slot>
</div>
<!-- 关闭按钮 -->
<div v-if="showClose" class="vr-message__close">
<vr-icon icon="xmark" @click.stop="close" />
</div>
</div>
</Transition>
</template>

关键解释

Transition

  • name="transitionName":绑定动画名称(如 fade-up)。
  • @after-leave:动画结束后触发 onDestory 移除 DOM。

v-show="visible"

控制消息的显示/隐藏,由 visible 响应式变量驱动。


@mouseenter/@mouseleave

  • 鼠标悬停时暂停自动关闭,移出后恢复定时器。

图标与内容

  • type !== 'text':文本类型不显示图标。
  • <render-vnode>:渲染 message 属性传递的 VNode 内容(兼容字符串或自定义 VNode)。

methods.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import type { CreateMessageProps, MessageInstance } from './types';
import { render, h, shallowReactive } from 'vue';
import { useZIndex } from '@veyra/hooks';

const instances = shallowReactive<MessageInstance[]>([]);
const { nextZIndex } = useZIndex();

export const messageDefaults = {
type: 'info',
duration: 3000,
offset: 10,
transitionName: 'fade-up',
};

// 创建消息实例
export function createMessage(props: CreateMessageProps): MessageInstance {
const id = `message-${Date.now()}`;
const container = document.createElement('div');
const destory = () => {
const idx = instances.findIndex(i => i.id === id);
if (idx !== -1) {
instances.splice(idx, 1);
render(null, container);
}
};
const _props = {
...props,
id,
zIndex: nextZIndex(),
onDestory: destory,
};
const vnode = h(MessageConstructor, _props);
render(vnode, container);
document.body.appendChild(container.firstElementChild!);
return {
id,
vnode,
props: _props,
handler: {
close: () => vnode.component!.exposed!.close(),
},
};
}

// 获取前一个消息的底部偏移
export function getLastBottomOffset(this: MessageProps) {
const prevInstance = instances.find(
i => i.id < this.id
);
return prevInstance?.exposed?.bottomOffset ?? 0;
}

// 关闭所有消息
export function closeAll(type?: messageType) {
instances.forEach(instance => {
if (!type || instance.props.type === type) {
instance.handler.close();
}
});
}

关键解释

createMessage

  • 生成唯一 id,并创建消息 DOM。
  • 通过 useZIndex 管理层级,确保新消息始终在最上层。
  • 将实例存入 instances 数组,支持批量操作。

getLastBottomOffset

根据当前消息的 id,查找前一个消息的 bottomOffset,用于计算当前消息的 top 值。


closeAll

关闭指定类型或所有消息:

1
2
message.closeAll('warning'); // 关闭所有警告类型消息
message.closeAll(); // 关闭所有消息

types.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { VNode } from 'vue';

export const messageTypes = ['info', 'success', 'warning', 'danger', 'error'] as const;
export type messageType = (typeof messageTypes)[number];

export interface MessageProps {
id: string;
message?: string | VNode | (() => VNode);
duration?: number;
showClose?: boolean;
center?: boolean;
type?: messageType;
offset?: number;
zIndex: number;
transitionName?: string;
onDestory: () => void;
}

export type MessageOptions = Partial<Omit<MessageProps, 'id' | 'onDestory'>>;

export interface MessageInstance {
id: string;
vnode: VNode;
props: MessageProps;
handler: {
close: () => void;
};
}

export type CreateMessageProps = Omit<
MessageProps,
'id' | 'zIndex' | 'onDestory'
>;

关键解释

messageTypes

定义支持的消息类型枚举:

1
2
3
4
5
6
7
export const messageTypes = [
'info',
'success',
'warning',
'danger',
'error',
] as const;

MessageProps

  • id: 消息唯一标识。
  • message: 支持字符串、VNode 或返回 VNode 的函数。
  • showClose: 是否显示关闭按钮。
  • center: 内容是否居中。

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup>
import { message } from 'veyra-ui';

// 基础用法
message('操作成功', {
type: 'success',
duration: 2000,
});

// 自定义内容
message({
message: () => h('div', { style: 'color: red' }, '自定义内容'),
showClose: true,
});

// 关闭所有警告消息
message.closeAll('warning');
</script>

核心 API

Props

属性 类型 默认值 说明
type messageType info 消息类型(info/success/warning/danger/error
message `string VNode (() => VNode)`
duration number 3000 自动关闭时间(毫秒),0 表示不关闭
showClose boolean false 是否显示关闭按钮
offset number 10 消息垂直间距
transitionName string fade-up 过渡动画名称

方法

方法名 参数 说明
message() MessageOptions 创建并显示消息
success() MessageOptions 创建成功类型的消息
closeAll() type?: messageType 关闭指定类型或所有消息

插槽

名称 说明
默认插槽 自定义消息内容(覆盖 message 属性)

注意事项

  1. 位置堆叠

    • 新消息会堆叠在前一个消息下方,通过 offset 控制间距。
    • 首个消息从顶部 offset 开始布局。
  2. 动画与销毁

    • transitionName 需配合 CSS 过渡类(如 fade-up-enter-active)。
    • @after-leave 触发后自动移除 DOM。
  3. 类型扩展

    • 通过 messageTypes 扩展支持的类型,需同步更新 typeIconMap 图标映射。
  4. 关闭逻辑

    • 点击关闭按钮、Escape 键、或超出 duration 时间后自动关闭。