feat(badge): 新增徽章组件及示例

This commit is contained in:
Hooray
2026-06-02 09:45:24 +08:00
parent 574d0fcccd
commit 410ce626a8
10 changed files with 228 additions and 0 deletions
@@ -29,6 +29,14 @@ const routes: RouteRecordRaw = {
title: '头像',
},
},
{
path: 'badge',
name: 'componentExampleBadge',
component: () => import('@/views/component_example/badge.vue'),
meta: {
title: '徽章',
},
},
{
path: 'button',
name: 'componentExampleButton',
@@ -0,0 +1,17 @@
<script setup lang="ts">
import { badge } from '@fantastic-admin/components/examples'
</script>
<template>
<div>
<FaPageHeader title="徽章" description="FaBadge" />
<FaPageMain
v-for="example in badge"
:key="example.title"
:code="example.componentRaw"
:title="example.title"
>
<component :is="example.component" />
</FaPageMain>
</div>
</template>
@@ -0,0 +1,39 @@
# FaBadge 徽章
用于展示数量、状态的小徽章,支持数字、文本和点状模式,带有入场动画。
## 使用场景
- 消息通知数量提示
- 未读消息数显示
- 状态指示器(在线/离线)
- 新内容标记(NEW、HOT 等)
- 购物车商品数量
## Props
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `value` | `string \| number \| boolean` | - | 徽章显示内容,为空/0/false 时自动隐藏 |
| `variant` | `'default' \| 'secondary' \| 'destructive'` | `'default'` | 徽章颜色变体 |
| `class` | `HTMLAttributes['class']` | - | 容器 CSS 类 |
| `badgeClass` | `HTMLAttributes['class']` | - | 徽章本身的 CSS 类 |
## Slots
| 名称 | 说明 |
|------|------|
| `default` | 被包裹的内容(如图标、头像等) |
## 注意事项
1. **自动隐藏**:
- 当 `value` 为 `0`、`false`、空字符串、`null` 或 `undefined` 时,徽章会自动隐藏
- 布尔值 `true` 会显示为一个小圆点
2. **动画效果**:徽章显示/隐藏时有淡入淡出动画
3. **变体说明**:
- `default`:主色(蓝色)
- `secondary`:次要色(灰色)
- `destructive`:危险色(红色)
@@ -0,0 +1,25 @@
<script setup lang="ts">
// 组件实际使用时无需手动导入,框架会自动导入
import FaIcon from '../../../basic/icon/index.vue'
import FaBadge from '../index.vue'
</script>
<template>
<div class="flex gap-8">
<FaBadge :value="true">
<FaIcon name="i-ri:notification-3-line" />
</FaBadge>
<FaBadge :value="99">
<FaIcon name="i-ri:notification-3-line" />
</FaBadge>
<FaBadge value="噢">
<FaIcon name="i-ri:notification-3-line" />
</FaBadge>
<FaBadge value="9" variant="secondary">
<FaIcon name="i-ri:notification-3-line" />
</FaBadge>
<FaBadge value="9" variant="destructive">
<FaIcon name="i-ri:notification-3-line" />
</FaBadge>
</div>
</template>
@@ -0,0 +1,10 @@
import Basic from './_basic.vue'
import BasicRaw from './_basic.vue?raw'
export default [
{
title: '基础',
component: Basic,
componentRaw: BasicRaw,
},
]
@@ -0,0 +1,26 @@
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { BadgeVariants } from '.'
import { reactiveOmit } from '@vueuse/core'
import { Primitive } from 'reka-ui'
import { cn } from '#utils'
import { badgeVariants } from '.'
const props = defineProps<PrimitiveProps & {
variant?: BadgeVariants['variant']
class?: HTMLAttributes['class']
}>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<Primitive
data-slot="badge"
:class="cn(badgeVariants({ variant }), props.class)"
v-bind="delegatedProps"
>
<slot />
</Primitive>
</template>
@@ -0,0 +1,26 @@
import type { VariantProps } from 'class-variance-authority'
import { cva } from 'class-variance-authority'
export { default as Badge } from './Badge.vue'
export const badgeVariants = cva(
'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',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
secondary:
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
destructive:
'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',
outline:
'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
},
},
defaultVariants: {
variant: 'default',
},
},
)
export type BadgeVariants = VariantProps<typeof badgeVariants>
@@ -0,0 +1,75 @@
<script setup lang="ts">
import type { VariantProps } from 'class-variance-authority'
import type { HTMLAttributes } from 'vue'
import { cva } from 'class-variance-authority'
import { computed, ref } from 'vue'
import { cn } from '#utils'
import { Badge } from './badge'
defineOptions({
name: 'BuiltInBadge',
})
const props = defineProps<{
variant?: BadgeVariant['variant']
value: string | number | boolean
class?: HTMLAttributes['class']
badgeClass?: HTMLAttributes['class']
}>()
const badgeDotVariant = cva(
'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',
{
variants: {
variant: {
default:
'bg-primary hover:bg-primary/80',
secondary:
'bg-secondary hover:bg-secondary/80',
destructive:
'bg-destructive hover:bg-destructive/80',
},
},
defaultVariants: {
variant: 'default',
},
},
)
type BadgeVariant = VariantProps<typeof badgeDotVariant>
const show = computed(() => {
switch (typeof props.value) {
case 'string':
return props.value.length > 0
case 'number':
return props.value > 0
case 'boolean':
return props.value
default:
return props.value !== undefined && props.value !== null
}
})
const transitionClass = ref({
enterActiveClass: 'ease-in-out duration-500',
enterFromClass: 'opacity-0',
enterToClass: 'opacity-100',
leaveActiveClass: 'ease-in-out duration-500',
leaveFromClass: 'opacity-100',
leaveToClass: 'opacity-0',
})
</script>
<template>
<div :class="cn('relative inline-flex', props.class)">
<slot />
<Transition v-bind="transitionClass">
<div v-if="show" class="h-full w-full absolute">
<div v-if="value === true" :class="badgeDotVariant({ variant })" />
<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)">
{{ value }}
</Badge>
</div>
</Transition>
</div>
</template>
+1
View File
@@ -1,5 +1,6 @@
export { default as alert } from './basic/alert/_examples'
export { default as avatar } from './basic/avatar/_examples'
export { default as badge } from './basic/badge/_examples'
export { default as buttonGroup } from './basic/button-group/_examples'
export { default as button } from './basic/button/_examples'
export { default as card } from './basic/card/_examples'
+1
View File
@@ -1,6 +1,7 @@
// 基础版组件
export { default as FaAlert } from './basic/alert/index.vue'
export { default as FaAvatar } from './basic/avatar/index.vue'
export { default as FaBadge } from './basic/badge/index.vue'
export { default as FaButtonGroup } from './basic/button-group/index.vue'
export { default as FaButton } from './basic/button/index.vue'
export { default as FaCard } from './basic/card/index.vue'