表格图表切换,图表无法展示问题复盘
项目背景
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,打印出的inventoryRef的offsetHeight都是0,尽管设置了元素的宽高均为100%,但是由于元素没有渲染出来,导致无法获取实际高度,而是高度为0。原本是想通过获取到父元素inventoryRef的具体宽高值,然后设置图表容器pieChart的宽高从而实现图表的正常渲染。
注意:页面初始化时,CPC卡库存情况分析图表组件中的onMounted会执行,但此时无法获取父元素宽高。
解决方案
采用ResizeObserver
ResizeObserver 是浏览器提供的一个 API,用于监听 DOM 元素尺寸的变化(如宽度、高度、边框、内边距等)。它比传统的 window.resize 或 MutationObserver 更高效,专门用于监测元素的尺寸变化。
代码实现
<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'?
解决方案:
-
确保
tsconfig.json包含 DOM 类型(推荐){"compilerOptions": {"lib": ["dom", "es6"] // 确保包含 "dom"} } -
有时方案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 = truelet 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 = falsethis.isBtnLoader = falsethis.tableData = data.datathis.$refs.cpcInventoryRef.updateData(this.tableData, this.searchForm.cityCode);}).catch((error) => {this.loading = falsethis.isBtnLoader = falsethis.$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>
相关文章:
表格图表切换,图表无法展示问题复盘
项目背景 103项目CPC卡使用模块在原有的表格展示数据的基础之上,增加环状饼图图表展示,采用tab切换的方式实现 问题描述 图表无法设置宽高,导致饼图无法渲染 具体代码 // 入口页<el-tabs type"card" class"cts_flex_t…...
css 实现闪烁光标
要实现闪烁光标(比如文本输入框内常见的闪烁效果),可以使用 CSS 动画。下面是一个简单的方法: 代码示例 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta n…...
AI赋能python数据处理、分析与预测操作流程
以数据集预测鱼类种类(Species)开展以下研究。数据格式如下: 以下是一个系统的分析思路及推荐的机器学习算法: 1. 数据预处理与探索性分析 缺失值与异常值处理: 检查数据完整性(如Roach类中Weight=0的记录需修正或删除)。 通过箱线图或Z-Score检测异常值,判断是否需…...
基于74LS192的十进制两位数正向计时器(proteus仿真)
在数字电路设计中,计时器是一个非常常见的应用。今天,我将分享一个基于 74LS192 双向计数器 的十进制两位数正向计时器电路设计。这个电路可以实现从 00 到 99 的十进制正向计数,并通过两个七段数码管显示结果。 最终效果如图: 各…...
#C8# UVM中的factory机制 #S8.5# 对factory机制的重载进一步思考(二)
今天我们反思,然后总结。 一 先看代码 `timescale 1ns/1ps module tb_top;class Base;function void print(int a);$display("Base: int = %0d", a);endfunction endclassclass Sub extends Base;function void print(string s);$display("Sub: string = %s&…...
算法-前缀和与差分
一、前缀和(Prefix Sum) 1. 核心思想 前缀和是一种预处理数组的方法,通过预先计算并存储数组的前缀和,使得后续的区间和查询可以在**O(1)**时间内完成。 2. 定义 给定数组 nums,前缀和数组 prefixSum 的每个元素 p…...
React(六)React过渡动画-CSS编写方式
React过渡动画 react-transition-group介绍 在开发中,我们想要给一个组件的显示和消失添加某种过渡动画,提高用户体验→可通过react-transition-group实现。React曾为开发者提供过动画插件 react-addons-css-transition-group,后由社区维护…...
第十五章:Python的Pandas库详解及常见用法
在数据分析领域,Python的Pandas库是一个不可或缺的工具。它提供了高效的数据结构和数据分析工具,使得数据处理变得简单而直观。本文将详细介绍Pandas库的基本功能、常见用法,并通过示例代码演示如何使用Pandas进行数据处理。最后,…...
Python自动化模块:开启高效编程新时代
一、写在前面 在数字化时代,自动化技术已成为提高效率、降低成本的关键手段。Python 作为一种简洁、高效且功能强大的编程语言,凭借其丰富的库和框架,在自动化领域占据了举足轻重的地位,成为众多开发者的首选工具之一。从简单的文…...
【蓝桥杯速成】| 15.完全背包
题目:携带研究材料 问题描述 52. 携带研究材料(第七期模拟笔试) 小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研…...
C++:allocator类(动态数组续)
1.为什么需要 allocator? 在 C 中,动态内存管理通常通过 new 和 delete 完成: int* p new int; // 分配内存 构造对象 delete p; // 析构对象 释放内存 但 new 和 delete 有两个问题: 耦合性:将内…...
libva基础
Libva(Lib Video Acceleration)是一个开源的库,实现了 **VA-API**(Video Acceleration API),旨在为视频处理提供跨平台的硬件加速支持。 1、核心功能与作用 硬件加速抽象层:Libva 作为中间层&…...
【C++20】format格式化输出
C20 format格式化输出 在C20之前,格式化能力都依赖于三方格式化库FMT, 而C20 标准委员会终于在C标准库引入了格式化功能,从使用方式和风格来看其实就是FMT库转正了 直接使用 包含<format.h>头文件既可以直接使用,类似pyt…...
c++游戏开发第一期
以后我将要发c游戏开发的教程,可能更得比较慢。(目测几个星期一更)。 今天先讲个配置编译器。 我用的是Visual studio 2022和EasyX。 安装studio: 首先找到下载链接(点我)下拉找到下面图片的东西。 下完…...
Elasticsearch:人工智能时代的公共部门数据治理
作者:来自 Elastic Darren Meiss 人工智能(AI)和生成式人工智能(GenAI)正在迅速改变公共部门,从理论探讨走向实际应用。正确的数据准备、管理和治理将在 GenAI 的成功实施中发挥关键作用。 我们最近举办了…...
Web开发:数据的加密和解密
一、常见通用术语解析 加盐:在密码中加入随机数据,提高安全性。摘要:固定长度的输出,用于数据完整性验证。加密:将数据转换为不可读形式,确保安全。撞库:通过暴力破解比对常见密码的攻击方式。…...
低功耗LPWAN模块开发指南:远距离无线通信与边缘计算融合实战
在远程资产追踪、野外环境监测等场景中,稳定可靠的长距离通信与超低功耗是系统设计的核心挑战。eFish-SBC-RK3576通过 原生双UART接口 USB OTG扩展能力 ,可无缝集成主流LPWAN模组(LoRa/NB-IoT),实现“数据采集-边…...
RHCA核心课程技术解析5:红帽高可用性集群架构与深度实践
一、红帽高可用集群架构全景 1.1 核心组件交互逻辑 graph TD A[节点1] -->|Corosync 心跳| B[节点2] A -->|Pacemaker 资源管理| C[共享存储] B --> C D[Fencing设备] -->|STONITH| A D -->|STONITH| B C -->|GFS2锁管理| A C -->|GFS2锁管理| B 1.2 集…...
Python切片中的步长秘密
Python切片中的步长秘密 大家好!今天我们来聊聊Python切片中一个有趣的话题 - 步长(step)。 基本格式回顾 Python切片的完整格式是: [起点:终点:步长] 但你是否注意到,很多代码里的切片都只写了起点和终点?没错,步长是可以省略的! 步长的默认…...
Spring Boot事务管理详解(附银行转账案例)
一、事务基础概念 事务的ACID特性: 原子性(Atomicity):操作要么全部成功,要么全部失败一致性(Consistency):数据在事务前后保持合法状态隔离性(Isolation)&…...
【超详细教程】2025年3月最新Pytorch安装教程(同时讲解安装CPU和GPU版本)
目录 一、前言二、pytorch简介三、安装准备工作3.1、下载Anaconda 四、判断是否有NVIDIA显卡五、安装pytorch-CPU版本六、安装pytorch-GPU版本6.1、查看CUDA显卡驱动版本6.2、安装CUDA6.3、安装CuDNN(加速器)6.4、安装pytorch-GPU6.5 其他方法安装注意 七…...
Unity光线传播体积(LPV)技术实现详解
一、LPV技术概述 光线传播体积(Light Propagation Volumes)是一种实时全局光照技术,通过将场景中的间接光信息存储在3D网格中,实现动态物体的间接光照效果。 核心优势: 实时性能:相比传统光照贴图,支持动态场景 硬件…...
Git和GitCode使用(从Git安装到上传项目一条龙)
第一步 菜鸟教程-Git教程 点击上方链接,完成Git的安装,并了解Git 工作流程,知道Git 工作区、暂存区和版本库的区别 第二步 GitCode官方帮助文档-SSH 公钥管理 点击上方链接,完成SSH公钥设置 第三步(GitCode的官方引…...
通信之光纤耦合器
以下是关于光纤耦合器的详细介绍: 定义与原理 - 定义:光纤耦合器是一种能使传输中的光信号在特殊结构的耦合区发生耦合,并进行再分配的器件,也叫分歧器、连接器、适配器、光纤法兰盘。 - 原理:利用不同光纤面紧邻光纤芯…...
5G核心网(5GC)开户中,DNN(Data Network Name,数据网络名称)
在5G核心网(5GC)开户中,DNN(Data Network Name,数据网络名称)是关键概念之一,以下是关于它的详细介绍: 定义 DNN是5G网络中用于标识外部数据网络的名称,相当于4G中的APN(Access Point Name),两者功能等价。 组成 DNN由两部分组成: 网络ID(NI):必选,至少包…...
OpenCV、YOLO与大模型的区别与关系
OpenCV、YOLO 和大模型的区别与关系 1. OpenCV(Open Source Computer Vision Library) 定位:开源的计算机视觉基础库。功能:提供传统的图像处理算法(如图像滤波、边缘检测、特征提取)和基础工具ÿ…...
虚拟电商-话费充值业务(二)话费充值对接供应商模块开发
一、对接供应商模块开发 供应商对接模块chongba_recharge_supplier主要负责的就是调用外部的供应商系统进行充值下单,这种调用是一种基于HTTP协议的调用。 此外在供应商对接模块中主要是实现的业务逻辑有: 1:余额或押金不足情况下的失败轮…...
练习题:110
目录 Python题目 题目 题目分析 需求理解 关键知识点 实现思路分析 代码实现 代码解释 函数定义: 计算值的总和: 测试函数: 运行思路 结束语 Python题目 题目 定义一个函数,接受一个字典作为参数,返回字…...
c#winform,倒鸭子字幕效果,typemonkey字幕效果,抖音瀑布流字幕效果
不废话 直接上效果图 C# winform 开发抖音的瀑布流字幕。 也是typemonkey插件字幕效果 或者咱再网上常说的倒鸭子字幕效果 主要功能 1,软件可以自定义添加字幕内容 2,软件可以添加字幕显示的时间区间 3,可以自定义字幕颜色,可以随…...
游戏被外挂攻破?金融数据遭篡改?AI反作弊系统实战方案(代码+详细步骤)
一、背景与需求分析 随着游戏行业与金融领域的数字化进程加速,作弊行为(如游戏外挂、金融数据篡改)日益复杂化。传统基于规则的防御手段已难以应对新型攻击,而AI技术通过动态行为分析、异常检测等能力,为安全领域提供了革命性解决方案。本文以游戏反作弊系统和金融数据安…...
