Wiidede's blog Wiidede's blog
  • 前端
  • Python
  • 算法
  • 生活
  • 其他
  • 分类
  • 标签
  • 归档
  • 关于我
  • 赞赏
  • 我的小站 (opens new window)
GitHub (opens new window)

Wiidede

小的的写前端
  • 前端
  • Python
  • 算法
  • 生活
  • 其他
  • 分类
  • 标签
  • 归档
  • 关于我
  • 赞赏
  • 我的小站 (opens new window)
GitHub (opens new window)
  • 整理一些css样式
  • vue隔代组件层层动态插槽并且附带数据
  • vue判断字符串是否溢出来显示弹窗、解决el-table tooltip 内过多导致无法显示,内容闪烁
  • 整理一些js写法
  • ElementUI timePicker 增加此刻按钮 引发的dom操作的学习
  • 毕业设计(水表识别)前端知识整理
  • html小知识
  • axios请求api然后下载文件
  • vue3+ts根据高度改变元素的透明度
  • vue3 + ElementPlus 换肤方案(Css变量)
  • Moment的一些使用方法
  • echarts基础vue组件
  • element UI el-date-picker 年月日切换组件
  • 可以不选择的el-radio单选框
  • vue的小技巧总结
  • 全局动态权限判断(Vue指令)
  • vue-anchor 探索
  • Deep Dive with Evan You 笔记
  • 前端基础知识查漏补缺
  • WebPack 知识总结
  • 我写的一些可以日后参考的代码
  • 接口变化后,封装接口函数,改变返回内容
  • 项目组件整理
  • 前端框架设计想发
  • 全局进度条
  • 带有token的图片vue组件:authImg,使用axios下载图片
  • 前端npm包推荐
  • 给ElInputNumber添加prefix
  • ElPagination添加页数总数
  • el-tab做成chrome类似的tab样式
  • vue-grid-layout-组件配置
  • 项目数据字典封装
  • 图表组件响应式探索
    • 响应式布局常规解决方案
      • px2rem px2vw
    • VueGridLayout 响应式组件封装
    • Echarts 响应式组件封装
      • 响应容器大小的变化
      • 移动端自适应
      • 解决方法
      • Echarts组件封装
  • ElementPlus表格table列自动合并composition
  • 简单的curd组件封装
  • ElementPlus表格自定义合计列composition
  • 一些处理表格数据composition api
  • div内容溢出后,内容向左悬浮,vue组件封装
  • 文本数字溢出后,按比例缩小,vue组件封装
  • 表格使用async-validator检验composition
  • ElementPlus Form一些简单的组件整合
  • arco-design快速使用tailwind的颜色、unocss动态颜色
  • 前端
wiidede
2022-11-11

图表组件响应式探索

# POM BI 项目图表组件响应式探索(VueGridLayout、Echarts)

# 响应式布局常规解决方案

# px2rem px2vw

对于响应式布局,最容易联想到的就是用postcss等工具把css中的px转换rem或者vw,然后根据浏览器视窗的大小,字体大小也会响应式变化。(使用rem时需要根据窗口大小变化不断调整根节点的font-size)

但是这两种方案有个缺点,就是它只能针对生成的css样式去改变css的值,对于一些引入进来的组件生成的行内样式,是无法生效的。就比如element-plus,很多组件的width属性,如果你直接传数字的话,它会直接转成px放到生成组件的行内样式上,它无法被插件转换单位。

el-avatar的size,想要自定义大小,只能传number,number又被转成px:

image-20221114103731583

同样的 BI 项目还用到 vue-grid-layout和echarts 这两个组件,默认单位其实都是px,无法更改。

vue-grid-layout

image-20221114104009574

echarts

image-20221114103838134

# VueGridLayout 响应式组件封装

这个组件的解决方案其实很简单:它的大小是由这几个属性决定的:

  1. 宽度是完全由容器的宽度决定,只需要用css修改宽度即可
  2. 高度由rowHeight和margin两个值,加上组件layout共同决定

这个组件会根据rowHeight和margin的值变化,自动调整页面元素的高度。所以只要在浏览器窗口大小变化的时候,重新计算这两个值就行。

这里可以使用vueuse库中的useWindowSize,快速拿到浏览器窗口大小。

用vue的计算属性,根据窗口大小计算rowHeight和margin,然后组件接收到的值就会更新:

响应式组件中计算出的值:

image-20221114105731381

GridLayoutDynamic.vue

<template>
  <grid-layout :row-height="rowHeightDynamic" :margin="marginDynamic">
    <slot></slot>
  </grid-layout>
</template>

