Vue3进度条(Progress)

简介: 这是一个基于 Vue2 的进度条组件,支持线性 (`line`) 和圆形 (`circle`) 两种模式。可通过多种属性自定义进度条的样式和行为,包括宽度、进度百分比、颜色、线宽、线帽样式等。此外,还支持显示进度文本或图标,并允许通过插槽自定义内容。该组件提供了丰富的配置选项,适用于多种应用场景。

可自定义设置以下属性:

  • 进度条总宽度(width),类型:string | number,默认 '100%'

  • 当前进度百分比(percent),类型:number,默认 0

  • 进度条线的宽度(strokeWidth),类型:number,单位px,当 type: 'circle' 时,单位是进度圈画布宽度的百分比,默认 8

  • 进度条的色彩,传入 string 时为纯色,传入 object 时为渐变,类型:string | {'0%'?: string, '100%'?: string, from?: string, to?: string, direction?: 'left'|'right'},默认 '#1677FF',进度圈时 direction: 'left' 为逆时针,direction: 'right' 为顺时针

  • 进度条的样式(strokeLinecap),类型:'round' | 'butt' | 'square',默认 'round'

  • 是否显示进度数值或状态图标(showInfo),类型:boolean,默认 true

  • 进度完成时的信息(success),类型:string | slot,默认 undefined

  • 内容的模板函数(format),类型:(percent: number) => (string | number) | slot,默认:(percent: number) => percent + '%'

  • 进度条类型(type),类型:'line' | 'circle',默认 'line'

效果如下图:在线预览

①创建进度条组件Progress.vue:

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

