数据可视化大屏小结

一、项目背景

        自己上学校论坛接的一个前端兼职,后面发现是后端师兄自己接的项目,然后不想做前端,再在学校论坛上发布帖子招募前端师弟(that‘s me),硬件是一个装载电线杆上的设备,可以重启光猫、重启摄像头,在UI界面上可以看到全国各个省市该设备的分布及维修情况,并对设备状况做简单的展示,且能对其做重启等操作。

        以下就是后端提供的原型图:

二、最终实现

  •  登录界面

  • 主界面

  • 各个省市界面 (以重庆市为例)

  • 对设备进行操作

 三、项目实现以及难点总结

        技术栈采用vue + datav + echarts + vrouter +vuex +vueAmap(高德插件)+ Element-UI 等实现

  • 自定义组件

        这块在页面上出现多次,可以抽象封装为小组件提高代码复用度,实现上如下:

<template>
  <div style="display: flex; justify-content: space-evenly">
    <p v-for="number in nums" :key="number.id" class="showNumber">
      {{ number.value }}
    </p>
  </div>
</template>

<script>
export default {
  name: "SixNumber",
  props: {
    numbers: {
      type: Number,
    },
  },
  mounted() {
    this.nums = convert2Six(this.numbers);
  },
  data() {
    return {
      nums: [],
    };
  },
  method: {
    convert2Six(number) {
      //将一个数字转换为6位的数字
      const arr = [];
      let count = 0;
      while (number) {
        arr.push(number % 10);
        number = parseInt(number / 10);
        count++;
      }
      //不足6位则补0到6位
      while (count < 6) {
        arr.push(0);
        count++;
      }
      //现在arr里面是逆序的
      return arr.reverse();
    },
  },
};
</script>

<style lang="scss" scoped>
.showNumber {
  @media screen and (max-width: 1440px) and (min-width: 1280px) {
    width: 20px;
    height: 40px;
    font-size: 25px;
    padding-top: 12px;
    margin-top: 10px;
  }
  width: 30px;
  height: 100px;
  color: white;
  background: rgb(15, 224, 250, 0.8);
  padding-left: 3px;
  padding-top: 40px;
  margin-top: 20px;
  margin-bottom: 30px;
  font-size: 45px;
}
</style>

        在外部引入该组件并使用,先定义请求后端的方法,mounted时候请求,将实际的站址数、工单数放入data里定义的相应变量,再传入子组件的props

...
<six-number :number = downCount/>
...

<script>
import SixNumber from "./SixNumber.vue"

export default {
    data() {
        return{
            downCount: 0;
        }
    },
    components: {
        SixNumber
    },

    mounted() {
        //向后端请求数据,放进downCount里,再作为props传入子组件
        this.getData();
    }
}
</script>
  • 地图下钻以及数据联动

        这里请求的高德地图的AMapUI.loadUI来获取地图json数据,再调用后端的数据接口获得对应区域的设备数量,在echarts里进行填充

  遇到显示上一些问题,例如海南三沙群岛太琐碎,就暂时将其屏蔽了。且只有几个省市有数据,则只有那几个省市能触发点击事件

<template>
  <div class="echarts">
    <div style="width:100;height:100%" ref="sctterMap"></div>
    <div class="mapChoose">
      <span v-for="(item,index) in parentInfo" :key="item.cityName">
        <span
          class="title"
          @click="chooseArea(item,index)"
        >{{item.cityName=='全国'?'中国':item.cityName}}</span>
        <span class="icon" v-show="index+1!=parentInfo.length">></span>
      </span>
    </div>
  </div>
</template>

