组件库Message 消息提示组件
BreezliMessage 消息提示组件
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>();
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(); });
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
:根据前一个消息的 bottomOffset
和 offset
累加计算当前消息的 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 属性) |
注意事项
位置堆叠:
- 新消息会堆叠在前一个消息下方,通过
offset
控制间距。
- 首个消息从顶部
offset
开始布局。
动画与销毁:
transitionName
需配合 CSS 过渡类(如 fade-up-enter-active
)。
@after-leave
触发后自动移除 DOM。
类型扩展:
- 通过
messageTypes
扩展支持的类型,需同步更新 typeIconMap
图标映射。
关闭逻辑:
- 点击关闭按钮、Escape 键、或超出
duration
时间后自动关闭。