当前位置: 首页 > news >正文

表格图表切换,图表无法展示问题复盘

项目背景

103项目CPC卡使用模块在原有的表格展示数据的基础之上,增加环状饼图图表展示,采用tab切换的方式实现

问题描述

图表无法设置宽高,导致饼图无法渲染

具体代码
// 入口页

<el-tabs type="card" class="cts_flex_tabs height-calc">
    <el-tab-pane label="CPC卡使用情况">
        <CtsTable :table-id="tableId"
                :version="version"
                ref="cpcCtsTable"
                class="cpc-use-table"
                :spanMethod="spanMethod"
                :loading="loading"
                :table-data="tableData"
                :add-row-class-name="addRowClassName"
                :columns="columns" />
        
    </el-tab-pane>
    <el-tab-pane label="CPC卡库存情况分析">
        <cpc-inventory ref="cpcInventoryRef" />
    </el-tab-pane>
</el-tabs>

// cpc-inventory组件

<div class="cpc-inventory" ref="inventoryRef">
	<div class="pie-chart" ref="pieChart"></div>
</div>

<style lang="less" scoped>
.cpc-inventory {
    height: 100%;
    width: 100%;
    .pie-chart {
        height: 100%;
        width: 100%;
    }
}
</style>

页面在初始化时会同时加载两个tab,但是CPC卡库存情况分析图表tab中的元素具体的宽高值是无法获取到的(display: none隐藏了),如下图:

在这里插入图片描述

onMounted钩子函数中无论是否使用nextTick,打印出的inventoryRefoffsetHeight都是0,尽管设置了元素的宽高均为100%,但是由于元素没有渲染出来,导致无法获取实际高度,而是高度为0。原本是想通过获取到父元素inventoryRef的具体宽高值,然后设置图表容器pieChart的宽高从而实现图表的正常渲染。

注意:页面初始化时,CPC卡库存情况分析图表组件中的onMounted会执行,但此时无法获取父元素宽高。
解决方案
采用ResizeObserver

ResizeObserver 是浏览器提供的一个 API,用于监听 DOM 元素尺寸的变化(如宽度、高度、边框、内边距等)。它比传统的 window.resizeMutationObserver 更高效,专门用于监测元素的尺寸变化。

代码实现
<div class="cpc-inventory" ref="inventoryRef">
	<div class="pie-chart" ref="pieChart"></div>
</div>

const setupResizeObserver = () => {
	if (!inventoryRef.value) return;
	if (resizeObserver.value) {
		resizeObserver.value.disconnect();
	}
	resizeObserver.value = new ResizeObserver((entries) => {
		for (const entry of entries) {
			const { height } = entry.contentRect;
			if (height > 0 && !chart) {
				initPieChart();
			}
		}
	});
	resizeObserver.value.observe(inventoryRef.value);
};
onMounted(() => {
	setupResizeObserver();
});
onBeforeUnmount(() => {
	if (resizeObserver.value) {
		resizeObserver.value.disconnect();
	}
});
代码解释:

在页面初始化时,钩子函数onMounted中调用setupResizeObserver函数,通过resizeObserver.value.observe(inventoryRef.value)监听inventoryRef元素,当切换tab到CPC卡库存情况分析图表展示组件时,此时能够监测到inventoryRef元素有了宽高值,那么由于设置了宽高为100%,因此子元素pieChart也就有了宽高,就可以进行图表的渲染,至此也就解决了上述的问题。

问题回顾

上面的是解决方案中的一种,还有另外一种方案:为两个tab添加name属性,在图表组件中添加v-if判断,代码如下:

<el-tabs type="card" class="cts_flex_tabs height-calc" v-model="activeTab" @click="...">
    <el-tab-pane label="CPC卡使用情况" name="use">
        <CtsTable :table-id="tableId"
                :version="version"
                ref="cpcCtsTable"
                class="cpc-use-table"
                :spanMethod="spanMethod"
                :loading="loading"
                :table-data="tableData"
                :add-row-class-name="addRowClassName"
                :columns="columns" />
        
    </el-tab-pane>
    <el-tab-pane label="CPC卡库存情况分析" name="inventory">
        <cpc-inventory ref="cpcInventoryRef" v-if="activeTab == 'inventory'" />
    </el-tab-pane>