<script>
import echarts from "echarts";
import resize from "./mixins/resize";
import "echarts/map/js/china";
export default {
  name: "sctterMap",
  mixins: [resize],
  data() {
    return {
      myCharts: null,
      geoJson: {
        features: []
      },
      parentInfo: [
        {
          cityName: "全国",
          code: 100000
        }
      ],
      curMapData: []
    };
  },
  //监听vuex里面内容需要通过计算属性套一层
  computed: {
    cityNameComputed() {
      return this.$store.state.city;
    }
  },
  watch: {
    cityNameComputed: function(newCity, oldCity) {
      const cityMap = new Map([
        ["全国", 100000],
        ["广东省", 440000],
        ["重庆市", 500000],
        ["海南省", 460000],
        ["河北省", 130000],
        ["四川省", 510000],
        ["山东省", 370000]
      ]);
      let adCode = cityMap.get(newCity);
      this.getGeoJson(adCode);
      if (
        adCode !== 100000 &&
        this.parentInfo[this.parentInfo.length - 1].code !== adCode
      ) {
        this.parentInfo.push({
          cityName: newCity,
          code: adCode
        });
      }
    }
  },
  mounted() {
    this.getGeoJson(100000);
  },
  methods: {
    getGeoJson(adcode) {
      //填充geoJson
      const adCodeSet = new Set([100000, 440000, 460000, 500000, 510000,130000,370000]);
      if (!adCodeSet.has(adcode)) {
        return;
      }
      let that = this;
      AMapUI.loadUI(["geo/DistrictExplorer"], DistrictExplorer => {
        var districtExplorer = new DistrictExplorer();
        districtExplorer.loadAreaNode(adcode, function(error, areaNode) {
          if (error) {
            console.error(error);
            return;
          }
          let Json = areaNode
            .getSubFeatures()
            .filter(it => it.properties.name !== "三沙市");
          if (Json.length > 0) {
            that.geoJson.features = Json;
          } else if (Json.length === 0) {
            that.geoJson.features = that.geoJson.features.filter(
              item => item.properties.adcode == adcode
            );
            if (that.geoJson.features.length === 0) return;
          }
          that.getMapData();
        });
      });
    },
    getParams() {
      const cityMap = new Map([
        ["全国"],
        ["广东省", 440000],
        ["重庆市", 500000],
        ["海南省", 460000],
        ["河北省", 130000],
        ["四川省", 510000],
        ["山东省", 370000]
      ]);
      let adCode = cityMap.get(this.cityNameComputed);
      let params = {
        adCode: adCode
      };
      return params;
    },
    //获取数据
    getMapData() {
      let params = this.getParams();
      this.$axios.findMonitor(params).then(res => {
        this.curMapData = [];
        if (res.code === 200 && res.message === "success") {
          this.curMapData = res.data.deviceMonitorVoList;
          let mapData = this.geoJson.features.map(item => {
            let value = 0;
            let deviceCountObj = this.curMapData.filter(
              it => it.adCode == item.properties.adcode
            );
            if (deviceCountObj.length) {
              value = deviceCountObj[0].deviceCount;
            }
            return {
              name: item.properties.name,
              value: value,
              cityCode: item.properties.adcode
            };
          });
          mapData = mapData.sort(function(a, b) {
            return b.value - a.value;
          });
          this.initEcharts(mapData);
        }
      });
    },
    initEcharts(mapData) {
      var min = mapData[mapData.length - 1].value;
      var max = mapData[0].value;
      if (mapData.length === 1) {
        min = 0;
      }
      this.myChart = echarts.init(this.$refs.sctterMap);
      echarts.registerMap("Map", this.geoJson); //注册
      let myOption = {
        tooltip: {
          trigger: "item",
          formatter: "设备数量<br />{b}: {c}"
        },
        title: {
          show: false,
          left: "center",
          top: "15",
          text: this.parentInfo[this.parentInfo.length - 1].cityName,
          textStyle: {
            color: "rgb(179, 239, 255)",
            fontSize: 16
          }
        },
        toolbox: {
          feature: {
            restore: {
              show: false
            },
            dataView: {
              optionToContent: function(opt) {
                let series = opt.series[0].data; //折线图数据
                let tdHeads =
                  '<th  style="padding: 0 20px">所在地区</th><th style="padding: 0 20px">IOT设备数量</th>'; //表头
                let tdBodys = ""; //数据
                let table = `<table border="1" style="margin-left:20px;border-collapse:collapse;font-size:14px;text-align:left;"><tbody><tr>${tdHeads} </tr>`;
                for (let i = 0; i < series.length; i++) {
                  table += `<tr>
                              <td style="padding: 0 50px">${series[i].name}</td>
                              <td style="padding: 0 50px">${series[
                                i
                              ].value.toFixed(0)}个</td>
                              </tr>`;
                }
                table += "</tbody></table>";
                return table;
              }
            },
            saveAsImage: {
              name:
                this.parentInfo[this.parentInfo.length - 1].cityName + "地图"
            },
            dataZoom: {
              show: false
            },
            magicType: {
              show: false
            }
          },
          iconStyle: {
            normal: {
              borderColor: "#1990DA"
            }
          },
          top: 15,
          right: 35
        },
        visualMap: {
          min: min,
          max: max,
          right: "3%",
          bottom: "25%",
          calculable: true,
          seriesIndex: [0],
          inRange: {
            color: ["#26cfe4","#26cfe4", "#ec5845"]
          },
          textStyle: {
            color: "#fff"
          }
        },
        series: [
          {
            name: "地图",
            type: "map",
            map: "Map",
            roam: true, //是否可缩放
            zoom: 1.0, //缩放比例
            data: mapData,
            label: {
              normal: {
                show: true,
                textStyle: { color: "#c71585" }, //省份标签字体颜色
                color: "rgb(249, 249, 249)", //省份标签字体颜色
                formatter: p => {
                  const arr = [
                    "新疆维吾尔自治区",
                    "西藏自治区",
                    "青海省",
                    "甘肃省",
                    "云南省",
                    "贵州省",
                    "湖南省",
                    "广西壮族自治区",
                    "山西省",
                    "安徽省",
                    "天津市",
                    "香港特别行政区",
                    "澳门特别行政区",
                    "宁夏回族自治区",
                    "内蒙古自治区",
                    "黑龙江省",
                    "台湾省",
                    "福建省",
                    "江西省",
                    "陕西省",
                    "湖北省",
                    "河南省",
                    "江苏省",
                    "上海市",
                    "浙江省",
                    "北京市",
                    "辽宁省",
                    "吉林省"
                  ];
                  const set = new Set(arr);
                  if (set.has(p.name)) {
                    return "";
                  }
                  const filterMap = new Map([
                    ["酉阳土家族苗族自治县", "酉阳县"],
                    ["彭水苗族土家族自治县", "彭水县"],
                    ["石柱土家族自治县", "石柱县"],
                    ["秀山土家族苗族自治县", "秀山县"],
                    ["渝中区", ""],
                    ["沙坪坝区", ""],
                    ["九龙坡区", ""],
                    ["大渡口区", ""],
                    ["南岸区", ""],
                    ["渝北区", ""],
                    ["江北区", ""],
                    ["昌江黎族自治县", "昌江县"],
                    ["保亭黎族苗族自治县", "保亭县"],
                    ["陵水黎族自治县", "陵水县"],
                    ["琼中黎族苗族自治县", "琼中县"],
                    ["白沙黎族自治县", "白沙县"],
                    ["乐东黎族自治县", "乐东县"]
                  ]);
                  if (filterMap.has(p.name)) {
                    return filterMap.get(p.name);
                  }
                  return p.name;
                }
              },
              emphasis: {
                show: true,
                textStyle: { color: "#800080" },
                color: "#f75a00"
              }
            },
            itemStyle: {
              normal: {
                borderWidth: 0.5, //区域边框宽度
                borderColor: "#009fe8", //区域边框颜色
                areaColor: "#ffffff" //区域颜色
              },
              emphasis: {
                borderWidth: 0.5,
                borderColor: "#4b0082",
                areaColor: "#ffdead"
              }
            }
          }
        ]
      };
      this.myChart.setOption(myOption, true);
      let that = this;
      this.myChart.off("click");
      this.myChart.on("click", params => {
        if (
          that.parentInfo[that.parentInfo.length - 1].code ==
          params.data.cityCode
        ) {
          return;
        }
        let data = params.data;
        const adCodeSet = new Set([100000, 440000, 460000, 500000, 510000,130000,370000]);
        if (!adCodeSet.has(data.cityCode)) {
          return;
        }
        that.parentInfo.push({
          cityName: data.name,
          code: data.cityCode
        });
        let newCity = data.name;
        this.$store.commit("changeCity", newCity);
        that.getGeoJson(data.cityCode);
      });
    },

    //选择切换市县
    chooseArea(val, index) {
      if (this.parentInfo.length === index + 1) {
        return;
      }
      this.parentInfo.splice(index + 1);
      let newCity = this.parentInfo[this.parentInfo.length - 1].cityName;
      this.$store.commit("changeCity", newCity);
      this.getGeoJson(this.parentInfo[this.parentInfo.length - 1].code);
    }
  }
};
</script>
<style lang="scss" scoped>
.echarts {
  width: 100%;
  height: 82%;
  position: relative;
}

