【前端】Vue2 项目 ECharts 统计图表组件封装与使用实践(附组件源码)

Vue2 项目 ECharts 统计图表组件封装与使用实践

本文主要说明在前后端分离的 Vue2 项目 中,如何用 ECharts 实现统计图表展示,并把多种图表类型封装成可复用组件。

核心思路:页面组件(容器)负责请求后端数据并整理成固定结构 → 图表组件(纯展示)只接收 props 渲染 。innovation.vue(创新能力)是使用组件的典型示例。


一、技术栈与依赖

  • Vue 2.6.12 (前端框架,vue-cli-service 构建)
  • ECharts 5.1.1 (统计图表库,见 package.json 依赖)
  • 前后端分离 :数据通过接口(FormFunctions.invokeCustomQueryPromise 等)异步获取

package.json 依赖片段:

json 复制代码
{
  "dependencies": {
    "echarts": "5.1.1"
  }
}

组件内统一按需引入方式(当前项目用的是全量引入):

js 复制代码
import * as echarts from "echarts";

二、组件化设计思路

  1. 展示与数据解耦
    • 图表组件内部不请求接口 ,只接收 data(以及高度、单位、颜色等配置)props。
    • 页面组件负责调接口、把返回数据转换成「组件约定的 data 结构」,再传给图表组件。
  2. 一图一组件
    • 每种图表类型(柱状图、折线图、饼图、雷达图、地图、组合图...)封装成一个独立 .vue 组件,通过 props 控制外观。
  3. 统一更新机制
    • 用 watch: { data: { deep: true, handler } } 监听数据变化,数据一变就重新 initChart() 重绘;也常用 :key="timer" 强制重建实例。
  4. 按需注册,不做全局注册
    • 组件默认局部引入 (import + components),不挂到 Vue.component 全局,避免体积膨胀与命名冲突。

三、组件公共约定

这些图表组件虽然种类不同,但遵循一套统一约定,便于替换与组合:

约定 说明
width / height 容器宽高(字符串,如 "100%"、"350px")。必须给容器有效高度,否则 ECharts 尺寸为 0 不显示
data 图表数据,格式随图表类型而定(见「数据契约」)
unit 数值单位(如 件、户、%),用于 tooltip / 坐标轴
fontSize 字号(默认 14)
tips 是否显示 tooltip / 图例可点击(默认 true)
colorList / color 颜色配置,支持渐变或自定义色板
初始化方式 三种:echarts.init(this.$el)、echarts.init(this.$refs.xxx)、echarts.init(document.getElementById(id))(用 id 方式时必须保证 id 唯一)
数据更新 watch data(deep) → 重新 setOption;部分组件用 dispose() 后重建
窗口自适应 window.onresize = () => chart.resize()

注意:多数老组件用 window.onresize = function(){...} 覆盖式 绑定 resize。同一页面若同时存在多个图表实例,只有最后一个生效------这是历史实现的一个隐患,新组件建议改用 window.addEventListener("resize", ...) 或统一 resize 管理。


四、组件清单与图表类型总览

所有图表纯展示组件都放在同一个图表组件目录下,按类型分为基础图表与组合图表两类:

组件 图表类型 一句话说明
echart_barA.vue 竖向柱状图(多系列) 最常用柱状图,支持多系列、渐变、图例、dataZoom
echart_barB.vue 横向条形图(排行榜) 名称在左、数值在右的排行条,高度按条数自适应
echart_line.vue 折线图 / 面积图 多系列折线,带渐变面积填充,可平滑
echart_pieA.vue 饼图 / 玫瑰图 双环饼图,支持下钻、百分比图层、"其它"合并
echart_radar.vue 雷达图 多维度对比,支持多系列叠加、面积填充
echart_bar_line.vue 柱状图 + 折线图(双 Y 轴) 一根柱 + 一条线(右轴),带自动轮播
echart_barLine_compare.vue 柱状图 + 折线面积图(对比分析) 多组柱 + 折线,双轴,自动轮播并高亮 tooltip
echart_barLine_group.vue 柱状图 + 折线图(分组分析) 与 compare 类似,折线为圆点、非面积
echart_barMore.vue 多系列柱(可堆叠)+ 多折线 更灵活的组合图,支持堆叠与占比标签、鼠标联动
echart_mapC.vue 地图(中国地图) geo + map 系列的行政区着色图

五、各组件详解

5.1 echart_barA.vue ------ 竖向柱状图

  • 图表类型 :bar(多系列竖向柱)。
  • props :className / width / height(默认 350px)/ zoomNumber(默认 8)/ zoomLock(默认 true)/ tips / fontSize / unit / data / colorList。
  • data 结构:
js 复制代码
{
  xAxis: ["2021年", "2022年", "2023年"],
  series: [
    { name: "商标", data: [10, 20, 30] },
    { name: "发明专利", data: [5, 8, 12] }
  ]
}
  • 关键实现 :
    • series 全部 type: "bar",barWidth: "8%"、barGap: "25%";
    • 每根柱用 echarts.graphic.LinearGradient 做垂直渐变,颜色取自 colorList[index]([起始色, 结束色, 描边色]);
    • 图例在顶部、grid.top: "30%"(给图例留位);
    • dataZoom 由 zoomLock 控制:锁定则不显示滑动条。
  • 适用:分类对比、逐年趋势(离散)。
完整代码
vue 复制代码
<!-- 柱状图 -->
<template>
	<div :class="className" :style="{ height: height, width: width }" />
</template>

<script>
import * as echarts from "echarts";

const animationDuration = 1000;

export default {
	props: {
		className: {
			type: String,
			default: "chart",
		},
		width: {
			type: String,
			default: "100%",
		},
		height: {
			type: String,
			default: "350px",
		},
		zoomNumber: {
			type: Number,
			default: 8,
		},
		zoomLock: {
			type: Boolean,
			default: true,
		},
		tips: {
			type: Boolean,
			default: true,
		},
		fontSize: {
			type: Number,
			default: 14,
		},
		unit: String,
		data: {
			type: Object,
			default() {
				return {
					xAxis: ["区域一", "区域二", "区域三"],
					series: [
						{
							name: "运行中",
							data: [250, 150, 170],
						},
					],
				};
			},
		},
		colorList: {
			type: Array,
			default() {
				return [
					["#6daeff", "#4292f5", "#2a7ce3"],
					["#81ebdd", "#26d6bf", "#34b2a2"],
					["#fee386", "#e7b814", "#d1a407"],
					["#feb7d4", "#f44e7b", "#fa2961"],
					["#f39d69", "#eb7630", "#d65c12"],
					["#8dc87f", "#5fae4c", "#3e9d27"],
					["#ee7fef", "#d747d9", "#c627c8"],
					["#8e8324", "#c1b80a", "#574525"],
				];
			},
		},
	},
	data() {
		return {
			chart: null,
		};
	},
	mounted() {
		let self = this;
		this.$nextTick(() => {
			self.initChart();
		});
	},
	beforeDestroy() {
		if (!this.chart) {
			return;
		}
	},
	watch: {
		data: {
			handler() {
				let self = this;
				self.initChart();
			},
			deep: true,
		},
	},
	methods: {
		initChart() {
			let unit = this.unit;
			let color = this.colorList;
			this.chart = echarts.init(this.$el);

			let series = [];
			let legendArray = [];

			this.data.series.forEach((item, index) => {
				legendArray.push(item.name);
				series.push({
					name: item.name,
					type: "bar",
					barWidth: "8%",
					barGap: "25%",
					animationDuration,
					itemStyle: {
						normal: {
							borderWidth: color[index][2] ? 1 : 0,
							borderColor: color[index][2] ? color[index][2] : "transparent",
							color: new echarts.graphic.LinearGradient(
								0, 0, 0, 1,
								[
									{ offset: 0, color: color[index][0] },
									{ offset: 1, color: color[index][1] },
								],
								false
							),
						},
					},
					data: item.data,
				});
			});

			let option = {
				tooltip: {
					show: this.tips,
					trigger: "axis",
					axisPointer: {
						type: "line",
						lineStyle: {
							color: "#1890ff",
						},
					},
					formatter: function (params) {
						let relVal = params[0].name;
						params.forEach((item) => {
							relVal += "<br/>" + item.marker + item.seriesName + "&nbsp;&nbsp;&nbsp;&nbsp;";
							relVal += "<span style='float:right;font-weight: bold'>";
							relVal += item.value + unit;
							relVal += "</span>";
						});
						return relVal;
					},
				},
				legend: {
					selectedMode: this.tips,
					itemWidth: 12,
					itemHeight: 12,
					itemGap: 13,
					data: legendArray,
					top: "2%",
					textStyle: {
						fontSize: this.fontSize,
						color: "#8699AD",
					},
					itemStyle: {
						borderWidth: 0,
					},
				},
				grid: {
					top: "30%",
					bottom: "30",
					left: "0%",
					right: "0%",
					containLabel: true,
				},
				xAxis: [
					{
						type: "category",
						data: this.data.xAxis,
						axisLine: {
							lineStyle: { color: "#EEF0F4" },
						},
						axisTick: { show: false },
						axisLabel: {
							interval: 0,
							textStyle: {
								fontSize: this.fontSize,
								color: "#8699AD",
							},
						},
					},
				],
				yAxis: [
					{
						type: "value",
						minInterval: 1,
						axisTick: { show: false },
						axisLine: {
							lineStyle: { color: "#EEF0F4" },
						},
						axisLabel: {
							formatter: "{value}" + unit,
							textStyle: {
								fontSize: this.fontSize,
								color: "#8699AD",
							},
						},
						splitLine: {
							lineStyle: { color: "#EEF0F4" },
						},
					},
				],
				dataZoom: [
					{
						show: !this.zoomLock,
						type: "slider",
						startValue: 0,
						bottom: 10,
						height: 10,
						zoomLock: this.zoomLock,
						brushSelect: false,
						moveHandleSize: 10,
						fillerColor: "rgba(167,183,204,0.4)",
						endValue: this.zoomNumber,
						showDetail: false,
					},
					{
						type: "inside",
						disabled: false,
						show: !this.zoomLock,
						zoomLock: this.zoomLock,
						zoomOnMouseWheel: false,
						moveOnMouseMove: false,
						moveOnMouseWheel: true,
					},
				],
				series: series,
			};
			this.chart.setOption(option);
		},
	},
};
</script>