<script setup lang="ts">
import { computed } from 'vue'
import { useSlotsExist } from '../utils'
interface Gradient {
  '0%'?: string
  '100%'?: string
  from?: string
  to?: string
  direction?: 'left' | 'right' // 默认 'right'
}
interface Props {
  width?: number | string // 进度条总宽度
  percent?: number // 当前进度百分比
  strokeWidth?: number // 进度条线的宽度,单位 px,当 type: 'circle' 时,单位是进度圈画布宽度的百分比
  strokeColor?: string | Gradient // 进度条的色彩,传入 string 时为纯色,传入 Gradient 时为渐变,进度圈时 direction: 'left' 为逆时针,direction: 'right' 为顺时针
  strokeLinecap?: 'round' | 'butt' | 'square' // 进度条的样式
  showInfo?: boolean // 是否显示进度数值或状态图标
  success?: string // 进度完成时的信息 string | slot
  format?: (percent: number) => string | number // 内容的模板函数 function | slot
  type?: 'line' | 'circle' // 进度条类型
}
const props = withDefaults(defineProps<Props>(), {
  width: '100%',
  percent: 0,
  strokeWidth: 8,
  strokeColor: '#1677FF',
  strokeLinecap: 'round',
  showInfo: true,
  success: undefined,
  format: (percent: number) => percent + '%',
  type: 'line'
})
const totalWidth = computed(() => {
  // 进度条总宽度
  if (typeof props.width === 'number') {
    return props.width + 'px'
  } else {
    return props.width
  }
})
const perimeter = computed(() => {
  // 圆条周长
  return (100 - props.strokeWidth) * Math.PI
})
const path = computed(() => {
  // 圆条轨道路径指令
  const long = 100 - props.strokeWidth
  return `M 50,50 m 0,-${long / 2}
   a ${long / 2},${long / 2} 0 1 1 0,${long}
   a ${long / 2},${long / 2} 0 1 1 0,-${long}`
})
const gradientColor = computed(() => {
  // 是否为渐变色
  return typeof props.strokeColor !== 'string'
})
const lineColor = computed(() => {
  if (typeof props.strokeColor === 'string') {
    return props.strokeColor
  } else {
    return `linear-gradient(to ${props.strokeColor.direction || 'right'}, ${props.strokeColor['0%'] || props.strokeColor.from}, ${props.strokeColor['100%'] || props.strokeColor.to})`
  }
})
const circleColorFrom = computed(() => {
  if (gradientColor.value) {
    const gradientColor = props.strokeColor as Gradient
    if (!gradientColor.direction || gradientColor.direction === 'right') {
      return gradientColor['0%'] || gradientColor.from
    } else {
      return gradientColor['100%'] || gradientColor.to
    }
  }
  return
})
const circleColorTo = computed(() => {
  if (gradientColor.value) {
    const gradientColor = props.strokeColor as Gradient
    if (!gradientColor.direction || gradientColor.direction === 'right') {
      return gradientColor['100%'] || gradientColor.to
    } else {
      return gradientColor['0%'] || gradientColor.from
    }
  }
  return
})
const showPercent = computed(() => {
  return props.format(props.percent > 100 ? 100 : props.percent)
})
const slotsExist = useSlotsExist(['success'])
const showSuccess = computed(() => {
  return slotsExist.success || props.success
})
</script>
<template>
  <div
    v-if="type === 'line'"
    class="m-progress-line"
    :style="`width: ${totalWidth}; height: ${strokeWidth < 24 ? 24 : strokeWidth}px;`"
  >
    <div class="m-progress-inner">
      <div
        :class="['progress-bg', { 'line-success': percent >= 100 && !gradientColor }]"
        :style="`background: ${lineColor}; width: ${percent >= 100 ? 100 : percent}%; height: ${strokeWidth}px; --border-radius: ${strokeLinecap === 'round' ? '100px' : 0};`"
      ></div>
    </div>
    <template v-if="showInfo">
      <Transition name="fade" mode="out-in">
        <span v-if="percent >= 100" class="progress-success">
          <svg
            v-if="showSuccess === undefined"
            class="icon-svg"
            focusable="false"
            data-icon="check-circle"
            width="1em"
            height="1em"
            fill="currentColor"
            aria-hidden="true"
            viewBox="64 64 896 896"
          >
            <path
              d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"
            ></path>
          </svg>
          <p v-else class="progress-success-info">
            <slot name="success">{
  
  { success }}</slot>
          </p>
        </span>
        <p v-else class="progress-text">
          <slot name="format" :percent="percent">{
  
  { showPercent }}</slot>
        </p>
      </Transition>
    </template>
  </div>
  <div v-else class="m-progress-circle" :style="`width: ${totalWidth}; height: ${totalWidth};`">
    <svg class="progress-circle" viewBox="0 0 100 100">
      <defs v-if="gradientColor">
        <linearGradient id="circleGradient" x1="100%" y1="0%" x2="0%" y2="0%">
          <stop offset="0%" :stop-color="circleColorFrom as string"></stop>
          <stop offset="100%" :stop-color="circleColorTo as string"></stop>
        </linearGradient>
      </defs>
      <path
        :d="path"
        :stroke-linecap="strokeLinecap"
        class="circle-trail"
        :stroke-width="strokeWidth"
        :style="`stroke-dasharray: ${perimeter}px, ${perimeter}px;`"
        fill-opacity="0"
      ></path>
      <path
        :d="path"
        :stroke-linecap="strokeLinecap"
        class="circle-path"
        :class="{ 'circle-path-success': percent >= 100 && !gradientColor }"
        :stroke-width="strokeWidth"
        :stroke="gradientColor ? 'url(#circleGradient)' : lineColor"
        :style="`stroke-dasharray: ${(percent / 100) * perimeter}px, ${perimeter}px;`"
        :opacity="percent === 0 ? 0 : 1"
        fill-opacity="0"
      ></path>
    </svg>
    <template v-if="showInfo">
      <Transition name="fade" mode="out-in">
        <svg
          v-if="showSuccess === undefined && percent >= 100"
          class="icon-svg"
          focusable="false"
          data-icon="check"
          width="1em"
          height="1em"
          fill="currentColor"
          aria-hidden="true"
          viewBox="64 64 896 896"
        >
          <path
            d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
          ></path>
        </svg>
        <p v-else-if="percent >= 100" class="progress-success-info">
          <slot name="success">{
  
  { success }}</slot>
        </p>
        <p v-else class="progress-text">
          <slot name="format" :percent="percent">{
  
  { showPercent }}</slot>
        </p>
      </Transition>
    </template>
  </div>
