本文已参与掘金创作者训练营第三期,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。
前言
在大数据时代,我们经常听到“用数据说话”这句话。但是数据本身只是一个个冰冷的数字,很难直接告诉我们哪些数据是有价值的信息。 只有通过适当的可视化工具来展示和表达数据,才能更直观地向用户传达数据的价值。
现在各行各业逐渐普及化可视化大屏,通过这篇文章,你会全方位了解到可视化大屏项目如何从零到一、让你不再为数据可视化而困扰。
由于近期在做可视化大屏项目,所以结合大部分大屏项目需要的功能,简单写个的大屏项目模板,可供下载使用,不断完善中...也很期待你的加入 k-LargeScreen
起步-如何适配
适配方案
近期我在开始做可视化大屏项目时,查阅了不少关于大屏适配的资料。 方案有很多举些方案:
- 手机适配那一套(之前做H5混合开发写过一篇,点击速览)
- px转rem
- 媒体查询
- viewpoint 视口
- scale方案
px转rem
px 转 rem
是基于页面的fontSize 设置一个 rem
与 px
换算比例 比如 16px=1rem
, 20px=1rem
根据不同的屏幕设计不同的, 页面fontSize, 在移动端开发中这种方案非常常见.
html { /* 普通状态 */
font-size: 20px
}
html { /* 1.5倍分辨率 */
font-size: 30px
}
html { /* 2倍分辨率 */
font-size: 40px
}
.div-box {
width: 5rem; /* 5倍的font-size 普通状态下 = 100px */
}
但是当前的项目可能并不适用:
- 页面中有其他已经开发有其他的元素有导航文字, 设置
fontSize
时会影响到页面中已经写好的内容 - 页面中使用了
echarts
图表, 里面的参数没办法应用rem
的比例
媒体查询
媒体查询是比较常见的屏幕适配方案了, 可以根据不同的屏幕大小提供不同的样式方案, 媒体查询可以很好的支持多数的pc端网页布局需要了.
@media only screen and (max-width: 1000px) {
.div-class {
width: 720px;
}
}
但是问题也是比较明显:
- 大量书写媒体查询代码, 比较繁琐
- 针对多种屏幕进行适配, 也无法保证完全兼容所有的屏幕
- 也无法支持
echarts
图表中的参数进行适配
viewpoint 视口
viewpoint
基本是目前多数移动端开发都会使用的适配方式, 可以设置对移动端设备的的界面进行整体的缩放, 这种方式的适配是最佳的方案. 但是缺点是很明显的, 只能在移动端进行 viewpoint
适配, 我们目前的数据大屏项目就没办法用了.
<meta name="viewport" content="target-densitydpi=high-dpi" />
关于方案选择
我选择了方案二,也推荐大家选择方案二。可直接写px
,实属方便。
在一些必要的地方配合使用视口单位vw、vh
什么是视口?
在PC端,视口指的是在PC端,指的是浏览器的可视区域;
而在移动端,它涉及3个视口:Layout Viewport(布局视口),Visual Viewport(视觉视口),Ideal Viewport(理想视口)
。
视口单位中的“视口”,PC端指的是浏览器的可视区域;移动端指的就是Viewport中的Layout Viewport。
根据CSS3规范,视口单位主要包括以下4个:
1.vw
:1vw等于视口宽度的1%。
2.vh
:1vh等于视口高度的1%。
3.vmin
:选取vw和vh中最小的那个。
4.vmax
:选取vw和vh中最大的那个。
vh and vw
:相对于视口的高度和宽度,而不是父元素的(CSS百分比是相对于包含它的最近的父元素的高度和宽度)。1vh 等于1/100的视口高度,1vw 等于1/100的视口宽度。
比如:浏览器高度950px,宽度为1920px, 1 vh = 950px/100 = 9.5 px,1vw = 1920px/100 =19.2 px。
vmax
相对于视口的宽度或高度中较大的那个。其中最大的那个被均分为100单位的vmax。
vmin
相对于视口的宽度或高度中较小的那个。其中最小的那个被均分为100单位的vmin。
简单介绍scale方案
scale()
CSS 函数 scale()
用于修改元素的大小。可以通过向量形式定义的缩放值来放大或缩小元素,同时可以在不同的方向设置不同的缩放值。
该变换通过一个二维向量确定在一个方向缩放的多少。如果缩放向量的两个坐标是相等的,那么所讲是均等的,或者说是各向同的,同时元素的形状是被保持的。这种情况下进行的是位似变换。
当坐标值处于区间 [-1, 1]
之外时,该变换将在相应的坐标方向上放大该元素,当处在区间之中时,该变换将在相应的坐标方向上缩小该元素。当值为1时将不进行任何处理,当为负数时,将进行像素点反射之后再进行大小的修改。
scale()
仅适用于在欧几里德平面(二维平面)上的变换。如果需要进行空间中的缩放,必须使用 scale3D()
。
语法
scale(sx)
或
scale(sx, sy)
值
示例
单一维度缩放
HTML
<p>foo</p>
<p class="transformed">bar</p>
CSS
p {
width: 50px;
height: 50px;
background-color: teal;
}
.transformed {
/* 等同于变换: scaleX(2) scaleY(2);*/
transform: scale(2);
background-color: blue;
}
在X和Y两个维度缩放并移动缩放中心
HTML
<p>foo</p>
<p class="transformed">bar</p>
CSS
p {
width: 50px;
height: 50px;
background-color: teal;
}
.transformed {
/* 等同于 scaleX(2) scaleY(0.5) */
transform: scale(2, 0.5);
transform-origin: left;
background-color: blue;
}
思路:
1.初始化的时候获得大屏幕的比例
2.把这个比例设置给css
的scale
变量
3.监听浏览器的窗口大小, 将新的比例赋给scale
变量
这样不管屏幕有多大,分辨率有多高,只要屏幕的比例跟你定的比例一致,就都能适配了
实践
简单实现下:
<div class="ScaleBox"
ref="ScaleBox"
>
mounted() {
this.setScale();
window.addEventListener("resize", this.setScale);
},
methods: {
getScale() {
const { width, height } = this;
let ww = window.innerWidth / width;
let wh = window.innerHeight / height;
return ww < wh ? ww : wh;
},
setScale() {
this.scale = this.getScale();
this.$refs.ScaleBox.style.setProperty("--scale", this.scale);
},
}
#ScaleBox {
--scale: 1;
}
.ScaleBox {
transform: scale(var(--scale)) ;
}
改造成组件
<template>
<div class="bsd-frame" :style="{ background: bgColor }" ref="bsdFrame">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'frame',
props: {
width: Number,
height: Number,
bgColor: {
default: 'rgb(11, 19, 32)'
}
},
data() {
return {
frameWidth: 0,
frameHeight: 0
}
},
methods: {
setSize() {
this.frameWidth = this.width || screen.width
this.frameHeight = this.height || screen.height
const frame = this.$refs.bsdFrame
frame.style.width = this.frameWidth + 'px'
frame.style.height = this.frameHeight + 'px'
},
setScale() {
this.setSize()
const bodyWidth = document.body.clientWidth
const bodyHeight = document.body.clientHeight
const scaleX = bodyWidth / this.frameWidth
const scaleY = bodyHeight / this.frameHeight
this.$refs.bsdFrame.style.transform = `scale(${ scaleX },${ scaleY })`
}
},
mounted() {
this.setSize()
},
destroyed() {
window.removeEventListener('resize', this.setScale)
},
}
</script>
<style lang="scss">
.bsd-frame{
position: fixed;
transform-origin: left top;
}
</style>
各种适配方案很多,但是不一定适合你的项目。好的开始就赢了一半,下面是我的一点小建议:
- 一定要根据自己的明确需求进行筛选,切不可盲目选择。
- 选择好方案,要适当创新,“取其精华,弃其糟粕。”
- 相信自己的选择,没有万能的方案,只有无懈可击的你
结合我的项目需求简单说说
需求
业务
- 4K大屏项目(
7680X2640
) - 分为左中右三块屏幕划区
- 左中右三块中的某个小模块可以单击区域全屏或者全屏
举个栗子:左模块中某一块点击可以左边部分全屏,也可以三屏合一全屏
4.管理员可调整数据,部分数字双击可编辑。
权限需求
权限比较简单:分为管理员和用户两类 管理员可以修改数据对部分模块可以适当调整。
最简单的做法是直接存Vuex
或者本地,用的时候调一下完事。
功能实现总结
搭建框架大致应考虑以下几点
- 建立登录权限
- 针对权限-管理员可修改的可编辑组件
- 一屏分三,中屏地图联动
- 局部全屏
- 全屏切换
Vue2.x+Echarts+百度地图
足够用了,使用起来也方便。
结合需求封装一些组件
登录权限系统
简单来说,也就是登录成功后-->调取用户信息-->存进Vuex-->部分功能判断
这一块很简单,因为我这个大屏项目也就分用户和管理员,没了,复杂的可以参考我之前写过一篇
其实可以直接把Vue-admin
那一块拿过来改改就可以直接用了
针对权限-管理员可修改的可编辑组件
权限搞定了这个组件也好做
写个公共组件
<template>
<div>
<div id="test" class="test" :class="{ editing: isChecked }">
<div class="view">
<!-- 有tooltip -->
<template v-if="value.tooltip&&userType==='admin'">
<!-- 有tooltip可编辑 -->
<label v-if="value.edit" class="titles isEdit" :style="fontsize">
<el-tooltip placement="top">
<div slot="content" class="view-content">
<span>数据说明:</span><span>{{ value.description }}</span>
<br />
<span>更新时间:</span><span>{{ value.updatedOn }}</span>
<br />
<span>操作人:</span><span>{{ value.updatedBy }}</span>
</div>
<span :style="fontsize" @click.meta="dbTest()" @dblclick="dbTest()" class="view-number">{{ value.value | numberToCurrency }}</span>
</el-tooltip>
<i v-if="userType==='admin'" class="el-icon-deit el-icon-edit-outline"></i>
</label>
<!-- 有tooltip不可编辑 -->
<label v-else class="titles">
<el-tooltip placement="top">
<div slot="content" class="view-content">
<span>数据说明:</span><span>{{ value.description }}</span>
<br />
<span>更新时间:</span><span>{{ value.updatedOn }}</span>
<br />
<span>操作人:</span><span>{{ value.updatedBy }}</span>
</div>
<span :style="fontsize">{{ value.value | numberToCurrency }}</span>
</el-tooltip>
</label>
</template>
<!-- 无tooltip -->
<template v-else>
<!-- 无tooltip可编辑 -->
<label v-if="value.edit&&userType==='admin'" class="titles isEdit" :style="fontsize">
<span @click.meta="dbTest()" @dblclick="dbTest()" :style="fontsize" class="view-number">{{ value.value | numberToCurrency }}</span>
<i v-if="userType==='admin'" class="el-icon-deit el-icon-edit-outline"></i>
</label>
<!-- 无tooltip不可编辑 -->
<label v-else class="titles">
<span :style="fontsize">{{ value.value | numberToCurrency }}</span>
</label>
</template>
</div>
<input v-myfoucs="isChecked" class="edit" v-model="inputStr" @blur="blur" @keyup.13="inputStred" />
</div>
</div>
</template>
<script>
//可编辑组件使用---全局组件
//value:{
// edit:false 是否可编辑
// tooltip: true 是否可以看到更新时间(预留功能)
// value:Number
// id:''
//}
// v-on:click.ctrl.exact="dbTest()"
import { mapGetters } from 'vuex'
import { editSurvey } from '@/api/communal'
// import { numberToCurrency } from '@/filters/index'
export default {
name: 'EditText',
props: {
value: {
type: Object,
default: () => { }
},
//数字大小,默认30px
fontsize: {
type: String,
}
},
watch: {},
data() {
return {
item: this.value.value,
isChecked: false,
inputStr: ''
}
},
methods: {
//ctrl+click事件
dbTest() {
this.item = this.value._value || this.value.value
this.isChecked = true
this.inputStr = this.item
},
//失焦事件
blur() {
this.item = this.inputStr
this.isChecked = false
this.item = this.value.value
},
//enter事件
inputStred() {
this.item = this.inputStr
this.isChecked = false
//修改数据
editSurvey(this.value.id, this.inputStr).then(res => {
if (res.success) {
this.$emit('childMsg', { update: true, id: this.value.id, value: this.value.value })
}
})
}
},
// 自定义指令
directives: {
'myfoucs': {
update(el, binding) {
if (binding.value) {
el.focus()
}
}
}
},
computed: {
...mapGetters({
userType: 'userType'
}),
}
}
</script>
<style lang="scss" scoped>
.test.editing .edit {
display: block;
width: 150px;
height: 28px;
}
.test.editing .view {
display: none;
}
.titles {
display: flex;
align-items: center;
justify-content: center;
height: 28px;
font-size: 36px;
font-weight: bold;
color: #00ccff;
}
// .isEdit:hover {
// cursor: pointer;
// span{
// color: #3a45ff
// }
// }
.test .edit {
display: none;
// background: none;
outline: none;
border: none;
margin: 0 auto;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
}
input[type="number"] {
-moz-appearance: textfield;
}
.el-icon-deit {
font-size: 16px;
margin-left: 5px;
}
.view-number {
border: transparent solid 1px;
cursor: cell;
}
.view-number:hover {
border: #00ccff solid 1px;
}
</style>
一屏分三,中屏地图联动、局部全屏
使用Vue-router
的命名视图很容易实现局部全屏index.js
<template>
<Frame>
<div class="platform page-index">
<router-view :name="page_left" style="width:2212px;height:2000px;flex-shrink: 0"></router-view>
<router-view :name="page_middle" style="width:3200px;flex-shrink: 0"></router-view>
<router-view :name="page_right" style="width:2214px;height:2000px;flex-shrink: 0"></router-view>
</div>
</Frame>
</template>
<script>
import Frame from '@/components/Frame'
import { mapGetters } from 'vuex'
export default {
components: { Frame },
data() {
return {
};
},
computed: {
...mapGetters({
page_right: 'page_right',
page_middle: 'page_middle',
page_left: 'page_left',
})
},
watch: {},
methods: {
},
created() {
},
mounted() {
},
}
</script>
<style scoped>
.platform{
display: flex;
}
</style>
局部全屏
局部全屏的思路:
1.创建store
,在里面存下每个命名视图的name
,mutations
里面写个局部切换方法
/** eslint disabled */
import { setSession, getSession } from '@/utils/auth'
const state = {
page_right: getSession('right') || 'right',
page_middle: getSession('middle') || 'middle',
page_left: getSession('left') || 'left',
}
const mutations = {
// 修改页面路由 并记住路由name
SET_PAGE_TYPE(state, { type, value }) {
state['page_' + type] = value
setSession(type, value)
},
RESRT_PAGE_TYPE(state) {
state['page_right'] = 'right'
state['page_middle'] = 'middle'
state['page_left'] = 'left'
setSession('right', 'right')
setSession('middle', 'middle')
setSession('left', 'left')
},
// 修改局部全屏
CHANGE_PAGE(state, data) {
console.log(data);
state[data.page] = data.name
},
}
export default {
namespaced: true,
state,
mutations,
}
2.点击局部全屏调用
<template>
<div class='show'>
<Title title="左侧模块" arrow borderRadius @clickArrow="handleArrow">
</Title>
</div>
</template>
<script>
import Title from '@/components/Title'
export default {
components: {Title},
data() {
return {
};
},
computed: {},
watch: {},
methods: {
// ...mapMutations("vuexTest", ['mutationsHello']),
handleArrow(){
this.$store.commit("moddle/CHANGE_PAGE",{page:'page_left',name:'left_source'});
}
},
created() {
},
mounted() {
},
}
</script>
<style scoped>
.show{
width: 100%;
height: 500px;
background: lightpink;
}
</style>
3.局部全屏返回
<template>
<div class='full-left'>
<Title title="左侧全屏" arrow borderRadius @clickArrow="handleArrow" direction="left"> </Title>
</div>
</template>
<script>
import Title from '@/components/Title'
export default {
components: { Title },
data() {
return {
};
},
computed: {},
watch: {},
methods: {
//大屏
handleArrow() {
this.$store.commit("moddle/CHANGE_PAGE",{page:'page_left',name:'left'});
},
},
created() {
},
mounted() {
},
}
</script>
<style scoped>
</style>
全屏切换
其实全屏切换也很容易实现,换下路由就完事了。理清思路一步一步走。
this.$router.push('')
在router.js
里面新建个路由就好了
import Vue from 'vue'
import Router from 'vue-router'
// import { getToken } from '@/utils/auth'
// import store from '../store'
Vue.use(Router)
const routes = [
{
path: '/',
component: () => import('@/views/moddle/index'),
children: [
{
path: '/',
components: {
middle: () => import('@/views/moddle/middle/Index'),
left: () => import('@/views/moddle/left/Index'),
right: () => import('@/views/moddle/right/Index'),
// 左侧全屏
left_source:()=> import('@/views/moddle/left/full-screen'),
},
},
]
},
// 三屏全屏切换路由
// {
// path: '/',
// component: () => import('@/views/Shcc/index'),
// children: [
// {
// path: '/shcc',
// components: {
// middle: () => import('@/views/Shcc/middle'),
// left: () => import('@/views/Shcc/left'),
// right: () => import('@/views/Shcc/right'),
// },
// },
// ]
// },
]
const Rout = new Router({
// base: process.env.BASE_URL,
routes,
})
// 路由白名单
// const whiteList = ['/Login',]
// Rout.beforeEach(async (to, from, next) => {
// const hasToken = getToken()
// if (hasToken) {
// if (to.path === '/Login') {
// next({ path: '/' })
// } else {
// if (!store.getters.userType) {
// await store.dispatch('user/whoami')
// }
// next()
// }
// } else {
// if (whiteList.indexOf(to.path) !== -1) {
// next()
// } else {
// next(`/Login?redirect=${ to.path }`)
// }
// }
// })
export default Rout
结尾
(此模板是固定宽高情况下使用可)大屏模板可供下载使用,目前还在不断完善中,大家有什么问题可以提出来,也好进一步优化。
完整源项目在gitHub,可以下载直接使用,后续会持续更新
我给起了个名k-LargeScreen以k开头代表快速的意思
k-LargeScreen 如果对你有帮助,请点亮你的小星星⭐⭐⭐哦~(疯狂暗示)
希望能给迷茫的你带去些许帮助
补充(必看)
修复了一些问题,新建了一个Vue2+echarts5
大屏不固定宽度和高度的大屏模板,下载即用,后续会完善推出Vue3
版本,git地址bigscreen
写在最后
我是凉城a,一个前端,热爱技术也热爱生活。
与你相逢,我很开心。
-
文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注😊
-
本文首发于掘金,未经许可禁止转载💌