Vue3 + TypeScript + ECharts 实战:打造一个精美的环形图统计卡片

大家好,今天我们来分享一个在前端数据可视化中非常常见的需求:如何使用 Vue3、TypeScript 和 ECharts 创建一个既美观又信息丰富的统计卡片。
我们将实现的效果如下图所示:一个卡片中,左侧是一个紧凑的环形图,中心显示总数;右侧是自定义格式的图例,清晰地展示各分类的数值和名称。这种布局在仪表盘(Dashboard)设计中非常流行。

在这里插入图片描述

技术栈

  • Vue3: 采用 Composition API 和 <script setup> 语法。
  • TypeScript: 提供类型安全,增强代码健壮性。
  • ECharts: 强大、开源的数据可视化库。
  • Element Plus: 用于快速搭建 UI 框架(本例中的 el-card)。

第一步:环境准备

首先,请确保你已经有一个 Vue3 + TypeScript 的项目。如果还没有,可以使用 Vite 快速创建:

npm create vue@latest

然后,安装 ECharts:

npm install echarts

第二步:组件结构 (<template>)

我们的模板结构非常简单。使用 Element Plus 的 el-card 作为卡片容器,内部放置一个 div 元素作为 ECharts 的渲染容器。关键在于使用 ref="chart1" 来获取这个 DOM 元素的引用。

<template>
  <div class="layout-padding" style="background-color: #d7e9f6;">
    <el-row :gutter="10">
      <el-col :span="9">
        <el-card style="background-color: #f1f6fb; border-radius: 8px; box-shadow: none; height: 120px;">
          <span style="font-weight: bold;">航母数量</span>
          <!-- ECharts 图表容器 -->
          <div style="height: 60px; margin-left: 30px; margin-top: 8px;" ref="chart1"></div>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>

第三步:核心逻辑 (<script setup>)

这是实现我们自定义效果的关键部分。

1. 引入依赖和定义引用

我们需要从 vue 中引入 ref, reactive, onMounted, onUnmounted,并引入 echarts