<style scoped>
</style>

5.2 echart_barB.vue ------ 横向条形图(排行榜)

  • 图表类型 :bar(横向,yAxis 类目 + inverse: true 实现从上到下排序)。
  • props :className / width / zoomNumber / zoomLock / tips / fontSize / unit / data(Array)。
  • data 结构:
js 复制代码
[
  { name: "企业A", value: 53, sum: 50 },
  { name: "企业B", value: 44, sum: 60 }
]
  • 关键实现 :
    • 没有 height prop ,高度按数据条数自适应:this.height = data.length * 50 + "px"(在 watch 中设置);
    • xAxis.show: false;yAxis 用两个隐藏轴,第二个轴的 axisLabel.formatter 显示 data[index].value(数值跟条走);
    • 柱子横向渐变(#2998f6 → #5baeea → #5EDEE5),showBackground: true 显示底槽,label 显示名称。
  • 适用:企业/区域排行榜 Top N。
完整代码
vue 复制代码
<!-- 横向条形图(排行榜) -->
<template>
  <div :class="className" :style="{ height: height, width: width }" />
</template>

<script>
import * as echarts from "echarts";

export default {
  props: {
    className: {
      type: String,
      default: "chart",
    },
    width: {
      type: String,
      default: "100%",
    },
    zoomNumber: {
      type: Number,
      default: 8,
    },
    zoomLock: {
      type: Boolean,
      default: true,
    },
    tips: {
      type: Boolean,
      default: true,
    },
    fontSize: {
      type: Number,
      default: 14,
    },
    unit: String,
    data: {
      type: Array,
      default() {
        return [
          { name: "item1", value: 23, sum: 50 },
          { name: "item2", value: 44, sum: 60 },
        ];
      },
    },
  },
  data() {
    return {
      chart: null,
      height: null,
    };
  },
  mounted() {},
  beforeDestroy() {
    if (!this.chart) {
      return;
    }
  },
  watch: {
    data: {
      handler() {
        if (this.data.length > 0) {
          let self = this;
          this.height = this.data.length * 50 + "px";
          this.chart = null;
          self.$nextTick(() => {
            self.initChart();
          });
        }
      },
      deep: true,
      immediate: true,
    },
  },
  methods: {
    initChart() {
      let data = this.data;
      this.chart = echarts.init(this.$el);

      // 取出某个字段的数组
      let getValuesByKey = (list, key) => {
        let field = key || "value";
        let result = [];
        if (list) {
          list.forEach((item) => {
            result.push(item[field]);
          });
        }
        return result;
      };

      let option = {
        grid: {
          top: "2%",
          bottom: -15,
          right: 20,
          left: 0,
          containLabel: false,
        },
        xAxis: {
          show: false,
        },
        yAxis: [
          {
            triggerEvent: true,
            show: true,
            inverse: true,
            data: getValuesByKey(data, "name"),
            axisLine: { show: false },
            splitLine: { show: false },
            axisTick: { show: false },
            axisLabel: { show: false },
          },
          {
            triggerEvent: true,
            show: true,
            inverse: true,
            data: getValuesByKey(data, "name"),
            axisLine: { show: false },
            splitLine: { show: false },
            axisTick: { show: false },
            axisLabel: {
              interval: 0,
              shadowOffsetX: "20px",
              color: "#2998f6",
              align: "right",
              verticalAlign: "bottom",
              lineHeight: 30,
              fontSize: 13,
              formatter: function (value, index) {
                return data[index].value;
              },
            },
          },
        ],
        series: [
          {
            name: "条",
            type: "bar",
            showBackground: true,
            barBorderRadius: 30,
            yAxisIndex: 0,
            data: data,
            barWidth: 12,
            itemStyle: {
              normal: {
                color: new echarts.graphic.LinearGradient(
                  0, 0, 1, 0,
                  [
                    { offset: 0, color: "#2998f6" },
                    { offset: 0.7, color: "#5ba8ea" },
                    { offset: 1, color: "#5EDEE5" },
                  ],
                  false
                ),
                barBorderRadius: 10,
              },
              barBorderRadius: 4,
            },
            label: {
              normal: {
                color: "#666",
                show: true,
                position: [0, "-20px"],
                textStyle: {
                  fontSize: 14,
                  fontWeight: "bold",
                },
                formatter: function (item) {
                  return item.name;
                },
              },
            },
          },
        ],
      };
      this.chart.setOption(option);
    },
  },
};
</script>

<style scoped>
</style>

5.3 echart_line.vue ------ 折线图 / 面积图

  • 图表类型 :line(多系列,带渐变面积)。
  • props :height(默认 350px)/ width / id(默认 'ec')/ fontSize / tips / unit / data。
  • data 结构:
js 复制代码
{
  xAxis: ["1月", "2月", "3月"],
  series: [
    { name: "销售额", data: [10, 20, 15], unit: "万元" },
    { name: "利润", data: [3, 5, 4], nosmooth: true } // 加 nosmooth 则该系列不平滑
  ]
}
  • 关键实现 :
    • type: "line",symbolSize: 1;默认 smooth: true(e.nosmooth 时为折线);
    • areaStyle 用颜色列表做垂直渐变,opacity: 0.05;
    • tooltip 自定义 formatter,支持每个系列单独配置单位。
  • 适用:连续时间趋势。
完整代码
vue 复制代码
<template>
	<div class="Echart_line">
		<div class="bg"></div>
		<div class="tb" :id="id" ref="chartRef" :style="{ width: width, height: height }"></div>
	</div>
</template>

<script>
import * as echarts from "echarts";
export default {
	name: "Echart_line",
	props: {
		height: {
			type: String,
			default: "350px",
		},
		width: {
			type: String,
			default: "100%",
		},
		id: {
			type: String,
			default: "ec",
		},
		fontSize: {
			type: Number,
			default: 14,
		},
		tips: {
			type: Boolean,
			default: true,
		},
		unit: {
			type: String,
			default: "",
		},
		data: {
			type: Object,
			default() {
				return {
					xAxis: [],
					series: [],
				};
			},
		},
	},
	data() {
		return {
			myChart: {},
		};
	},
	watch: {
		data() {
			this.init();
		},
	},
	mounted() {
		let self = this;
		self.init();
		window.addEventListener("resize", function () {
			self.myChart.resize();
		});
	},
	methods: {
		init() {
			let self = this;
			let unit = this.unit;
			this.myChart = echarts.init(this.$refs.chartRef);
			let colorList = [
				"#3E7BF4", "#FAAD13", "#FF3F70", "#ffdb5c", "#ff9f7f",
				"#fb7293", "#e7bcf3", "#3CC1F3",
			];
			let series = [];
			this.data.series.forEach((item, index) => {
				series.push({
					name: item.name,
					type: "line",
					data: item.data,
					symbolSize: 1,
					smooth: !item.nosmooth,
					areaStyle: { // 区域填充渐变颜色
						color: {
							type: "linear",
							x: 0,
							y: 0,
							x2: 0,
							y2: 1,
							colorStops: [
								{ offset: 0, color: colorList[index] },
								{ offset: 1, color: "rgba(255, 221, 211, 0)" },
							],
							global: false,
						},
						opacity: 0.05,
					},
				});
			});
			let option = {
				legend: {
					show: true,
					selectedMode: this.tips,
					icon: "roundRect",
					itemWidth: 20,
					itemHeight: 4,
					itemGap: 20,
					top: 0,
					right: 20,
					textStyle: {
						fontSize: this.fontSize,
						color: "#7270A6",
						fontFamily: "OPPOSans-M",
					},
				},
				grid: {
					left: 30,
					right: 30,
					top: 50,
					bottom: 0,
					containLabel: true,
				},
				color: colorList,
				xAxis: [
					{
						type: "category",
						boundaryGap: false,
						data: this.data.xAxis,
						axisLine: { lineStyle: { color: "#EEF0F4" } },
						axisTick: { show: false },
						axisLabel: {
							textStyle: {
								fontSize: this.fontSize,
								color: "#999",
							},
						},
					},
				],
				yAxis: [
					{
						type: "value",
						minInterval: 1,
						name: this.unit,
						nameTextStyle: { color: "#999999" },
						axisTick: { show: false },
						axisLine: { lineStyle: { color: "#EEF0F4" } },
						axisLabel: {
							formatter: "{value}",
							textStyle: {
								fontSize: this.fontSize,
								color: "#999",
							},
						},
						splitLine: { lineStyle: { color: "#EEF0F4" } },
					},
				],
				tooltip: {
					show: this.tips,
					confine: true,
					trigger: "axis",
					formatter: function (params) {
						let relVal = params[0].name;
						let handled = [];
						params.forEach((item) => {
							let unitStr = unit;
							if (handled.includes(item.seriesName)) {
								return;
							}
							handled.push(item.seriesName);
							let matched = self.data.series.filter(
								(p) => p.name === item.seriesName
							);
							if (matched.length > 0 && matched[0].unit) {
								unitStr = matched[0].unit;
							}
							relVal += "<br/>" + item.marker + item.seriesName + "&nbsp;&nbsp;&nbsp;&nbsp;";
							relVal += "<span style='float:right;font-weight: bold'>";
							relVal += item.value + unitStr;
							relVal += "</span>";
						});
						return relVal;
					},
				},
				series: series,
			};
			this.myChart.setOption(option);
		},
	},
};
</script>

<style scoped lang="scss">
</style>

5.4 echart_pieA.vue ------ 饼图 / 玫瑰图

  • 图表类型 :pie,可选玫瑰图(roseType: 'radius',默认开启)。
  • props :id(默认 Ec_时间戳)/ data / radius / radius1 / center / className / styleName / width / height / fontSize / color / tips / unit / formatter / roseType / label / labelColor / labelFormatter / labelLine / toolbox / restore / percent / filter。
  • data 结构:
js 复制代码
[
  { label: "商标", name: "商标", value: 30, id: "1", pid: "0" },
  { label: "发明专利", name: "发明专利", value: 20, id: "2", pid: "0" }
]
  • 关键实现 :
    • 双环效果 :主环(radius)+ 内环占位(radius1,半透明白);
    • getData():filter 过滤 value == 0;按 pid 过滤实现下钻 ;只保留前 7 项,其余合并为「其它」;
    • restore: true 时启用工具箱「重置」按钮 + 点击下钻(pid);
    • percent: true 时追加一层 pie 在扇区内显示百分比;
    • 用 document.getElementById(this.id) 初始化,必须传唯一 id。
  • 适用:占比、构成、可下钻的分类结构。
完整代码
vue 复制代码
<template>
	<div class="Echart_pie" :class="className" :style="styleName">
		<div class="bg" />
		<div class="tb" :id="id" :style="{ width: width, height: height }" />
	</div>
</template>

<script>
import * as echarts from "echarts";
export default {
	name: "EchartPie",
	props: {
		// 唯一标识
		id: { type: String, default: "Ec_" + new Date().getTime() },
		// 数据来源
		data: { type: Array, default: function () { return []; } },
		// 主要展示层
		radius: { type: Array, default: function () { return ["30%", "90%"]; } },
		radius1: { type: Array, default: function () { return ["30%", "60%"]; } },
		center: { type: Array, default: function () { return ["50%", "50%"]; } },
		// 自定义样式
		className: { type: String, default: "" },
		styleName: { type: String, default: "" },
		// 宽高
		width: { type: String, default: "100%" },
		height: { type: String, default: "100%" },
		// 字体大小
		fontSize: { type: Number, default: 14 },
		// 颜色配置
		color: {
			type: Array,
			default: function () {
				return [
					"#4292f5", "#26d6bf", "#e7b814", "#f44e7b",
					"#eb7630", "#5fae4c", "#d747d9", "#c1b80a",
				];
			},
		},
		// 浮框
		tips: { type: Boolean, default: true },
		// 单位
		unit: { type: String, default: "" },
		// 浮框自定义方法
		formatter: { type: Function, default: null },
		// roseType
		roseType: { type: Boolean, default: true },
		// label
		label: { type: Boolean, default: false },
		labelColor: { type: String, default: "inherit" },
		labelFormatter: { type: Function, default: null },
		labelLine: { type: Boolean, default: false },
		// 是否展示可视化的工具箱
		toolbox: { type: Boolean, default: false },
		// 下钻事件
		restore: { type: Boolean, default: false },
		// 百分比图层
		percent: { type: Boolean, default: false },
		// 过滤空值
		filter: { type: Boolean, default: true },
	},
	data() {
		return {
			myChart: {},
			pid: "0",
		};
	},
	watch: {
		data() {
			this.init(this.getData());
		},
		pid() {
			this.init(this.getData());
			window.eventBus.$emit("EchartPie1", this.pid);
		},
	},
	mounted() {
		let self = this;
		self.init(self.getData());
		window.addEventListener("resize", function () {
			self.myChart.resize();
		});
		if (this.restore) {
			this.myChart.on("click", function (params) {
				if (params.data.id && params.data.pid === "0") {
					self.pid = params.data.id;
				}
			});
		}
	},
	methods: {
		getData() {
			let pid = this.pid;
			let data = this.filter
				? this.data.filter((item) => item.value != 0)
				: this.data;
			if (pid != null) {
				data = data.filter(
					(item) => !item.pid || String(item.pid) === String(pid)
				);
			}

			let result = [];
			let other = { name: "其它", value: 0 };
			data.forEach((item, index) => {
				if (index < 7) {
					result.push({ name: item.label, value: item.value });
				} else {
					other.value += item.value;
				}
			});
			if (other.value > 0) {
				result.push(other);
			}
			return result;
		},
		init(data) {
			let self = this;
			this.myChart = echarts.init(document.getElementById(this.id));
			let colorList = self.color;
			let option = {
				legend: {
					selectedMode: this.tips,
					itemWidth: 12,
					itemHeight: 12,
					itemGap: 13,
					y: "center",
					top: "2%",
					textStyle: {
						fontSize: this.fontSize,
						color: "#8699AD",
					},
				},
				grid: {
					left: 0,
					right: "0%",
				},
				toolbox: { // 可视化的工具箱
					show: self.toolbox,
					feature: {
						myTool1: {
							show: self.restore,
							title: "重置",
							icon: "path://M969.916221 501.771031c-11.116175 0-20.128433 9.012258-20.128433 20.128433 0 57.860802-11.328 113.980961-33.668821 166.799934-21.583574 51.027164-52.485375 96.860025-91.848894 136.222521-39.362496 39.36352-85.195358 70.26532-136.222521 91.848894-52.819996 22.340821-108.940155 33.668821-166.800957 33.668821s-113.979938-11.328-166.79891-33.668821c-51.027164-21.583574-96.859002-52.485375-136.221498-91.847871-39.362496-39.36352-70.264297-85.195358-91.846848-136.223544-22.340821-52.818973-33.667797-108.939131-33.667797-166.799934s11.328-113.980961 33.667797-166.799934c21.582551-51.027164 52.485375-96.859002 91.846848-136.221498 39.362496-39.362496 85.194334-70.264297 136.221498-91.846848 52.818973-22.340821 108.939131-33.667797 166.79891-33.667797 88.588641 0 174.554548 27.421536 246.359944 77.895091L662.939711 188.895161c-10.961656 1.847068-18.351973 12.231579-16.503882 23.193235 1.655709 9.830903 10.17678 16.787338 19.824511 16.787338 1.110287 0 2.237971-0.092098 3.368724-0.283456l151.254889-25.487487c10.961656-1.847068 18.351973-12.230556 16.503882-23.193235L813.903981 40.545438c-1.847068-10.961656-12.229532-18.349927-23.193235-16.503882-10.961656 1.847068-18.351973 12.230556-16.503882 23.193235l15.182794 90.107227C711.11185 82.737376 617.58985 53.105498 521.246595 53.105498c-63.277162 0-124.673487 12.397355-182.482101 36.848233-55.82647 23.612791-105.95824 57.411571-149.005662 100.457969-43.046399 43.046399-76.845179 93.179193-100.457969 149.004639C64.851007 397.224953 52.453652 458.621279 52.453652 521.898441c0 63.277162 12.397355 124.673487 36.848233 182.483124 23.612791 55.82647 57.411571 105.959264 100.457969 149.006685 43.046399 43.046399 93.179193 76.846202 149.005662 100.458993 57.808614 24.451902 119.203916 36.849256 182.482101 36.849256 63.278185 0 124.674511-12.397355 182.483124-36.849256 55.82647-23.612791 105.959264-57.411571 149.006685-100.458993 43.046399-43.047422 76.846202-93.181239 100.458993-149.006685 24.451902-57.808614 36.849256-119.204939 36.849256-182.483124C990.044654 510.782266 981.032396 501.771031 969.916221 501.771031z",
							onclick: () => {
								self.pid = 0;
							},
						},
					},
				},
				tooltip: {
					show: this.tips,
					confine: true,
					trigger: "item",
					formatter: function (params) {
						if (self.formatter) {
							return self.formatter(params);
						} else {
							return `${params.marker}${params.name}:${params.data.value}` + (self.unit || "");
						}
					},
				},
				series: [ // 主要展示层
					{
						radius: self.radius,
						center: self.center,
						type: "pie",
						roseType: self.roseType ? "radius" : "",
						itemStyle: {
							normal: {
								color: function (params) {
									return colorList[params.dataIndex];
								},
							},
						},
						labelLine: {
							normal: {
								show: self.labelLine,
								lineStyle: { color: "#E2E2E2" },
							},
						},
						label: {
							normal: {
								show: self.label,
								fontSize: self.fontSize,
								color: self.labelColor,
								formatter: self.labelFormatter
									? (params) => self.labelFormatter(params)
									: "{b}\n{c|{d}}%",
								lineHeight: 20,
								rich: {
									c: {
										fontWeight: "bold",
										fontSize: self.fontSize,
									},
								},
							},
						},
						data: data,
					},
					{
						radius: self.radius1,
						center: self.center,
						type: "pie",
						roseType: self.roseType ? "radius" : "",
						itemStyle: {
							normal: {
								color: "rgba(255,255,255,0.3)",
							},
						},
						labelLine: {
							normal: { show: false },
						},
						label: {
							normal: { show: false },
						},
						data: [1],
						tooltip: { show: false },
					},
				],
			};
			if (self.percent) {
				option.series.push({
					radius: self.radius,
					center: self.center,
					type: "pie",
					itemStyle: {
						normal: {
							color: "rgba(0,0,0,0)",
							borderWidth: 2,
							borderColor: "#fff",
						},
					},
					labelLine: {
						normal: {
							show: false,
							lineStyle: { color: "#E2E2E2" },
						},
					},
					label: {
						normal: {
							show: true,
							position: "inside",
							formatter: function (params) {
								let percent = params.percent ? params.percent.toFixed(2) : 0;
								return parseFloat(percent) + "%";
							},
							textStyle: {
								color: "#fff",
								fontSize: self.fontSize,
							},
						},
					},
					data: data,
				});
			}
			this.myChart.setOption(option);
		},
	},
};
</script>

<style scoped lang="scss">
</style>

5.5 echart_radar.vue ------ 雷达图

  • 图表类型 :radar。
  • props :height(默认 450px)/ width / data。
  • data 结构:
js 复制代码
{
  nameList: [{ name: "研发", max: 100 }, { name: "生产", max: 100 }],
  series: [
    { name: "本企业", data: [80, 60] },
    { name: "行业均值", data: [70, 70] }
  ]
}
  • 关键实现 :indicator 由 nameList 生成(max 默认 100);每个系列一套颜色 + areaStyle 面积。
  • 适用:多维能力对比(如企业创新能力画像)。
完整代码
vue 复制代码
<template>
	<div class="Echart_radar">
		<div class="bg" :style="{ width: width, height: height }"></div>
		<div class="tb" ref="chartRef" :style="{ width: width, height: height }"></div>
	</div>
</template>

<script>
import * as echarts from "echarts";
export default {
	name: "Echart_radar",
	props: {
		height: {
			type: String,
			default: "450px",
		},
		width: {
			type: String,
			default: "100%",
		},
		data: {
			type: Object,
			default() {
				return {
					nameList: [],
					series: [],
				};
			},
		},
	},
	data() {
		return {
			myChart: {},
			color: [
				{
					color: "rgba(57, 126, 240, 1)",
					areaColor: "rgba(170, 217, 255, 0.35)",
				},
				{
					color: "rgb(240,167,57)",
					areaColor: "rgba(255,180,170,0.35)",
				},
			],
		};
	},
	watch: {
		data() {
			this.init();
		},
	},
	mounted() {
		let self = this;
		self.init();
		window.addEventListener("resize", function () {
			self.myChart.resize();
		});
	},
	methods: {
		// 计算数组平均值
		average(list) {
			let sum = list.reduce(function (prev, curr) {
				return prev + curr;
			});
			return (sum / list.length).toFixed(1);
		},
		init() {
			this.myChart = echarts.init(this.$refs.chartRef);

			let indicator = [];
			this.data.nameList.forEach((item) => {
				indicator.push({
					name: item.name ? item.name : item,
					max: item.max ? item.max : 100,
				});
			});
			let series = [];
			this.data.series.forEach((item, index) => {
				series.push({
					name: item.name,
					type: "radar",
					color: this.color[index].color,
					label: { show: false },
					areaStyle: { color: this.color[index].areaColor },
					data: [{ value: item.data }],
				});
			});
			let option = {
				tooltip: {},
				radar: {
					name: {
						formatter: "{value}",
						textStyle: {
							color: "#4D5A69",
							fontSize: 14,
						},
					},
					axisLine: {
						lineStyle: { color: "#F0F0F0" },
					},
					splitLine: {
						lineStyle: { color: "#F0F0F0" },
					},
					splitArea: {
						areaStyle: {
							color: ["#fff", "transparent"],
							opacity: 1,
						},
					},
					indicator: indicator,
					center: ["50%", "50%"],
					radius: "70%",
				},
				series: series,
			};
			this.myChart.setOption(option);
		},
	},
};
</script>

<style scoped lang="less">
.Echart_radar {
	position: relative;
}
.bg {
	display: flex;
	justify-content: center;
	align-items: center;
	height: 100%;
	position: absolute;
}
</style>

5.6 echart_bar_line.vue ------ 柱 + 折线(双轴,带轮播)

  • 图表类型 :bar + line(折线用 yAxisIndex: 1 走右轴)。
  • props :height(默认 200px)/ dataA / dataZoom(默认 5)。
  • data 结构:
js 复制代码
{
  id: "EC-1",                 // 用于 getElementById 初始化,需唯一
  colors: ["#a8d3ff", "#70b0ff", "#bae4fb", "#2fd1d3"],
  dataname: ["数量", "增长率"],
  data: [220, 300, 500, 400, 300],   // 柱
  data1: [80, 70, 66, 55, 90],       // 折线(右轴,%)
  xAxis: [2017, 2018, 2019, 2020, 2021],
  unit: "户"
}
  • 关键实现 :固定两根系列(柱 + 线);dataZoom 隐藏滑动条,setInterval 每 2 秒自动向后滚动一位 ;beforeDestroy 清理定时器。
  • 适用:总量 + 增长率/占比的组合展示。
完整代码
vue 复制代码
<template>
	<div class="Echart_bar" :style="{ height: height }">
		<div class="tb" :id="dataA.id" :style="{ width: '100%', height: '100%' }"></div>
	</div>
</template>

<script>
import * as echarts from "echarts";

export default {
	name: "Echart_bar",
	props: {
		height: { type: String, default: "200px" },
		dataA: {
			id: "EC",
			colors: ["#a8d3ff", "#70b0ff", "#bae4fb", "#2fd1d3"],
			dataname: ["name1", "name2"],
			data: [220, 300, 500, 400, 300],   // 柱
			data1: [80, 70, 66, 55, 90],       // 折线(右轴)
			xAxis: [2017, 2018, 2019, 2020, 2021],
			unit: "户",
		},
		dataZoom: { type: Number, default: 5 },
	},
	data() {
		return { myChart: {}, timer: null };
	},
	mounted() {
		this.myChart = echarts.init(document.getElementById(this.dataA.id));
		this.flashData();
	},
	beforeDestroy() {
		if (this.timer) clearInterval(this.timer);
		this.myChart.dispose();
	},
	watch: {
		data() {
			this.flashData();
		},
		dataA: {
			deep: true,
			handler() {
				this.flashData();
			},
		},
	},
	methods: {
		flashData() {
			const unit = this.dataA.unit;
			const option = {
				textStyle: { color: "#c0c3cd", fontSize: 14 },
				tooltip: {
					trigger: "axis",
					axisPointer: { lineStyle: { color: "#dadada" } },
					formatter: function (params) {
						let result = params[0].name + "<br>";
						params.forEach(function (item) {
							if (item.componentSubType === "line") {
								result += item.marker + item.seriesName + ":" + item.value + "%<br>";
							} else {
								result += item.marker + item.seriesName + ":" + item.value + unit + "<br>";
							}
						});
						return result;
					},
				},
				toolbox: {
					show: false,
					feature: {
						saveAsImage: { backgroundColor: "#031245" },
						restore: {},
					},
					iconStyle: { borderColor: this.dataA.colors[1] },
				},
				legend: {
					bottom: 0,
					itemWidth: 20,
					itemHeight: 6,
					left: "center",
					padding: 0,
					textStyle: { color: "#858dc7", fontSize: 14 },
					data: this.dataA.dataname,
				},
				grid: {
					containLabel: true,
					left: 0,
					right: 0,
					bottom: 20,
					top: 10,
				},
				xAxis: {
					type: "category",
					data: this.dataA.xAxis,
					boundaryGap: [10, 10],
					axisTick: { show: false },
					splitLine: { show: false },
					axisLine: { show: true, lineStyle: { color: "#e7e7f5", width: 1 } },
					axisLabel: { color: "#8699ad", fontSize: 14, interval: 0 },
				},
				yAxis: [
					{
						axisTick: { show: false },
						axisLine: { show: true, lineStyle: { color: "#e7e7f5", width: 1 } },
						axisLabel: { color: "#8699ad", fontSize: 14 },
						splitLine: { show: true, lineStyle: { color: "#e7e7f5", type: "dashed" } },
					},
					{
						position: "right",
						axisTick: { show: false },
						axisLine: { show: true, lineStyle: { color: "#e7e7f5", width: 1 } },
						axisLabel: { color: "#8699ad", fontSize: 14, formatter: "{value}%" },
						splitLine: { show: false },
					},
				],
				dataZoom: [
					{
						xAxisIndex: 0,
						show: false,
						type: "slider",
						startValue: 0,
						endValue: this.dataZoom,
					},
				],
				series: [
					{
						name: this.dataA.dataname[0],
						data: this.dataA.data,
						type: "bar",
						barMaxWidth: "auto",
						barWidth: "40%",
						itemStyle: {
							color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
								{ offset: 1, color: this.dataA.colors[0] },
								{ offset: 0, color: this.dataA.colors[1] },
							]),
							shadowBlur: 1,
							shadowColor: this.dataA.colors[2],
							shadowOffsetX: 2,
							shadowOffsetY: -2,
						},
						label: { show: false },
					},
					{
						name: this.dataA.dataname[1],
						yAxisIndex: 1,
						type: "line",
						smooth: false,
						data: this.dataA.data1,
						symbol: "circle",
						symbolSize: 8,
						itemStyle: {
							normal: {
								color: this.dataA.colors[3],
								borderColor: this.dataA.colors[3],
								lineStyle: { color: this.dataA.colors[3] },
							},
						},
					},
				],
				animation: true,
				animationDuration: 500,
			};
			this.myChart.setOption(option);

			// 数据多于一屏时,dataZoom 自动轮播
			const total = this.dataA.xAxis.length;
			const scrollSize = this.dataZoom;
			if (total > scrollSize) {
				if (this.timer) clearInterval(this.timer);
				this.timer = setInterval(() => {
					if (option.dataZoom[0].endValue === total - 1) {
						option.dataZoom[0].endValue = scrollSize;
						option.dataZoom[0].startValue = 0;
					} else {
						option.dataZoom[0].endValue += 1;
						option.dataZoom[0].startValue += 1;
					}
					this.myChart.setOption(option);
				}, 2000);
			}

			this.myChart.resize();
		},
	},
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="less">
.Echart_bar {
	text-align: left;
}
</style>

