从零开始搭建4G DTU设备对应的云平台(二)

前端使用vue,后端用spring boot + kotlin + mybatis + mysql。

一、前端部分

刚开始前端部分我是用flutter开源项目魔改的,看上去是这样的:

从零开始搭建4G DTU设备对应的云平台(二)_第1张图片

感觉还挺好看,但是最大的问题是,用flutter web编译出来的web文件性能太差了,就这一个页面,js文件足有2M:

从零开始搭建4G DTU设备对应的云平台(二)_第2张图片

后果就是点一下都能直接卡成ppt。

所以这条路线被我放弃了(所以更改的内容我也直接省略了),改用了vue来搭建前端页面。

跟flutter使用现有项目改造不同,vue的界面全部是自己写的。

1、搭建vue开发平台

这个部分网上很多,不再赘述。

2、编写整体布局

样子参照的是这个设备官方的云平台:

从零开始搭建4G DTU设备对应的云平台(二)_第3张图片

也就是:左边是设备列表,右边是数据面板。

创建一个新的vue项目之后,把原来的helloWorld.vue里面的内容删光。

然后根View用一个div,id设置为dashboard:

然后写dashboard的样式,布局采用flex布局,宽高设置为和屏幕一致,从开头开始排列子项目,方向为横向排列:

#dashboard{
  display: flex;
  justify-content: start;
  flex-direction: row;
  width: 100vw;
  height: 100vh;
}

然后是设备列表和数据面板部分,也都是用div,id分别设置为devices,datapanel:

devices宽度设置为固定值200px,内部的布局方式同样是flex,方向为从开头纵向布局,在交叉轴上拉伸子项目,背景色灰色:

#devices{
  width: 200px;
  background-color: gray;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-content: stretch;
}

datapanel背景色浅灰色,宽度不设常量,但是flex-grow设置为1,而devices的flex-grow默认是0,这样当dashboard有多余空间的时候,datapanel就能够占据所有多余空间:

#datapanel{
  background-color: #eee;
  flex-grow: 1;
}

3、设备列表

设备列表使用flex布局加button来实现的,先用h3写一个标题:

  

设备列表

给这个标题一个比较大的上外边距,因为背景色是灰色,所以这个标题的颜色改成白色:

h3{
  margin-bottom: 100px;
  color: white;
}

用v-for来遍历生成三个按键,首先在export default的data里面写一个字符串数组来代表所有的按键,一个变量来代表当前选中的按键:

export default {
  name: 'HelloWorld',
  data () {
    return {
      currentButton : "设备一",
      buttons : ["设备一", "设备二", "设备三"],
    }
  }

}

这里选中的按键背景色和样式要和其他的两个按键不一样,所以这个地方要用不用的class来实现:

 

设备列表

按键的样式,以及选中的按键的样式:

.tab-button{
padding: 10px;
background-color: aqua;
}

.tab-button.active{
  background-color: #eee;
  border-top-left-radius: 10px;
  border-bottom-left-radius: 10px;
  border-right-color: transparent;
}

效果是这样子的:

从零开始搭建4G DTU设备对应的云平台(二)_第4张图片

4、数据面板

数据面板分成了两部分,第一部分是设备的名字和在线提示,第二部分才是所有的数据。

这里用一个div来表示这个整体的面板:


 布局:

#panel{
  width: 100%;
  height: auto;
  box-shadow: 0px 0px 10px black;
  margin: 5px;
  margin-left: 20px;
  background-color: white;
}

 这里加了一点阴影效果,更有层次感。

给titleline里面增加一个标题和在线提示:

设备数据

在线

 在线的背景色设置为绿色:

#titleline{
  height: 50px;
  display: flex;
  flex-direction: row;
  justify-items: flex-start;
  align-items: center;
}
#title{
  font: bold;
  margin: 20px;
}
#desc{
  background: green;
  margin: 5px;
  color: white;
  padding: 2px;
}

 效果:

数据面板显示所有的数据分项,一行两个。

设备数据

在线


整体还是用flex布局,方向为row,flex-wrap设置为wrap,多出来的子项目就会自动排列在下一行,子项目的宽度都设备为50%,就能一行排列两个子项目:

#single-field{
  display: flex;
  flex-wrap: wrap;
  flex-direction: row;
  justify-content: flex-start;
  
}