<script setup>
import { computed } from 'vue';
import { useWindowSize } from '@vueuse/core';

const props = defineProps({
  rowHeight: {
    type: Number,
    default: 150,
  },
  margin: {
    type: Array,
    default: () => [10, 10],
  },
  viewPortWidth: {
    type: Number,
    default: 1920, // 0 关闭自适应
  },
  viewPortHeight: {
    type: Number,
    default: (rawProps) => (rawProps.viewPortWidth === undefined ? 1080 : (rawProps.viewPortWidth / 16) * 9), // 0 关闭自适应,画面默认比例 16 / 9
  },
});

const { width, height } = useWindowSize();
const usingDynamic = props.viewPortHeight !== 0 && props.viewPortWidth !== 0;
const rowHeightDynamic = computed(() => (usingDynamic
  ? height.value * (props.rowHeight / props.viewPortHeight)
  : props.rowHeight
));
const marginDynamic = computed(() => (usingDynamic
  ? [width.value * (props.margin[0] / props.viewPortWidth), height.value * (props.margin[1] / props.viewPortHeight)]
  : props.margin
));
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# Echarts 响应式组件封装

在echarts官网,关于响应式的应该就这两点

  1. 响应容器大小的变化 (opens new window)(echartsInstance.resize)
  2. 移动端自适应 (opens new window)(Media Query)

# 响应容器大小的变化

对于官网的响应容器大小的变化,官网采用监听window resize事件,其实是有几个缺点的:

  • 如果窗口大小没有变化,但是页面中容器大小变了,echarts并不会resize。比如收起侧边栏,echarts容器变宽
  • 如果echarts所在的dom上级被隐藏起来(display: none),这时候去缩放窗口(echarts实例的resize被调用),然后再让回到echarts显示的状态,你会发现echarts的大小已经变成10px \* 10px(或者其他奇怪的大小)了,因为resize事件触发的时候,echarts并没有在页面中显示,这时候获取不到dom元素的宽高,然后就resize成默认的10px \* 10px(或者其他奇怪的大小)了。比如element-plus中的el-tabs,不是当前页面的echarts就不会显示

el-tab 未激活的页面:

image-20221114102619482

比如在官网实例中,你给iframe里面的body加上display: none之后,缩放窗口,然后再把display: none取消掉,你就会发现echarts没了: image-20221114115313886

为了解决以上两点,你可能会在页面上多写一些东西,在相应的时刻调用resize。

所以有没有一种方法可以统一的解决上面的两个问题呢?

答案就是echarts外部容器加上容器大小的监听(而不是windows的resize),这样就不会出现上述两种问题。加上容器大小的监听有两种方法:ResizeObserver (opens new window)、element-resize-detector (opens new window),使用方法,可以看看我的这篇博客 (opens new window)。

# 移动端自适应

对于官网的移动端自适应,也就是媒体查询,需要给出在不同分辨率下的option(/样式),这种方法写起来可能比较繁琐,也不能很好的和px2vw这种插件配合。

# 解决方法

所以,可以不可以让echarts也能有px2vw这种类似的效果呢?

比如想让echarts的fontSize动态响应,那么就应该拿到option中所有的fontSize,然后再窗口大小变化的时候,重新给收集到的这些fontSize赋值。

有了这个思路,配合上上面提到给容器加上大小变化监听,就可以抽出一个echarts的通用组件。

fontSize自适应效果:

image-20221114105418438

image-20221114104942806

# Echarts组件封装

这个组件有几个特点:

  1. 使用echarts时,只需要传入option,就会自动更新option,无需自己选择dom元素、调用init
  2. echarts的点击事件会转成vue组件的事件
  3. 监听容器大小并且resize,使用时不在需要关心什么时候需要resize
  4. 传入屏幕宽度,所有fontSize就会自动根据屏幕宽度响应式改变大小
  5. 你甚至可以传入虚假的viewPortWidth让图表整体字体大小改变

提示:

  • 这个组件setOption采用不合并的模式,需要全量传入option
  • 对于echarts普通合并模式,有些数据项需要给到name、id,不然走的是增量更新,无法重设字体大小,参考echartsInstance.setOption (opens new window)

可以改造扩展的地方:

  1. 现在只是针对fontSize做响应式,后续可以加上其他的,比如margin,padding,itemWidth等等
  2. 现在是根据屏幕宽度响应式,如果想根据容器宽度响应式也是可以的

Echarts.vue

<template>
  <div ref="echartsRef" class="echarts" />
</template>

<script setup>
import { debounce, get, set, cloneDeep } from 'lodash';
import * as echarts from 'echarts';
import {
  ref, onMounted, onUnmounted, watch,
} from 'vue';
import { getObjectPaths } from '@/utils/utils';

