Vue3徽标(Badge)

简介: 该组件库包含 `Descriptions` 和 `DescriptionsItem` 两种组件,需配合使用。

效果如下图:在线预览

Badge

参数 说明 类型 默认值
color 自定义小圆点的颜色,优先级高于 status PresetColor | string undefined
value 展示的数字或文字,为数字时大于 max 显示为 max+,为 0 时隐藏 number | string | slot undefined
max 展示封顶的数字值 number 99
showZero 当数值为 0 时,是否展示 Badge boolean false
dot 不展示数字,只有一个小红点 boolean false
offset 设置状态点的位置偏移,距默认位置左侧、上方的偏移量 [x, y]: [水平偏移, 垂直偏移] [number | string, number | string] undefined
status 设置 Badge 为状态点 Status undefined
text 在设置了 status 的前提下有效,设置状态点的文本 string | slot undefined
valueStyle 设置徽标的样式 CSSProperties {}
zIndex 设置徽标的 z-index number 9
title 设置鼠标放在状态点上时显示的文字 string undefined
ripple 是否开启涟漪动画效果 boolean true

PresetColor Enum Type

成员名
pink ‘pink’
red ‘red’
yellow ‘yellow’
orange ‘orange’
cyan ‘cyan’
green ‘green’
blue ‘blue’
purple ‘purple’
geekblue ‘geekblue’
magenta ‘magenta’
volcano ‘volcano’
gold ‘gold’
lime 'lime

Status Enum Type

成员名
success ‘success’
processing ‘processing’
default ‘default’
error ‘error’
warning ‘warning’

创建徽标数组件Badge.vue

其中引入使用了以下工具函数:

<script setup lang="ts">
import {
    computed } from 'vue'
import type {
    CSSProperties } from 'vue'
import {
    useSlotsExist } from '../utils'
enum PresetColor {
   
  pink = 'pink',
  red = 'red',
  yellow = 'yellow',
  orange = 'orange',
  cyan = 'cyan',
  green = 'green',
  blue = 'blue',
  purple = 'purple',
  geekblue = 'geekblue',
  magenta = 'magenta',
  volcano = 'volcano',
  gold = 'gold',
  lime = 'lime'
}
enum Status {
   
  success = 'success',
  processing = 'processing',
  default = 'default',
  error = 'error',
  warning = 'warning'
}
interface Props {
   
