OK..哈喽大家好,I am 肖大侠😷😷😷
最近疫情反反复复,很烦人
然后虽然我不考研,但是却沉迷于徐涛老师的考研政治课,
也许就是这两个原因,导致我最近做了超级多和百分比相关的东西😏😏😏
什么水球图,环形进度条,等等..
接着我就发现了一个我认为特别有趣的效果,就是使用离散的块模拟出一种连续的进度条的视觉效果
尽管丢失了部分精度,但也大差不差,而且真的很酷😍
那就先看看效果:
这是它百分之0的样子:
这是它百分之100的样子:
接下来就简单看看我的实现思路😃😃😃
首先需要一个大圆,
在大圆内部再画一个居中的小圆,小圆中可以显示具体的数值和标题,(小圆的内部可以处理成一个插槽)
接下来就详细说说离散的块如何弄出来,咱们先定义一个参数,用来控制块的密度,我这边默认的是显示24个块
然后我们先不要执着于小块本身,可以先考虑放这些块的容器,容器可以为我们提供定位的能力
如果密度为24,那就需要24个容器,同时还有一个已知的条件就是大圆的直径,
让容器的宽度等于小球的直径,再将它们都使用绝对定位,定位大圆的圆心位置,就像下图这样
接下来让这些容器依次选择15°,30°...,效果如下:
这个时候容器的作用就显而易见了,首先它的高度就是最终小块的宽度,其次,容器的两端都落在了圆上,这就是前面提到的定位能力
在这些容器中再去画小块,让小块置于容器的某一端就可以得到下面的效果:
最后无非就是要显示几个小块的问题了,这部分很简单,用密度和当前的百分比就可以很轻易的算出来
但这还不够,还可以做的更好,为了表示百分比,使用渐变也是不错的选择,因此我们可以给小块加上渐变的样式让它看上去更酷😎😎😎
有各种各样的策略,越复杂的越完备的策略会让它看起来更好,例如在计算块的个数时,总是向上取整,在最后一个块中使用渐变来表示更加精确的百分比,
也可以制定一些其他的策略,让它看上去更好看,没有必要去考虑精度
那我就说说我的'丑陋'策略吧😗
首先就是无论百分之多少,都把块画出来,只是区分亮块和暗块,
亮块用由亮到暗的渐变,暗块用由暗到更暗的渐变
接下来计算亮块的个数,用密度减去亮块的个数就是暗块的个数
然后我规定,亮块的第一个不做渐变,亮块的前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组件,代码我贴在最后,它的主要参数:
参数名 | 类型 | 说明 | 示例 | 默认值 |
---|---|---|---|---|
size | String | 用于控制整体的尺寸,支持'px','rem'等单位 | '200px','2rem' | '200px' |
width | Number | 用于控制条的宽途,即小块的长度 | 20 | 20 |
mainColor | String | 控制颜色,仅支持'#ffffff'这种格式 | "#fa8072" | "#fa8072" |
density | Number | 块密度 | 24 | 24 |
value | Number | 百分比值 | 76.2 | 0 |
title | String | 标题 | '开发进度' | '自定义标题' |
它还有一个标题的插槽,可以自定义内圆中的内容
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,还是希望疫情能早点结束,大家保重身体🌞
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。