</el-tabs>

这种方案是通过v-if判断,使得cpc-inventory组件的钩子函数onMounted在页面初始化加载时不会执行,只有切换到此tab时才会执行。然后我们可以在nextTick中进行图表初始化渲染。此方案的劣势是用户从表格切换到图表时,每次都要执行图表的初始化过程。方案1只有初次加载图表时才会进行初始化过程,后续切换只是完成数据更新,用户无法看到图标初始化过程。

在这里插入图片描述

注意

Node.js 或特殊环境不支持或者无法识别**ResizeObserver**,会有报错信息:Cannot find name 'ResizeObserver'. Did you mean 'resizeObserver'?

解决方案:

  1. 确保 tsconfig.json 包含 DOM 类型(推荐)
    {
      "compilerOptions": {
        "lib": ["dom", "es6"] // 确保包含 "dom"
      }
    }
    
  2. 有时方案1没生效,则需要安装 @types/resize-observer-browser(适用于某些特殊环境)

    npm install --save-dev @types/resize-observer-browser
    # 或
    yarn add --dev @types/resize-observer-browser
    

    在页面中引入

    <script>
    ....
    import ResizeObserver from "resize-observer-polyfill";
    ....
    
    </script>
    
适用场景

表格展示与图表展示通过tab组件切换(或者通过v-show),而不是通过v-if的方式动态设置显示与隐藏

完整代码分享
// 父组件

<el-tabs type="card" class="cts_flex_tabs height-calc">
    <el-tab-pane label="CPC卡使用情况">
        <CtsTable :table-id="tableId"
                :version="version"
                ref="cpcCtsTable"
                class="cpc-use-table"
                :spanMethod="spanMethod"
                :loading="loading"
                :table-data="tableData"
                :add-row-class-name="addRowClassName"
                :columns="columns" />
        
    </el-tab-pane>
    <el-tab-pane label="CPC卡库存情况分析">
        <cpc-inventory ref="cpcInventoryRef" />
    </el-tab-pane>
</el-tabs>

...
// 查询获取列表数据
listRefresh(pageIndex = 1) {
  this.loading = true
  let params = {
    ...api.getCpcUseInfoReportList,
    data: {
        ...this.searchForm,
        startHour: parseInt(this.searchForm.startHour.split(':')[0], 10),
        endHour: parseInt(this.searchForm.endHour.split(':')[0], 10),
    },
  }
  request(params)
      .then((data) => {
        this.loading = false
        this.isBtnLoader = false
        this.tableData = data.data
        this.$refs.cpcInventoryRef.updateData(this.tableData, this.searchForm.cityCode);
      })
      .catch((error) => {
        this.loading = false
        this.isBtnLoader = false
        this.$message.error(error.message)
      })
},
// 图表组件

<template>
	<div class="cpc-inventory" ref="inventoryRef">
		<div class="pie-chart" ref="pieChart"></div>
	</div>
</template>

<script lang='ts'>
import useCommon from "@/hooks/use-common";
import {
	ref,
	reactive,
	defineComponent,
	onMounted,
	computed,
	toRefs,
	watch,
	onBeforeUnmount,
} from "@vue/composition-api";
import useResizeSearch from "@/hooks/use-resizeSearch";
import api from "@/api";
import request from "@/axios/fetch";
import * as echarts from "echarts";
import ResizeObserver from "resize-observer-polyfill";