</template>
<style lang="less" scoped>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.2s;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
@success: #52c41a;
.m-progress-line {
  display: flex;
  align-items: center;
  .m-progress-inner {
    width: 100%;
    background: rgba(0, 0, 0, 0.06);
    border-radius: 100px;
    overflow: hidden;
    .progress-bg {
      position: relative;
      background-color: @themeColor;
      border-radius: var(--border-radius);
      transition: all 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
      &::after {
        content: '';
        background-image: linear-gradient(90deg, rgba(255, 255, 255, 0.3) 0%, rgba(255, 255, 255, 0.5) 100%);
        animation: progressRipple 2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
      }
      @keyframes progressRipple {
        0% {
          position: absolute;
          inset: 0;
          right: 100%;
          opacity: 1;
        }
        66% {
          position: absolute;
          inset: 0;
          opacity: 0;
        }
        100% {
          position: absolute;
          inset: 0;
          opacity: 0;
        }
      }
    }
    .line-success {
      background: @success !important;
    }
  }
  .progress-success {
    width: 40px;
    text-align: center;
    display: inline-flex;
    align-items: center;
    padding-left: 8px;
    flex-shrink: 0; // 默认 1.即空间不足时,项目将缩小
    .icon-svg {
      display: inline-block;
      width: 16px;
      height: 16px;
      fill: @success;
    }
    .progress-success-info {
      flex-shrink: 0; // 默认 1.即空间不足时,项目将缩小
      width: 40px;
      font-size: 14px;
      padding-left: 8px;
      color: @success;
    }
  }
  .progress-text {
    /*
      如果所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小
      如果一个项目的flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小。
    */
    flex-shrink: 0; // 默认 1.即空间不足时,项目将缩小
    width: 40px;
    font-size: 14px;
    padding-left: 8px;
    color: rgba(0, 0, 0, 0.88);
  }
}
.m-progress-circle {
  display: inline-block;
  position: relative;
  .progress-circle {
    .circle-trail {
      stroke: rgba(0, 0, 0, 0.06);
      stroke-dashoffset: 0;
      transition:
        stroke-dashoffset 0.3s ease 0s,
        stroke-dasharray 0.3s ease 0s,
        stroke 0.3s ease 0s,
        stroke-width 0.06s ease 0.3s,
        opacity 0.3s ease 0s;
    }
    .circle-path {
      stroke-dashoffset: 0;
      transition:
        stroke-dashoffset 0.3s ease 0s,
        stroke-dasharray 0.3s ease 0s,
        stroke 0.3s ease 0s,
        stroke-width 0.06s ease 0.3s,
        opacity 0.3s ease 0s;
    }
    .circle-path-success {
      stroke: @success !important;
    }
  }
  .icon-svg {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    display: inline-block;
    width: 30%;
    height: 30%;
    fill: @success;
  }
  .progress-success-info {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 100%;
    font-size: 27px;
    line-height: 1;
    text-align: center;
    color: @success;
  }
  .progress-text {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 100%;
    font-size: 27px;
    line-height: 1;
    text-align: center;
    color: rgba(0, 0, 0, 0.85);
  }
}
</style>

②在要使用的页面引入:

<script setup lang="ts">
import Progress from './Progress.vue'
import { ref } from 'vue'

const percent = ref(80)

function onIncrease(scale: number) {
  const res = percent.value + scale
  if (res > 100) {
    percent.value = 100
  } else {
    percent.value = res
  }
}
function onDecline(scale: number) {
  const res = percent.value - scale
  if (res < 0) {
    percent.value = 0
  } else {
    percent.value = res
  }
}
</script>
<template>
  <div>
    <h1>{
  
  { $route.name }} {
  
  { $route.meta.title }}</h1>
    <h2 class="mt30 mb10">基本使用</h2>
    <Progress :width="900" :stroke-width="10" :percent="percent" />
    <h2 class="mt30 mb10">进度圈</h2>
    <Space align="center">
      <Progress type="circle" :width="120" :stroke-width="12" :percent="percent" />
      <Button @click="onDecline(5)" size="large">Decline -</Button>
      <Button @click="onIncrease(5)" size="large">Increase +</Button>
    </Space>
    <h2 class="mt30 mb10">完成进度条</h2>
    <Flex vertical :width="900">
      <Progress :stroke-width="10" :percent="100" />
      <Progress type="circle" :width="120" :stroke-width="10" :percent="100" />
    </Flex>
    <h2 class="mt30">渐变进度条</h2>
    <h3 class="mb10">
      strokeColor: { '0%': '#108ee9', '100%': '#87d068', direction: 'right' } 或 { from: '#108ee9', to: '#87d068',
      direction: 'right' }
    </h3>
    <Flex vertical :width="900">
      <Progress
        :stroke-width="10"
        :stroke-color="{
          '0%': '#108ee9',
          '100%': '#87d068',
          direction: 'right'
        }"
        :percent="percent"
      />
      <Space align="center">
        <Progress
          type="circle"
          :width="120"
          :stroke-width="12"
          :stroke-color="{
            '0%': '#108ee9',
            '100%': '#87d068',
            direction: 'right'
          }"
          :percent="percent"
        />
        <Button @click="onDecline(5)" size="large">Decline -</Button>
        <Button @click="onIncrease(5)" size="large">Increase +</Button>
      </Space>
    </Flex>
    <h2 class="mt30 mb10">自定义样式</h2>
    <Flex vertical :width="600">
      <Progress
        :stroke-width="28"
        :stroke-color="{
          '0%': '#108ee9',
          '100%': '#87d068',
          direction: 'left'
        }"
        stroke-linecap="butt"
        :percent="percent"
      />
      <Space align="center">
        <Progress
          type="circle"
          :width="180"
          :stroke-width="18"
          :stroke-color="{
            '0%': '#108ee9',
            '100%': '#87d068',
            direction: 'left'
          }"
          stroke-linecap="butt"
          :percent="percent"
        />
        <Button @click="onDecline(5)" size="large">Decline -</Button>
        <Button @click="onIncrease(5)" size="large">Increase +</Button>
      </Space>
    </Flex>
    <h2 class="mt30 mb10">自定义文字</h2>
    <Space align="center">
      <Progress
        type="circle"
        :width="160"
        :stroke-width="12"
        :percent="percent"
        :format="(percent: number) => `${percent} Days`"
        success="Done"
      />
      <Progress type="circle" :width="160" :stroke-width="12" :percent="percent">
        <template #format="{ percent }">
          <span style="color: magenta">{
  
  { percent }}%</span>
        </template>
        <template #success>
          <span style="color: magenta">Bingo</span>
        </template>
      </Progress>
      <Button @click="onDecline(5)" size="large">Decline -</Button>
      <Button @click="onIncrease(5)" size="large">Increase +</Button>
    </Space>
  </div>
