Badge 元件是一種簡單但功能強大的資料展示元件,通常以小圓形的形式出現,依附於其他 UI 元件上。Badge 元件主要用於通知提醒,顯示未讀訊息、通知數量,或是狀態指示如上線中、離線、忙碌等。
在開始實作前,我們先研究各個 UI Library 的 Badge 元件是如何設計的。
Element Plus
<template>
<ElBadge :value="12" class="item">
<ElButton>Button</ElButton>
</ElBadge>
<ElBadge :value="99" :max="9" class="item">
<ElButton>Button</ElButton>
</ElBadge>
<ElBadge :value="1" class="item" is-dot>
<ElButton>Button</ElButton>
</ElBadge>
<ElBadge :value="2" class="item" type="warning">
<ElButton>Button</ElButton>
</ElBadge>
<ElBadge :value="1" class="item" color="green">
<ElButton>Button</ElButton>
</ElBadge>
</template>
Element Plus 選用了 color
及 type
來設定 Badge 的樣式,value
用來設定 Badge 的內容,如果希望只顯示的是圓點可以設定 is-dot
,最後 max
可以設定顯示數字的最大值,如果設定為 9,當 value
超過這個數值則只會顯示 9+。
color
及 type
在這裡設定的都是 Badge 的顏色,不過看起來 color
的權重更高一點,並且可以傳入任何 CSS 顏色值。
Vuetify
<template>
<VBtn class="text-none" stacked>
<VBadge color="success" dot>
<VIcon>mdi-home-outline</VIcon>
</VBadge>
</VBtn>
<VBtn class="text-none" stacked>
<VBadge color="error" content="200" max="9">
<VIcon>mdi-store-outline</VIcon>
</VBadge>
</VBtn>
<VBtn class="text-none" stacked>
<VBadge color="error" content="2">
<VIcon>mdi-bell-outline</VIcon>
</VBadge>
</VBtn>
</template>
Vuetify 的 Badge 元件也是以 color
來設定 Badge 的顏色,content
用來設定 Badge 的內容,而圓點樣式可以使用 dot
來開啟,一樣支援 max
設定。
Nuxt UI
<template>
<UChip size="2xl">
<UButton icon="i-heroicons-inbox" color="gray" />
</UChip>
</template>
在 Nuxt UI 中,這個元件的名稱叫做 <UChip>
,除了名稱之外其他部分與 Element Plus 跟 Vuetify 都差不多。
另外,Nuxt UI 可以透過 position
來設定 Badge 的位置,Vuetify 也可以透過 location
設定,而 Element Plus 從目前的文件上看起來尚未支援定位的功能。
綜合以上並結合自身經驗,我們統整出 <AtomicBadge>
的功能:
content
設定 Badge 要顯示的內容。max
設定最大顯示數字。placement
設定 Badge 的位置,支援 top-left
、top-right
、bottom-left
、bottom-right
四個位置。size
設定 Badge 的大小,這裡除了 medium
與 large
外,也將 dot
整合在這個設定裡面。color
設定 Badge 的顏色。showZero
決定當 content
為 0 時是否顯示 Badge。使用結構如下:
<template>
<AtomicBadge
color="primary"
content="12"
max="9"
placement="top-right"
size="medium"
>
<AtomicAvatar size="60" />
</AtomicBadge>
</template>
首先,我們將需求中提到的功能整理成 props
的介面,我們會需要下列屬性:
名稱 | 型別 | 預設值 | 說明 |
---|---|---|---|
content | string |
undefined |
Badge 的內容 |
max | number |
99 |
最大顯示數字 |
placement | top-left , top-right , bottom-left , bottom-right |
top-right |
Badge 的位置 |
size | medium , large , dot |
medium |
Badge 的大小 |
color | primary , success , warning , danger , info |
primary |
Badge 的顏色 |
showZero | boolean |
false |
是否顯示 0 的 Badge |
type Numberish = number | `${number}`;
interface AtomicBadgeProps {
content?: string | number | null;
max?: Numberish;
placement?:
| 'top-left'
| 'top-right'
| 'bottom-left'
| 'bottom-right';
size?: 'dot' | 'medium' | 'large';
color?: 'primary' | 'success' | 'warning' | 'danger' | 'info';
showZero?: boolean;
}
const props = withDefaults(defineProps<AtomicBadgeProps>(), {
content: undefined,
max: 99,
placement: 'top-right',
size: 'medium',
color: 'danger',
showZero: false,
});
首先我們來規劃元件的模板
<template>
<span class="atomic-badge">
<slot name="default" />
<span class="atomic-badge__content">
{{ content }}
</span>
</span>
</template>
Content 的部分我們要加上顯示判斷
content
為 null
或 undefined
則不顯示。content
為數字,要檢查有沒有超出 max
的值,如果超出則顯示 {max}+
。size
為 dot
則不顯示 content
。<span class="atomic-badge__content">
<template v-if="!isNullOrUndefined(content) && size !== 'dot'">
{{ Number(content) > Number(max) ? `${max}+` : content }}
</template>
</span>
接著我們來處理 Badge 的樣式。關於樣式有 Badge 的位置、大小、顏色要處理。
const invisible = computed(() => {
const { content, showZero } = props;
return !showZero && Number(content) === 0;
});
const CONTENT_CLASS = 'atomic-badge__content';
const contentClass = computed(() =>
[
`${CONTENT_CLASS}--${props.placement}`,
`${CONTENT_CLASS}--${props.size}`,
`${CONTENT_CLASS}--${props.color}`,
invisible.value ? `${CONTENT_CLASS}--invisible` : '',
].join(' ')
);
<template>
<span class="atomic-badge">
<slot name="default" />
<span
class="atomic-badge__content"
:class="contentClass"
>
<!-- 略 -->
</span>
</span>
</template>
大小跟顏色的部份我們應該都很熟悉了!這裡主要要處理的是 Badge 的位置。
基本樣式
.atomic-badge {
position: relative;
&__content {
position: absolute;
}
}
.atomic-badge {
&__content {
&--top-right {
top: 0;
right: 0;
transform: translateX(50%) translateY(-50%);
}
}
}
但還有一個要在這裡一併處理的是 showZero
的情況,在這裡為了讓 content
從 0 到 1 的過程中有縮放效果,我們可選用 transform: scale(0)
到 transform: scale(1)
的方式來處理。
所以原本的樣式就要稍微擴充一下:
.atomic-badge {
position: relative;
&__content {
position: absolute;
&--top-right {
top: 0;
right: 0;
transform: translateX(50%) translateY(-50%) scale(1);
}
&--top-right#{&}--invisible {
transform: translateX(50%) translateY(-50%) scale(0);
}
}
}
不多,我們只要重複四次就好了?!
如果跟我一樣覺得這樣很冗長很煩的話,可以參考看看這個受 Tailwind CSS 啟發的作法,我們先新增一個 %transform
的樣式
%transform {
transform: translateX(var(--badge-translate-x)) translateY(var(--badge-translate-y))
scale(var(--badge-scale));
}
再新增 %top
與 %right
的樣式
%top {
--badge-translate-y: -50%;
top: 0;
}
%right {
--badge-translate-x: 50%;
right: 0;
}
整合起來會像是這樣:
.atomic-badge {
position: relative;
&__content {
--badge-scale: 1;
position: absolute;
@extend %transform;
&--invisible {
--badge-scale: 0;
}
&--top-right {
@extend %top;
@extend %right;
}
&--top-left {
@extend %top;
@extend %left;
}
}
}
這樣看起來就簡潔許多了呢!
在現在的版本中,我們的 <AtomicBadge>
遇到圓形的 UI 會離邊界有一點距離
如果能支援貼到邊上的話就太好了!要做到這個功能我們得拿出紙筆算一下數學,我們要算出 <AtomicBadge>
遇到圓形的時候,top
與 right
要偏移的百分比。
第一步,假設圓的直徑為 2
,我們得先算出紅色三角形的斜邊長。
在這裡的紅色三角形是一個「等腰直角三角形」,所以斜邊長與對邊(臨邊)長為 1 : 1 : √2,所以斜邊長為 2√2
。
第二步,斜邊長減掉圓的直徑 2
並除以 2,就會算出虛線方形的對角線長度
第三步,根據第一步提到的 1 : 1 : √2 這個比例我們知道,當我們將上一步的結果除以 √2 就會得到 top
與 right
的值。
再來,√2 約等於 1.414,所以我們可以算出
一開始我們的圓直徑是 2,所以我們要將這個值除以 2 乘以 100 就會得到 top
與 right
要偏移的百分比。
所以我們可以算出來,如果要讓 <AtomicBadge>
貼到圓形邊上,top
與 right
要偏移的百分比為 14.65%
。
我們新增一個 props
叫 overlap
,讓使用者決定要使用圓形還是方形定位的 Badge。
interface AtomicBadgeProps {
overlap?: 'circular' | 'rectangular';
}
const props = withDefaults(defineProps<AtomicBadgeProps>(), {
overlap: 'circular',
});
const CONTENT_CLASS = 'atomic-badge__content';
const contentClass = computed(() =>
[
`${CONTENT_CLASS}--${props.overlap}`,
// 略
].join(' ')
);
.atomic-badge {
position: relative;
&__content {
// 略
&--rectangular {
--badge-offset: 0;
}
&--circular {
--badge-offset: 14.65%;
}
}
}
%top {
--badge-translate-y: -50%;
top: var(--badge-offset);
}
%right {
--badge-translate-x: 50%;
right: var(--badge-offset);
}
這樣我們就可以讓使用者在遇到圓形的時候,可以選擇要貼到邊上還是留一點距離。
這次我們實作了一個 <AtomicBadge>
元件,這個元件可以讓我們在 UI 上加上一個小小的提示,讓使用者知道這個元件有一些特殊的狀態。
在實作元件定位的過程中我們借鑒了 Tailwind CSS 處理 Util Class 的作法,免去了我們不斷定義重複樣式的麻煩,這樣的作法讓我們的樣式更加簡潔。
最後我們也實作了一個進階需求,讓使用者可以選擇要讓 <AtomicBadge>
貼到圓形邊上還是留一點距離,過程中用了基礎的幾何計算,強烈建議遇到計算問題實際拿紙筆算一次或是自己畫圖畫一次,這樣更能深刻吸收並且應用在其他地方。
<AtomicBadge>
原始碼:AtomicBadge.vue