const props = defineProps({
  option: {
    type: Object,
    required: true,
  },
  viewPortWidth: {
    type: Number,
    default: 1920, // 0 关闭自适应
  },
});

const emit = defineEmits(['resize', 'mouseover', 'mouseout', 'click', 'legendselectchanged']);

let chart;
const echartsRef = ref();
let destroyFunc;

const defaultResizingOption = {}; // 自适应配置(echarts 普通合并模式,需要用到name、id)
const fontSizePaths = new Set(); // fontSize 需要的 key

const getEchartsInstance = () => chart;
const setOption = (option) => {
  option && chart.setOption({ ...option }, true);
  // 这里没有走echarts合并模式,所以就不传参数,直接拿到echarts实例的option。
  // 如果是echarts合并模式,需要传入option,并且要想办法拿到echarts默认的option。
  collectFontSizePath();
};

// 注册echarts事件到Vue组件
const initChartEvent = () => {
  const emitEvents = ['mouseover', 'mouseout', 'click', 'legendselectchanged'];
  emitEvents.forEach((eventName) => {
    chart.on(eventName, (params) => {
      emit(eventName, params, chart);
    });
  });
};

// echarts 容器大小自适应
const makeChartsResponsive = () => {
  const targetNode = echartsRef.value;
  const resizeDebounced = debounce(() => {
    chart.resize();
    setFontSize(); // resize的时候也要重新设置字体大小
  }, 50);
  const resizeObserver = new ResizeObserver((targets) => {
    resizeDebounced();
    const [target] = targets;
    const { width, height } = target.contentRect;
    emit('resize', { width, height }, chart);
  });
  resizeObserver.observe(targetNode);
  destroyFunc = () => {
    resizeObserver.disconnect();
  };
};

// 拿到option中所有的fontSize(包括基础的id、name)
const collectFontSizePath = (option) => {
  option === undefined && (option = chart.getOption());
  const pathList = getObjectPaths(option);
  const optionPathList = pathList.filter((path) => !path.includes('data'));
  const basePathList = optionPathList.filter((path) => path.endsWith('.name') || path.endsWith('id'));
  const fontSizePathList = optionPathList.filter((path) => path.endsWith('.fontSize'));
  basePathList.forEach((path) => set(defaultResizingOption, path, get(option, path)));
  fontSizePathList.forEach((path) => {
    let fontSize = get(option, path);
    if (typeof fontSize === 'string' && fontSize.endsWith('px')) {
      fontSize = Number(fontSize.replace('px', '')) || 12;
    }
    set(defaultResizingOption, path, fontSize);
    fontSizePaths.add(path);
  });
  setFontSize();
};

// 遍历收集到的fontSize,根据页面宽度重新设置字体大小
const setFontSize = () => {
  if (!props.viewPortWidth) return;
  const width = document.body.clientWidth;
  const ratio = width / props.viewPortWidth;
  const fontSizeOption = cloneDeep(defaultResizingOption);
  fontSizePaths.forEach((path) => {
    const fontSize = get(defaultResizingOption, path);
    const newFontSize = Number((ratio * fontSize).toFixed(5)); // 精度
    set(fontSizeOption, path, newFontSize);
  });
  chart.setOption(fontSizeOption);
};

onMounted(() => {
  chart = echarts.init(echartsRef.value);
  setOption(props.option);
  initChartEvent();
  makeChartsResponsive();
});

onUnmounted(() => {
  chart && chart.dispose();
  destroyFunc && destroyFunc();
});

watch(
  () => props.option,
  (option) => {
    setOption(option);
  },
  // { deep: true }, // 如果默认不走echarts合并模式,就不需要deep了
);

defineExpose({
  getEchartsInstance,
});
</script>

<style scoped>
.echarts {
  width: 100%;
  height: 100%;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133

取到对象所有的path

const getObjectPaths = (object) => {
  const paths = [];
  const getPaths = (obj, path) => {
    if (obj && typeof obj === 'object') {
      Object.keys(obj).forEach((key) => {
        getPaths(obj[key], path ? `${path}.${key}` : key);
      });
    } else {
      paths.push(path);
    }
  };
  getPaths(object, '');
  return paths;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

至此,两个组件的响应式就已经弄好了

#前端#项目#Echarts#VueGridLayout
上次更新: 2023/06/01, 12:40:50

← 项目数据字典封装 ElementPlus表格table列自动合并composition→

Theme by Vdoing | Copyright © 2021-2023 Wiidede | Website use MIT License | Article content & logo use CC-BY-SA-4.0 License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式