export default defineComponent({
	name: "CpcInventory",
	components: {},
	props: {},
	setup(props, { emit }) {
		const { proxy } = useCommon(); // 作为this使用
		const { isXLCol } = useResizeSearch();
		const pieChart = ref(null);
		let chart = null;
		const inventoryRef = ref(null);
		const resizeObserver = ref(null);
		const chartData = ref([]); // 饼图数据
		const centerShowInfo = reactive({
			name: "浙江省中心",
			kc: 0,
			pb: 0,
		});
		const setCenterShwoInfo = ({ city, inventory, matching }) => {
			centerShowInfo.name = city;
			centerShowInfo.kc = inventory;
			centerShowInfo.pb = matching;
		};
		const initPieChart = () => {
			if (pieChart.value) {
				chart = echarts.init(pieChart.value);
				const option = {
					tooltip: {
						trigger: "item",
						renderMode: "html", // 启用 HTML 模式
						formatter: ({ data }) => {
							return `
                        <div style="font-size: 14px;">
                            <p style="font-weight: bold; margin: 0 0 5px 0;">${data.name}</p>
                            <p style="margin: 2px 0;">库存: ${data.value}</p>
                            <p style="margin: 2px 0;">配比: ${data.pb}</p>
                            <p style="margin: 2px 0;">建议调入量: ${data.jytrl}</p>
                        </div>
                        `;
						},
						// 定义富文本样式
						rich: {
							name: {
								fontSize: 16,
								fontWeight: "bold",
								color: "#333",
								padding: [0, 0, 5, 0],
							},
							value: {
								color: "#666",
								fontSize: 14,
							},
							ratio: {
								color: "#ff9900",
								fontSize: 14,
							},
							suggest: {
								color: "#ff0000",
								fontSize: 14,
								fontWeight: "bold",
							},
						},
					},
					legend: {
						orient: "vertical",
						bottom: "bottom",
					},
					series: [
						{
							name: "",
							type: "pie",
							radius: ["30%", "40%"],
							data: chartData.value,
							padAngle: 0,
							// value 字段是必需的,用于确定扇形区域的大小。
							// 可以通过 formatter 自定义标签内容,展示其他字段。
							label: {
								show: true,
								position: "outside", // 标签显示在外部
								formatter: function ({ data }) {
									if (data.jytrl > 0) {
										return `{name|${data.name}}\n{value|库存: ${data.value}} {ratio|配比: ${data.pb}} {desc|建议调入量: ${data.jytrl}}`;
									} else if (data.jytrl < 0) {
										return `{negativeName|${data.name}}\n{negativeValue|库存: ${data.value}} {negativeRatio|配比: ${data.pb}} {negativeDesc|建议调入量: ${data.jytrl}}`;
									}
									return `{zeroName|${data.name}}\n{zeroValue|库存: ${data.value}} {zeroRatio|配比: ${data.pb}} {zeroDesc|建议调入量: ${data.jytrl}}`;
								},
								rich: {
									name: {
										fontSize: 14,
										fontWeight: "bold",
										lineHeight: 24,
										color: "#F56C6C",
									},
									value: {
										fontSize: 12,
										color: "#F56C6C",
										lineHeight: 20,
									},
									ratio: {
										fontSize: 12,
										color: "#F56C6C",
										lineHeight: 20,
									},
									desc: {
										fontSize: 12,
										color: "#F56C6C",
										lineHeight: 20,
									},
									negativeName: {
										fontSize: 14,
										fontWeight: "bold",
										lineHeight: 24,
										color: "#67C23A",
									},
									negativeValue: {
										fontSize: 12,
										color: "#67C23A",
										lineHeight: 20,
									},
									negativeRatio: {
										fontSize: 12,
										color: "#67C23A",
										lineHeight: 20,
									},
									negativeDesc: {
										fontSize: 12,
										color: "#67C23A",
										lineHeight: 20,
									},
									zeroName: {
										fontSize: 14,
										fontWeight: "bold",
										lineHeight: 24,
										color: "#333",
									},
									zeroValue: {
										fontSize: 12,
										color: "#333",
										lineHeight: 20,
									},
									zeroRatio: {
										fontSize: 12,
										color: "#333",
										lineHeight: 20,
									},
									zeroDesc: {
										fontSize: 12,
										color: "#333",
										lineHeight: 20,
									},
								},
							},
							labelLine: {
								show: true, // 显示引导线
							},
							emphasis: {
								itemStyle: {
									shadowBlur: 10,
									shadowOffsetX: 0,
									shadowColor: "rgba(0, 0, 0, 0.5)",
								},
							},
						},
					],
					graphic: {
						type: "text",
						left: "center",
						top: "center",
						style: {
							text: [
								`{title|${centerShowInfo.name}}`,
								`{value|库存: ${centerShowInfo.kc}}`,
								`{desc|配比: ${centerShowInfo.pb}}`,
							].join("\n"), // 用 \n 换行
							rich: {
								// 定义每一行的样式
								title: {
									fontSize: 24,
									fontWeight: "bold",
									color: "#333",
									lineHeight: 36,
									textAlign: "center",
								},
								value: {
									fontSize: 16,
									color: "#333",
									lineHeight: 30,
									textAlign: "center",
								},
								desc: {
									fontSize: 16,
									color: "#333",
									lineHeight: 30,
									textAlign: "center",
								},
							},
						},
					},
				};
				chart.setOption(option);
			}
		};
		const updateData = (val, isSingleCity) => {
			if (val.length) {
				chartData.value = val
					.filter((ele) => ele.city)
					.map((item) => ({
						value: item.inventory,
						name: item.city,
						pb: item.matching,
						jytrl: item.suggest,
					}));
				if (isSingleCity) {
					// 单区域
					const totalInfo = val.find((ele) => ele.area == "合计");
					setCenterShwoInfo(totalInfo);
				} else {
					// 全域
					const totalInfo = val.find((ele) => ele.area == "总计");
					setCenterShwoInfo({ ...totalInfo, city: "浙江省中心" });
				}
			} else {
				chartData.value = [];
				setCenterShwoInfo({ city: "", inventory: 0, matching: 0 });
			}

			if (chart) {
				chart.clear();
				initPieChart();
			}
		};

		const setupResizeObserver = () => {
			if (!inventoryRef.value) return;
			if (resizeObserver.value) {
				resizeObserver.value.disconnect();
			}
			resizeObserver.value = new ResizeObserver((entries) => {
				for (const entry of entries) {
					const { height } = entry.contentRect;
					if (height > 0 && !chart) {
						initPieChart();
					}
				}
			});
			resizeObserver.value.observe(inventoryRef.value);
		};

		onMounted(() => {
			setupResizeObserver();
		});
		onBeforeUnmount(() => {
			if (resizeObserver.value) {
				resizeObserver.value.disconnect();
			}
			if (chart) {
				chart.dispose();
				chart = null;
			}
		});
		return {
			pieChart,
			inventoryRef,
			updateData,
		};
	},
});
</script>

