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>
相关文章
|
22天前
|
存储 JavaScript 前端开发
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
【10月更文挑战第21天】 vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
|
26天前
|
API
vue3知识点:provide 与 inject
vue3知识点:provide 与 inject
31 4
vue3知识点:provide 与 inject
|
26天前
|
API
vue3知识点:readonly 与 shallowReadonly
vue3知识点:readonly 与 shallowReadonly
25 1
vue3知识点:readonly 与 shallowReadonly
|
19天前
|
JavaScript 前端开发 开发者
Vue 3中的Proxy
【10月更文挑战第23天】Vue 3中的`Proxy`为响应式系统带来了更强大、更灵活的功能,解决了Vue 2中响应式系统的一些局限性,同时在性能方面也有一定的提升,为开发者提供了更好的开发体验和性能保障。
46 7
|
21天前
|
前端开发 数据库
芋道框架审批流如何实现(Cloud+Vue3)
芋道框架审批流如何实现(Cloud+Vue3)
40 3
|
19天前
|
JavaScript 数据管理 Java
在 Vue 3 中使用 Proxy 实现数据双向绑定的性能如何?
【10月更文挑战第23天】Vue 3中使用Proxy实现数据双向绑定在多个方面都带来了性能的提升,从更高效的响应式追踪、更好的初始化性能、对数组操作的优化到更优的内存管理等,使得Vue 3在处理复杂的应用场景和大量数据时能够更加高效和稳定地运行。
39 1
|
19天前
|
JavaScript 开发者
在 Vue 3 中使用 Proxy 实现数据的双向绑定
【10月更文挑战第23天】Vue 3利用 `Proxy` 实现了数据的双向绑定,无论是使用内置的指令如 `v-model`,还是通过自定义事件或自定义指令,都能够方便地实现数据与视图之间的双向交互,满足不同场景下的开发需求。
41 1
|
22天前
|
前端开发 JavaScript
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
|
22天前
Vue3 项目的 setup 函数
【10月更文挑战第23天】setup` 函数是 Vue3 中非常重要的一个概念,掌握它的使用方法对于开发高效、灵活的 Vue3 组件至关重要。通过不断的实践和探索,你将能够更好地利用 `setup` 函数来构建优秀的 Vue3 项目。
|
26天前
|
JavaScript Java API
vue3知识点:setup
vue3知识点:setup
28 5
下一篇
无影云桌面