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

编程入门 行业动态 更新时间:2024-10-26 22:17:21

<a href=https://www.elefans.com/category/jswz/34/1769682.html style=从零开始搭建4G DTU设备对应的云平台(二)"/>

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

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

一、前端部分

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

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

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

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

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

1、搭建vue开发平台

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

2、编写整体布局

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

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

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

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

<template>
<div id="dashboard"></div>
</template>

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

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

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

<div id="dashboard"><div id="devices"></div><div id="datapanel"></div></div>

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写一个标题:

  <div id="devices"><h3>设备列表</h3></div>

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

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

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

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

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

 <div id="devices"><h3>设备列表</h3><button v-for="device in buttons":key="device":class="['tab-button', { active: currentButton === device }]"@click="currentButton = device">{{device}}</button></div>

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

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

效果是这样子的:

4、数据面板

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

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

<div id="panel"><div id="titleline"></div><hr/><div id="single-field"></div></div>

 布局:

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

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

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

<div id="titleline"><span id="title">设备数据</span><p id="desc">在线</p></div>

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

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

 效果:

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

<div id="panel"><div id="titleline"><span id="title">设备数据</span><p id="desc">在线</p></div><hr/><div id="single-field"><div class="field" v-for="elec in electricData" :key="elec.index"><p v-text="elec.name"></p><p v-text="elec.value"></p><a href="javascript:;" @click="showChart(elec.name,elec.index)">详情 ></a></div></div></div>

整体还是用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布局。将一行平均分成三份,就可以上下对齐了。

整体效果:

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用来存放弹出框:

  <div id="chartBottom" v-show="popup"><div id="chart-container"><div id="picker-container">请选择日期,默认当天:<datepicker id="picker" placeholder="请选择日期,默认当天" v-model="chartDate" :format="format" @closed="showSelectedDateData"></datepicker></div><hr/><div id="chart"></div></div><div class="over" @click="closepopup"></div></div>
</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这个数据,如果设置不正确,弹出来的日期选择框会被遮挡。

效果:

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),"");});}

效果:

二、后端部分

数据库表这个时候已经设计好了,只需要用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<DtuAmmeter>@Select("select createtime,${'$'}{field} from dtu_ammeter where datediff(createtime,#{date})=0 order by createtime")fun getSelectedDateData(field: String,date : String) : List<DtuAmmeter>
}

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

${'$'}。

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

 

新建一个DtuService文件,加上

@Service注解

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

@Service
class DTUService {@Autowiredprivate lateinit var dtuMapper: DTUMapperprivate 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.declaredFieldsval method = DtuAmmeter::class.java.methods.find { m -> m.name.toLowerCase() == "get${field.toLowerCase()}" }var value = 0.0for (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()@Autowiredprivate lateinit var service: DTUService//返回单条最新dtu数据@RequestMapping("latest")@ResponseBodyfun getLatestData(response: HttpServletResponse,request: HttpServletRequest): DtuAmmeter {//avoidNetConflict(response, request)return service.getLatestData()}//返回页面@RequestMapping("dtu")fun getDTUPage() : String{return "dtu/index"}//返回单个字段的图表数据@RequestMapping("field")@ResponseBodyfun getSingleFieldData(@RequestParam("fieldIndex") index :Int,response: HttpServletResponse,request: HttpServletRequest) : List<SingleFieldModel>{//avoidNetConflict(response, request)val fields = DtuAmmeter ::class.java.declaredFieldsif (index > fields.size) throw IndexOutOfBoundsException("下标越界异常")return service.getSingleFieldList(fields[index].name)}//返回特定日期的单个字段的图表数据@RequestMapping("selected")@ResponseBodyfun getSelectedDateData(@RequestParam("date") date : String,@RequestParam("index") index : Int,request: HttpServletRequest,response: HttpServletResponse) : List<SingleFieldModel>{//avoidNetConflict(response, request)println(date)val fields = DtuAmmeter ::class.java.declaredFieldsif (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")}}
}

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

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

 

更多推荐

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

本文发布于:2024-03-06 10:32:37,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1715097.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:从零开始   设备   平台   DTU

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!