  color?: PresetColor | string // 自定义小圆点的颜色,优先级高于 status
  value?: number | string // 展示的数字或文字,为数字时大于 max 显示为 max+,为 0 时隐藏 number | string | slot
  max?: number // 展示封顶的数字值
  showZero?: boolean // 当数值为 0 时,是否展示 Badge
  dot?: boolean // 不展示数字,只有一个小红点
  offset?: [number | string, number | string] // 设置状态点的位置偏移,距默认位置左侧、上方的偏移量 [x, y]: [水平偏移, 垂直偏移]
  status?: Status // 设置 Badge 为状态点
  text?: string // 在设置了 status 或 color 的前提下有效,设置状态点的文本 string | slot
  valueStyle?: CSSProperties // 设置徽标的样式
  zIndex?: number // 设置徽标的 z-index
  title?: string // 设置鼠标放在状态点上时显示的文字
  ripple?: boolean // 是否开启涟漪动画效果
}
const props = withDefaults(defineProps<Props>(), {
   
  color: undefined,
  value: undefined,
  max: 99,
  showZero: false,
  dot: false,
  offset: undefined,
  status: undefined,
  text: undefined,
  valueStyle: () => ({
   }),
  zIndex: 9,
  title: undefined,
  ripple: true
})
const customStyle = computed(() => {
   
  if (props.color && !Object.keys(PresetColor).includes(props.color)) {
   
    if ((props.value !== undefined && props.value !== 0) || (props.showZero && props.value === 0)) {
   
      return {
   
        backgroundColor: props.color
      }
    } else {
   
      return {
   
        color: props.color,
        backgroundColor: props.color
      }
    }
  }
})
const presetClass = computed(() => {
   
  if (props.color) {
   
    if (Object.keys(PresetColor).includes(props.color)) {
   
      if ((props.value !== undefined && props.value !== 0) || (props.showZero && props.value === 0)) {
   
        return `color-${
     props.color} white`
      } else {
   
        return 'color-' + props.color
      }
    }
  }
  if (props.status) {
   
    if ((props.value !== undefined && props.value !== 0) || (props.showZero && props.value === 0)) {
   
      return `status-${
     props.status} white`
    } else {
   
      return 'status-' + props.status
    }
  }
  return
})
const slotsExist = useSlotsExist(['default', 'value'])
const showContent = computed(() => {
   
  if (props.value !== undefined || props.dot || (!props.color && !props.status)) {
   
    return slotsExist.default
  }
  return false
})
const showValue = computed(() => {
   
  if (!props.color && !props.status) {
   
    return slotsExist.value
  }
  return false
})
const showBadge = computed(() => {
   
  if ((props.value !== undefined && props.value !== 0) || (props.showZero && props.value === 0) || props.dot) {
   
    return true
  }
  return false
})
const dotOffestStyle = computed(() => {
   
  if (props.offset?.length) {
   
    return {
   
      right: isNumber(props.offset[0]) ? -props.offset[0] + 'px' : handleOffset(props.offset[0] as string),
      marginTop: isNumber(props.offset[1]) ? props.offset[1] + 'px' : props.offset[1]
    }
  }
  return {
   }
})
function isNumber(value: number | string): boolean {
   
  return typeof value === 'number'
}
function handleOffset(value: string): string {
   
  if (value.includes('-')) {
   
    return value.replace('-', '')
  } else {
   
    return `-${
     value}`
  }
}
</script>
<template>
  <div
    class="m-badge"
    :class="{ 'badge-status-color': value === undefined && (color || status) }"
    :style="[`--z-index: ${zIndex}`, value === undefined && !dot ? dotOffestStyle : null]"
  >
    <template v-if="value === undefined && !dot && (color || status)">
      <span class="status-dot" :class="[presetClass, { 'dot-ripple': ripple }]" :style="customStyle"></span>
      <span class="status-text">
        <slot>{
   {
    text }}</slot>
      </span>
    </template>
    <template v-else>
      <template v-if="showContent">
        <slot></slot>
      </template>
      <span v-if="showValue" class="m-value" :class="{ 'only-number': !showContent }">
        <slot name="value"></slot>
      </span>
      <Transition name="zoom" v-else>
        <div
          v-show="showBadge"
          class="m-badge-value"
          :class="[
            {
   
              'small-num': typeof value === 'number' && value < 10,
              'only-number': !showContent,
              'only-dot': showBadge && (value === undefined || (value === 0 && !showZero) || dot)
            },
            presetClass
          ]"
          :style="[customStyle, dotOffestStyle, valueStyle]"
          :title="title || (value !== undefined ? String(value) : '')"
        >
          <span v-if="!dot" class="m-number" style="transition: none 0s ease 0s">
            <span class="u-number">{
   {
    typeof value === 'number' && value > max ? max + '+' : value }}</span>
          </span>
        </div>
      </Transition>
    </template>
  </div>
