{T}

NoticeMenu 组件设计与样式演进

概述

通知中心是后台系统头部的标配能力,但成熟的组件不会一上来就做完整中心。更合理的演进路径是:先用 Badge + Dropdown 搭出头部通知入口,再逐步补齐角标样式扩展、整体缩放、弹出层面板与列表数据化。本文沿着这条主线,拆解 NoticeMenu 从静态入口走向可复用组件的关键决策点,重点落在「能力透传的取舍」「CSS 变量桥接」「scale 与位移的联动映射」「弹出层结构分层」四个层面。

学习目标

  • 理解通知菜单「先入口、后中心」的分层推进思路,以及 Badge 与 Dropdown 的职责边界
  • 掌握基于 Partial<BadgeProps> 的能力透传,以及官方 API 与 CSS 变量扩展的优先级
  • 用 CSS 变量桥接把自定义 props 映射到角标深层样式,并兜住默认值
  • scale() 配合 translateX 的联动映射实现角标整体缩放,避免缩放后定位跑偏
  • 把弹出层拆成「面板 / 列表 / 单项 / 操作区」四层,并用数据驱动与 Props 透传提升复用性

一、分层推进:先入口,后中心

课程没有一开始就做复杂通知系统,而是先围绕 el-badgeel-dropdown 搭出头部通知入口。这两层职责必须分清:

  • el-badge 负责「入口状态显示」(未读数、红点、角标)
  • el-dropdown(或 el-popover)负责「展开内容承载」
Vue SFC
<template>
  <el-dropdown trigger="click">
    <el-badge :value="12">
      <el-icon><Bell /></el-icon>
    </el-badge>

    <template #dropdown>
      <el-dropdown-menu>
        <el-dropdown-item
          v-for="item in items"
          :key="item.title"
        >
          {{ item.title }}
        </el-dropdown-item>
      </el-dropdown-menu>
    </template>
  </el-dropdown>
</template>

当列表开始由 items 数据驱动、并对外暴露 badgeProps 时,NoticeMenu 就已经从静态 UI 进入了可配置组件阶段。这一步的关键不在于代码量,而在于「数据结构先于复杂交互」——先把通知项结构稳定下来,后续扩展才有锚点。

二、角标能力透传与样式扩展取舍

封装 NoticeMenu 时,底层用的是 el-badge,最自然的做法是复用 Element Plus 的 props 类型做透传:

ts
import type { BadgeProps } from "element-plus"

interface Props {
  badgeProps?: Partial<BadgeProps>
}

Partial<BadgeProps> 让原生字段都变为可选,用户可继续透传 maxis-dothiddenoffset 等。但要注意取舍优先级:

  1. 官方已支持的字段(valuecolorbadge-stylebadge-classoffset)优先直接复用,不要让用户误以为 size 是 Badge 原生字段
  2. 官方不支持的视觉能力(如自定义 size),再自己封装并映射到 badge-style 或 CSS 变量
  3. 样式覆盖最后才用 :deep(.el-badge__content) 这类深度选择器,因为它强依赖内部类名、可维护性差

CSS 变量桥接是比写死更稳的方案:在根节点用 :style 塞入变量并设置默认值,再在深层样式里读取,props 与样式之间建立稳定映射,用户不传时也能退回正常样式。

ts
const cssVars = computed(() => ({
  "--notice-badge-color": props.color || "var(--el-color-danger)",
  "--notice-badge-font-size": `${props.size ?? 12}px`
}))

补充:Vue SFC 也支持 <style> 内直接用 v-bind(color) 把响应式数据接进样式,是无须手写 :style 的对象写法。它与 CSS 变量桥接思路一致,选哪种取决于你是否要把变量下沉到深层 :deep 选择器——深层样式仍建议走 CSS 变量。

三、整体缩放:scale 与 translateX 的联动映射

只调 font-size 解决的是文字大小,不是角标整体几何尺寸——内边距、最小宽度、定位偏移、圆角都会共同决定视觉大小。当 font-size 已经很小还想继续缩小时,需要引入 transform: scale() 做整体几何变换。

