Vue开发可视化大屏项目解决方案

17,365

本文已参与掘金创作者训练营第三期,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力

前言

在大数据时代,我们经常听到“用数据说话”这句话。但是数据本身只是一个个冰冷的数字,很难直接告诉我们哪些数据是有价值的信息。 只有通过适当的可视化工具来展示和表达数据,才能更直观地向用户传达数据的价值。

现在各行各业逐渐普及化可视化大屏,通过这篇文章,你会全方位了解到可视化大屏项目如何从零到一、让你不再为数据可视化而困扰。

由于近期在做可视化大屏项目,所以结合大部分大屏项目需要的功能,简单写个的大屏项目模板,可供下载使用,不断完善中...也很期待你的加入 k-LargeScreen

起步-如何适配

适配方案

近期我在开始做可视化大屏项目时,查阅了不少关于大屏适配的资料。 方案有很多举些方案:

  1. 手机适配那一套之前做H5混合开发写过一篇,点击速览
    • px转rem
    • 媒体查询
    • viewpoint 视口
  2. 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 */
}

但是当前的项目可能并不适用:

  1. 页面中有其他已经开发有其他的元素有导航文字, 设置fontSize时会影响到页面中已经写好的内容
  2. 页面中使用了 echarts 图表, 里面的参数没办法应用 rem 的比例

媒体查询

媒体查询是比较常见的屏幕适配方案了, 可以根据不同的屏幕大小提供不同的样式方案, 媒体查询可以很好的支持多数的pc端网页布局需要了.

@media only screen and (max-width: 1000px) {
    .div-class {
        width: 720px;
    }
}

但是问题也是比较明显:

  1. 大量书写媒体查询代码, 比较繁琐
  2. 针对多种屏幕进行适配, 也无法保证完全兼容所有的屏幕
  3. 也无法支持 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)

  • sx

    <number>,表示缩放向量的横坐标。

  • sy

    <number> ,表示缩放向量的纵坐标。如果未设置,则他的默认值被设置为 sx。 从而使得元素在保持原有形状下均等缩放

image.png

示例

单一维度缩放

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;
}

image.png

在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;
}

image.png

思路:

1.初始化的时候获得大屏幕的比例
2.把这个比例设置给cssscale变量
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>

可视化大屏项目模板git地址

各种适配方案很多,但是不一定适合你的项目。好的开始就赢了一半,下面是我的一点小建议:

  • 一定要根据自己的明确需求进行筛选,切不可盲目选择。
  • 选择好方案,要适当创新,“取其精华,弃其糟粕。”
  • 相信自己的选择,没有万能的方案,只有无懈可击的你

结合我的项目需求简单说说

需求

业务

  1. 4K大屏项目(7680X2640
  2. 分为左中右三块屏幕划区
  3. 左中右三块中的某个小模块可以单击区域全屏或者全屏

举个栗子:左模块中某一块点击可以左边部分全屏,也可以三屏合一全屏

image.png

4.管理员可调整数据,部分数字双击可编辑。

权限需求

权限比较简单:分为管理员用户两类 管理员可以修改数据对部分模块可以适当调整。

最简单的做法是直接存Vuex或者本地,用的时候调一下完事。

功能实现总结

搭建框架大致应考虑以下几点

  1. 建立登录权限
  2. 针对权限-管理员可修改的可编辑组件
  3. 一屏分三,中屏地图联动
  4. 局部全屏
  5. 全屏切换

Vue2.x+Echarts+百度地图足够用了,使用起来也方便。

结合需求封装一些组件

登录权限系统

简单来说,也就是登录成功后-->调取用户信息-->存进Vuex-->部分功能判断

这一块很简单,因为我这个大屏项目也就分用户和管理员,没了,复杂的可以参考我之前写过一篇

image.png
点击图片无效直通车

其实可以直接把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>

image.png
点击跳转Vue-router命名视图详情

局部全屏

局部全屏的思路:
1.创建store,在里面存下每个命名视图的namemutations里面写个局部切换方法


/** 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-LargeScreenk开头代表快速的意思

k-LargeScreen 如果对你有帮助,请点亮你的小星星⭐⭐⭐哦~(疯狂暗示)

希望能给迷茫的你带去些许帮助

补充(必看)

修复了一些问题,新建了一个Vue2+echarts5大屏不固定宽度和高度的大屏模板,下载即用,后续会完善推出Vue3版本,git地址bigscreen

bigscreen.gif

写在最后

我是凉城a,一个前端,热爱技术也热爱生活。

与你相逢,我很开心。

如果你想了解更多,请点这里,期待你的小⭐⭐

  • 文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注😊

  • 本文首发于掘金,未经许可禁止转载💌