最后是每个分项,前后没有边框,只有上下有一条淡淡的边框,鼠标放上去之后,背景色改变:

.field{
  /* display: flex;
  flex-direction: row;
  justify-content: space-around;
  align-items: center; */
  display: grid;
  grid-template-columns: repeat(3,33.33%);
  align-items: center;
  width: 50%;
  border-bottom: 1px solid gray;
  
}

.field:hover{
  background-color:#eee;
}

这里的field内部的布局本来是用的flex,但是flex布局的问题是剩余空间会影响view的位置,导致中间的view无法上下对齐,所以这里改用grid布局。将一行平均分成三份,就可以上下对齐了。

整体效果:

从零开始搭建4G DTU设备对应的云平台(二)_第5张图片

5、弹出框

点击每个详情之后,需要弹出一个对话框,显示当前项目的图标,可以选择日期等,所以这里要安装两个库:

npm install echarts -s
npm install vuejs-datepicker -save

上面一个是echarts图表库,后面一个是datepicker日期选择库。

在script里面引入这两个库:

import * as echarts from 'echarts'
import Datepicker from 'vuejs-datepicker';

然后将Datepicker设定为组件:

components:{
    Datepicker
  },

这样就能在html中使用datepicker。

在dashboard里面最后面增加一个div用来存放弹出框:

  
请选择日期,默认当天:

chart就是要显示的图表,over为灰色的底层:

#picker{
  background-color: white;
  z-index: 1001;
  height: 40px;
  border-radius: 0.5em;
}
input{
  border: 1px solid #ccc;
  border-radius: 0.5em;
}
#chart-container{
  height: auto;
  z-index: 1000;
  background-color: white;
  border-radius: 0.5em;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%,-50%);
}

#picker-container{
  display: flex;
  flex-direction: row;
  justify-content: center;
  flex-grow: 0;
  height: 40px;
  z-index: 1001;
  width: 960px;
  background-color: white;
  align-items: center;
}

#chartBottom{
  position: fixed;
  top: 0px;
  left: 0px;
}

#chart {
  font-size: 24px;
  height: 640px;
  width: 960px;
  background-color: white;
  flex-grow: 1;
  /* transform: translate(-50%, -50%); */
  z-index: 1000;
}
.over {
  position: fixed;
  width: 100%;
  height: 100%;
  opacity: 0.7; 
  filter: alpha(opacity=70);
  top: 0;
  left: 0;
  z-index: 999;
  background-color: #111111;
}

这里面最需要注意的是z-index这个数据,如果设置不正确,弹出来的日期选择框会被遮挡。

效果:

从零开始搭建4G DTU设备对应的云平台(二)_第6张图片

6、网络请求

首先在页面加载的时候,初始化echarts组件,然后请求各种分项数据,所以这一部分代码写在vue生命周期的mounted里面:

mounted(){
    fetch("/dtu/latest")
    .then(response => response.json())
    .then(json =>{
      this.electricData = [
        {name: "A 相电压",value : json["voltaA"],index : 3},
        {name: "B 相电压",value : json["voltaB"],index : 4},
        {name: "C 相电压",value : json["voltaC"],index : 5},
        {name: "A 相电流",value : json["currentA"],index : 6},
        {name: "B 相电流",value : json["currentB"],index : 7},
        {name: "C 相电流",value : json["ccurrentC"],index : 8},
        {name: "PA 相有功功率",value : json["powerA"],index : 9},
        {name: "PB 相有功功率",value : json["powerB"],index : 10},
        {name: "PC 相有功功率",value : json["powerC"],index : 11},
        {name: "COSA 相功率因数",value : json["factorA"],index : 12},
        {name: "COSB 相功率因数",value : json["factorB"],index : 13},
        {name: "COSC 相功率因数",value : json["factorC"],index : 14},
        {name: "P 总有功功率",value : json["totalP"],index : 15},
        {name: "Q 总无功功率",value : json["totalQ"],index : 16},
        {name: "三相平均功率因数",value : json["avgFactor"],index : 17},
        {name: "F 频率",value : json["frequeency"],index : 18},
        {name: "正向有功电度",value : json["positiveP"],index : 19},
        {name: "正向无功电度",value : json["positiveQ"],index : 20},
        {name: "反向有功电度",value : json["negativeP"],index : 21},
        {name: "反向无功电度",value : json["negativeQ"],index : 22},
        {name: "A 相无功功率",value : json["aq"],index : 23},
        {name: "B 相无功功率",value : json["bq"],index : 24},
        {name: "C 相无功功率",value : json["cq"],index : 25},
        {name: "A 相视在功率",value : json["sightA"],index : 26},
        {name: "B 相视在功率",value : json["sightB"],index : 27},
        {name: "C 相视在功率",value : json["sightC"],index : 28},
        {name: "S 总视在功率",value : json["totalSight"],index : 29}
      ]
    }
    );
    this.myChart = echarts.init(document.getElementById("chart"));
  }