5.7 echart_barLine_compare.vue(对比分析)与 echart_barLine_group.vue(分组分析)

  • 图表类型 :bar + line 双轴组合,系列类型由数据决定。
  • props :className / width / height / dataZoom / dataZoomNumber / legend / grid / data。
  • data 结构:
js 复制代码
{
  xAxis: ["区域一", "区域二"],
  yAxis: { name: "单位:次", name2: "增长率:%" },
  barWidth: "18%",
  barGap: "60%",
  series: [
    { name: "运行中", type: "bar",  data: [250, 150], color: ["#0cbaec", "#037ede"] },
    { name: "增速",   type: "line", data: [12, 8],    color: ["#8b74ff"] }
  ]
}
  • 两者差异:
组件 折线样式 自动轮播 / 高亮
echart_barLine_compare.vue 平滑折线 + 面积渐变填充 有:每 3 秒滚动 dataZoom 并 dispatchAction(showTip) 高亮
echart_barLine_group.vue 折线(圆点 symbolSize: 8)+ 无面积(areaStyle 被注释) 轮播代码被注释,当前为静态
  • 适用:多区域对比、分组趋势分析。
完整代码:echart_barLine_compare.vue(对比分析,含自动轮播 + 高亮)
vue 复制代码
<template>
	<div :class="className" :style="{ height: height, width: width }"/>
