2

OK..哈喽大家好,I am 肖大侠😷😷😷

最近疫情反反复复,很烦人

然后虽然我不考研,但是却沉迷于徐涛老师的考研政治课,

也许就是这两个原因,导致我最近做了超级多和百分比相关的东西😏😏😏

什么水球图,环形进度条,等等..

接着我就发现了一个我认为特别有趣的效果,就是使用离散的块模拟出一种连续的进度条的视觉效果

尽管丢失了部分精度,但也大差不差,而且真的很酷😍

那就先看看效果:

image.png

这是它百分之0的样子:

image.png

这是它百分之100的样子:

image.png

接下来就简单看看我的实现思路😃😃😃

首先需要一个大圆,

image.png

在大圆内部再画一个居中的小圆,小圆中可以显示具体的数值和标题,(小圆的内部可以处理成一个插槽)

image.png

接下来就详细说说离散的块如何弄出来,咱们先定义一个参数,用来控制块的密度,我这边默认的是显示24个块

然后我们先不要执着于小块本身,可以先考虑放这些块的容器,容器可以为我们提供定位的能力

如果密度为24,那就需要24个容器,同时还有一个已知的条件就是大圆的直径,

让容器的宽度等于小球的直径,再将它们都使用绝对定位,定位大圆的圆心位置,就像下图这样

image.png

接下来让这些容器依次选择15°,30°...,效果如下:
image.png

这个时候容器的作用就显而易见了,首先它的高度就是最终小块的宽度,其次,容器的两端都落在了圆上,这就是前面提到的定位能力

在这些容器中再去画小块,让小块置于容器的某一端就可以得到下面的效果:

image.png

最后无非就是要显示几个小块的问题了,这部分很简单,用密度和当前的百分比就可以很轻易的算出来

image.png

但这还不够,还可以做的更好,为了表示百分比,使用渐变也是不错的选择,因此我们可以给小块加上渐变的样式让它看上去更酷😎😎😎

有各种各样的策略,越复杂的越完备的策略会让它看起来更好,例如在计算块的个数时,总是向上取整,在最后一个块中使用渐变来表示更加精确的百分比,

也可以制定一些其他的策略,让它看上去更好看,没有必要去考虑精度

那我就说说我的'丑陋'策略吧😗

首先就是无论百分之多少,都把块画出来,只是区分亮块和暗块,

亮块用由亮到暗的渐变,暗块用由暗到更暗的渐变

接下来计算亮块的个数,用密度减去亮块的个数就是暗块的个数

