# 关于项目源码

项目地址:https://github.com/Hbuilderx/screen-adapter-demo
# 关于兼容性问题
看到很多小伙伴出现了各种各样的兼容性问题,这里大概率是兼容性问题,下面是我的工程配置,版本比较老 注意 package.json 中 vue-cli、vue 、 sass 和 sass-loader 的版本
{ | |
"name": "chongqing-building-pc", | |
"version": "0.1.0", | |
"private": true, | |
"scripts": { | |
"serve": "vue-cli-service serve", | |
"build": "vue-cli-service build" | |
}, | |
"dependencies": { | |
"axios": "^0.25.0", | |
"bus-front": "^0.5.29", | |
"core-js": "^3.6.5", | |
"echarts": "^5.1.2", | |
"element-resize-detector": "^1.2.3", | |
"element-ui": "^2.15.5", | |
"highcharts": "^9.3.2", | |
"lodash": "^4.17.21", | |
"pubsub-js": "^1.9.4", | |
"vue": "^2.6.11", | |
"vue-router": "^3.2.0", | |
"vuex": "^3.4.0" | |
}, | |
"devDependencies": { | |
"@vue/cli-plugin-babel": "~4.5.0", | |
"@vue/cli-plugin-router": "~4.5.0", | |
"@vue/cli-plugin-vuex": "~4.5.0", | |
"@vue/cli-service": "~4.5.0", | |
"mockjs": "^1.1.0", | |
"sass": "^1.26.5", | |
"sass-loader": "^8.0.2", | |
"vue-template-compiler": "^2.6.11" | |
}, | |
"browserslist": [ | |
"> 1%", | |
"last 2 versions", | |
"not dead" | |
] | |
} |
# 关于大分辨率尺寸调试的问题
有时候我们开发大屏,分辨率可能很大,比如 6000*3200 、 5200*2000 、 3840*2160 等 6k、5k、4k 屏幕时,如果自己的显示器分辨率不够,又申请不到合适的显示器时,可以使用谷歌浏览器模拟一个,具体操作如下
1. 打开控制台,点击设备按钮

2. 选择自定义设备

3. 选择 Add custom device...

4. 输入设备参数

4. 然后就可以选择设备列表了

# 前言
在做了很多大屏的项目之后,针对于屏幕适配,说实话有很多解决方案,例如媒体查询、scale 缩放、Rem、vw 等等,但每种方案都有其特定的使用场景,面对不同的项目,我们首先考虑的不是哪种方案最好,而是最合适。
总结了我司做的大屏项目以后,我写了这篇大屏适配完全解决方案,希望对您有所帮助。
# 先看项目效果图
# 项目 1:地铁三维可视化大屏系统
总分辨率为固定为 9600*2160,分为左 -- 中 -- 右三块屏幕拼接而成, 不会适配过大过小的屏幕
# 左屏分辨率:2880*2160

# 中屏分辨率:3840*2160

# 右屏分辨率:2880*2160

# 左中右拼接效果

# 项目 2:景区数字孪生平台
基础分辨率为 1920*1080,要求向上向下能适配 16:9 的屏幕
# 1920*1080

# 1280*720

# 2560*1440

# 3840*2160

# 7480*3240

# 项目 3:xxx 园区态势感知平台
多个 2880*2160 拼接
# 2880*2160

# 项目 4: xxx 综合执法平台
# 6000*2160