</template>
<style lang="less" scoped>
.zoom-enter-active {
   
  animation: zoomBadgeIn 0.3s cubic-bezier(0.12, 0.4, 0.29, 1.46);
  animation-fill-mode: both;
}
.zoom-leave-active {
   
  animation: zoomBadgeOut 0.3s cubic-bezier(0.12, 0.4, 0.29, 1.46);
  animation-fill-mode: both;
}
@keyframes zoomBadgeIn {
   
  0% {
   
    transform: scale(0) translate(50%, -50%);
    opacity: 0;
  }
  100% {
   
    transform: scale(1) translate(50%, -50%);
  }
}
@keyframes zoomBadgeOut {
   
  0% {
   
    transform: scale(1) translate(50%, -50%);
  }
  100% {
   
    transform: scale(0) translate(50%, -50%);
    opacity: 0;
  }
}
.m-badge {
   
  position: relative;
  display: inline-block;
  width: fit-content;
  font-size: 14px;
  color: rgba(0, 0, 0, 0.88);
  line-height: 1;
  .status-dot {
   
    position: relative;
    top: -1px;
    display: inline-block;
    vertical-align: middle;
    width: 6px;
    height: 6px;
    border-radius: 50%;
  }
  .dot-ripple {
   
    &::after {
   
      box-sizing: border-box;
      position: absolute;
      top: 0;
      inset-inline-start: 0;
      width: 100%;
      height: 100%;
      border-width: 1px;
      border-style: solid;
      border-color: inherit;
      border-radius: 50%;
      animation-name: dotRipple;
      animation-duration: 1.2s;
      animation-iteration-count: infinite;
      animation-timing-function: ease-in-out;
      content: '';
    }
    @keyframes dotRipple {
   
      0% {
   
        transform: scale(0.8);
        opacity: 0.5;
      }
      100% {
   
        transform: scale(2.4);
        opacity: 0;
      }
    }
  }
  .status-text {
   
    margin-inline-start: 8px;
    color: rgba(0, 0, 0, 0.88);
    font-size: 14px;
  }
  .m-value {
   
    position: absolute;
    top: 0;
    z-index: var(--z-index);
    inset-inline-end: 0;
    transform: translate(50%, -50%);
    transform-origin: 100% 0%;
  }
  .m-badge-value {
   
    .m-value();
    overflow: hidden;
    padding: 0 8px;
    min-width: 20px;
    height: 20px;
    color: #ffffff;
    font-weight: normal;
    font-size: 12px;
    line-height: 20px;
    white-space: nowrap;
    text-align: center;
    background: #ff4d4f;
    border-radius: 10px;
    box-shadow: 0 0 0 1px #ffffff;
    transition: background 0.2s;
    .m-number {
   
      position: relative;
      display: inline-block;
      height: 20px;
      transition: all 0.3s cubic-bezier(0.12, 0.4, 0.29, 1.46);
      transform-style: preserve-3d;
      -webkit-transform-style: preserve-3d; // 设置元素的子元素是位于 3D 空间中还是平面中 flat | preserve-3d
      backface-visibility: hidden;
      -webkit-backface-visibility: hidden; // 当元素背面朝向观察者时是否可见 hidden | visible
      .u-number {
   
        display: inline-block;
        height: 20px;
        margin: 0;
        transform-style: preserve-3d;
        -webkit-transform-style: preserve-3d;
        backface-visibility: hidden;
        -webkit-backface-visibility: hidden;
      }
    }
  }
  .small-num {
   
    padding: 0;
  }
  .only-number {
   
    position: relative;
    top: auto;
    display: block;
    transform-origin: 50% 50%;
    transform: none;
  }
  .only-dot {
   
    width: 6px;
    min-width: 6px;
    height: 6px;
    background: #ff4d4f;
    border-radius: 100%;
    box-shadow: 0 0 0 1px #ffffff;
    padding: 0;
    transition: background 0.3s;
  }
  .status-success {
   
    color: #52c41a;
    background-color: #52c41a;
  }
  .status-error {
   
    color: #ff4d4f;
    background-color: #ff4d4f;
  }
  .status-default {
   
    color: rgba(0, 0, 0, 0.25);
    background-color: rgba(0, 0, 0, 0.25);
  }
  .status-processing {
   
    color: @themeColor;
    background-color: @themeColor;
  }
  .status-warning {
   
    color: #faad14;
    background-color: #faad14;
  }
  .color-pink {
   
    color: #eb2f96;
    background-color: #eb2f96;
  }
  .color-red {
   
    color: #f5222d;
    background-color: #f5222d;
  }
  .color-yellow {
   
    color: #fadb14;
    background-color: #fadb14;
  }
  .color-orange {
   
    color: #fa8c16;
    background-color: #fa8c16;
  }
  .color-cyan {
   
    color: #13c2c2;
    background-color: #13c2c2;
  }
  .color-green {
   
    color: #52c41a;
    background-color: #52c41a;
  }
  .color-blue {
   
    color: @themeColor;
    background-color: @themeColor;
  }
  .color-purple {
   
    color: #722ed1;
    background-color: #722ed1;
  }
  .color-geekblue {
   
    color: #2f54eb;
    background-color: #2f54eb;
  }
  .color-magenta {
   
    color: #eb2f96;
    background-color: #eb2f96;
  }
  .color-volcano {
   
    color: #fa541c;
    background-color: #fa541c;
  }
  .color-gold {
   
    color: #faad14;
    background-color: #faad14;
  }
  .color-lime {
   
    color: #a0d911;
    background-color: #a0d911;
  }
  .white {
   
    color: #ffffff;
  }
}
.badge-status-color {
   
  line-height: inherit;
  vertical-align: baseline;
}
</style>

在要使用的页面引入

<script setup lang="ts">
import Badge from './Badge.vue'
import {
    ref } from 'vue'
import {
    ClockCircleOutlined, MinusOutlined, PlusOutlined } from '@ant-design/icons-vue'
