Skip to content

Commit 410ce62

Browse files
committed
feat(badge): 新增徽章组件及示例
1 parent 574d0fc commit 410ce62

10 files changed

Lines changed: 228 additions & 0 deletions

File tree

‎apps/example/src/router/modules/component.example.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ const routes: RouteRecordRaw = {
2929
title: '头像',
3030
},
3131
},
32+
{
33+
path: 'badge',
34+
name: 'componentExampleBadge',
35+
component: () => import('@/views/component_example/badge.vue'),
36+
meta: {
37+
title: '徽章',
38+
},
39+
},
3240
{
3341
path: 'button',
3442
name: 'componentExampleButton',
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<script setup lang="ts">
2+
import { badge } from '@fantastic-admin/components/examples'
3+
</script>
4+
5+
<template>
6+
<div>
7+
<FaPageHeader title="徽章" description="FaBadge" />
8+
<FaPageMain
9+
v-for="example in badge"
10+
:key="example.title"
11+
:code="example.componentRaw"
12+
:title="example.title"
13+
>
14+
<component :is="example.component" />
15+
</FaPageMain>
16+
</div>
17+
</template>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# FaBadge 徽章
2+
3+
用于展示数量、状态的小徽章,支持数字、文本和点状模式,带有入场动画。
4+
5+
## 使用场景
6+
7+
- 消息通知数量提示
8+
- 未读消息数显示
9+
- 状态指示器(在线/离线)
10+
- 新内容标记(NEW、HOT 等)
11+
- 购物车商品数量
12+
13+
## Props
14+
15+
| 属性 | 类型 | 默认值 | 说明 |
16+
|------|------|--------|------|
17+
| `value` | `string \| number \| boolean` | - | 徽章显示内容,为空/0/false 时自动隐藏 |
18+
| `variant` | `'default' \| 'secondary' \| 'destructive'` | `'default'` | 徽章颜色变体 |
19+
| `class` | `HTMLAttributes['class']` | - | 容器 CSS 类 |
20+
| `badgeClass` | `HTMLAttributes['class']` | - | 徽章本身的 CSS 类 |
21+
22+
## Slots
23+
24+
| 名称 | 说明 |
25+
|------|------|
26+
| `default` | 被包裹的内容(如图标、头像等) |
27+
28+
## 注意事项
29+
30+
1. **自动隐藏**
31+
-`value``0``false`、空字符串、`null``undefined` 时,徽章会自动隐藏
32+
- 布尔值 `true` 会显示为一个小圆点
33+
34+
2. **动画效果**:徽章显示/隐藏时有淡入淡出动画
35+
36+
3. **变体说明**
37+
- `default`:主色(蓝色)
38+
- `secondary`:次要色(灰色)
39+
- `destructive`:危险色(红色)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<script setup lang="ts">
2+
// 组件实际使用时无需手动导入,框架会自动导入
3+
import FaIcon from '../../../basic/icon/index.vue'
4+
import FaBadge from '../index.vue'
5+
</script>
6+
7+
<template>
8+
<div class="flex gap-8">
9+
<FaBadge :value="true">
10+
<FaIcon name="i-ri:notification-3-line" />
11+
</FaBadge>
12+
<FaBadge :value="99">
13+
<FaIcon name="i-ri:notification-3-line" />
14+
</FaBadge>
15+
<FaBadge value="噢">
16+
<FaIcon name="i-ri:notification-3-line" />
17+
</FaBadge>
18+
<FaBadge value="9" variant="secondary">
19+
<FaIcon name="i-ri:notification-3-line" />
20+
</FaBadge>
21+
<FaBadge value="9" variant="destructive">
22+
<FaIcon name="i-ri:notification-3-line" />
23+
</FaBadge>
24+
</div>
25+
</template>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import Basic from './_basic.vue'
2+
import BasicRaw from './_basic.vue?raw'
3+
4+
export default [
5+
{
6+
title: '基础',
7+
component: Basic,
8+
componentRaw: BasicRaw,
9+
},
10+
]
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<script setup lang="ts">
2+
import type { PrimitiveProps } from 'reka-ui'
3+
import type { HTMLAttributes } from 'vue'
4+
import type { BadgeVariants } from '.'
5+
import { reactiveOmit } from '@vueuse/core'
6+
import { Primitive } from 'reka-ui'
7+
import { cn } from '#utils'
8+
import { badgeVariants } from '.'
9+
10+
const props = defineProps<PrimitiveProps & {
11+
variant?: BadgeVariants['variant']
12+
class?: HTMLAttributes['class']
13+
}>()
14+
15+
const delegatedProps = reactiveOmit(props, 'class')
16+
</script>
17+
18+
<template>
19+
<Primitive
20+
data-slot="badge"
21+
:class="cn(badgeVariants({ variant }), props.class)"
22+
v-bind="delegatedProps"
23+
>
24+
<slot />
25+
</Primitive>
26+
</template>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { VariantProps } from 'class-variance-authority'
2+
import { cva } from 'class-variance-authority'
3+
4+
export { default as Badge } from './Badge.vue'
5+
6+
export const badgeVariants = cva(
7+
'inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
8+
{
9+
variants: {
10+
variant: {
11+
default:
12+
'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
13+
secondary:
14+
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
15+
destructive:
16+
'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
17+
outline:
18+
'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
19+
},
20+
},
21+
defaultVariants: {
22+
variant: 'default',
23+
},
24+
},
25+
)
26+
export type BadgeVariants = VariantProps<typeof badgeVariants>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<script setup lang="ts">
2+
import type { VariantProps } from 'class-variance-authority'
3+
import type { HTMLAttributes } from 'vue'
4+
import { cva } from 'class-variance-authority'
5+
import { computed, ref } from 'vue'
6+
import { cn } from '#utils'
7+
import { Badge } from './badge'
8+
9+
defineOptions({
10+
name: 'BuiltInBadge',
11+
})
12+
13+
const props = defineProps<{
14+
variant?: BadgeVariant['variant']
15+
value: string | number | boolean
16+
class?: HTMLAttributes['class']
17+
badgeClass?: HTMLAttributes['class']
18+
}>()
19+
20+
const badgeDotVariant = cva(
21+
'absolute start-[100%] h-1.5 w-1.5 rounded-full px-0 ring-1 ring-background before:(absolute inset-0 bg-inherit block h-full w-full animate-ping rounded-full content-empty) -translate-x-[50%] -translate-y-[50%] rtl:(translate-x-[50%]) -indent-9999',
22+
{
23+
variants: {
24+
variant: {
25+
default:
26+
'bg-primary hover:bg-primary/80',
27+
secondary:
28+
'bg-secondary hover:bg-secondary/80',
29+
destructive:
30+
'bg-destructive hover:bg-destructive/80',
31+
},
32+
},
33+
defaultVariants: {
34+
variant: 'default',
35+
},
36+
},
37+
)
38+
type BadgeVariant = VariantProps<typeof badgeDotVariant>
39+
40+
const show = computed(() => {
41+
switch (typeof props.value) {
42+
case 'string':
43+
return props.value.length > 0
44+
case 'number':
45+
return props.value > 0
46+
case 'boolean':
47+
return props.value
48+
default:
49+
return props.value !== undefined && props.value !== null
50+
}
51+
})
52+
53+
const transitionClass = ref({
54+
enterActiveClass: 'ease-in-out duration-500',
55+
enterFromClass: 'opacity-0',
56+
enterToClass: 'opacity-100',
57+
leaveActiveClass: 'ease-in-out duration-500',
58+
leaveFromClass: 'opacity-100',
59+
leaveToClass: 'opacity-0',
60+
})
61+
</script>
62+
63+
<template>
64+
<div :class="cn('relative inline-flex', props.class)">
65+
<slot />
66+
<Transition v-bind="transitionClass">
67+
<div v-if="show" class="h-full w-full absolute">
68+
<div v-if="value === true" :class="badgeDotVariant({ variant })" />
69+
<Badge v-else :variant :class="cn('absolute start-[50%] top-0 z-20 whitespace-nowrap px-1.5 py-0 ring-1 ring-primary-foreground -translate-y-[50%] hover:bg-none!', props.badgeClass)">
70+
{{ value }}
71+
</Badge>
72+
</div>
73+
</Transition>
74+
</div>
75+
</template>

‎packages/components/src/examples.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { default as alert } from './basic/alert/_examples'
22
export { default as avatar } from './basic/avatar/_examples'
3+
export { default as badge } from './basic/badge/_examples'
34
export { default as buttonGroup } from './basic/button-group/_examples'
45
export { default as button } from './basic/button/_examples'
56
export { default as card } from './basic/card/_examples'

‎packages/components/src/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// 基础版组件
22
export { default as FaAlert } from './basic/alert/index.vue'
33
export { default as FaAvatar } from './basic/avatar/index.vue'
4+
export { default as FaBadge } from './basic/badge/index.vue'
45
export { default as FaButtonGroup } from './basic/button-group/index.vue'
56
export { default as FaButton } from './basic/button/index.vue'
67
export { default as FaCard } from './basic/card/index.vue'

0 commit comments

Comments
 (0)