# 适配方案
适配方案不单单只是屏幕适配,包括里面的图标、DOM 元素都要考虑到,下面分开来介绍
# 屏幕适配
# vw 和 vh 适配方案
- 按照设计稿的尺寸,将
px按比例计算转为vw和vh - 转换公式如下
假设设计稿尺寸为1920*1080(做之前一定问清楚UI设计稿的尺寸) | |
即: | |
网页宽度=1920px | |
网页高度=1080px | |
我们都知道 | |
网页宽度=100vw | |
网页宽度=100vh | |
所以,在1920x*1080px的屏幕分辨率下 | |
1920px = 100vw | |
1080px = 100vh | |
这样一来,以一个宽300px和200px的div来说,其作所占的宽高,以vw和vh为单位,计算方式如下: | |
vwDiv = (300px / 1920px ) * 100vw | |
vhDiv = (200px / 1080px ) * 100vh | |
所以,就在1920*1080的屏幕分辨率下,计算出了单个div的宽高 | |
当屏幕放大或者缩小时,div还是以vw和vh作为宽高的,就会自动适应不同分辨率的屏幕 |
所以,我们每次写 css 时转换一下就好了,但是用计算器很慢,所以,我们要借助 scss 的函数来帮我们计算
# 安装 scss
npm install sass@1.26.5 sass-loader@8.0.2 --save |
# 封装计算工具函数
- 在
src/styles下新建一个utils.scss文件,定义好设计稿的宽度和高度两个变量 - 在这里使用 scss 内置的
math.div函数,定义两个vw和vh的计算函数 - 我们传入具体的像素值,其帮我们自动计算出 vw 和 vh 的值
util.scss
// 使用 scss 的 math 函数,https://sass-lang.com/documentation/breaking-changes/slash-div | |
@use "sass:math"; | |
// 默认设计稿的宽度 | |
$designWidth:1920; | |
// 默认设计稿的高度 | |
$designHeight:1080; | |
//px 转为 vw 的函数 | |
@function vw($px) { | |
@return math.div($px , $designWidth) * 100vw; | |
} | |
//px 转为 vh 的函数 | |
@function vh($px) { | |
@return math.div($px , $designHeight) * 100vh; | |
} |
# 路径配置
我这里使用的是 vue2.6 和 vue-cli3 搭建的 vue 项目,所以,我只需要在 vue.config.js 里配置一下 utils.scss 的路径,就可以全局使用了
const path = require('path') | |
function resolve(dir) { | |
return path.join(__dirname, dir) | |
} | |
module.exports={ | |
publicPath: '', | |
configureWebpack: { | |
name: "app name", | |
resolve: { | |
alias: { | |
'@': resolve('src') | |
} | |
} | |
}, | |
css:{ | |
// 全局配置 utils.scss, 详细配置参考 vue-cli 官网 | |
loaderOptions:{ | |
sass:{ | |
prependData:`@import "@/styles/utils.scss";` | |
} | |
} | |
} | |
} |
# 在.vue 文件中使用
<template> | |
<div class="box"> | |
</div> | |
</template> | |
<script> | |
export default{ | |
name: "Box", | |
} | |
</script> | |
<style lang="scss" scoped="scoped"> | |
/* | |
直接使用 vw 和 vh 函数,将像素值传进去,得到的就是具体的 vw vh 单位 | |
*/ | |
.box{ | |
width: vw(300); | |
height: vh(100); | |
font-size: vh(16); | |
background-color: black; | |
margin-left: vw(10); | |
margin-top: vh(10); | |
border: vh(2) solid red; | |
} | |
</style> |
# 动态 DOM 元素适配
有的时候可能不仅在 .vue 文件中使用,比如在 js 中动态创建的 DOM 元素
它可能是直接渲染到 html 里面的
let oDiv = document.createElement('div') | |
document.body.appendChild(oDiv) |
这样的话,我用了以下两种处理方式,来给创建的 div 设置样式
# 1. 定义全局的 class 样式
在 scr/styles 目录下新建一个 global.scss 文件,在 main.js 中引入
global.css
.global-div{ | |
width: vw(300); | |
height: vw(200); | |
background-color: green; | |
} |
main.js
import './styles/global.scss' |
使用时,给创建的 div 设置 className
let oDiv = document.createElement('div') | |
oDiv.className = "global-div" |
# 2. 定义 js 样式处理函数
这种处理方式和 scss 处理函数类似,只不过使用了纯 js 将 px 转为 vw 和 vh
在 src/utils 目录下新建一个 styleUtil.js 文件,内容如下
// 定义设计稿的宽高 | |
const designWidth = 1920; | |
const designHeight = 1080; | |
let styleUtil = { | |
//px 转 vw | |
px2vw: function (_px) { | |
return _px * 100.0 / designWidth + 'vw'; | |
}, | |
//px 转 vh | |
px2vh: function (_px) { | |
return _px * 100.0 / designHeight + 'vh'; | |
}, | |
}; | |
export default styleUtil; |
使用时,单独设置宽高等属性
import styleUtil from "./src/utils/styleUtil.js" | |
let oDiv = document.createElement('div') | |
oDiv.style.width = styleUtil.px2vw(300) | |
oDiv.style.height = styleUtil.px2vh(200) | |
oDiv.style.margin = styleUtil.px2vh(20) |
不过这种使用方式有种弊端,就是屏幕尺寸发生变化后,需要手动刷新一下才能完成自适应调整
# Echarts 图表适配和封装
# 为何要封装?
- 每种图表的 option 配置都有类似,每次都要在业务代码里重写,十分冗余
- 在同一个项目中,各类图表设计十分相似,甚至是相同,没必要一直做重复工作
- 可能有一些开发者忘记考虑 echarts 更新数据的特性,以及窗口缩放时的适应问题。这样导致数据更新了 echarts 视图却没有更新,窗口缩放引起 echarts 图形变形问题
# 封装后实现的效果
- 业务数据和样式配置数据分离,我只需要传入业务数据就行了
- 它的大小要完全由使用者决定
- 不会因为缩放出现变形问题,而是能很好地自适应
- 有时候某个图表的样式可能有点不一样,希望能保留自己配置样式的灵活性
- 无论传入什么数据都能正确地更新视图
- 如果我传入的数据为空,能展示一个空状态
# 封装的建议
- 将所有的图表组件都放在
components/Chart文件夹中 - 每种图表单独新建一个文件夹,如
components/Chart/PieChart,components/Chart/LineChart等 - 每种图表中都有一个默认的
defaultOption.js配置文件 - 图表的须有一个
README.md文件
# 依赖项
- "echarts": "^5.1.2",
- "element-resize-detector": "^1.2.3",
- "lodash": "^4.17.21",
# 具体封装,以饼图为例
<template> | |
<h3 v-if="isSeriesEmpty">暂无数据</h3> | |
<div v-else class="chart"> | |
</div> | |
</template> | |
<script> | |
import * as Echarts from "echarts" | |
import ResizeListener from "element-resize-detector"; | |
import { merge, isEmpty } from "lodash"; | |
import {basicOption} from "./defaultOption.js" | |
import {pieChartColor} from "./../color.js" | |
export default{ | |
name: "PieChart", | |
props: { | |
// 正常的业务数据,对应 echarts 饼图配置中 series [0].data | |
seriesData: { | |
type: Array, | |
required: true, | |
default: () => [], | |
}, | |
// 表示需要特殊定制的配置 | |
// 一般 UI 会规定一个统一的设计规范(比如颜色,字体,图例格式,位置等) | |
// 但不排除某个图标会和设计规范不同,需要特殊定制样式,所以开放这个配置,增强灵活性 | |
extraOption: { | |
type: Object, | |
default: () => ({}), | |
}, | |
}, | |
data() { | |
return { | |
chart: null, | |
}; | |
}, | |
computed:{ | |
isSeriesEmpty() { | |
return ( | |
isEmpty(this.seriesData) || this.seriesData.every((item) => !item.value) | |
); | |
}, | |
}, | |
watch: { | |
seriesData: { | |
deep: true, | |
handler() { | |
this.updateChartView(); | |
}, | |
}, | |
}, | |
mounted() { | |
this.chart = Echarts.init(this.$el); | |
this.updateChartView(); | |
window.addEventListener("resize", this.handleWindowResize); | |
this.addChartResizeListener(); | |
}, | |
beforeDestroy() { | |
window.removeEventListener("resize", this.handleWindowResize); | |
}, | |
methods:{ | |
/* 合并配置项和数据,对于需要自定义的配置项以及数据,使用 merge 函数将其合并为一个 option */ | |
assembleDataToOption() { | |
// 这部分的图例 formatter 取决于 UI 要求,如果你的项目中不需要,就可以不写 formatter | |
// 由于 echarts 版本的迭代,这里的写法也有稍许改变 | |
const formatter = (name) => { | |
const total = this.seriesData.reduce((acc, cur) => acc + cur.value, 0); | |
const data = this.seriesData.find((item) => item.name === name) || {}; | |
const percent = data.value | |
? `${Math.round((data.value / total) * 100)}%` | |
: "0%"; | |
return `${name} ${percent}`; | |
}; | |
return merge( | |
{}, | |
basicOption, | |
{ color: pieChartColor }, | |
{ | |
legend: { formatter }, | |
series: [{ data: this.seriesData }], | |
}, | |
this.extraOption | |
); | |
}, | |
/** | |
* 对 chart 元素尺寸进行监听,当发生变化时同步更新 echart 视图 | |
*/ | |
addChartResizeListener() { | |
const instance = ResizeListener({ | |
strategy: "scroll", | |
callOnAdd: true, | |
}); | |
instance.listenTo(this.$el, () => { | |
if (!this.chart) return; | |
this.chart.resize(); | |
}); | |
}, | |
/** | |
* 更新 echart 视图 | |
*/ | |
updateChartView() { | |
if (!this.chart) return; | |
const fullOption = this.assembleDataToOption(); | |
this.chart.setOption(fullOption, true); | |
}, | |
/** | |
* 当窗口缩放时,echart 动态调整自身大小 | |
*/ | |
handleWindowResize() { | |
if (!this.chart) return; | |
this.chart.resize(); | |
}, | |
} | |
} | |
</script> | |
<style scoped="scoped" lang="scss"> | |
.chart { | |
width: 100%; | |
height: 100%; | |
} | |
</style> |
# 图表宽高自适应
这里直接设置为 100%,使用期父元素的宽高,这父元素可以直接使用我们封装的 scss 函数
.chart { | |
width: 100%; | |
height: 100%; | |
} |
# 图表字体、间距等尺寸自适应
- echarts 的字体大小只支持具体数值(像素),不能用百分比或者 vw 等尺寸,一般字体不会去做自适应,如果需要的话,这里可以对字体写一个自适应的处理函数
- 默认情况下,这里以你的设计稿是 1920*1080 为例,即网页宽度是 1920px (做之前一定问清楚 UI 设计稿的尺寸)
- 我把这个函数写在一个单独的工具文件
dataUtil.js里面,在需要的时候调用 - 其原理是计算出当前屏幕宽度和默认设计宽度的比值,将原始的尺寸乘以该值
- 另外,其它 echarts 的配置项,比如间距、定位、边距也可以用该函数
dataUtil.js
/* Echarts 图表字体、间距自适应 */ | |
export const fitChartSize = (size,defalteWidth = 1920) => { | |
let clientWidth = window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth; | |
if (!clientWidth) return size; | |
let scale = (clientWidth / defalteWidth); | |
return Number((size*scale).toFixed(3)); | |
} |
我使用的是 vue 框架,可以将此函数挂载到原型上
import {fitChartSize} from '@src/utils/dataUtil.js' | |
Vue.prototype.fitChartFont = fitChartSize; |
这样你可以在 .vue 文件中直接使用 this.fitChartSize() 调用
# 封装后的图表具体使用
<template> | |
/** 饼图的父元素 **/ | |
<div class="echart-pie-wrap"> | |
<pie-chart :series-data="dataList" :extra-option="extraOption"></pie-chart> | |
</div> | |
</template> | |
<script> | |
import PieChart from "@/components/Chart/PieChart/PieChart.vue" | |
export default { | |
components: { | |
PieChart, | |
}, | |
data() { | |
return { | |
dataList: [ | |
{ | |
name: "西瓜", | |
value: 20, | |
}, | |
{ | |
name: "橘子", | |
value: 13, | |
}, | |
{ | |
name: "杨桃", | |
value: 33, | |
}, | |
], | |
extraOption: { | |
color: ["#fe883a", "#2d90d1", "#f75981", "#90e2a9"], | |
grid: { | |
top: this.fitChartSize(30), | |
right: this.fitChartSize(10), | |
left: this.fitChartSize(20), | |
bottom: this.fitChartSize(20) // 间距自适应 | |
}, | |
textStyle: { | |
color: "green", | |
fontSize: this.fitChartSize(10) // 字体自适应 | |
}, | |
}, | |
}; | |
}, | |
}; | |
</script> | |
<style lang="scss"> | |
.echart-pie-wrap { | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
width: vw(300); | |
height: vh(300); | |
margin: 1vw auto; | |
} | |
</style> |
# 关于图表封装的源码说明
- 在源码中,我用到了 lodash 的一个公共函数 merge, 用它来合并图表的配置项。后续的来源对象属性会覆盖之前同名的属性
- 另外一个用到的函数 isEmpty,当我传入的业务数据为空时,比如空数组 []、undefined、null 时,都会被认为这是一个无数据的情况,这时候我们就展示一个空状态的组件,它可能由一张背景图构成;
- 在绑定到具体的 DOM 元素时,我没有用 querySelector 选择器去选择一个类或者是用 Math.random 生成的 id,因为这两者都不是绝对可靠的,我直接使用当前 vue 示例关联的根 DOM 元素 $el
- 我监听窗口大小的变化,并为这种情况添加对应的事件处理函数 --echarts 自带的 resize 方法,使 echarts 图形不会变形
- 将对应 DOM 的宽高设为 100%,让其大小完全由使用者提供的容器控制
- setOption 方法的第二个参数表示传入的新 option 是否不与之前的旧 option 进行合并,默认居然是 false,即合并。这显然不行,我们需要每次的业务配置都是完全独立的
- 命名非常语义化,一看就懂
- 保留了自己需要单独配置一些定制样式的灵活性,即 extraOption
- 将图表字体自适应函数单独解耦,保持灵活性
# defaultOption.js 应该包括哪些内容?
- 正常情况下暴露一个基础配置
basicOption就可以了 - 如果你需要处理字体和间距大小自适应,请在这里引入
fitChartSize函数
import {fitChartSize} from "@src/utils/dataUtil.js" | |
export const basicOption = { | |
title: { | |
text: "某某手机数据", | |
subtext: "来自xx公司", | |
left: "center", | |
}, | |
tooltip: { | |
trigger: "item", | |
}, | |
legend: { | |
orient: "vertical", | |
left: "left", | |
textStyle: { | |
color: "RGB(254,66,7)", | |
fontSize: fitChartSize(10) // 使用字体自适应 | |
}, | |
}, | |
series: [ | |
{ | |
name: "手机购买占比", | |
type: "pie", | |
radius: "50%", | |
emphasis: { | |
itemStyle: { | |
shadowBlur: 10, | |
shadowOffsetX: 0, | |
shadowColor: "rgba(0, 0, 0, 0.5)", | |
}, | |
}, | |
data: [], // 这里在使用的时候会被业务数据替换 | |
}, | |
], | |
} |
# element-resize-detector 有何作用?
这是一个用于监听 DOM 元素尺寸变化的插件。我们已经对窗口缩放做了监听,但是有时候其父级容器的大小也会动态改变的。 我们对父级容器的宽度进行监听,当父级容器的尺寸发生变化时,echart 能调用自身的 resize 方法,保持视图正常。
当然,这个不适用于 tab 选项卡的情况,在 tab 选项卡中,父级容器从 display:none 到有实际的 clientWidth,可能会比注册一个 resizeDetector 先完成,所以等开始监听父级容器 resize 的时候,可能为时已晚。
解决这个问题,最有效的方法还是在切换选项卡时手动去通过 ref 获取 echart 实例,并手动调用 resize 方法,这是最安全的,也是最有效的。
# 总结
以上就是项目适配的所有思路,可看到,我司采用的适配方案比较简单,原理上还是像素转为 vw,vh。
这种方案虽说不是最完美的,但是很大程度上符合我们的业务需求。
转自:https://juejin.cn/post/7009081081760579591#heading-31