</template>

<script>
import * as echarts from "echarts";

export default {
	props: {
		className: { type: String, default: "chart" },
		width: { type: String, default: "100%" },
		height: { type: String, default: "250px" },
		dataZoom: { type: Boolean, default: false },
		dataZoomNumber: { type: Number, default: 99 },
		legend: {
			type: Object,
			default() {
				return { show: false, left: "center", bottom: "0" };
			},
		},
		grid: {
			type: Object,
			default() {
				return { top: "13%", bottom: "0", left: "5%", right: "6%" };
			},
		},
		data: {
			type: Object,
			default() {
				return {
					xAxis: ["区域一", "区域二", "区域三", "区域四"],
					yAxis: { name: "单位:次", name2: "增长率:%" },
					barWidth: "18%",
					barGap: "60%",
					series: [
						{ name: "运行中", data: [250, 150, 170, 150], color: ["#0cbaec", "#037ede", "#8b74ff"] },
						{ name: "维护中", data: [300, 210, 220, 210], color: ["#f0d561", "#f7bc33", "#ffa301"] },
						{ name: "异常", data: [250, 240, 190, 260], color: ["#feb7d4", "#fa2961", "#fa2961"] },
					],
				};
			},
		},
	},
	data() {
		return { chart: null, timer: null };
	},
	mounted() {
		this.$nextTick(() => {
			setTimeout(() => {
				this.initChart();
			}, 1000);
		});
	},
	beforeDestroy() {
		if (this.timer) clearInterval(this.timer);
		if (this.chart) this.chart.dispose();
	},
	watch: {
		data: {
			deep: true,
			handler() {
				this.chart.dispose();
				setTimeout(() => {
					this.initChart();
				}, 1000);
			},
		},
	},
	methods: {
		initChart() {
			this.chart = echarts.init(this.$el);

			let legendArray = [];
			this.data.series.forEach((item) => legendArray.push(item.name));

			let series = [];
			this.data.series.forEach((item) => {
				if (item.type === "bar") {
					series.push({
						name: item.name,
						type: "bar",
						barWidth: this.data.barWidth,
						barGap: this.data.barGap,
						data: item.data,
						itemStyle: {
							normal: {
								shadowBlur: 0,
								shadowColor: item.color[0],
								shadowOffsetX: 0,
								shadowOffsetY: 0,
								color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
									{ offset: 0, color: item.color[0] },
									{ offset: 1, color: item.color[1] },
								], false),
							},
						},
					});
				} else if (item.type === "line") {
					series.push({
						name: item.name,
						type: "line",
						smooth: true,
						areaStyle: {
							color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
								{ offset: 0, color: item.color[0] },
								{ offset: 1, color: "rgba(98, 227, 255, 0)" },
							]),
						},
						data: item.data,
						symbol: "circle",
						yAxisIndex: 1,
						symbolSize: 0,
						itemStyle: {
							normal: {
								color: item.color[0],
								borderColor: item.color[0],
								lineStyle: { color: item.color[0] },
							},
						},
					});
				}
			});

			let option = {
				legend: {
					show: this.legend.show,
					icon: "rect",
					itemGap: 13,
					data: legendArray,
					left: this.legend.left,
					bottom: this.legend.bottom,
					itemWidth: 6,
					itemHeight: 20,
				},
				tooltip: {
					trigger: "axis",
					axisPointer: { type: "shadow" },
					textStyle: { color: "#222" },
					backgroundColor: "rgba(255,255,255,0.9)",
					borderColor: "#e2e7ec",
					borderWidth: 1,
					extraCssText: "z-index: 2",
				},
				grid: {
					top: this.grid.top,
					bottom: this.grid.bottom,
					left: this.grid.left,
					right: this.grid.right,
					containLabel: true,
				},
				xAxis: [
					{
						type: "category",
						data: this.data.xAxis,
						axisTick: { show: false },
						axisLabel: {
							interval: 0,
							textStyle: { color: "#000", fontSize: 12 },
						},
						axisLine: { lineStyle: { color: "#EEF0F4" } },
					},
				],
				yAxis: [
					{
						name: this.data.yAxis.name,
						type: "value",
						nameTextStyle: { color: "#000", fontSize: 12 },
						axisLabel: { show: true, textStyle: { color: "#000", fontSize: 12 } },
						axisLine: { lineStyle: { color: "#EEF0F4" } },
						axisTick: { show: false },
						splitLine: { lineStyle: { color: "#EEF0F4", type: "solid" } },
					},
					{
						name: this.data.yAxis.name2,
						type: "value",
						nameTextStyle: { color: "#000", fontSize: 12 },
						axisLabel: { show: true, textStyle: { color: "#000", fontSize: 12 } },
						axisLine: { show: false },
						axisTick: { show: false },
						splitLine: { show: false },
					},
				],
				dataZoom: [
					{
						show: this.dataZoom,
						height: 12,
						xAxisIndex: [0],
						bottom: "8%",
						startValue: 0,
						endValue: this.dataZoomNumber,
						handleSize: "110%",
						handleStyle: { color: "#d3dee5" },
						textStyle: { color: "#444" },
						borderColor: "#90979c",
					},
					{
						type: "inside",
						show: true,
						height: 15,
						start: 1,
						end: 35,
					},
				],
				series: series,
			};
			this.chart.setOption(option);

			// 每 3 秒自动滚动 dataZoom,并高亮提示
			const self = this;
			let playIndex = 0;
			clearInterval(this.timer);
			this.timer = setInterval(function () {
				if (playIndex === 0) {
					option.dataZoom[0].startValue = 0;
					option.dataZoom[0].endValue = self.dataZoomNumber;
					self.chart.setOption(option);
				}
				if (playIndex >= self.dataZoomNumber + 1) {
					option.dataZoom[0].startValue += 1;
					option.dataZoom[0].endValue += 1;
					self.chart.setOption(option);
				}
				self.chart.dispatchAction({
					type: "showTip",
					seriesIndex: 0,
					dataIndex: playIndex,
				});
				playIndex++;
				if (playIndex >= self.data.xAxis.length) {
					playIndex = 0;
				}
			}, 3000);

			window.addEventListener("resize", () => {
				this.chart.resize();
			});
		},
	},
};
</script>
完整代码:echart_barLine_group.vue(分组分析,折线为圆点)
vue 复制代码
<template>
	<div :class="className" :style="{ height: height, width: width }"/>