const value = ref(5)
const dot = ref(true)
const colors = [
  'pink',
  'red',
  'yellow',
  'orange',
  'cyan',
  'green',
  'blue',
  'purple',
  'geekblue',
  'magenta',
  'volcano',
  'gold',
  'lime'
]
function decline() {
   
  if (value.value >= 1) {
   
    value.value--
  }
}
function increase() {
   
  value.value++
}
</script>
<template>
  <div>
    <h1>{
   {
    $route.name }} {
   {
    $route.meta.title }}</h1>
    <h2 class="mt30 mb10">基本使用</h2>
    <Space>
      <Badge :value="5">
        <Avatar shape="square" size="large" />
      </Badge>
      <Badge :value="0" show-zero>
        <Avatar shape="square" size="large" />
      </Badge>
      <Badge>
        <template #value>
          <ClockCircleOutlined style="color: #f5222d" />
        </template>
        <Avatar shape="square" size="large" />
      </Badge>
    </Space>
    <h2 class="mt30 mb10">独立使用</h2>
    <Space>
      <Badge :value="25" />
      <Badge
        :value="4"
        :value-style="{
   
          backgroundColor: '#fff',
          color: '#999',
          boxShadow: '0 0 0 1px #d9d9d9 inset'
        }"
      />
      <Badge :value="109" :value-style="{ backgroundColor: '#52c41a' }" />
    </Space>
    <h2 class="mt30 mb10">封顶数字</h2>
    <Space gap="large">
      <Badge :value="99">
        <Avatar shape="square" size="large" />
      </Badge>
      <Badge :value="100">
        <Avatar shape="square" size="large" />
      </Badge>
      <Badge :value="99" :max="10">
        <Avatar shape="square" size="large" />
      </Badge>
      <Badge :value="1000" :max="999">
        <Avatar shape="square" size="large" />
      </Badge>
    </Space>
    <h2 class="mt30 mb10">自定义内容</h2>
    <Space gap="large">
      <Badge value="hello" :value-style="{ backgroundColor: '#1677FF' }">
        <Avatar shape="square" size="large" />
      </Badge>
      <Badge>
        <template #value>
          <span class="u-value">world</span>
        </template>
        <Avatar shape="square" size="large" />
      </Badge>
    </Space>
    <h2 class="mt30 mb10">自定义徽标样式</h2>
    <Space gap="large">
      <Badge :value="99" :value-style="{ backgroundColor: 'magenta' }">
        <Avatar shape="square" size="large" />
      </Badge>
      <Badge value="hello" :value-style="{ backgroundColor: 'gold' }">
        <Avatar shape="square" size="large" />
      </Badge>
      <Badge dot :value-style="{ width: '10px', height: '10px', backgroundColor: 'purple' }">
        <Avatar shape="square" size="large" />
      </Badge>
    </Space>
    <h2 class="mt30 mb10">徽标偏移</h2>
    <Space gap="large">
      <Badge value="9" :offset="[-20, 10]">
        <Avatar shape="square" size="large" />
      </Badge>
      <Badge dot :offset="[-15, 10]">
        <Avatar shape="square" size="large" />
      </Badge>
      <Badge dot status="success" :offset="['-50%', '30%']">
        <Avatar shape="square" size="large" />
      </Badge>
    </Space>
    <h2 class="mt30 mb10">小红点</h2>
    <Badge dot>
      <a href="#">Link something</a>
    </Badge>
    <h2 class="mt30 mb10">状态点</h2>
    <Space>
      <Badge status="success" />
      <Badge status="error" />
      <Badge status="default" />
      <Badge status="processing" />
      <Badge status="warning" />
    </Space>
    <br />
    <Space style="margin-top: 10px" vertical>
      <Badge status="success" text="Success" />
      <Badge status="error" text="Error" />
      <Badge status="default" text="Default" />
      <Badge status="processing" text="Processing" />
      <Badge status="warning" text="warning" />
    </Space>
    <h2 class="mt30 mb10">动态</h2>
    <Flex vertical>
      <Space gap="large" align="center">
        <Badge :value="value">
          <Avatar shape="square" size="large" />
        </Badge>
        <Button @click="decline">
          <MinusOutlined />
        </Button>
        <Button @click="increase">
          <PlusOutlined />
        </Button>
      </Space>
      <Space gap="large" align="center">
        <Badge :dot="dot">
          <Avatar shape="square" size="large" />
        </Badge>
        <Switch v-model="dot" />
      </Space>
    </Flex>
    <h2 class="mt30 mb10">自定义悬浮状态点的显示文字</h2>
    <Badge :value="5" title="Custom hover text">
      <Avatar shape="square" size="large" />
    </Badge>
    <h2 class="mt30 mb10">多彩徽标</h2>
    <h4 class="mb10">Presets</h4>
    <Space>
      <Badge v-for="color in colors" :key="color" :color="color" :text="color" />
    </Space>
    <h4 class="mt10 mb10">Custom</h4>
    <Space>
      <Badge color="#f50" text="#f50" />
      <Badge color="#2db7f5" text="#2db7f5" />
      <Badge color="#87d068" text="#87d068" />
      <Badge color="#108ee9" text="#108ee9" />
    </Space>
  </div>
