消息组件:测试组件事件与属性
本节目标
- 准备完整的 Mock 数据测试 Notice 组件
- 修复 TypeScript 类型错误(Partial、extends、icon 绑定)
- 实现事件透传与类型提示
- 完成样式微调(宽度、间距、hover 效果)
1. 准备 Mock 测试数据
1.1 Actions 数据
// pages/component/notice/NoticeMessage.vue
const actions: NoticeActionItem[] = [
{
title: '清空',
icon: 'ep:delete',
click: () => {
console.log('清空消息')
},
},
{
title: '更多',
icon: 'ep:more',
click: () => {
console.log('查看更多')
},
},
]
ts
1.2 List 数据
const list: NoticeMessageListTab[] = [
{
title: '通知',
contents: [
{
title: '系统升级通知',
content: '系统将于今晚 22:00 进行升级维护,届时服务将暂停约 30 分钟',
time: '2024-10-01 10:30',
tag: '重要',
tagProps: { type: 'danger' },
avatar: { src: 'https://example.com/avatar1.png' },
},
{
title: '新功能上线',
content: '消息中心新增批量已读功能,支持一键标记所有消息为已读',
time: '2024-10-01 09:15',
tag: '新功能',
tagProps: { type: 'success' },
avatar: { src: 'https://example.com/avatar2.png' },
},
{
title: '安全提醒',
content: '检测到您的账号在新设备上登录,如非本人操作请及时修改密码',
time: '2024-09-30 18:00',
avatar: { src: 'https://example.com/avatar3.png' },
},
],
},
{
title: '消息',
contents: [
{
title: '张三 评论了你的文章',
content: '写得很棒,学到了很多关于组件封装的知识点',
time: '2024-10-01 11:20',
avatar: { src: 'https://example.com/avatar4.png' },
},
],
},
{
title: '待办',
contents: [
{
title: '代码审查任务',
content: '你有 3 个待审查的 Pull Request 需要处理',
time: '2024-10-01 08:00',
tag: '紧急',
tagProps: { type: 'warning' },
},
],
},
]
ts
2. 修复 TypeScript 类型错误
2.1 extends 报错:不能扩展不满足约束的类型
问题:NoticeProps extends 了 NotificationProps(其中的 BadgeProps 字段是必选的),但实际使用时这些字段不必传。
解决:使用 Partial 包裹 NotificationProps:
// types.d.ts
export interface NoticeProps
extends NoticeMessageListProps,
Partial<NotificationProps> {} // 关键:Partial 包裹
ts
2.2 Icon 属性绑定类型不匹配
问题:直接用 v-bind="action.icon" 绑定到 Icon 组件时,action.icon 是 string 类型,但 Icon 组件的 Props 可能包含更多属性。
解决:显式绑定每个属性:
<!-- 不推荐:类型不匹配 -->
<Icon v-bind="action.icon" />
<!-- 推荐:显式绑定 -->
<Icon
:icon="action.icon"
:style="action.style"
/>
html
2.3 TagProps 的 Partial 处理
export interface MessageListItem {
// ...
tagProps?: Partial<TagProps> // 用 Partial,因为不需要传所有 Tag 属性
}
ts
3. 事件透传与类型提示
3.1 问题:v-bind="$attrs" 无法提供事件类型提示
当使用 v-bind="$attrs" 透传属性时,父组件无法获得事件的类型提示。
3.2 解决:在 Notice 组件中显式定义 Emits
<!-- components/notice/Notice.vue -->
<script setup lang="ts">
import type { MessageListItem } from './types'
import type { AvatarProps, TabsPaneContext } from 'element-plus'
const emit = defineEmits<{
(e: 'click-item', item: MessageListItem): void
(e: 'click-avatar', avatar: Partial<AvatarProps>): void
(e: 'click-tab', context: TabsPaneContext, event: Event): void
}>()
// 转发事件
const forwardedEvents = {
onClickItem: (item: MessageListItem) => emit('click-item', item),
onClickAvatar: (avatar: Partial<AvatarProps>) => emit('click-avatar', avatar),
onClickTab: (context: TabsPaneContext, event: Event) => emit('click-tab', context, event),
}
</script>
vue
3.3 模板中绑定转发事件
<NoticeMessageList
:list="list"
:actions="actions"
wrap-class="w-300px"
@click-item="forwardedEvents.onClickItem"
@click-avatar="forwardedEvents.onClickAvatar"
@click-tab="forwardedEvents.onClickTab"
/>
html
3.4 父组件获得类型提示
<!-- pages/component/notice/NoticeMessage.vue -->
<Notice
:list="list"
:actions="actions"
icon="ep:bell"
:value="12"
@click-item="handleClickItem"
@click-avatar="handleClickAvatar"
@click-tab="handleClickTab"
/>
vue
此时 handleClickItem 的参数自动获得 MessageListItem 类型提示。
4. Tabs 默认选中与切换
4.1 默认选中第一个 Tab
// NoticeMessageList.vue
const props = defineProps<NoticeMessageListProps>()
const activeName = ref(props.list?.[0]?.title || '')
ts
4.2 Tab 标签绑定
<el-tabs v-model="activeName" @tab-click="handleTabClick">
<el-tab-pane
v-for="(tab, tabIndex) in list"
:key="tabIndex"
:label="tab.title"
:name="tab.title"
>
html
注意:label 和 name 都绑定到 tab.title,确保 v-model 能正确匹配。
5. 样式微调
5.1 弹出面板宽度
通过 wrapClass 属性控制:
<NoticeMessageList wrap-class="w-300px" />
vue
或使用 wrapStyle:
<NoticeMessageList :wrap-style="{ width: '360px' }" />
vue
5.2 Tabs 内边距
/* 使用深度选择器调整 Element Plus Tabs 样式 */
:deep(.el-tabs__header) {
padding-left: 12px;
}
:deep(.el-tabs__content) {
padding-left: 12px;
padding-right: 12px;
}
css
5.3 消息项交互效果
.message-item {
@apply p-3 cursor-pointer hover:bg-sky-100 transition-colors;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.message-item:last-child {
border-bottom: none;
}
css
5.4 操作栏样式
.action-bar {
@apply flex items-center justify-center border-t border-gray-200;
}
.action-item {
@apply flex items-center justify-center flex-1 py-3
text-gray-500 text-sm cursor-pointer
hover:bg-sky-100 transition-colors;
}
.action-item:not(:last-child) {
@apply border-r border-gray-200;
}
css
6. 完整测试页面
<!-- pages/component/notice/NoticeMessage.vue -->
<template>
<div class="p-4">
<h2 class="text-xl font-bold mb-4">通知组件测试</h2>
<Notice
:list="list"
:actions="actions"
icon="ep:bell"
:value="12"
:max="99"
color="#ff6600"
:size="12"
wrap-class="w-360px"
@click-item="handleClickItem"
@click-avatar="handleClickAvatar"
@click-tab="handleClickTab"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Notice from '~/components/notice/Notice.vue'
import type { NoticeActionItem, NoticeMessageListTab, MessageListItem } from '~/components/notice/types'
import type { AvatarProps, TabsPaneContext } from 'element-plus'
const actions = ref<NoticeActionItem[]>([
{
title: '清空',
icon: 'ep:delete',
click: () => console.log('清空消息'),
},
{
title: '更多',
icon: 'ep:more',
click: () => console.log('查看更多'),
},
])
const list = ref<NoticeMessageListTab[]>([
{
title: '通知',
contents: [
{
title: '系统升级通知',
content: '系统将于今晚 22:00 进行升级维护',
time: '2024-10-01 10:30',
tag: '重要',
tagProps: { type: 'danger' },
avatar: { src: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png' },
},
{
title: '新功能上线',
content: '消息中心新增批量已读功能',
time: '2024-10-01 09:15',
tag: '新功能',
tagProps: { type: 'success' },
avatar: { src: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png' },
},
],
},
{
title: '消息',
contents: [
{
title: '张三 评论了你的文章',
content: '写得很棒,学到了很多',
time: '2024-10-01 11:20',
avatar: { src: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png' },
},
],
},
{
title: '待办',
contents: [
{
title: '代码审查任务',
content: '你有 3 个待审查的 Pull Request',
time: '2024-10-01 08:00',
tag: '紧急',
tagProps: { type: 'warning' },
},
],
},
])
// 事件处理
const handleClickItem = (item: MessageListItem) => {
console.log('点击消息项:', item)
}
const handleClickAvatar = (avatar: Partial<AvatarProps>) => {
console.log('点击头像:', avatar)
}
const handleClickTab = (context: TabsPaneContext, event: Event) => {
console.log('切换标签页:', context.paneName)
}
</script>
vue
测试验证
- 点击通知图标弹出消息面板
- Tab 标签正常切换,内容跟随变化
- 点击消息项打印
item数据 - 点击头像打印
avatar数据 - 底部操作按钮触发对应回调
- 鼠标 hover 消息项有高亮效果
7. 关键知识点总结
| 知识点 | 说明 |
|---|---|
| Mock 数据生成 | 按 interface 生成测试数据,确保类型安全 |
Partial<NotificationProps> | 避免不必要字段被标记为必选 |
| 显式属性绑定 | 避免 v-bind 整个对象导致类型不匹配 |
defineEmits 类型声明 | 提供父组件事件回调的类型提示 |
| 事件转发 | 中间组件定义 Emits 后逐层转发 |
activeName 默认值 | 取 list[0].title 作为默认选中 Tab |
:deep() 样式穿透 | 调整 Element Plus 组件内部样式 |
8. 消息组件完整架构回顾
Notice.vue # 业务组合组件(对外接口)
├── Notification.vue # 基础通知徽章
│ ├── ElBadge (Element Plus) # 原生 Badge
│ └── IconDefine.vue # 动态图标
├── NoticeMessageList.vue # 消息列表弹出层
│ ├── ElDropdown (Element Plus) # 下拉容器
│ ├── ElTabs (Element Plus) # 标签页切换
│ ├── ElAvatar (Element Plus) # 头像
│ ├── ElTag (Element Plus) # 标签
│ └── 自定义 Action 按钮
└── types.d.ts # 统一类型定义
├── NotificationProps
├── NoticeMessageListProps
├── NoticeMessageListTab
├── MessageListItem
├── NoticeActionItem
└── NoticeProps (聚合接口)
text
↑