</template>

<script>
import * as echarts from "echarts";

export default {
	props: {
		className: { type: String, default: "chart" },
		width: { type: String, default: "100%" },
		height: { type: String, default: "250px" },
		dataZoom: { type: Boolean, default: false },
		dataZoomNumber: { type: Number, default: 99 },
		legend: {
			type: Object,
			default() {
				return { show: false, left: "center", bottom: "0" };
			},
		},
		grid: {
			type: Object,
			default() {
				return { top: "13%", bottom: "0", left: "5%", right: "6%" };
			},
		},
		data: {
			type: Object,
			default() {
				return {
					xAxis: ["区域一", "区域二", "区域三", "区域四"],
					yAxis: { name: "单位:次", name2: "增长率:%" },
					barWidth: "18%",
					barGap: "60%",
					series: [
						{ name: "运行中", data: [250, 150, 170, 150], color: ["#0cbaec", "#037ede", "#8b74ff"] },
						{ name: "维护中", data: [300, 210, 220, 210], color: ["#f0d561", "#f7bc33", "#ffa301"] },
						{ name: "异常", data: [250, 240, 190, 260], color: ["#feb7d4", "#fa2961", "#fa2961"] },
					],
				};
			},
		},
	},
	data() {
		return { chart: null };
	},
	mounted() {
		this.$nextTick(() => {
			setTimeout(() => {
				this.initChart();
			}, 1000);
		});
	},
	beforeDestroy() {
		if (this.chart) this.chart.dispose();
	},
	watch: {
		data: {
			deep: true,
			handler() {
				this.chart.dispose();
				setTimeout(() => {
					this.initChart();
				}, 1000);
			},
		},
	},
	methods: {
		initChart() {
			this.chart = echarts.init(this.$el);

			let legendArray = [];
			this.data.series.forEach((item) => legendArray.push(item.name));

			let series = [];
			this.data.series.forEach((item) => {
				if (item.type === "bar") {
					series.push({
						name: item.name,
						type: "bar",
						barWidth: this.data.barWidth,
						barGap: this.data.barGap,
						data: item.data,
						itemStyle: {
							normal: {
								shadowBlur: 0,
								shadowColor: item.color[0],
								shadowOffsetX: 0,
								shadowOffsetY: 0,
								color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
									{ offset: 0, color: item.color[0] },
									{ offset: 1, color: item.color[1] },
								], false),
							},
						},
					});
				} else if (item.type === "line") {
					series.push({
						name: item.name,
						type: "line",
						smooth: false,
						data: item.data,
						symbol: "circle",
						yAxisIndex: 1,
						symbolSize: 8,
						itemStyle: {
							normal: {
								borderWidth: 3,
								color: "#fff",
								borderColor: item.color[0],
								lineStyle: { color: item.color[0] },
							},
						},
					});
				}
			});

			let option = {
				legend: {
					show: this.legend.show,
					icon: "rect",
					itemGap: 13,
					data: legendArray,
					left: this.legend.left,
					bottom: this.legend.bottom,
					itemWidth: 6,
					itemHeight: 20,
				},
				tooltip: {
					trigger: "axis",
					axisPointer: { type: "shadow" },
					textStyle: { color: "#222" },
					backgroundColor: "rgba(255,255,255,0.9)",
					borderColor: "#e2e7ec",
					borderWidth: 1,
					extraCssText: "z-index: 2",
				},
				grid: {
					top: this.grid.top,
					bottom: this.grid.bottom,
					left: this.grid.left,
					right: this.grid.right,
					containLabel: true,
				},
				xAxis: [
					{
						type: "category",
						data: this.data.xAxis,
						axisTick: { show: false },
						axisLabel: {
							interval: 0,
							textStyle: { color: "#000", fontSize: 12 },
						},
						axisLine: { lineStyle: { color: "#EEF0F4" } },
					},
				],
				yAxis: [
					{
						name: this.data.yAxis.name,
						type: "value",
						nameTextStyle: { color: "#000", fontSize: 12 },
						axisLabel: { show: true, textStyle: { color: "#000", fontSize: 12 } },
						axisLine: { lineStyle: { color: "#EEF0F4" } },
						axisTick: { show: false },
						splitLine: { lineStyle: { color: "#EEF0F4", type: "solid" } },
					},
					{
						name: this.data.yAxis.name2,
						type: "value",
						nameTextStyle: { color: "#000", fontSize: 12 },
						axisLabel: { show: true, textStyle: { color: "#000", fontSize: 12 } },
						axisLine: { show: false },
						axisTick: { show: false },
						splitLine: { show: false },
					},
				],
				dataZoom: [
					{
						show: this.dataZoom,
						height: 12,
						xAxisIndex: [0],
						bottom: "8%",
						startValue: 0,
						endValue: this.dataZoomNumber,
						handleSize: "110%",
						handleStyle: { color: "#d3dee5" },
						textStyle: { color: "#444" },
						borderColor: "#90979c",
					},
					{
						type: "inside",
						show: true,
						height: 15,
						start: 1,
						end: 35,
					},
				],
				series: series,
			};
			this.chart.setOption(option);

			// 说明:与 compare 版不同,本组件未启用 dataZoom 自动轮播
			window.addEventListener("resize", () => {
				this.chart.resize();
			});
		},
	},
};
</script>