</template>
相关文章
|
1月前
|
JavaScript 前端开发 安全
Vue 3
Vue 3以组合式API、Proxy响应式系统和全面TypeScript支持,重构前端开发范式。性能优化与生态协同并进,兼顾易用性与工程化,引领Web开发迈向高效、可维护的新纪元。(238字)
501 139
|
1月前
|
缓存 JavaScript 算法
Vue 3性能优化
Vue 3 通过 Proxy 和编译优化提升性能,但仍需遵循最佳实践。合理使用 v-if、key、computed,避免深度监听,利用懒加载与虚拟列表,结合打包优化,方可充分发挥其性能优势。(239字)
213 1
|
6月前
|
缓存 JavaScript PHP
斩获开发者口碑!SnowAdmin:基于 Vue3 的高颜值后台管理系统,3 步极速上手!
SnowAdmin 是一款基于 Vue3/TypeScript/Arco Design 的开源后台管理框架,以“清新优雅、开箱即用”为核心设计理念。提供角色权限精细化管理、多主题与暗黑模式切换、动态路由与页面缓存等功能,支持代码规范自动化校验及丰富组件库。通过模块化设计与前沿技术栈(Vite5/Pinia),显著提升开发效率,适合团队协作与长期维护。项目地址:[GitHub](https://github.com/WANG-Fan0912/SnowAdmin)。
948 5
|
2月前
|
开发工具 iOS开发 MacOS
基于Vite7.1+Vue3+Pinia3+ArcoDesign网页版webos后台模板
最新版研发vite7+vue3.5+pinia3+arco-design仿macos/windows风格网页版OS系统Vite-Vue3-WebOS。
374 11
|
1月前
|
JavaScript 安全
vue3使用ts传参教程
Vue 3结合TypeScript实现组件传参,提升类型安全与开发效率。涵盖Props、Emits、v-model双向绑定及useAttrs透传属性,建议明确声明类型,保障代码质量。
253 0
|
3月前
|
缓存 前端开发 大数据
虚拟列表在Vue3中的具体应用场景有哪些?
虚拟列表在 Vue3 中通过仅渲染可视区域内容,显著提升大数据列表性能,适用于 ERP 表格、聊天界面、社交媒体、阅读器、日历及树形结构等场景,结合 `vue-virtual-scroller` 等工具可实现高效滚动与交互体验。
429 1
|
3月前
|
缓存 JavaScript UED
除了循环引用,Vue3还有哪些常见的性能优化技巧?
除了循环引用,Vue3还有哪些常见的性能优化技巧?
245 0
|
4月前
|
JavaScript
vue3循环引用自已实现
当渲染大量数据列表时,使用虚拟列表只渲染可视区域的内容,显著减少 DOM 节点数量。
135 0
|
5月前
|
JavaScript 前端开发 开发者
Vue 自定义进度条组件封装及使用方法详解
这是一篇关于自定义进度条组件的使用指南和开发文档。文章详细介绍了如何在Vue项目中引入、注册并使用该组件,包括基础与高级示例。组件支持分段配置(如颜色、文本)、动画效果及超出进度提示等功能。同时提供了完整的代码实现,支持全局注册,并提出了优化建议,如主题支持、响应式设计等,帮助开发者更灵活地集成和定制进度条组件。资源链接已提供,适合前端开发者参考学习。
469 17
|
5月前
|
JavaScript 前端开发 UED
Vue 项目中如何自定义实用的进度条组件
本文介绍了如何使用Vue.js创建一个灵活多样的自定义进度条组件。该组件可接受进度段数据数组作为输入,动态渲染进度段,支持动画效果和内容展示。当进度超出总长时,超出部分将以红色填充。文章详细描述了组件的设计目标、实现步骤(包括props定义、宽度计算、模板渲染、动画处理及超出部分的显示),并提供了使用示例。通过此组件,开发者可根据项目需求灵活展示进度情况,优化用户体验。资源地址:[https://pan.quark.cn/s/35324205c62b](https://pan.quark.cn/s/35324205c62b)。
250 0

热门文章

最新文章