</template>
<style lang="less" scoped>
.u-value {
   
  display: inline-block;
  line-height: 20px;
  padding: 0 6px;
  background-color: #faad14;
  color: #fff;
  border-radius: 10px;
  box-shadow: 0 0 0 1px #ffffff;
}
</style>
相关文章
|
2月前
|
JavaScript 前端开发 安全
Vue 3
Vue 3以组合式API、Proxy响应式系统和全面TypeScript支持,重构前端开发范式。性能优化与生态协同并进,兼顾易用性与工程化,引领Web开发迈向高效、可维护的新纪元。(238字)
565 139
|
2月前
|
缓存 JavaScript 算法
Vue 3性能优化
Vue 3 通过 Proxy 和编译优化提升性能,但仍需遵循最佳实践。合理使用 v-if、key、computed,避免深度监听,利用懒加载与虚拟列表,结合打包优化,方可充分发挥其性能优势。(239字)
263 1
|
7月前
|
缓存 JavaScript PHP
斩获开发者口碑!SnowAdmin:基于 Vue3 的高颜值后台管理系统,3 步极速上手!
SnowAdmin 是一款基于 Vue3/TypeScript/Arco Design 的开源后台管理框架,以“清新优雅、开箱即用”为核心设计理念。提供角色权限精细化管理、多主题与暗黑模式切换、动态路由与页面缓存等功能,支持代码规范自动化校验及丰富组件库。通过模块化设计与前沿技术栈(Vite5/Pinia),显著提升开发效率,适合团队协作与长期维护。项目地址:[GitHub](https://github.com/WANG-Fan0912/SnowAdmin)。
1017 5
|
3月前
|
开发工具 iOS开发 MacOS
基于Vite7.1+Vue3+Pinia3+ArcoDesign网页版webos后台模板
最新版研发vite7+vue3.5+pinia3+arco-design仿macos/windows风格网页版OS系统Vite-Vue3-WebOS。
428 11
|
2月前
|
JavaScript 安全
vue3使用ts传参教程
Vue 3结合TypeScript实现组件传参,提升类型安全与开发效率。涵盖Props、Emits、v-model双向绑定及useAttrs透传属性,建议明确声明类型,保障代码质量。
296 0
|
4月前
|
缓存 前端开发 大数据
虚拟列表在Vue3中的具体应用场景有哪些?
虚拟列表在 Vue3 中通过仅渲染可视区域内容,显著提升大数据列表性能,适用于 ERP 表格、聊天界面、社交媒体、阅读器、日历及树形结构等场景,结合 `vue-virtual-scroller` 等工具可实现高效滚动与交互体验。
479 1
|
4月前
|
缓存 JavaScript UED
除了循环引用,Vue3还有哪些常见的性能优化技巧?
除了循环引用,Vue3还有哪些常见的性能优化技巧?
285 0
|
5月前
|
JavaScript
vue3循环引用自已实现
当渲染大量数据列表时,使用虚拟列表只渲染可视区域的内容,显著减少 DOM 节点数量。
145 0
|
7月前
|
JavaScript API 容器
Vue 3 中的 nextTick 使用详解与实战案例
Vue 3 中的 nextTick 使用详解与实战案例 在 Vue 3 的日常开发中,我们经常需要在数据变化后等待 DOM 更新完成再执行某些操作。此时,nextTick 就成了一个不可或缺的工具。本文将介绍 nextTick 的基本用法,并通过三个实战案例,展示它在表单验证、弹窗动画、自动聚焦等场景中的实际应用。
636 17
|
8月前
|
JavaScript 前端开发 算法
Vue 3 和 Vue 2 的区别及优点
Vue 3 和 Vue 2 的区别及优点

热门文章

最新文章