原创 快乐就是哈哈哈 2024-12-10 08:31 重庆
点击关注公众号,“技术干货” 及时达!
需求简介
这几天下来个新需求:「要在页面上动态渲染多个表格,每个表格内还要实现若干圆环!」
刚拿到这个需求的时候,我第一反应是用echarts实现,简单快捷
然而,老板无情的拒绝了我!他说:
❝咱这个项目就一个独立页面,你引入个ECharts项目又要大很多!而且一个页面这么多ECharts实例,性能怎么保障?不准用ECharts,用CSS实现!
❞
没办法,我只好百度如何用CSS画圆环。幸运的是,我确实找到了类似的文章:
「不幸的事」,效果和我的差异很大,代码根本 无法复用!没办法,只能用别的办法实现了。经过一番研究,最终借助「Canvas」搞出来了,简单的分享一下我的实现思路吧。
圆环组件简介
为了方便复用,我把这个功能封装成了项目可直接复用的组件。并支持自定义圆环大小、圆环宽度和圆环颜色比例配置属性。
<Ring:storkWidth="5":size="60":ratioList="[{ ratio: 0.3, color: '#FF5733' },{ ratio: 0.6, color: '#33FF57' },{ ratio: 0.1, color: '#3357FF' }]"></Ring>
技术方案
实现目标
根据我们的UX需求,我们需要实现一个简单的组件,该组件可以展示一个圆环图表,并根据外部传入的比例数据(如 ratioList)绘制不同颜色的环形区域。
使用 Vue 3 和 TypeScript。
动态绘制环形图,根据传入的数据绘制多个环。
支持自定义环形图的大小和环宽。
创建 Vue 组件框架
首先,我们创建一个名为 RingChart.vue的组件。组件的初始结构非常简单,只包含一个 canvas 元素。
<template><!-- 创建一个 canvas 元素,用于绘制图表 --><canvas ref="canvasDom"></canvas></template><script lang="ts" setup>import { ref, onMounted } from 'vue';// 获取 canvas DOM 元素的引用const canvasDom = ref<HTMLCanvasElement | null>(null);// 初始化 canvas 和上下文变量let ctx: CanvasRenderingContext2D | null = null;let width: number, height: number;// 初始化 canvas 尺寸和绘图环境const initCanvas = () => {const dom = canvasDom.value;if (!dom) return;ctx = dom.getContext('2d');if (!ctx) return;// 设置 canvas 的宽高dom.width = dom.offsetWidth;dom.height = dom.offsetHeight;width = dom.offsetWidth;height = dom.offsetHeight;};// 在组件挂载后执行初始化onMounted(() => {initCanvas();});</script><style scoped>canvas {width: 100%;height: 100%;}</style>
上述代码中,我们初始化了 canvas 元素,并且设定了 width 和 height 属性。
绘制基本的圆环
接下来,我们添加「绘制圆环」的功能:通过 arc 方法来绘制圆环,设置 lineWidth 来调整环的宽度。
<script lang="ts" setup>import { ref, onMounted } from 'vue';// 获取 canvas DOM 元素的引用const canvasDom = ref<HTMLCanvasElement | null>(null);// 初始化 canvas 和上下文变量let ctx: CanvasRenderingContext2D | null = null;let width: number, height: number;// 初始化 canvas 尺寸和绘图环境const initCanvas = () => {const dom = canvasDom.value;if (!dom) return;ctx = dom.getContext('2d');if (!ctx) return;// 设置 canvas 的宽高dom.width = dom.offsetWidth;dom.height = dom.offsetHeight;width = dom.offsetWidth;height = dom.offsetHeight;// 调用绘制圆环的方法drawCircle({ctx,x: width / 2,y: height / 2,radius: 8,lineWidth: 4,color: '#C4C9CF4D',startAngle: -Math.PI / 2,endAngle: Math.PI * 1.5,});};// 绘制一个圆环的方法const drawCircle = ({ctx,x,y,radius,lineWidth,color,startAngle,endAngle,}: {ctx: CanvasRenderingContext2D;x: number;y: number;radius: number;lineWidth: number;color: string;startAngle: number;endAngle: number;}) => {ctx.beginPath();ctx.arc(x, y, radius, startAngle, endAngle);ctx.lineWidth = lineWidth;ctx.strokeStyle = color;ctx.stroke();ctx.closePath();};onMounted(() => {initCanvas();});</script>
drawCircle 函数是绘制圆环的核心。我们通过 arc 方法绘制圆形路径,使用 lineWidth 来调整环的宽度,并用 strokeStyle 给圆环上色。
startAngle 和 endAngle 参数决定了圆环的起始和结束角度,通过改变它们可以控制环的覆盖区域。
绘制多个环形区域
现在,我们来实现绘制多个环形区域的功能。我们将通过传入一个 ratioList 数组来动态生成多个环,每个环代表不同的比例区域。
<script lang="ts" setup>import { ref, computed, onMounted } from 'vue';// 定义 props 的类型interface RatioItem {ratio: number;color: string;}const props = defineProps<{size?: number; // 画布大小storkWidth?: number; // 环的宽度ratioList?: RatioItem[]; // 比例列表}>();// 默认值const defaultSize = 200;const defaultStorkWidth = 4;const defaultRatioList: RatioItem[] = [{ ratio: 1, color: '#C4C9CF4D' },];// canvas DOM 和上下文const canvasDom = ref<HTMLCanvasElement | null>(null);let ctx: CanvasRenderingContext2D | null = null;// 动态计算 canvas 的中心点和半径const size = computed(() => props.size || defaultSize);const center = computed(() => ({x: size.value / 2,y: size.value / 2,}));const radius = computed(() => size.value / 2 - (props.storkWidth || defaultStorkWidth));// 初始化 canvasconst initCanvas = () => {const dom = canvasDom.value;if (!dom) return;ctx = dom.getContext('2d');if (!ctx) return;dom.width = size.value;dom.height = size.value;drawBackgroundCircle();drawDataRings();};// 绘制背景圆环const drawBackgroundCircle = () => {if (!ctx) return;drawCircle({ctx,x: center.value.x,y: center.value.y,radius: radius.value,lineWidth: props.storkWidth || defaultStorkWidth,color: '#C4C9CF4D',startAngle: -Math.PI / 2,endAngle: Math.PI * 1.5,});};// 绘制数据圆环const drawDataRings = () => {const { ratioList = defaultRatioList } = props;if (!ctx) return;let startAngle = -Math.PI / 2;ratioList.forEach(({ ratio, color }) => {const endAngle = startAngle + ratio * Math.PI * 2;drawCircle({ctx,x: center.value.x,y: center.value.y,radius: radius.value,lineWidth: props.storkWidth || defaultStorkWidth,color,startAngle,endAngle,});startAngle = endAngle;});};// 通用绘制函数const drawCircle = ({ctx,x,y,radius,lineWidth,color,startAngle,endAngle,}: {ctx: CanvasRenderingContext2D;x: number;y: number;radius: number;lineWidth: number;color: string;startAngle: number;endAngle: number;}) => {ctx.beginPath();ctx.arc(x, y, radius, startAngle, endAngle);ctx.lineWidth = lineWidth;ctx.strokeStyle = color;ctx.stroke();ctx.closePath();};// 监听画布大小变化onMounted(() => {initCanvas();});</script>
上述代码中,我们通过 ratioList 数组传递每个环的比例和颜色,使用 startAngle 和 endAngle 来控制每个环的绘制区域。其中,drawDataRings 函数遍历 ratioList,根据每个数据项的比例绘制环形区域。
现在,我们的组件就实现完毕了,可以在其他地方引入使用了
<RingChart:storkWidth="8":size="60":ratioList="[{ ratio: 0.3, color: '#F8766F' },{ ratio: 0.6, color: '#69CD90' },{ ratio: 0.1, color: '#FFB800' }]"></RRingChart>
组件代码
<template><canvas ref="canvasDom"></canvas></template><script lang="ts" setup>import { ref, computed, onMounted, watchEffect } from 'vue';// 定义 props 的类型interface RatioItem {ratio: number;color: string;}const props = defineProps<{size?: number; // 画布大小storkWidth?: number; // 环的宽度ratioList?: RatioItem[]; // 比例列表}>();// 默认值const defaultSize = 200; // 默认画布宽高const defaultStorkWidth = 4;const defaultRatioList: RatioItem[] = [{ ratio: 1, color: '#C4C9CF4D' }];// canvas DOM 和上下文const canvasDom = ref<HTMLCanvasElement | null>(null);let ctx: CanvasRenderingContext2D | null = null;// 动态计算 canvas 的中心点和半径const size = computed(() => props.size || defaultSize);const center = computed(() => ({x: size.value / 2,y: size.value / 2}));const radius = computed(() => size.value / 2 - (props.storkWidth || defaultStorkWidth));// 初始化 canvasconst initCanvas = () => {const dom = canvasDom.value;if (!dom) return;ctx = dom.getContext('2d');if (!ctx) return;dom.width = size.value;dom.height = size.value;drawBackgroundCircle();drawDataRings();};// 绘制背景圆环const drawBackgroundCircle = () => {if (!ctx) return;drawCircle({ctx,x: center.value.x,y: center.value.y,radius: radius.value,lineWidth: props.storkWidth || defaultStorkWidth,color: '#C4C9CF4D',startAngle: -Math.PI / 2,endAngle: Math.PI * 1.5});};// 绘制数据圆环const drawDataRings = () => {const { ratioList = defaultRatioList } = props;if (!ctx) return;let startAngle = -Math.PI / 2;ratioList.forEach(({ ratio, color }) => {const endAngle = startAngle + ratio * Math.PI * 2;drawCircle({ctx,x: center.value.x,y: center.value.y,radius: radius.value,lineWidth: props.storkWidth || defaultStorkWidth,color,startAngle,endAngle});startAngle = endAngle;});};// 通用绘制函数const drawCircle = ({ctx,x,y,radius,lineWidth,color,startAngle,endAngle}: {ctx: CanvasRenderingContext2D;x: number;y: number;radius: number;lineWidth: number;color: string;startAngle: number;endAngle: number;}) => {ctx.beginPath();ctx.arc(x, y, radius, startAngle, endAngle);ctx.lineWidth = lineWidth;ctx.strokeStyle = color;ctx.stroke();ctx.closePath();};// 监听画布大小变化watchEffect(() => {initCanvas();});onMounted(() => {initCanvas();});</script><style scoped>canvas {display: block;margin: auto;border-radius: 50%;}</style>
使用
<Ring:storkWidth="5":size="60":ratioList="[{ ratio: 0.3, color: '#FF5733' },{ ratio: 0.6, color: '#33FF57' },{ ratio: 0.1, color: '#3357FF' }]"></Ring>
点击关注公众号,“技术干货” 及时达!