5.8 echart_barMore.vue ------ 多系列柱(可堆叠)+ 多折线

  • 图表类型 :bar(可 stack 堆叠)+ line(右轴)灵活组合。
  • props :height / leP / dataZoom / barWidth / barGap / dataA / colorGroub。
  • data 结构:
js 复制代码
{
  id: "EC-more",              // 需唯一
  inverse: false,
  dataName: [["系列1", "系列2"]],
  dataZoom: 10,
  xAxis: [2017, 2018, 2019, 2020, 2021],
  unit: "户",
  dataBar: [                  // 柱:可多个
    { colors: ["#a8d3ff", "#70b0ff", "#bae4fb"], dataname: "数量",
      data: [220, 300, 500], dataPer: [22, 30, 50], stack: null, labelShow: true }
  ],
  dataLine: [                 // 折线:可多个
    { colors: "#a8d3ff", dataname: "增速", data: [12, 8, 6], dataPer: [] }
  ]
}
  • 关键实现 :
    • dataBar → bar(渐变、支持 stack 堆叠、labelShow 显示 dataPer 占比);
    • dataLine → line(yAxisIndex: 1);
    • 无数据时 用 showNone 覆盖「暂无数据」占位图;
    • mouseover / mouseout 通过 $emit('show-ser', { series_i }) 向外部广播鼠标所在项,便于与页面其他区域联动。
  • 适用:复杂的多维度指标组合面板。

