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>
相关文章
|
16天前
vue3学习(3)
vue3学习(3)
|
13天前
|
JavaScript API
Vue3中的计算属性能否动态修改
【9月更文挑战第5天】Vue3中的计算属性能否动态修改
45 10
|
6天前
|
JavaScript
Vue3中路由跳转的语法
Vue3中路由跳转的语法
109 58
|
8天前
|
前端开发
vue3+ts项目中使用mockjs
vue3+ts项目中使用mockjs
205 58
|
4天前
|
JavaScript 索引
Vue 2和Vue 3的区别以及实现原理
Vue 2 的响应式系统通过Object.defineProperty来实现,它为对象的每个属性添加 getter 和 setter,以便追踪依赖并响应数据变化。
20 9
|
6天前
|
JavaScript 开发工具
vite如何打包vue3插件为JSSDK
【9月更文挑战第10天】以下是使用 Vite 打包 Vue 3 插件为 JS SDK 的步骤:首先通过 `npm init vite-plugin-sdk --template vue` 创建 Vue 3 项目并进入项目目录 `cd vite-plugin-sdk`。接着,在 `src` 目录下创建插件文件(如 `myPlugin.js`),并在 `main.js` 中引入和使用该插件。然后,修改 `vite.config.js` 文件以配置打包选项。最后,运行 `npm run build` 进行打包,生成的 `my-plugin-sdk.js` 即为 JS SDK,可在其他项目中引入使用。
|
6天前
|
JavaScript 开发者
彻底搞懂 Vue3 中 watch 和 watchEffect是用法
彻底搞懂 Vue3 中 watch 和 watchEffect是用法
|
13天前
|
JavaScript API
如何使用Vue3的可计算属性
【9月更文挑战第5天】如何使用Vue3的可计算属性
44 13
|
4天前
|
JavaScript 调度
Vue3 使用 Event Bus
Vue3 使用 Event Bus
10 1
|
4天前
|
JavaScript
Vue3 : ref 与 reactive
Vue3 : ref 与 reactive
9 1