这里的网络请求使用的是fetch,得到结果后,改变数据列表,界面也随之刷新。

在点击详情后,用fetch来请求分项的图表数据并显示:

showChart(title,index){
      console.log(index);
      this.title = title;
      this.index = index;
      this.popup = 1;
      fetch("/dtu/field",{
        method : "POST",
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: Object.entries({fieldIndex : index}).map(([key, value]) => `${key}=${value}`).join('&')
      }).then(res => res.json())
      .then(json => {
        console.log(json);
        var series = [{name : title,type :'line'}];
        var data = [];
        json.forEach(element => {
          data.push(element.value);
        });
        series[0].data = data;
        this.showEChart(title,series,json.map(r => r.time),"");
      });
    },
showEChart(chart_title, chart_data, chart_time,yName) {
      
      const option = {
        title: {
            text: chart_title
        },
        tooltip: {},
        legend: {
            data: chart_data.map(item => item.name),
            top: 20,
            align: 'right'
        },
        toolbox: {
            show: true,
            feature: {
                dataZoom: {
                    yAxisIndex: "none"
                }
            }
        },
        xAxis: {
            data: chart_time,
            axisLabel: {
                rotate: 90
            }
        },
        yAxis: {
            name : yName
        },
        series: chart_data,
        grid: {
            bottom: 60
        }
      };
      // 使用刚指定的配置项和数据显示图表。
      this.myChart.setOption(option);
    }

在选定了日期后,显示特定日期的数据:

showSelectedDateData(){
      console.log('日期是' + this.chartDate);
      
      fetch("/dtu/selected",{
        method : "POST",
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: Object.entries({index : this.index,date : [this.chartDate.getFullYear(),this.chartDate.getMonth() + 1,this.chartDate.getDate()].join('-')}).map(([key, value]) => `${key}=${value}`).join('&')
      }).then(res => res.json())
      .then(json => {
        console.log(json);
        var series = [{name : this.title,type :'line'}];
        var data = [];
        json.forEach(element => {
          data.push(element.value);
        });
        series[0].data = data;
        this.showEChart(this.title,series,json.map(r => r.time),"");
      });
    }

效果:

从零开始搭建4G DTU设备对应的云平台(二)_第7张图片

二、后端部分

数据库表这个时候已经设计好了,只需要用mybatis-generator插件来导出相应的model类,但是这个插件跟kotlin并不兼容,需要手动把@model注解删掉,然后手动生成一遍getter和setter。

新建一个DtuMapper接口文件,加上

@Repository注解。

来与数据库进行通信:

@Repository
interface DTUMapper {
    @Select("select * from dtu_ammeter order by id desc limit 1")
    fun getLatestData() : DtuAmmeter

    @Select("select createtime,${'$'}{field} from dtu_ammeter where datediff(createtime,now())=0 order by createtime desc limit 100")
    fun getSingleFieldList(field : String) : List

    @Select("select createtime,${'$'}{field} from dtu_ammeter where datediff(createtime,#{date})=0 order by createtime")
    fun getSelectedDateData(field: String,date : String) : List
}

这里需要说明的是mybatis的模板语法也和kotlin不太配,这里${}的语法,kotlin中识别为字符串格式化语法。所以这个$符号要特别写成

${'$'}。

第一个接口是获取当前最新的数据,第二个接口是获取每个分项的图表数据,第三个接口是获取特定日期分项的图表数据,与前端的网络请求相对应。

 

新建一个DtuService文件,加上

@Service注解

来沟通数据源和业务逻辑。

@Service
class DTUService {
    @Autowired
    private lateinit var dtuMapper: DTUMapper