然后我规定,亮块的第一个不做渐变,亮块的前20%由亮到比较暗渐变,亮块的20%~40%由亮到更暗渐变...,依次类推,暗块也是一样,只是暗块渐变的开始就已经很暗的,具体代码就像下面这样

      let lightNum = Math.round(density * (this.value / 100)); // 亮块数量
      // 这里为了避免误差引起的报错
      if (lightNum > 24) lightNum = 24;
      let darkNum = density - lightNum;
      for (let i = 1; i <= density; i++) {
        let headColor = "";
        let footColor = "";
        if (i <= lightNum) {
          // 亮块
          if (i == 1) {
            // 第一个块最亮
            headColor = this.mainColor;
            footColor = this.mainColor + "80";
          } else if (i / lightNum < 0.4166666) {
            headColor = this.mainColor;
            footColor = this.mainColor + "60";
          } else if (i / lightNum < 0.75) {
            headColor = this.mainColor;
            footColor = this.mainColor + "40";
          } else if (i / lightNum < 0.9) {
            headColor = this.mainColor;
            footColor = this.mainColor + "20";
          } else {
            headColor = this.mainColor;
            footColor = this.mainColor + "10";
          }
        } else {
          let di = i - lightNum;
          if (di / darkNum < 0.2166666) {
            headColor = this.mainColor + "30";
            footColor = this.mainColor + "10";
          } else if (di / darkNum < 0.4166666) {
            headColor = this.mainColor + "25";
            footColor = this.mainColor + "10";
          } else if (di / darkNum < 0.75) {
            headColor = this.mainColor + "20";
            footColor = this.mainColor + "10";
          } else if (di / darkNum < 0.9) {
            headColor = this.mainColor + "15";
            footColor = this.mainColor + "10";
          } else {
            headColor = this.mainColor + "10";
            footColor = this.mainColor + "10";
          }
        }

这样就实现了文章开头的效果

我把这玩意儿封装成了一个vue组件,代码我贴在最后,它的主要参数:

参数名类型说明示例默认值
sizeString用于控制整体的尺寸,支持'px','rem'等单位'200px','2rem''200px'
widthNumber用于控制条的宽途,即小块的长度2020
mainColorString控制颜色,仅支持'#ffffff'这种格式"#fa8072""#fa8072"
densityNumber块密度2424
valueNumber百分比值76.20
titleString标题'开发进度''自定义标题'

它还有一个标题的插槽,可以自定义内圆中的内容

name说明
title自定义内圆内容

完整代码:

<!--
 * @Description: 块状环形进度条
 * @Author: XiaoShiqiang
 * @LastEditors: XiaoShiqiang
 * @Date: 2022-10-19 09:43:41
 * @LastEditTime: 2022-10-24 17:46:17
-->
<template>
  <div class="blockCircleProgress">
    <!-- 大球 -->
    <section
      ref="mainCircle"
      class="main-circle"
      :style="{ width: size, height: size, borderColor: mainColor }"
    >
      <!-- 内圆 -->
      <div
        class="insideCircle"
        :style="{
          width: inCircleW + 'px',
          height: inCircleW + 'px',
          borderColor: mainColor,
          backgroundImage: `radial-gradient(${mainColor + '80'}, transparent)`
        }"
      >
        <div slot="title" style="width:100%;height:100%">
          <div class="percentLabel">
            <div class="percentValue">
              <span
                class=""
                :style="{
                  fontSize: inCircleW / 3 + 'px'
                }"
              >
                {{ value }}</span
              >
              <span
                :style="{
                  fontSize: inCircleW / 5 + 'px'
                }"
                >%</span
              >
            </div>
            <div class="percentTitle ellipsis">
              <span
                :style="{
                  fontSize: inCircleW / 6.8 + 'px'
                }"
                >{{ title }}</span
              >
            </div>
          </div>
        </div>
      </div>

      <!-- 进度条内小块 -->
      <template v-for="(item, idx) in proBlockList">
        <div
          :key="'inPro' + idx"
          class="inProItem"
          :style="{
            width: item.width + 'px',
            padding: `${inProBlockM - 3}px 0`,
            transform: `rotate(${idx * (360 / density)}deg)`
          }"
        >
          <div
            class="inProItem-item"
            :style="{
              height: item.height + 'px',
              backgroundImage: item.color
            }"
          ></div>
          <!-- <div
            class="inProItem-item"
            :style="{
              height: item.height + 'px'
            }"
          ></div> -->
        </div>
      </template>
    </section>
  </div>
