Appearance
自定义指令 v-click-outside
点击当前元素外部时,会执行传入的函数。一般用于点击其他地方,关闭弹窗或者关闭下拉框,隐藏元素等。
html
<div v-click-outside="hide">
<button @click="open">打开弹窗</button>
<div v-if="show">我是弹窗</div>
</div>
<script>
function open() {
this.show = true
}
function hide() {
this.show = false
}
</script>
js
Vue.directive('clickOutside', {
bind(el, bindings, vnode) {
el.handler = function (e) {
if (!el.contains(e.target)) {
const methed = bindings.expression
vnode.context[methed]()
}
}
document.addEventListener('click', el.handler)
},
unbind(el) {
document.removeEventListener('click', el.handler)
},
})
大屏适配
开发可视化数据大屏如何做自适应?vw vh、rem、scale 到底哪种比较好?
适配方案分析
方案 | 实现方式 | 优点 | 缺点 |
---|---|---|---|
vw + vh(推荐) | 按照设计稿的尺寸,通过 postcss-px-to-viewport 插件,将 px 转换成 vw 和 vh | 1、可以动态变更图表宽高、字体等。2、屏幕比列不一致的时候,不会出现留白。 | 1、每个图表都需要单独设置字体、间距等适配。2、postcss-px-to-viewport 无法把行内样式中的 px 转换,需要自己写方法。 |
scale | 通过 transform: scale 属性,根据屏幕大小,进行整体缩放 | 配置简单、不需要对每个图表进行适配 | 1、当屏幕比例跟设计稿不一样时,可能会出现留白情况。2、弹窗无法对应缩放。3、当缩放比例过大时,图表事件热区会偏移,图表字体会有一点点模糊。4、会导致地图坐标偏移。 |
rem | 通过 postcss-pxtorem 插件,自动将 px 转成 rem | 配置简单、不需要对每个图表进行适配 | 1、当屏幕比例跟设计稿不一样时,可能会出现留白情况。2、每个图表都需要单独设置字体、间距等适配 |
vw、vh 方案
- 通过
postcss-px-to-viewport
插件把 css 文件中的 px 自动转成 vw、vh。
js
// postcss.config.js
module.exports = {
plugins: [
postcssPxToViewport({
unitToConvert: 'px', // 要转化的单位
viewportWidth: 1920, // UI设计稿的宽度
unitPrecision: 6, // 转换后的精度,即小数点位数
propList: [
'width',
'left',
'right',
'margin-left',
'margin-right',
'padding-left',
'padding-right',
], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
viewportUnit: 'vw', // 指定需要转换成的视窗单位,默认vw
fontViewportUnit: 'vw', // 指定字体需要转换成的视窗单位,默认vw
minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
replace: true, // 是否转换后直接更换属性值
exclude: [/node_modules/], // 设置忽略文件,用正则做目录名匹配
landscape: false, // 是否处理横屏情况
}),
postcssPxToViewport({
unitToConvert: 'px', // 要转化的单位
viewportWidth: 1080, // UI设计稿的宽度
unitPrecision: 6, // 转换后的精度,即小数点位数
propList: [
'height',
'line-height',
'top',
'bottom',
'margin-top',
'margin-bottom',
'padding-top',
'padding-bottom',
], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
viewportUnit: 'vh', // 指定需要转换成的视窗单位,默认vw
fontViewportUnit: 'vh', // 指定字体需要转换成的视窗单位,默认vw
minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
replace: true, // 是否转换后直接更换属性值
exclude: [/node_modules/], // 设置忽略文件,用正则做目录名匹配
landscape: false, // 是否处理横屏情况
}),
],
}
- 处理行内样式
js
// 定义设计稿的宽高
const designWidth = 1920
const designHeight = 1080
// px转vw
export const px2vw = (_px) => {
return (_px * 100) / designWidth + 'vw'
}
export const px2vh = (_px) => {
return (_px * 100) / designHeight + 'vh'
}
export const px2font = (_px) => {
return (_px * 100) / designWidth + 'vw'
}
- 处理图表中的文字及偏移等样式
echarts 的字体大小只支持具体数值(像素),不能用百分比或者 vw 等尺寸,一般字体不会去做自适应,当宽高比跟 ui 稿比例出入太大时,会出现文字跟图表重叠的情况。
所以我们利用当前屏幕宽度跟设计稿宽度的比例,去重新计算 px 值。
js
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))
}
// echarts 配置项中使用
const options = {
grid: {
left: fitChartSize(10),
right: fitChartSize(20),
},
textStyle: {
fontSize: fitChartSize(14),
},
}
- 窗口大小变化之后,图表自动调整,利用
element-resize-detector
插件,再加上封装自定义指令,实现自动调用 echarts 的 resize 方法。省得每个图表都写一个监听事件。
js
// directive.js
import * as ECharts from 'echarts'
import elementResizeDetectorMaker from 'element-resize-detector'
// 自定义指令:v-chart-resize 或 v-chart-resize="fn"
Vue.directive('chart-resize', {
bind(el, binding) {
el['_vue_resize_handler'] = binding.value
? binding.value
: () => {
let chart = ECharts.getInstanceByDom(el)
if (!chart) {
return
}
chart.resize()
}
// 监听绑定的div大小变化,更新 echarts 大小
elementResizeDetectorMaker().listenTo(el, el['_vue_resize_handler'])
},
unbind(el) {
// window.removeEventListener("resize", el['_vue_resize_handler']);
elementResizeDetectorMaker().removeListener(el, el['_vue_resize_handler'])
delete el['_vue_resize_handler']
},
})
// <div v-chart-resize class="chart"></div>
这里要注意的是,图表中如果需要 tab 切换动态更新图表数据,在更新数据时一定不要用 echarts 的 dispose 方法先将图表移除,再重新绘制,因为 resize 指令中挂载到的图表实例还是旧的,就监听不到新的 chart 元素的 resize 了,更新数据只需要用 chart 的 setOption 方法重新设置配置项即可。
scale 方案
实现思路:
- 当
屏幕宽高比 < 设计稿宽高比
,我们需要缩放的比例是屏幕宽度 / 设计稿宽度
。 - 当
屏幕宽高比 > 设计稿宽高比
,我们需要缩放的比例是屏幕高度 / 设计稿高度
。 - 如何居中,默认动画的基点是元素中心点,但是我们需要设置成左上角
transform-origin: 0 0;
如果我们拿到的设计稿宽高为: 1920 * 1080
,而我们的屏幕大小是 1440 * 900
,那么 1440/900 = 1.6,1920/1080 = 1.8, 我们需要缩放的比例是:屏幕宽度除以设计稿宽度 = 1440/1920 = 0.75
html
<div class="screen-wrapper">
<div class="screen" id="screen"></div>
</div>
<style>
.screen-root {
height: 100%;
width: 100%;
}
.screen {
display: inline-block;
width: 1920px; /*设计稿的宽度*/
height: 960px; /*设计稿的高度*/
transform-origin: 0 0;
position: absolute;
left: 50%;
top: 50%;
}
</style>
js
export default {
mounted() {
// 初始化自适应 ----在刚显示的时候就开始适配一次
handleScreenAuto()
// 绑定自适应函数 ---防止浏览器栏变化后不再适配
window.onresize = () => handleScreenAuto()
},
destory() {
window.onresize = null
},
methods: {
// 数据大屏自适应函数
handleScreenAuto() {
const designDraftWidth = 1920 //设计稿的宽度
const designDraftHeight = 960 //设计稿的高度
// 根据屏幕的变化适配的比例
const scale =
document.documentElement.clientWidth /
document.documentElement.clientHeight <
designDraftWidth / designDraftHeight
? document.documentElement.clientWidth / designDraftWidth
: document.documentElement.clientHeight / designDraftHeight
// 缩放比例
document.querySelector(
'#screen'
).style.transform = `scale(${scale}) translate(-50%, -50%)`
},
},
}
也可以偷懒用第三方插件 v-scale-screen
rem 方案
动态设置 html 的 font-size
,再利用 postcss-pxtorem
插件把 px 转成 rem
js
;(function init(screenRatioByDesign = 16 / 9) {
let docEle = document.documentElement
function setHtmlFontSize() {
var screenRatio = docEle.clientWidth / docEle.clientHeight
var fontSize =
((screenRatio > screenRatioByDesign
? screenRatioByDesign / screenRatio
: 1) *
docEle.clientWidth) /
10
docEle.style.fontSize = fontSize.toFixed(3) + 'px'
console.log(docEle.style.fontSize)
}
setHtmlFontSize()
window.addEventListener('resize', setHtmlFontSize)
})()
js
// postcss.config.js
module.exports = {
plugins: [
postcssPxToRem({
rootValue: 16,
unitPrecision: 3,
propList: ['font', 'font-size', 'line-height', 'letter-spacing'],
selectorBlackList: [],
replace: true,
mediaQuery: false,
minPixelValue: 0,
exclude: /node_modules/i,
}),
],
}