    private val dateFormat = SimpleDateFormat("HH:mm:ss").apply {
        timeZone = TimeZone.getTimeZone("Asia/Shanghai")
    }

    fun getLatestData() = dtuMapper.getLatestData()
    fun getSingleFieldList(field: String) = dtuMapper.getSingleFieldList(field).map { d ->
        mapDtuToSingleField(d,field)
    }

    fun getSelectedDateData(field: String, date: String) = dtuMapper.getSelectedDateData(field, date).map{d -> mapDtuToSingleField(d,field)}
    private fun mapDtuToSingleField(d : DtuAmmeter,field: String) : SingleFieldModel{
        val fields = DtuAmmeter::class.java.declaredFields
        val method = DtuAmmeter::class.java.methods.find { m -> m.name.toLowerCase() == "get${field.toLowerCase()}" }

        var value = 0.0
        for (f in fields) {
            if (f.name == field) value = when (f.type) {
                Integer::class.java -> (method!!.invoke(d) as Int).toDouble()
                java.lang.Long::class.java -> (method!!.invoke(d) as Long).toDouble()
                else -> 0.0
            }
        }
        return SingleFieldModel(
            dateFormat.format(d.createtime),
            value)
    }
}

上面两个接口都是按部就班写的。第三个接口用了反射,避免只能穷举才能处理所有字段数据。

需要注意的是,用插件生成的model类是java写的,所以model里面的类型也是java原生类型,包括int和它的装箱类型Integer,long和Long,Integer :class.java如果写成Int :class.java,就会直接跳过这个分支。

Long:class.java也必须写成全路径形式的java.lang.Long:class.java才能正确识别.

创建一个kotlin类,

DTUController,并且加上
@Controller
@RequestMapping("dtu")

两个注解来接受网络请求:

@Controller
@RequestMapping("dtu")
class DTUController {
    private val jMapper = JsonMapper()

    @Autowired
    private lateinit var service: DTUService

    //返回单条最新dtu数据
    @RequestMapping("latest")
    @ResponseBody
    fun getLatestData(response: HttpServletResponse,request: HttpServletRequest): DtuAmmeter {
        //avoidNetConflict(response, request)
        return service.getLatestData()
    }
    //返回页面
    @RequestMapping("dtu")
    fun getDTUPage() : String{
        return "dtu/index"
    }
    
    //返回单个字段的图表数据
    @RequestMapping("field")
    @ResponseBody
    fun getSingleFieldData(@RequestParam("fieldIndex") index :Int,response: HttpServletResponse,request: HttpServletRequest) : List{
        //avoidNetConflict(response, request)
        val fields = DtuAmmeter ::class.java.declaredFields
        if (index > fields.size) throw IndexOutOfBoundsException("下标越界异常")
        return service.getSingleFieldList(fields[index].name)
    }
    //返回特定日期的单个字段的图表数据
    @RequestMapping("selected")
    @ResponseBody
    fun getSelectedDateData(@RequestParam("date") date : String,
                            @RequestParam("index") index : Int,
                            request: HttpServletRequest,
                            response: HttpServletResponse) : List{
        //avoidNetConflict(response, request)
        println(date)
        val fields = DtuAmmeter ::class.java.declaredFields
        if (index > fields.size) throw IndexOutOfBoundsException("下标越界异常")
        return service.getSelectedDateData(fields[index].name,date)
    }

    //防止前端调试的时候出现跨域问题
    private fun avoidNetConflict(
        response: HttpServletResponse,
        request: HttpServletRequest
    ) {
        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"))
        response.setHeader("Access-Control-Allow-Credentials", "true")
        response.setHeader("P3P", "CP=CAO PSA OUR");
        if (request.getHeader("Access-Control-Request-Method") != null && "OPTIONS" == request.method) {
            response.addHeader("Access-Control-Allow-Methods", "POST,GET,TRACE,OPTIONS")
            response.addHeader("Access-Control-Allow-Headers", "Content-Type,Origin,Accept")
            response.addHeader("Access-Control-Max-Age", "120")
        }
    }
}

所有的方法的作用都在注释里面。 

涉及到单个字段的数据,都是用反射来处理,避免穷举。

 

你可能感兴趣的:(物联网,javascript,css,vue.js,html)