.mapChoose {
  position: absolute;
  left: 20px;
  top: 5px;
  color: #eee;
  font-size: 18px;
  .title {
    padding: 5px;
    border-top: 1px solid rgba(147, 235, 248, 0.8);
    border-bottom: 1px solid rgba(147, 235, 248, 0.8);
    cursor: pointer;
  }

  .icon {
    font-family: "simsun";
    font-size: 25px;
    margin: 0 11px;
  }
}
</style>

        数据联动解决: 这里将当前所在区域代码存入了vuex,当点击地图上省份时,或点击回到全国,都先更改vuex里的区域代码,然后在所有组件监听区域代码,发生了变化则重新请求后端数据更新到页面上

  • 设备数据实时更新到页面

        采用的是轮询解决,更优雅的做法应该是通过websocket

  • 表格滚动 以及 表格点击事件

        表格滚动组件采用的是DataV的轮播表,由于全国和省市的表格项有不一样的地方,且点击事件不同,为此在最外层需引入这两个轮播表组件,通过v-if判断当前区域来进行显示

<div class="jc-center body-box deviceTable" v-if="showCountry">
    <dv-scroll-board @click="showProvince" :config="config_country" ref="scrollBoard1" />
</div>

<div class="jc-center body-box deviceTable" v-if="!showCountry">
    <dv-scroll-board @click="showDetail" :config="config_province" ref="scrollBoard2" />
</div>


methods: {
  showProvince(ri) {
      this.$store.commit("changeCity", ri.row[1]);
  },
  showDetail(ri) {
      if (ri.ceil === "设备详情") {
        this.deviceDlg = true;
        this.districtAdCode = ri.row[0];
        console.log(ri.row[1] + "设备详情");
      }
      if (ri.ceil === "告警详情") {
        this.alarmDlg = true;
        this.districtAdCode = ri.row[0];
        console.log(ri.row[1] + "告警详情");
      }
    }

}
  • 多分辨率适配

        老板的笔记本分辨率是720p的,为此采用css媒体查询进行了一套720p分辨率下的适配

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值