ShiningDan的博客

一天一个 Element 组件 - Badge

本文是 Element 的组件源码学习系列。

项目源码:ElemeFE/element | GitHub,badge:v2.13.0

Badge 组件使用文档:badge 标记

.vue 文件:/packages/badge

.scss 文件:/packages/theme-chalk/badge.scss

.d.ts 文件:/types/badge.d.ts

Props

我们先来看一下 el-badge 这个组件的入参

1
2
3
4
5
6
7
8
9
10
11
12
props: {
value: [String, Number],
max: Number,
isDot: Boolean,
hidden: Boolean,
type: {
type: String,
validator(val) {
return ['primary', 'success', 'warning', 'info', 'danger'].indexOf(val) > -1;
}
}
},

is-dot

is-dot 这个属性比较简单,当这个属性赋值为 true 的时候,右上角的标记是一个小圆点。我们来看一下是如何实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div class="el-badge">
<slot></slot>
<transition name="el-zoom-in-center">
<sup
v-show="!hidden && (content || content === 0 || isDot)"
v-text="content"
class="el-badge__content"
:class="[
'el-badge__content--' + type,
{
'is-fixed': $slots.default,
'is-dot': isDot
}
]">
</sup>
</transition>
</div>

首先,el-badge 的用法,是 <el-badge> 标签,包裹住需要右上角有标签的元素,所以 <template> 中的实现,是返回了一个 <div>,包裹住被需要有标签的元素 <slot>,在外面形成一个壳。然后通过 CSS,将标签定位到该元素的右上角。

动效

首先,标签被包裹在一个动效里面,这个动效是 ElementUI 设计的动效,具体的效果可以参考 内置过渡动画

动效大致的实现思路是,利用 Vue 提供的原生组件 <transition>,然后实现了具体的类。比如这里的 name = el-zoom-in-center,就需要实现 el-zoom-in-center-enterel-zoom-in-center-activeel-zoom-in-center-leave 等。具体要实现哪些类,在 Vue 文档中可以找到:过渡 & 动画 | 过渡的类名

在 ElementUI 中,这些类的实现,可以在 transition.scss 这里找到,大家有兴趣可以去看一下 ~

CSS

在不考虑动效之后,我们来看一下 is-dot 是如何实现的。

首先,当用户传入 is-dot 的时候,此时 contentundefined

1
2
3
4
5
computed: {
content() {
if (this.isDot) return;
}
}

所以 <sup>classel-badge__contentis-dot

然后 :class 的逻辑里面,还有一句是 'is-fixed': $slots.default。啥意思呢?$slots.default 指的是被 el-badge 包裹住的组件,如果 el-badge 包裹了某个组件,比如一个文本,则 $slots.default 的值就不为空,calss 就还需要加上 is-fixed

我们来看一下这几个类的实现逻辑:

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
// badge.scss
@include b(badge) {
position: relative;
vertical-align: middle;
display: inline-block;

@include e(content) {
background-color: $--badge-background-color;
border-radius: $--badge-radius;
color: $--color-white;
display: inline-block;
font-size: $--badge-font-size;
height: $--badge-size;
line-height: $--badge-size;
padding: 0 $--badge-padding;
text-align: center;
white-space: nowrap;
border: 1px solid $--color-white;

@include when(fixed) {
position: absolute;
top: 0;
right: #{1 + $--badge-size / 2};
transform: translateY(-50%) translateX(100%);

@include when(dot) {
right: 5px;
}
}

@include when(dot) {
height: 8px;
width: 8px;
padding: 0;
right: 0;
border-radius: 50%;
}
}

大体的实现思路如下:

  1. 使用 <sup> 标签。<sup> 标签在浏览器中,有一个默认的 CSS 属性是 vertical-align: super,该属性的作用是,将 <sup> 垂直对齐文本的上标,也就是放在右上角
  2. 设置 position: relative 包裹住需要有角标的 HTML 元素,方便把角标通过 position: absolute 定位到右上角;设置 display: inline-block,让被包裹元素和角标在一行
  3. 如果被包裹元素存在,则触发 @include when(fixed) 函数,设置 position: absolute; top: 0; right: #{1 + $--badge-size / 2};,将角标调整到右上角向左 10px
  4. 针对元素的宽度和高度,做一些调整:transform: translateY(-50%) translateX(100%);。这句话大概的意思是:把元素沿着横向(x轴)移动自身宽度的100%,一般是从左侧为开始点也就是0点。而数值是100%,所以是从右侧0点向左移动自身宽度的100%。同时,向上移动自身高度的 50%

所以,整体实现的效果是,如果设置 right: 0px; transform: translateY(-50%) translateX(100%);。则 badge 放在被定位元素的右上角,从 X 轴上面没有重合,Y 轴上面,badge 上移了 50% 的高度。然后为了美观,将 right 设置为 #{1 + $--badge-size / 2};。通过 var.scss 文件,我们可以得到 $--badge-size 默认是 18px。所以默认情况下,badge 在被包裹元素的右上角,朝左移动 10px,高度有 50% 的重合。

当然,如果有 is-Dot 的时候,直接将 <sup> 通过 width = height = 8px; border-radius: 50%,画成一个小圆球就好。

max 和 value

value 有值的时候,content 就有值。此时赋值 v-text="content",就是让 <sup> 元素包裹一个文本就好。

我们可以看一下 max 是怎么实现的:

1
2
3
4
5
6
7
8
9
10
11
12
content() {
if (this.isDot) return;

const value = this.value;
const max = this.max;

if (typeof value === 'number' && typeof max === 'number') {
return max < value ? `${max}+` : value;
}

return value;
}

如果 maxvalue 都存在并且是数字类型,则比较 valuemax 的大小,如果 value 小于 max,则 badge 的内容就是 value 的值。如果 value 大于 max,则 badge 的内容就是 ${max}+ 字符串的值。比如 value = 100; max = 10,则 badge 的内容就是 10+