import { ref, reactive, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';
// 用于存储所有 ECharts 实例,方便统一管理
const state_chart = reactive({
  myCharts: [] as echarts.EChartsType[],
});
// 获取图表容器的 DOM 引用
const chart1 = ref<HTMLElement>();
2. 初始化图表函数 init_chart1

这是 ECharts 配置的核心。我们通过 echarts.init 初始化实例,然后通过 setOption 方法配置图表样式和数据。
核心配置解析:

  • legend: 我们没有使用默认的图例,而是通过 formatter 函数进行了深度定制。利用 ECharts 的富文本(rich)功能,我们将图例项格式化为“数值在上,名称在下”的布局,使其更紧凑、易读。
  • series:
    • type: 'pie'radius: ['80%', '100%']: 这是创建环形图(甜甜圈图)的标准配置。radius 数组第一个值是内半径,第二个是外半径。
    • center: ['10%', '50%']: 默认饼图在中间,我们通过这个配置将其整体向左移动,为右侧的图例腾出空间,实现左右布局。
    • label: 这是实现中心显示总数的“魔法”。同样使用 formatterrich 文本,我们计算出 data 中所有 value 的总和,并将其格式化后显示在环形图的中心。
    • data: 这里是我们的数据源。
const init_chart1 = async () => {
  if (!chart1.value) return;
  const myChart = echarts.init(chart1.value);
  const option = {
    // ... (此处省略 option 配置,具体见上文完整代码)
  };
  myChart.setOption(option);
  state_chart.myCharts.push(myChart);
};
3. 生命周期管理
  • onMounted: 在组件挂载到 DOM 后,我们才能获取到 ref="chart1" 对应的 DOM 元素,所以必须在此钩子中调用 init_chart1
  • onUnmounted: 当组件销毁时,ECharts 实例并不会自动销毁,这会导致内存泄漏。因此,在组件卸载前,我们需要手动调用 dispose() 方法来销毁所有 ECharts 实例,这是一个非常重要的最佳实践。
// 组件挂载后初始化图表
onMounted(() => {
  init_chart1();
});
// 组件卸载前,销毁图表实例,释放内存
onUnmounted(() => {
  state_chart.myCharts.forEach(chart => {
    chart.dispose();
  });
});

完整代码

将以上所有部分组合起来,就是完整的组件代码。你可以直接复制到你的 .vue 文件中运行。

<template>
  <div class="layout-padding" style="background-color: #d7e9f6;">
    <el-row :gutter="10">
      <el-col :span="9">
        <el-card style="background-color: #f1f6fb; border-radius: 8px; box-shadow: none; height: 120px;">
          <span style="font-weight: bold;">航母数量</span>
          <!-- ECharts 图表容器 -->
          <div style="height: 60px; margin-left: 30px; margin-top: 8px;" ref="chart1"></div>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';

// 用于存储所有 ECharts 实例,方便统一管理
const state_chart = reactive({
  myCharts: [] as echarts.EChartsType[],
});

// 获取图表容器的 DOM 引用
const chart1 = ref<HTMLElement>();

// 初始化图表
const init_chart1 = async () => {
  if (!chart1.value) return;

  const myChart = echarts.init(chart1.value);
  const option = {
    tooltip: {
      trigger: 'item',
      formatter: '{b}: {c} 艘'
    },
  legend: {
      top: '20%',
      orient: 'horizontal',
      icon: 'circle',
      itemWidth: 12,  // 设置圆的直径为 12px
      itemHeight: 12, // 设置圆的直径为 12px
      itemGap: 25,
      formatter: function (name) {
        var data = option.series[0].data;
        var value = 0;
        for (var i = 0; i < data.length; i++) {
          if (data[i].name === name) {
            value = data[i].value;
            break;
          }
        }
        return `{value|${value}}\n{name|${name}}`;
      },
      textStyle: {
        rich: {
          value: {
            fontSize: 20,
            fontWeight: 'bold',
            color: '#333',
            lineHeight: 30,
            align: 'center',
            padding: [-45, 0, 0, 0],
          },
          name: {
            fontSize: 12,
            color: 'black',
            align: 'center',
            padding: [-30, 0, 0, 0],
          }
        }
      }
    },
      textStyle: {
        rich: {
          value: {
            fontSize: 14,
            fontWeight: 'bold',
            color: '#333',
            lineHeight: 20,
            align: 'center'
          },
          name: {
            fontSize: 12,
            color: '#666',
            align: 'center'
          }
        }
      }
    },
    series: [
      {
        name: '航母数量',
        type: 'pie',
        // 通过设置内径和外径,实现环形图
        radius: ['80%', '100%'],
        // 将环形图位置调整到左侧,为右侧图例留出空间
        center: ['10%', '50%'],
        itemStyle: {
          borderColor: '#fff',
          borderWidth: 2
        },
        // 在环形图中心显示总计信息
        label: {
          show: true,
          position: 'center',
          formatter: function () {
            const data = option.series![0].data as { value: number }[];
            const total = data.reduce((sum, item) => sum + item.value, 0);
            return `{total|${total}}\n{totalText|总共}`;
          },
          rich: {
            total: {
              fontSize: 18,
              fontWeight: 'bold',
              color: '#333',
            },
            totalText: {
              fontSize: 12,
              fontWeight: 'normal',
              color: '#999'
            }
          }
        },
        emphasis: {
          scale: true, // 鼠标悬浮时允许放大
          label: {
            show: true,
            fontSize: 16,
            fontWeight: 'bold'
          }
        },
        labelLine: {
          show: false
        },
  		data: [
	        { value: 3, name: '中国', itemStyle: { color: '#5470c6' } },
	        { value: 9, name: '美国', itemStyle: { color: '#91cc75' } },
	        { value: 2, name: '日本', itemStyle: { color: '#fac858' } },
	        { value: 0, name: '德国', itemStyle: { color: '#ee6666' } },
	        { value: 0, name: '俄罗斯', itemStyle: { color: '#73c0de' } }
	     ]
      }
    ]
  };
  myChart.setOption(option);
  state_chart.myCharts.push(myChart);
};

// 组件挂载后初始化图表
onMounted(() => {
  init_chart1();
});

// 组件卸载前,销毁图表实例,释放内存
onUnmounted(() => {
  state_chart.myCharts.forEach(chart => {
    chart.dispose();
  });
});
</script>

<style scoped>
.layout-padding {
  padding: 16px;
}
</style>

总结

通过本教程,我们学习了如何结合 Vue3 和 ECharts 创建一个高度自定义的环形图卡片。关键点在于:

  1. 布局控制: 通过 series.center 调整图表位置,配合 legendright 属性实现左右布局。
  2. 富文本: 灵活运用 formatterrich 文本来定制 labellegend 的显示样式,实现标准配置无法达到的效果。
  3. 生命周期: 正确使用 onMountedonUnmounted 来管理图表实例的创建和销毁,保证应用的性能和稳定性。
    希望这篇文章对你有所帮助!快去试试打造属于你自己的数据可视化卡片吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值