<style lang="less" scoped>
.cpc-inventory {
    height: 100%;
    width: 100%;
    .pie-chart {
        height: 100%;
        width: 100%;
    }
}
</style>
http://www.dtcms.com/a/99511.html

相关文章:

  • 工作记录 2017-03-13
  • Python CSV 数据分析:问题排查与解决全记录
  • Windows学习笔记(3)关于事件日志
  • linux的文件
  • Visual Basic语言的折线图
  • Ubuntu 系统中,每日健康检查
  • vulnhub-Node1
  • C#:第一性原理拆解字段(fields)
  • ElasticSearch 分词器
  • kill子进程后再wait可以吗?
  • java性能优化实例
  • 如何估算和优化 Gas
  • 学习通-操作系统第五版费翔林-习题-标答
  • 【AI学习】深度学习,人工神经网络
  • 破解美业数字化迷思:中小门店的智慧转型之路
  • 代码随想录day31 贪心part05
  • 蓝桥杯真题------R格式(高精度乘法,高精度加法)
  • Flutter 开发环境配置--宇宙级教学!
  • 【通道注意力机制】【SENet】Squeeze-and-Excitation Networks
  • 高并发系统下的订单号生成服务设计与实现
  • Node.js Express 安装并使用 nodemon
  • 多路径 TCP 调度的另一面
  • Vue3 事件处理
  • 英伟达GB300新宠:新型LPDDR5X SOCAMM内存
  • 【12】Ajax的原理和解析
  • C语言之链表增删查改
  • 【Java SE】Java比较器:Comparable、Comparator
  • Python的ASGI Web 服务器之uvicorn
  • 图解AUTOSAR_SWS_SynchronizedTimeBaseManager
  • Vue——常用指令总结、指令修饰符、v-model原理、computed计算属性、watch监听器、ref和$refs