前言
进度条在H5,小程序,管理后台的应用都是非常常见的,一般常见的圆形进度条,条形进度条在各个UI框架都是有组件的,可谓是拿来即用。那问题来了,需求要求实现半圆进度条和圆角矩形进度条这可怎么办呢?废话不多说,直接上实现效果以及代码。
技术
基于Taro+vue3+svg
实现效果
实现思路
因为svg在小程序并不能识别,所以我们在开发的时候需要把svg转成data:image/svg+xml格式的URL,再用img渲染出来。
公共方法代码
index.ts文件
import Taro from '@tarojs/taro';
export function stringToBase64(html) {
const arrayBuffer = new Uint8Array(stringToArrayBuffer(html));
const base64 = Taro.arrayBufferToBase64(arrayBuffer);
// 构造data:image/svg+xml格式的URL
return `data:image/svg+xml;base64,${base64}`;
}
// 字符串转为ArrayBuffer对象,参数为字符串
function stringToArrayBuffer(str) {
const bytes = [] as any;
let c;
const len = str.length;
for (let i = 0; i < len; i++) {
c = str.charCodeAt(i);
if (c >= 0x010000 && c <= 0x10ffff) {
bytes.push(((c >> 18) & 0x07) | 0xf0);
bytes.push(((c >> 12) & 0x3f) | 0x80);
bytes.push(((c >> 6) & 0x3f) | 0x80);
bytes.push((c & 0x3f) | 0x80);
} else if (c >= 0x000800 && c <= 0x00ffff) {
bytes.push(((c >> 12) & 0x0f) | 0xe0);
bytes.push(((c >> 6) & 0x3f) | 0x80);
bytes.push((c & 0x3f) | 0x80);
} else if (c >= 0x000080 && c <= 0x0007ff) {
bytes.push(((c >> 6) & 0x1f) | 0xc0);
bytes.push((c & 0x3f) | 0x80);
} else {
bytes.push(c & 0xff);
}
}
const array = new Int8Array(bytes.length);
for (let i = 0; i <= bytes.length; i++) {
array[i] = bytes[i];
}
return array.buffer;
}
圆形进度条代码
<template>
<image :style="{ width: '100%', height: '100%' }" :src="dataURL"></image>
</template>
<script lang="tsx" setup>
import { stringToBase64 } from './utils/index';
import { ref, watchEffect } from 'vue';
import { renderToString } from '@vue/server-renderer';
interface IProps {
defaultDurTime?: number; //进度条完结时间
progress?: number; //当前进度0-100
color?: object | string; //进度条颜色,传入对象格式可以定义渐变色
size?: number; //直径
layerColor?: string; //轨道颜色
fill?: string; //填充颜色
strokeWidth?: number; //线条宽度
strokeLinecap?: string; //进度条端点(butt,round,square)
}
const props = withDefaults(defineProps<IProps>(), {
defaultDurTime: 500,
progress: 90,
color: () => {
return { '0': '#80EDFF', '100': '#4A7DFF' };
},
layerColor: '#e0e0e0',
fill: 'none',
size: 200,
strokeWidth: 20,
strokeLinecap: 'round',
});
const progressOffset = ref(0);
const totalArcLength = ref(0);
const strokeBgColor = ref<string | object>('#666666');
let dataURL = ref('');
async function handleSave() {
const html = await renderToString(renderTitle());
dataURL.value = stringToBase64(html);
console.log(dataURL);
}
const renderTitle = () => {
return (
<svg
width={props.size + props.strokeWidth}
height={props.size + props.strokeWidth}
xmlns="http://www.w3.org/2000/svg"
style={{ transform: 'rotate(-90deg)', transformOrigin: 'center' }}
>
{Object.prototype.toString.call(props.color) === '[object Object]' && (
<defs>
<linearGradient id="progressGradient" x1="0%" y1="0%" x2="100%" y2="0%">
{Object.keys(props.color).map((key) => (
<stop offset={`${key}%`} stop-color={props.color[key]} key={key}></stop>
))}
</linearGradient>
</defs>
)}
<circle
cx="50%"
cy="50%"
r={0.5 * props.size}
style={{
strokeWidth: props.strokeWidth,
fill: 'none',
stroke: props.layerColor,
}}
></circle>
<circle
style={{
strokeDashoffset: progressOffset.value,
strokeDasharray: totalArcLength.value,
strokeWidth: props.strokeWidth,
fill: props.fill,
strokeLinecap: props.strokeLinecap,
stroke: strokeBgColor.value,
}}
cx="50%"
cy="50%"
r={0.5 * props.size}
>
<animate
attributeName="stroke-dashoffset"
dur={`${props.defaultDurTime}ms`}
fill="freeze"
from={totalArcLength.value}
to={progressOffset.value}
></animate>
</circle>
</svg>
);
};
watchEffect(() => {
const circumference = Math.PI * props.size;
totalArcLength.value = circumference;
if (props.strokeLinecap !== 'butt' && props.progress !== 100 && props.progress > 95) {
progressOffset.value =
circumference - (props.progress / 100) * circumference + props.strokeWidth;
} else {
progressOffset.value = circumference - (props.progress / 100) * circumference;
}
if (Object.prototype.toString.call(props.color) === '[object Object]') {
strokeBgColor.value = `url(#progressGradient)`;
} else {
strokeBgColor.value = props.color;
}
handleSave();
});
</script>
props
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
defaultDurTime | 动画速度(也可以任务是进度条完结时间) | number | 500 |
progress | 进度百分比(0-100) | number | 90 |
color | 进度条颜色,传入对象格式可以定义渐变色 | object 或 string | { ‘0’: ‘#80EDFF’, ‘100’: ‘#4A7DFF’ } |
size | 直径 | number | 200 |
layerColor | 轨道颜色 | string | #e0e0e0 |
fill | 填充颜色 | string | ‘none’ |
strokeWidth | 进度条宽度 | number | 20 |
strokeLinecap | 进度条端点(butt,round,square) | string | ’round’ |
半圆形进度条
<script lang="tsx" setup>
import { stringToBase64 } from './utils/index';
import { ref, watchEffect } from 'vue';
import { renderToString } from '@vue/server-renderer';
interface IProps {
defaultDurTime?: number; //进度条完结时间
progress?: number; //当前进度0-100
color?: object | string; //进度条颜色,传入对象格式可以定义渐变色
size?: number; //直径
layerColor?: string; //轨道颜色
fill?: string; //填充颜色
strokeWidth?: number; //线条宽度
strokeLinecap?: string; //进度条端点(butt,round,square)
}
const props = withDefaults(defineProps<IProps>(), {
defaultDurTime: 1000, //进度条完结时间
progress: 50,
color: () => {
return { '0': '#80EDFF', '100': '#4A7DFF' };
},
size: 200,
layerColor: '#e0e0e0',
fill: 'none',
strokeWidth: 20,
strokeLinecap: 'round',
});
const viewBox = ref('');
const memoryTrackPath = ref('');
const progressPath = ref('');
const strokeDasharray = ref(0);
const strokeDashoffset = ref(0);
const strokeBgColor = ref<string | object>('#666666');
let dataURL = ref('');
async function handleSave() {
const html = await renderToString(renderTitle());
console.log(html);
dataURL.value = stringToBase64(html);
console.log(dataURL);
}
const renderTitle = () => {
return (
<svg viewBox={viewBox.value} xmlns="http://www.w3.org/2000/svg">
{Object.prototype.toString.call(props.color) === '[object Object]' && (
<defs>
<linearGradient id="progressGradient" x1="0%" y1="0%" x2="100%" y2="0%">
{Object.keys(props.color).map((key) => (
<stop offset={`${key}%`} stop-color={props.color[key]} key={key}></stop>
))}
</linearGradient>
</defs>
)}
<path
d={memoryTrackPath.value}
style={{
strokeWidth: props.strokeWidth,
stroke: props.layerColor,
fill: 'none',
strokeLinecap: props.strokeLinecap,
}}
></path>
<path
d={progressPath.value}
style={{
strokeWidth: props.strokeWidth,
stroke: strokeBgColor.value,
fill: props.fill,
strokeLinecap: props.strokeLinecap,
strokeDashoffset: strokeDashoffset.value,
strokeDasharray: strokeDasharray.value,
}}
>
<animate
attributeName="stroke-dashoffset"
dur={`${props.defaultDurTime}ms`}
fill="freeze"
from={strokeDasharray.value}
to={strokeDashoffset.value}
></animate>
</path>
</svg>
);
};
watchEffect(() => {
const radius = props.size / 2;
viewBox.value = `0 0 ${props.size + props.strokeWidth} ${0.5 * props.size + props.strokeWidth}`;
const progressAngle = (props.progress / 100) * Math.PI; // 根据进度计算弧度
const startX = props.strokeWidth / 2;
const startY = radius + props.strokeWidth / 2;
const endY = -Math.sin(progressAngle) * radius;
const endX = radius - Math.cos(progressAngle) * radius;
const largeArcFlag = progressAngle <= Math.PI ? '0' : '1';
memoryTrackPath.value = `M ${startX} ${startY} a ${radius} ${radius} 0 1 1 ${props.size} 0`;
progressPath.value = `M ${startX} ${startY} a ${radius} ${radius} 0 ${largeArcFlag} 1 ${endX} ${endY}`;
strokeDasharray.value = radius * Math.PI;
strokeDashoffset.value = strokeDasharray.value - (props.progress / 100) * strokeDasharray.value;
if (Object.prototype.toString.call(props.color) === '[object Object]') {
strokeBgColor.value = `url(#progressGradient)`;
} else {
strokeBgColor.value = props.color;
}
handleSave();
});
</script>
props
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
defaultDurTime | 动画速度(也可以任务是进度条完结时间) | number | 1000 |
progress | 进度百分比(0-100) | number | 50 |
color | 进度条颜色,传入对象格式可以定义渐变色 | object 或 string | { ‘0’: ‘#80EDFF’, ‘100’: ‘#4A7DFF’ } |
size | 直径 | number | 200 |
layerColor | 轨道颜色 | string | #e0e0e0 |
fill | 填充颜色 | string | ‘none’ |
strokeWidth | 进度条宽度 | number | 20 |
strokeLinecap | 进度条端点(butt,round,square) | string | ’round’ |
矩形进度条
<script lang="tsx" setup>
import { stringToBase64 } from './utils/index';
import { ref, watchEffect } from 'vue';
import { renderToString } from '@vue/server-renderer';
interface IProps {
defaultDurTime?: number; //进度条完结时间
progress?: number; //当前进度0-100
color?: object | string; //进度条颜色,传入对象格式可以定义渐变色
width?: number; //矩形宽度
height?: number; //矩形高度
radian?: number; //矩形圆角弧度
layerColor?: string; //轨道颜色
fill?: string; //填充颜色
strokeWidth?: number; //线条宽度
strokeLinecap?: string; //进度条端点(butt,round,square)
}
const props = withDefaults(defineProps<IProps>(), {
defaultDurTime: 1000, //进度条完结时间
progress: 50,
color: () => {
return { '0': '#80EDFF', '100': '#4A7DFF' };
},
width: 140,
height: 120,
radian: 20,
layerColor: '#e0e0e0',
fill: 'none',
strokeWidth: 10,
strokeLinecap: 'round',
});
const progressOffset = ref(0);
const allLen = ref(0);
const strokeBgColor = ref<string | object>('#666666');
let dataURL = ref('');
async function handleSave() {
const html = await renderToString(renderTitle());
dataURL.value = stringToBase64(html);
console.log(dataURL);
}
const renderTitle = () =>
return (
<svg
width={props.width + props.strokeWidth}
height={props.height + props.strokeWidth}
xmlns="http://www.w3.org/2000/svg"
>
{Object.prototype.toString.call(props.color) === '[object Object]' && (
<defs>
<linearGradient id="progressGradient" x1="0%" y1="0%" x2="100%" y2="0%">
{Object.keys(props.color).map((key) => (
<stop offset={`${key}%`} stop-color={props.color[key]} key={key}></stop>
))}
</linearGradient>
</defs>
)}
<rect
x={0.5 * props.strokeWidth}
y={0.5 * props.strokeWidth}
rx={props.radian}
ry={props.radian}
width={props.width}
height={props.height}
style={{ strokeWidth: props.strokeWidth, fill: props.fill, stroke: props.layerColor }}
/>
{props.progress !== 0 && (
<rect
x={0.5 * props.strokeWidth}
y={0.5 * props.strokeWidth}
rx={props.radian}
ry={props.radian}
width={props.width}
height={props.height}
style={{
strokeDashoffset: progressOffset.value,
strokeDasharray: allLen.value,
strokeWidth: props.strokeWidth,
fill: props.fill,
stroke: strokeBgColor.value,
strokeLinecap: props.strokeLinecap,
}}
>
<animate
attributeName="stroke-dashoffset"
dur={`${props.defaultDurTime}ms`}
fill="freeze"
from={allLen.value}
to={progressOffset.value}
></animate>
</rect>
)}
</svg>
);
};
watchEffect(() => {
allLen.value = (props.width + props.height) * 2 - props.radian * 8 + 2 * props.radian * Math.PI;
if (props.strokeLinecap !== 'butt' && props.progress !== 100 && props.progress > 95) {
progressOffset.value
allLen.value - (props.progress / 100) * allLen.value + props.strokeWidth;
} else {
progressOffset.value = allLen.value - (props.progress / 100) * allLen.value;
}
if (Object.prototype.toString.call(props.color) === '[object Object]') {
strokeBgColor.value = `url(#progressGradient)`;
} else {
strokeBgColor.value = props.color;
}
handleSave();
});
</script>
props
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
defaultDurTime | 动画速度(也可以任务是进度条完结时间) | number | 1000 |
progress | 进度百分比(0-100) | number | 50 |
color | 进度条颜色,传入对象格式可以定义渐变色 | object 或 string | { ‘0’: ‘#80EDFF’, ‘100’: ‘#4A7DFF’ } |
width | 宽度 | number | 140 |
height | 高度 | number | 120 |
radian | 圆角弧度(为0时是直角) | number | 20 |
layerColor | 轨道颜色 | string | #e0e0e0 |
fill | 填充颜色 | string | ‘none’ |
strokeWidth | 进度条宽度 | number | 10 |
strokeLinecap | 进度条端点(butt,round,square) | string | ’round’ |
最后希望对大家有所帮助哦!
原文链接:https://juejin.cn/post/7347905080508678171 作者:柠檬果茶