封装 Progress 进度条或柱状叠加图

212 阅读1分钟

1671437733151.jpg

在之前的一个项目中,UI 给我画了一个横向叠加的柱状图,因为没有涉及到用 echarts 及其他的插件,我就自己写了一个公共的组件来实现,今儿就来分享一下我的思路

HTML 部分

样式这块我主要是采用HTML加css来处理的。 tooltip 部分使用了 element-plus 组件, 采用了单例模式

<template>
  <div
    class="y-progress-wrap"
  >
  <div
    ref="progressRef"
    class="y-progress-main-wrap"
  >
    <transition-group
      class="y-progress-main"
      tag="div"
      name="progress-transition"
    >
      <div
        v-for="(item, index) in progressList"
        :key="index"
        class="y-progress-item"
        :style="{
          width: item.width + 'px',
          'background-color': progressList.length === 1 ? 'none' : item.color,
          'background-image': progressList.length === 1 ? 'linear-gradient( to left , rgba(145, 215, 255, 1),rgba(62, 139, 255, 1))' : 'none',
          'transition-delay': 0.1 * index + 's'
        }"
        @mouseover="handleMouseover($event, item)"
        @mouseleave="handleMouseleave"
      />
    </transition-group>
  </div>
    <div
      v-if="showSuffix"
      class="y-progress-suffix"
    >
      <slot
        name="suffix"
        :progressValue="progressValue"
        :maxProgress="maxProgress"
      >
        {{ progressValue }}/{{ maxProgress }}
      </slot>
    </div>
    <el-tooltip
      ref="progressTooltipRef"
      v-model:visible="visible"
      :virtual-ref="showProgressTooltipRef"
      virtual-triggering
      effect="dark"
      :content="tooltipContent"
      placement="top"
    />
  </div>
</template>

<style lang="scss" scoped>
.y-progress-wrap {
  display: flex;
  align-items: center;
  position: relative;

  .y-progress-main-wrap {
    flex: 1;
  }

  .y-progress-main {
    width: 100%;
    height: 8px;
    border-radius: 4px;
    background-color: rgba(64, 140, 255, .2);
    overflow: hidden;
    display: flex;

    .y-progress-item {
      cursor: pointer;
      width: 0;
      height: 100%;
      transition: all 0.3s;

      &:last-child {
        border-radius: 0 4px 4px 0;
      }
    }
  }

  .y-progress-suffix {
    margin-left: 8px;
    color: rgba(29, 33, 41, 1);
    font-size: 12px;
    font-weight: 400;
  }

  .progress-circle {
    transform: rotate(-90deg);

    .progress {
      transition: all .3s;
    }
  }

  .progress-circle-content {
    color: rgba(29, 33, 41, 1);
    font-size: 16px;
    font-weight: 400;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
  }
}

.progress-transition-enter-active {
  opacity: 0;
  transform-origin: left;
  transform: scaleX(0);
}

.progress-transition-enter-to {
  opacity: 1;
  transform-origin: left;
  transform: scaleX(1);
}
</style>

样式HTML部分大概就这样了,因为是涉及到多个的,我准备采用循环 <div class="y-progress-item"/> 然后计算每个的宽度来实现

JS

使用的是vue3 + TS的写法,我这块就直接使用了 setup 语法糖

<script lang="ts" setup>
import {
  withDefaults,
  defineProps,
  ref,
  nextTick,
  watchEffect
} from 'vue'

// 定义props
type Props = {
  showSuffix?: boolean // 是否显示后缀
  progress: number[] | number // 进度或者数据
  colors?: string[] | string // 显示的颜色
  maxProgress?: number // 最大值
  width?: string // 外层宽度
}
// 设置默认值
const props = withDefaults(defineProps<Props>(), {
  showSuffix: true,
  progress: () => ([]),
  colors: () => ([]),
  maxProgress: 100,
  width: '100%'
})
const progressList = ref<any[]>([]) // 初始化progressList,用于生成每个柱子
const colorList = ref([ // 默认内置颜色
  '#3e8bff',
  '#19D4AE',
  '#1593C5',
  '#FA6E86',
  '#FFB980',
  '#69840A',
  '#378256',
  '#B79757',
  '#8C73DD',
  '#EFAAB5',
  '#F272BE'
])
const progressValue = ref(0) // 累加数值
const progressRef = ref<HTMLElement | null>(null) // 外层ref
const progressTooltipRef = ref<HTMLElement | null>(null) // Tooltip
const showProgressTooltipRef = ref<HTMLElement | null>(null) // 显示 Tooltip 的柱子
const visible = ref<boolean>(false) // 控制显示 Tooltip
const tooltipContent = ref<string>('') // Tooltip里的内容
// 这里用到了 watchEffect。watchEffect内部自动追踪依赖
// 当 props.progress props.colors 发生变化就去更新
watchEffect(() => {
  const progressList: number[] = []
  // 处理 props.progress 类型差异
  if (typeof props.progress === 'number') {
    progressList.push(props.progress)
  } else {
    progressList.push(...props.progress)
  }
  const list = []
  if (typeof props.colors === 'string') {
    list.push(props.colors)
  } else {
    list.push(...props.colors)
  }
  colorList.value = [...new Set([...list, ...colorList.value])] // 覆盖颜色默认值
  progressValue.value = progressList.reduce((val, next) => {
      val = parseFloat((val + next).toFixed(2))
      return val
    }, 0)
  }
  nextTick(() => {
    // 调用方法生成
    initProgress(progressList)
  })
})
const initProgress = (newVal: number[]) => {
  const num = (newVal || []).length
  const w = progressRef.value?.getBoundingClientRect().width ?? 0 // 获取最大宽度
  progressList.value = []
  for (let i = 0; i < num; i++) {
    // 根据每个数值计算宽度,添加颜色
    progressList.value.push({
      width: newVal[i] / props.maxProgress * w,
      progress: newVal[i],
      color: colorList.value[i]
    })
  }
}
// 显示 Tooltip
const handleMouseover = (e: Event, item: any) => {
  showProgressTooltipRef.value = e.currentTarget
  visible.value = !visible.value
  tooltipContent.value = item.progress.toString()
}
// 隐藏 Tooltip
const handleMouseleave = () => {
  visible.value = !visible.value
  tooltipContent.value = ''
}
</script>

动画实现

动画使用 transition-group 来做的,当里面的值发生变化的时候或颜色变化都会动画过渡到最新的值

有写得不好的地方,请大家指出一起探讨。