5.9 echart_mapC.vue ------ 地图(中国地图)

  • 图表类型 :map(行政区着色,geo + series.type='map')。
  • props :height(默认 815)/ width / id / fontSize / title / color / tips / data。
  • data 结构:
js 复制代码
[
  { name: "广东省", value: 20 },
  { name: "北京市", value: 40 }
]
  • 关键实现 :
    • echarts.registerMap('china', chinaJson) 注册地图(需自行准备中国地图的 GeoJSON);
    • visualMap 连续型(show: false)做数值→颜色映射;
    • series.map 区域渐变填充,emphasis 高亮(黄色)。
  • 适用:按省份/区域展示分布(choropleth)。
完整代码
vue 复制代码
<template>
	<div class="Echart_map">
		<div class="tb" ref="chartRef" :style="{ width: width, height: height + 'px' }"></div>
	</div>
</template>

<script>
import * as echarts from "echarts";
import china from "@/assets/js/china.json";

export default {
	name: "Echart_map",
	props: {
		height: { type: Number, default: 815 },
		width: { type: String, default: "100%" },
		id: { type: String, default: "ec" },
		fontSize: { type: Number, default: 14 },
		title: { type: String, default: "title" },
		color: {
			type: Array,
			default() {
				return ["#1EB96B", "#FE8441", "#FD5439"];
			},
		},
		tips: { type: Boolean, default: true },
		data: {
			type: Array,
			default() {
				return [
					{ name: "广东省", value: 20 },
					{ name: "北京市", value: 40 },
					{ name: "重庆市", value: 100 },
					{ name: "上海市", value: 210 },
				];
			},
		},
	},
	data() {
		return { myChart: {} };
	},
	watch: {
		data: {
			handler() {
				let self = this;
				this.$nextTick(() => {
					self.init();
					self.myChart.resize();
				});
			},
			immediate: true,
			deep: true,
		},
	},
	mounted() {
		this.init();
		let self = this;
		window.addEventListener("resize", function () {
			self.myChart.resize();
		});
	},
	methods: {
		init() {
			this.myChart = echarts.init(this.$refs.chartRef);

			let mapJson = china;
			let legendData = [];
			let series = [
				{
					type: "map",
					zlevel: 20,
					map: "china",
					roam: false,
					zoom: 1,
					showLegendSymbol: false,
					label: {
						show: true,
						color: "#FFF",
					},
					itemStyle: {
						normal: {
							areaColor: {
								type: "linear",
								x: 0, y: 0, x2: 0, y2: 1600,
								colorStops: [
									{ offset: 0, color: "#A0D6FE" },
									{ offset: 1, color: "#A0D6FE" },
								],
								global: true,
							},
							borderColor: "#fff",
							borderWidth: 2,
						},
						emphasis: {
							areaColor: "#F6E232",
							label: { show: true },
						},
					},
					data: this.data,
				},
			];

			echarts.registerMap("china", mapJson);

			let option = {
				tooltip: { show: false },
				visualMap: {
					show: false,
					type: "continuous",
					showLabel: true,
					left: "20",
					min: 0,
					max: 10,
					inRange: {
						color: ["#91D0FF", "#0375FF"],
					},
				},
				legend: {
					show: true,
					orient: "vertical",
					icon: "roundRect",
					itemWidth: 16,
					itemHeight: 16,
					itemGap: 20,
					bottom: "0%",
					left: "0%",
					textStyle: {
						fontSize: 14,
						color: "#2F3757",
						fontFamily: "OPPOSans-M",
					},
					data: legendData,
				},
				geo: {
					show: true,
					map: "china",
					selectedMode: "single",
					zoom: mapJson.scale || 1,
					roam: false,
					label: {
						normal: { show: false, color: "#FFF" },
						emphasis: { show: false },
					},
					itemStyle: {
						normal: {
							areaColor: {
								type: "linear",
								x: 0, y: 0, x2: 0, y2: 1,
								colorStops: [
									{ offset: 0, color: "#9ED5FF" },
									{ offset: 1, color: "#3a95fd" },
								],
								global: true,
							},
							borderColor: "#fff",
							borderWidth: 0.2,
						},
					},
				},
				series: series,
			};
			this.myChart.setOption(option);
		},
	},
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="less">
.bg {
	position: absolute;
	background: #F9FAFE;
	border-radius: 4px;
	transform: translateX(-100%);
}
</style>

六、使用示例:innovation.vue(创新能力)

innovation.vue(创新能力)是一个完整的「页面容器 + 图表组件」示例,展示两个不同类型图表(柱状图 + 饼图)的接入方式。

6.1 引入并注册组件(局部注册)

js 复制代码
import echart_barA from "@/components/echart_barA.vue";
import echart_pieA from "@/components/echart_pieA.vue";

export default {
  name: "innovation",
  props: ["id", "data"],
  components: {
    echart_barA, echart_pieA
  },
  // ...
};

6.2 模板中使用(传 data / 单位 / 颜色 / 尺寸)

html 复制代码
<a-row :gutter="24">
  <a-col :span="12">
    <h2 class="titB"><b>知识产权授权分布</b></h2>
    <a-empty v-if="!echartBarData.timer"/>
    <echart_barA v-else :key="echartBarData.timer" :data="echartBarData" :colorList="colorList" unit="件"/>
  </a-col>
  <a-col :span="12">
    <h2 class="titB"><b>知识产权分布</b></h2>
    <a-empty v-if="echartPieData.length == 0"/>
    <div v-else>
      <echart_pieA :data="echartPieData" :labelLine="true" :roseType="false" unit="件"
                   :radius='["35%", "55%"]' :radius1='["35%", "45%"]' :center='["50%", "55%"]'
                   :height="'350px'" :label="true" :color="colorList.map(item => item[0])" />
    </div>
  </a-col>
</a-row>

要点:

  • 柱状图 用 :key="echartBarData.timer":数据更新时 timer 变化 → 组件整体重建,避免旧实例残留;
  • 饼图 传入 colorList.map(item => item[0]),把共享色板转成饼图需要的颜色数组;
  • 均用 <a-empty> 做无数据兜底。

6.3 获取数据并组装(后端返回 → 组件结构)

图表数据来自后端接口 。为便于文档自包含,下面用一份手动模拟的数据 代替真实接口返回;实际项目中,把 mockData 换成你的请求结果即可(例如 getApi(params).then(res => this.buildCharts(res.data)))。

js 复制代码
// 模拟后端返回的数据(真实项目里由接口返回)
const mockData = [
  { year: 2022, type: "trademark",        value: 10 },
  { year: 2022, type: "inventionPatent",  value: 5 },
  { year: 2023, type: "trademark",        value: 15 },
  { year: 2023, type: "inventionPatent",  value: 8 },
  { year: 2024, type: "trademark",        value: 20 },
  { year: 2024, type: "inventionPatent",  value: 12 },
];

export default {
  name: "innovation",
  props: ["id", "data"],
  data() {
    return {
      currentYear: 2024,   // 当前年份
      yearCount: 2,        // 展示近 3 年
      echartBarData: { timer: null, xAxis: [], series: [] },
      echartPieData: [],
    };
  },
  mounted() {
    this.loadData();
  },
  methods: {
    // 真实项目中:调用接口拿到数据后交给 buildCharts 组装
    loadData() {
      // 例如:getApi({ id: this.id, yearCount: this.yearCount }).then(res => this.buildCharts(res.data));
      this.buildCharts(mockData);
    },
    // 把后端原始数组整理成图表组件需要的结构
    buildCharts(list) {
      const { currentYear, yearCount } = this;
      const xAxis = [];
      const echartPieData = [];
      const series = [
        { key: "trademark",       name: "商标",     data: [], unit: "件" },
        { key: "inventionPatent", name: "发明专利", data: [], unit: "件" },
        // ...其余类型
      ];
      for (let y = currentYear - yearCount; y <= currentYear; y++) {
        xAxis.push(y + "年");
        const yearRows = list.filter(p => p.year === y);
        series.forEach(item => {
          const hit = yearRows.find(p => p.type === item.key);
          const value = hit ? hit.value : 0;
          if (y === currentYear) {
            // 当前年份的数据同时用于饼图
            echartPieData.push({ name: item.name, label: item.name, value });
          }
          item.data.push(value);
        });
      }
      // 组装成组件约定结构,并更新时间戳触发重绘
      Object.assign(this.echartBarData, {
        timer: new Date().getTime(), xAxis, series,
      });
      this.echartPieData = echartPieData;
    },
  },
};

数据流回顾 :后端接口 → 拿到原始数组 → 整理为 { xAxis, series }(柱状图)与 [{ name, label, value }](饼图)→ 通过 props 交给图表组件渲染。


七、数据契约汇总

组件 data 类型 结构要点
echart_barA Object { xAxis: string[], series: [{ name, data: number[] }] }
echart_barB Array [{ name, value, sum }]
echart_line Object { xAxis: string[], series: [{ name, data: number[], unit?, nosmooth? }] }
echart_pieA Array [{ name, label, value, id?, pid? }]
echart_radar Object { nameList: [{ name, max }], series: [{ name, data: number[] }] }
echart_bar_line Object(dataA) { id, colors, dataname, data, data1, xAxis, unit }
echart_barLine_compare / fzfx Object `{ xAxis, yAxis:{name,name2}, barWidth, barGap, series:[{ name, type:'bar'
echart_barMore Object(dataA) { id, xAxis, unit, dataBar:[...], dataLine:[...] }
echart_mapC Array [{ name, value }]

八、新增一个图表组件的步骤

  1. 在图表组件目录下新建 echart_xxx.vue;
  2. 模板放一个容器 div,:style="{ width, height }";
  3. import * as echarts from "echarts",在 mounted 里 echarts.init(...);
  4. 定义 props(至少包含 data、width、height,按需加 unit、fontSize、颜色等);
  5. 写 init():把 props 转换成 ECharts option,chart.setOption(option);
  6. watch data(deep) → 重新渲染(必要时先 dispose() 再 init());
  7. window.onresize(或 addEventListener)调用 chart.resize();
  8. 在页面组件中 import 并局部注册,接好接口数据后传 data。

九、注意事项与常见问题

现象 原因 处理
图表不显示 / 尺寸为 0 容器没有有效高度(父级高度为 0 或未给 height) 给组件传 height,并确保父容器有高度
数据更新了图表没变 只改了对象内部而没触发重绘 用 watch data(deep) 重绘,或改 :key="timer" 强制重建
同页面多个图表只有一个 resize 生效 老组件用 window.onresize = ... 覆盖绑定 改用 window.addEventListener("resize", ...) 或统一管理
用 id 初始化的图表串图 多个实例传了相同 id id 必须唯一(可加时间戳/序号)
切换 tab 后图表空白 切换时元素隐藏,resize 时尺寸为 0 切换后 $nextTick 再 chart.resize()
内存/定时器泄漏 组件销毁时未清理 setInterval / 实例 beforeDestroy 里 clearInterval、必要时 dispose()

十、小结

  • 图表能力以「纯展示组件 + 页面容器供数 」的方式组织:所有图表组件放在同一个目录下 ,包含基础图表 (柱状、横向条、折线、饼/玫瑰、雷达)与组合图表(柱+折线、多系列柱+折线、地图)。
  • 组件通过统一的 props 约定(data / width / height / unit / fontSize / colorList)和 watch + resize 机制,实现「换数据即换图」。
  • 使用时遵循:页面调接口 → 组装成组件约定的 data 结构 → 传给对应图表组件;无数据用 a-empty 兜底,数据频繁变化可用 :key 触发重建。
相关推荐
冒险的豆子1 小时前
JS 原型链 通俗易懂 解释说明(类比 关系)
开发语言·javascript·原型模式
西索斯coding1 小时前
在 中接入 教程:p配置、m填写、扩展思考参数一篇就够(2)
java·前端·javascript·ai
代码猎人1 小时前
前端怎么优化项目基础架构
前端
__sjfzllv___1 小时前
在职前端Leader学习/转行 AI Agent -DAY78
前端
代码猎人1 小时前
前端怎么封装工具类
前端
deli0071 小时前
PDF 合并别再用在线工具:12 份文件浏览器里合完,实测不上传
前端
shuoshuohaohao1 小时前
SpringBoot3-web开发-静态资源-内容协商
前端
kiros_wang1 小时前
RDB关系数据库实战:增删改查、事务、分页、版本迁移
前端
用户1733598075371 小时前
纯前端 PDF 双面打印避坑指南:翻页顺序错了整个白印?
前端·javascript·vue.js