scale() 不是孤立生效的:缩小后原来的 translateX 往往不再合适,角标会偏得太远或贴得太近。因此 scaletranslateX 必须建立联动映射,封装成函数统一收口,而不是在模板和样式里散落魔法数字。

ts
function calculateTransform(scale: number) {
  const minScale = 0.4
  const maxScale = 1
  const minTranslateX = 75
  const maxTranslateX = 100

  // 边界收敛,避免用户传入极端值导致组件失真
  const normalizedScale = Math.min(maxScale, Math.max(minScale, scale))
  const ratio = (normalizedScale - minScale) / (maxScale - minScale)
  const translateX = minTranslateX + ratio * (maxTranslateX - minTranslateX)

  return { translateX, scale: normalizedScale }
}

computed 承载映射结果并通过 CSS 变量传递,是最合适的方式——缩放值是输入,位移与最终缩放是派生结果,天然适合计算属性:

Vue SFC
<style scoped>
:deep(.el-badge__content) {
  transform:
    translateY(-50%)
    translateX(var(--notice-badge-translate-x, 100%))
    scale(var(--notice-badge-scale, 1));
}
</style>

注意 transform 内多个函数的顺序会影响最终视觉,不要随意交换 translateXscale 的位置。

四、弹出层结构:面板、列表、单项、操作区

当通知内容变丰富,弹出层更适合由 el-popover 承载(可放标题区、列表区、底部操作区),Badge 只保留入口提示职责。通知入口与通知内容要拆层,不要把列表 DOM 全部堆进基础组件:

text
NoticeMenu        负责:入口、角标、弹出层容器
NoticeMessageList 负责:消息列表渲染
NoticeMessageItem 负责:单条消息布局

单条通知项最常见的结构是「标题 / 内容 / 时间」三段式,再配合标签强化状态:

  • 标题:最重要,单行省略(line-clamp-1
  • 内容:次重要,最多两行省略(line-clamp-2
  • 时间:最弱,更小字号、更浅灰色

标题省略不生效的常见坑是 Flex 子项默认最小宽度限制住了,需要给标题元素加 flex: 1; min-width: 0; 才允许压缩省略。底部操作区(清空、更多)课程最终放弃了 el-button-group——因为它默认边框、间距、hover 覆盖成本过高,直接回归自定义 div + Flex 反而更可控。

五、列表数据化与 Props 透传

列表组件成熟的第一步,是把写死的内容改成 item 驱动。把标题、内容、时间、标签抽象成数据字段,再配合类型抽离与 v-bind 透传,父组件就能优雅地传入任意通知集合。

Vue SFC
<template>
  <div
    v-for="item in list"
    :key="item.id"
    class="notice-item"
  >
    <div class="notice-item__title line-clamp-1">{{ item.title }}</div>
    <div class="notice-item__content line-clamp-2">{{ item.content }}</div>
    <div class="notice-item__time">{{ item.time }}</div>
  </div>
</template>

数据化之后,复用性的关键点落在三处:Action 数量不固定时用数组驱动渲染而非写死 DOM;事件跨组件传递用 emit 或插槽回调;样式定制用 props、CSS 变量或主题 token 暴露。到这一步,NoticeMenu 已经不再是样式练习题,而是逐步走向真正的基础组件。


常见问题

问题原因解决方案
想传 sizeel-badge 没反应size 不是 Badge 官方 props自己封装 size,映射到 badge-style 或 CSS 变量
颜色传了没生效没用官方 color/badge-style,或 CSS 变量默认值没兜住优先用官方 color,变量方案补默认值
缩放后角标位置跑偏scale() 后仍沿用旧 translateXcalculateTransformtranslateXscale 建立映射
标题省略不生效Flex 子项最小宽度限制住了给标题元素加 min-width: 0 再配单行省略
el-button-group 很难改样式默认边框、间距、hover 覆盖成本过高直接改用自定义 div 结构渲染操作区
用户传了 0.12 显示异常没对 scale 做边界约束在映射函数里用 min/max 收敛

延伸阅读