</template>
<script>
export default {
  name: "blockCircleProgress",
  data() {
    return {
      // 进度条总宽度
      proW: 0,
      // 内圆的宽高
      inCircleW: 0,
      // 进度条内小块的宽度
      inProBlockW: 0,
      // 进度条内小块的高度
      inProBlockH: 0,
      // 进度条内小块与内外圆之间的间距
      inProBlockM: 0,

      proBlockList: [] // 进度块数组
    };
  },
  props: {
    // 尺寸
    size: {
      type: String,
      default: "200px"
    },
    // 进度条宽度
    width: {
      type: Number,
      default: 20
    },
    // 颜色
    mainColor: {
      type: String,
      default: "#fa8072"
    },
    // 块的密度
    density: {
      type: Number,
      default: 24
    },
    value: {
      type: Number,
      default: 0
    },
    title: {
      type: String,
      default: "自定义标题"
    }
  },
  computed: {
    sizeNumber() {
      return parseFloat(this.size);
    }
  },
  watch: {
    value: {
      handler() {
        this.init();
      }
    }
  },
  components: {},
  created() {},
  mounted() {
    this.init();
    window.addEventListener("resize", () => {
      this.init();
    });
  },
  methods: {
    // 初始化
    init() {
      const mainCircle = this.$refs.mainCircle;
      const { width } = mainCircle.getBoundingClientRect();
      // 初始化大球内圆,同时获得进度条实际宽度
      this.initMainInsideCircle(width);
      // 计算进度条内块的宽度
      this.initMainInProBlockWidth(width);
      // 计算进度条内块的高度
      this.initMainInProBlockHeight(width);
      // 组装小块数组
      this.buildProBlockList();
    },

    // 初始化大球内圆,同时获得进度条实际宽度
    initMainInsideCircle(w) {
      // 进度条宽度
      this.proW = w * (this.width / 100);
      // 获得内圆宽高
      this.inCircleW = w - this.proW * 2;
    },
    // 计算进度条内块的宽度
    initMainInProBlockWidth(w) {
      // 计算大球的周长
      const mainCirclePerimeter = (w / 2) * Math.PI;
      // 计算每个块的宽度
      this.inProBlockW = mainCirclePerimeter / this.density - 2; // 块的密度为24
    },
    // 计算进度条内块的高度
    initMainInProBlockHeight() {
      this.inProBlockM = this.proW * 0.22;
      this.inProBlockH = this.proW - 2 * this.inProBlockM;
    },
    // 组装小块数组
    buildProBlockList() {
      let density = this.density;
      this.proBlockList = [];
      let lightNum = Math.round(density * (this.value / 100)); // 亮块数量
      // 这里为了避免误差引起的报错
      if (lightNum > 24) lightNum = 24;
      let darkNum = density - lightNum;
      for (let i = 1; i <= density; i++) {
        let headColor = "";
        let footColor = "";
        if (i <= lightNum) {
          // 亮块
          if (i == 1) {
            // 第一个块最亮
            headColor = this.mainColor;
            footColor = this.mainColor + "80";
          } else if (i / lightNum < 0.4166666) {
            headColor = this.mainColor;
            footColor = this.mainColor + "60";
          } else if (i / lightNum < 0.75) {
            headColor = this.mainColor;
            footColor = this.mainColor + "40";
          } else if (i / lightNum < 0.9) {
            headColor = this.mainColor;
            footColor = this.mainColor + "20";
          } else {
            headColor = this.mainColor;
            footColor = this.mainColor + "10";
          }
        } else {
          let di = i - lightNum;
          if (di / darkNum < 0.2166666) {
            headColor = this.mainColor + "30";
            footColor = this.mainColor + "10";
          } else if (di / darkNum < 0.4166666) {
            headColor = this.mainColor + "25";
            footColor = this.mainColor + "10";
          } else if (di / darkNum < 0.75) {
            headColor = this.mainColor + "20";
            footColor = this.mainColor + "10";
          } else if (di / darkNum < 0.9) {
            headColor = this.mainColor + "15";
            footColor = this.mainColor + "10";
          } else {
            headColor = this.mainColor + "10";
            footColor = this.mainColor + "10";
          }
        }
        this.proBlockList.push({
          id: i,
          width: this.inProBlockW,
          height: this.inProBlockH,
          color: `linear-gradient(to top, ${headColor}  , ${footColor})`
        });
      }
    },
    // 工具方法,获取更亮或更暗的颜色
    LightenDarkenColor(color, range) {
      let newColor = "#";
      for (let i = 0; i < 3; i++) {
        const hxStr = color.substr(i * 2 + 1, 2);
        let val = parseInt(hxStr, 16);
        val += range;
        if (val < 0) val = 0;
        else if (val > 255) val = 255;
        newColor += val.toString(16).padStart(2, "0");
      }
      return newColor;
    }
  }
};
</script>
<style lang="scss" scoped>
.blockCircleProgress {
  display: inline-block;

  .main-circle {
    border: 2px solid;
    border-radius: 100%;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    .insideCircle {
      border: 2px solid;
      border-radius: 100%;
      .percentLabel {
        width: 100%;
        height: 100%;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        .percentValue {
          font-family: Impact, Haettenschweiler, "Arial Narrow Bold", sans-serif;
          color: #fff;
        }
        .percentTitle {
          color: #ffffff80;
        }
      }
    }
    .inProItem {
      position: absolute;
      height: 100%;
      display: flex;
      // background: #000;
      flex-direction: column;
      justify-content: space-between;
      box-sizing: border-box;
      &-item {
        width: 100%;
        border-radius: 3px;
      }
    }
  }
}
</style>

ok,还是希望疫情能早点结束,大家保重身体🌞


肖大侠
7 声望2 粉丝