前言
三维交互可视化平台(智慧海上牧场平台)学习开发之Vue(一)
三维交互可视化平台(智慧海上牧场平台)学习开发之Flask+Vue+Mysql(二)
三维交互可视化平台(智慧海上牧场平台)学习开发Flask+Vue+Echarts+Mysql实战(三)
之前已经写了三章了,之前方向本来是三维交互的,现在发现要学习的路程还是比较长,三维交互先放放吧,当前目标就是把基于物联网的设备调试好,然后把整个平台部署到服务器上。基于前几篇的基础上,本篇主要介绍的是websocket双工通讯,以及海康监控摄像头的部署调用。涉及的问题难点就在于当点击左侧选项时路由跳转过去,当回到总览界面,数据显示便丢失了,不仅如此,监控视频画面也是这样,现在就是要解决这个问题,接下来一边编写界面一边找解决办法。
大致框架搭建好了,现在开始填充内容,经过看视频,查资料,要解决数据问题,就是在没关掉路由(例如/overview)之前,overview的界面以及数据一直是保存在后台和前端的,什么意思呢,别着急,请往下看。(备注)
以下可能会缺失一些代码,大家可以去我之前的websocket文章中找思路。
Flask+echarts+mysql+websocket+vue实现前后端分离数据可视化刷新
先展示成果:
智慧渔业养殖系统视频演示
一、后台编写
思路:本质是数据的展示,因此只是调用数据库查询方法给前端使用即可。由于物联网水质监测仪目前还未调试好,因此模拟数据采集到数据库,做一个定时器做数据插入的功能(预计本周可以调试完毕,到时候传感器会定时采集数据传入服务器数据库,和此效果相同,先预留读取接口),然后websocket保持对服务器的读取,查询到数据返回给前端。
db_operate.py
import sys
sys.path.append(r"E:\pycharm2020\projects\platform1.0")
import json
import time
import random
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import pymysql
import threading
from util.ecodings import class_to_dict, Decimal_and_DateEncoder
from util.settings import DevelopmentConfig
pymysql.install_as_MySQLdb()
app = Flask(__name__)
# 读取配置,包含数据库配置
app.config.from_object(DevelopmentConfig)
# 创建数据库sqlalchemy工具对象
db = SQLAlchemy(app)
# Flask从数据库已有表自动生成
# model flask-sqlacodegen "mysql+pymysql://root:root@120.78.94.58:3306/monitor_sys_data" --tables
# monitor_data --outfile "model.py" --flask
class MonitorDatum(db.Model):
__tablename__ = 'monitor_data'
id = db.Column(db.Integer, primary_key=True)
date_time = db.Column(db.DateTime)
water_type = db.Column(db.Float(asdecimal=True))
device_id = db.Column(db.Float(asdecimal=True))
temperature = db.Column(db.Float(asdecimal=True))
ph = db.Column(db.Float(asdecimal=True))
solinity = db.Column(db.Float(asdecimal=True))
dissolved_oxygen = db.Column(db.Float(asdecimal=True))
light = db.Column(db.String(6))
velocity = db.Column(db.String(6))
data_type = db.Column(db.Float(asdecimal=True))
def insert():
print("定时器启动了")
print(threading.current_thread()) # 查看当前线程
record_t = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
temp = round(random.uniform(15, 25), 2) # 产生一个0-40之间的数字,保留两位小数
ph = round(random.uniform(7, 8), 3)
sol = round(random.uniform(100, 150), 1)
dod = round(random.uniform(5, 8), 2)
# 预留光照,如果是传入为字符串,需要将小数转为字符串,再插入数据库
ins = MonitorDatum(date_time=record_t, temperature=temp, ph=ph, solinity=sol, dissolved_oxygen=dod, light='30')
db.session.add(ins)
db.session.commit()
print('插入成功!')
timer = threading.Timer(5, insert) # 在insert函数结束之前我再开启一个定时器
timer.start()
def create():
# 创建所有表
db.create_all()
def drop():
# 删除所有表
db.drop_all()
def query():
# 清空缓存
db.session.commit()
# 查询最近一条数据
# 只有最后加.all()才能读到实例,order_by和limit是条件查询
new = db.session.query(MonitorDatum).order_by(MonitorDatum.id.desc()).limit(1).all()
# [{'temperature': 23.18, 'id': 5, 'record_t': datetime.datetime(2022, 10, 8, 10, 41, 35)}] list
result = class_to_dict(new)
# 取的时间json.dumps无法对字典中的datetime时间格式数据进行转化。因此需要添加特殊日期格式转化
result[0] = json.loads(json.dumps(result[0], cls=Decimal_and_DateEncoder))
# print(result[0]) # {'temperature': 23.18, 'id': 5, 'record_t': '"2022-10-08 10:41:35"'}
# 由于数据库中光照的类型只能为字符型"light": '30',在这里需要把光照转为int型,才能传给前端显示
result[0]['light'] = int(result[0]['light'])
return result[0]
# {"temperature": 23.72, "id": 16, "water_type": null, "solinity": 102.1, "light": 30,
# "data_type": null, "device_id": null, "date_time": "2022-10-17 00:12:37", "ph": 7.731,
# "dissolved_oxygen": 6.97, "velocity": null} 返回是一个字典
if __name__ == '__main__':
# res = query()
# print(res)
# # print(type(res))
# print(res['date_time'])
# print(type(res['date_time']))
# print(res['temperature'])
# print(type(res['temperature']))
# 创建一个定时器,在程序运行在之后我开启一个insert函数
t1 = threading.Timer(5, function=insert) # 第一个参数是时间,例:过5s之后我执行后面的一个函数,开启一个线程
t1.start()
# 控制台运行,5s定时向数据库插入
python db_operate.py
app_db_data.py
import sys
sys.path.append(r"E:\pycharm2020\projects\platform1.0")
from db_manage.db_operate import query
from threading import Lock
from flask import Flask, render_template
from flask_socketio import SocketIO
from flask_cors import CORS
app = Flask(__name__)
CORS(app) # 跨域问题
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO()
socketio.init_app(app, async_mode=None, cors_allowed_origins='*')
thread = None
thread_lock = Lock()
# 后台线程 产生数据,即刻推送至前端
def background_thread():
count = 0
while True:
socketio.sleep(5)
res = query()
# 1个时间,5个水质参数
record_t = res['date_time']
temperature = res['temperature']
ph = res['ph']
sol = res['solinity']
dod = res['dissolved_oxygen']
light = res['light']
socketio.emit('server_response',
{'data': [record_t, temperature, ph, sol, dod, light]},
namespace='/test')
@socketio.on('conn', namespace='/test')
def test_connect():
print('触发')
global thread
with thread_lock:
if thread is None:
thread = socketio.start_background_task(target=background_thread)
if __name__ == '__main__':
socketio.run(app, host='127.0.0.1', port='5000', debug=True)
# 控制台运行,服务器此时会不断地向前端推送数据(1个时间,5个水质参数)
python app_db_data.py
这是程序定时插入的数据库数据,后面开始光照强度是固定“30”,其余的都不需要。
二、前端编写
1.总体架构
vue就不介绍了,之前的文章都有写过,这里挑重要点写:
首先创建好vue项目后,登录啥的我也不写了,直接写Home.vue(主页),包含header(顶部栏)、sidebar(侧边栏)和content(主要内容)3个组件,顶部栏和侧边栏都是固定的,变化的是content,项目结构如下:
本demo只写主界面中的基地总览和水质检测两部分,前端设计的还很烂,慢慢完善吧。
main.js
: 引入jquery、ElementUI、socketio以及echarts
import Vue from 'vue'
import App from './App'
import router from './router'
import $ from 'jquery'
Vue.prototype.$ = $
// 使用element-ui
// 1 引入element-ui样式
import 'element-ui/lib/theme-chalk/index.css'
// 1 引入element-ui所有组件
import ElementUI from 'element-ui'
Vue.use(ElementUI)
import VueSocketIO from 'vue-socket.io'
import socketIO from 'socket.io-client'
Vue.use(new VueSocketIO({
debug: true,
// "ws://域名:端口号/namespace"
connection: socketIO.connect('ws://127.0.0.1:5000/test', {
autoConnect:false
})
}))
import echarts from "echarts";
Vue.prototype.$echarts = echarts;
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
index.js
: 路由跳转
import Vue from 'vue'
import Router from 'vue-router'
import Test from "../views/Test";
Vue.use(Router)
export default new Router({
// component引入有以下两种方式,其中直接写组件名称的需要在最上方引入。
routes: [
{
path: '/test',
name: 'Test',
component: Test
},
{
path: '/home',
name: 'Home',
component: () => import ( "../views/Home.vue"),
children: [
{
path: '/overview',
name: 'Overview',
component: () => import ( "../views/Overview.vue"),
},
{
path: '/monitor',
name: 'Monitor',
component: () => import ( "../views/Monitor.vue"),
},
]
}
]
})
在vue项目目录下安装less样式 npm install -g less
注意在app.vue中也需要引入全局样式global.less
:
@import url('此处为iconfont的引用');
* {
margin: 0;
padding: 0;
}
html,
body,
#app,
.wrapper {
width: 100%;
height: 100%;
overflow: hidden;
}
现在下面这个是分栏:
内容部分,elementIU布局把一行为24份
上部分:分左(监控占比5)右(介绍占比1)。
下部分:flex 5 份作为echarts图表。
上部分:gutter为分栏之间间隔
<el-row :gutter="20">
<!-- 5:1 改成 18:6我感觉更舒服,所以这里就改了-->
<el-col :span="18"><div class="grid-content bg-purple"></div></el-col>
<el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
</el-row>
下部分:挤满,五等份,每份应该是4.8,但不支持小数,因此写5
<el-row type="flex" class="row-bg" justify="space-around">
<el-col :span="5"><div class="grid-content bg-purple"></div><div>图一</div></el-col>
<el-col :span="5"><div class="grid-content bg-purple"></div><div>图二</div></el-col>
<el-col :span="5"><div class="grid-content bg-purple"></div><div>图三</div></el-col>
<el-col :span="5"><div class="grid-content bg-purple"></div><div>图四</div></el-col>
<el-col :span="5"><div class="grid-content bg-purple"></div><div>图五</div></el-col>
</el-row>
</el-row>
2. 4G海康摄像头调用
购买了摄像头之后,我是注册了萤石云,直接走API调用的接口,目前还不清楚如何采用websocket去操控监控的开关,免费版的是有并发上限的,一旦以下函数调用超过3次,就会超过权限:
ezopenInit(value1){
if(value1===true){
var domain = "https://open.ys7.com";
var EZOPENDemo;
window.EZOPENDemo = EZOPENDemo;
EZOPENDemo = new EZUIKit.EZUIKitPlayer({
id: 'playWind',
width: 1300,
height: 600,
template: "pcLive",
url: "ezopen://CHVOUB@open.ys7.com/产品编号/1.live",
accessToken: "自行申请"
});
}else {
// 关闭视频,断开视频连接
}
3.echarts图表
简单说一下,代码可能看不太明白,我封装了几个画图的方法,然后在vue采用websocket读取到数据以后传输到封装好的方法里,就可以。
draw.js
:
import echarts from "echarts";
export function myChart(elementID) {
// 拿到一个实例
return echarts.init(document.getElementById(elementID))
}
export function drawNone(elementID, x1, y1, y2, y3, unit, clazz) {//自己写吧,代码太多了放不下}
export function drawGet(elementID,x1, y1, y2, y3) {//自己写吧,思路就这样}
Overview.vue
:
methods:
socketSendmsg(value2) {
if(value2 === true){
// 开始连接socket
this.$socket.open();
console.log('现在开始发送消息')
// conn 是与后端约定好的名称
this.$socket.emit('conn');
console.log('发送过去了')
var record_t = ["","","","","","","","","",""],
tem = [0,0,0,0,0,0,0,0,0,0],
ph = [0,0,0,0,0,0,0,0,0,0],
sol = [0,0,0,0,0,0,0,0,0,0],
dod = [0,0,0,0,0,0,0,0,0,0],
light = [0,0,0,0,0,0,0,0,0,0]
drawNone('chart1',record_t,tem,tem,tem,'℃','温度')
drawNone('chart2',record_t,ph,ph,ph,' ','PH')
drawNone('chart3',record_t,sol,sol,sol,'%','盐度')
drawNone('chart4',record_t,dod,dod,dod,'mg/L','溶解氧')
drawNone('chart5',record_t,light,light,light,'Lux','光照强度')
// server_response 是前端传过来的信息说明
this.sockets.subscribe('server_response', (res) => {
// 测试打印
console.log(res.data[0]);
console.log(res.data[1]);
console.log(res.data[2]);
console.log(res.data[3]);
console.log(res.data[4]);
console.log(res.data[5]);
// 每次获取到最新数据之后应该是做出对图标的插入操作
record_t.push(res.data[0]);
tem.push(parseFloat(res.data[1]));
ph.push(parseFloat(res.data[2]));
sol.push(parseFloat(res.data[3]));
dod.push(parseFloat(res.data[4]));
light.push(parseFloat(res.data[5]));
if(record_t.length >= 10){
record_t.shift();
tem.shift();
ph.shift();
sol.shift();
dod.shift();
light.shift();
}
drawGet('chart1',record_t,tem,tem,tem)
drawGet('chart2',record_t,ph,ph,ph)
drawGet('chart3',record_t,sol,sol,sol)
drawGet('chart4',record_t,dod,dod,dod)
drawGet('chart5',record_t,light,light,light)
})
}
else {
this.$socket.disconnect();
}
4.主界面完成
往里面添加内容,这里我只运行了查询的后台代码,没有运行插入,因此线都是平的。
5.水质监测部分(侧边栏第二个)
先设计一下,如图所示:
然后页面设计:学到一个新知识点空格占位符3个, ; &ensp; &emsp;
注入逻辑部分:
1.以温度为例,显示默认获取过去7天的数据,每一日的最高值、最低值和平均值都需要计算,我打算这三个值都从数据库语句中计算好拿出来,between的区间就由前端传输给后端,获取的最值平均值再传回给前端,sql语句如下:
– 筛选出2022-10-18日当天的ph最高值、平均值和最低值
最高值:SELECT max(ph) ph FROM monitor_data WHERE date_time between ‘2022-10-18 00:00:00’ and ‘2022-10-18 23:59:59’
在这里我是用的flask-SQLAlchemy
查询得到最大值例子:Max = db.session.query(func.max(temp)).filter(MonitorDatum.date_time.like(f’{start}%')).all()
平均值:SELECT avg(ph) ph FROM monitor_data WHERE date_time between ‘2022-10-18 00:00:00’ and ‘2022-10-18 23:59:59’ ; (这个查询出来是多小数位的,需要后处理为保留小数点两位)
最低值:SELECT min(ph) ph FROM monitor_data WHERE date_time between ‘2022-10-18 00:00:00’ and ‘2022-10-18 23:59:59’
2.读取历史数据查询选择框:Monitor.vue
// template
<el-row default-active="1" class="button_choose">
<!--如果点击温度,传一个公共参数=1,点击ph,公共参数=2,-->
<el-button type="primary" :plain="isPlain[0]" @click="setFlag(0)" v-model="timeAndTem">温度</el-button>
<el-button type="primary" :plain="isPlain[1]" @click="setFlag(1)" v-model="timeAndPh">Ph</el-button>
<el-button type="primary" :plain="isPlain[2]" @click="setFlag(2)" v-model="timeAndSol">盐度</el-button>
<el-button type="primary" :plain="isPlain[3]" @click="setFlag(3)" v-model="timeAndDod">溶氧量</el-button>
<el-button type="primary" :plain="isPlain[4]" @click="setFlag(4)" v-model="timeAndLight">光照强度</el-button>
</el-row>
// js
methods:{
// 按钮组
setFlag(index){
this.isPlain = [true,true,true,true,true]
if(index === 0){
//当index=1,数据是温度,需要调整到温度,缺少这部分代码
//设置温度为非朴素按钮,其他为朴素按钮
this.isPlain[0] = false
this.isPlain[1] = true
this.isPlain[2] = true
this.isPlain[3] = true
this.isPlain[4] = true
}else if (index === 1){
this.isPlain[0] = true
this.isPlain[1] = false
this.isPlain[2] = true
this.isPlain[3] = true
this.isPlain[4] = true
。。。设定}
// 查询按钮
date_query(start,end){
// 获取start和end之间差几天,for循环 for (i=1,i<difDay,i++){从start开始查询}
// 封装好的difference方法,计算两个日期的时间差
let day = difference(start,end)
console.log(day)
let start1 = new Date(start)
let dateTemp = start1.setDate(start1.getDate() + 1 ) //增加1天
let time2 = dateFormat(new Date(dateTemp),'yyyy-MM-dd')
console.log(time2)
//想办法日期做减法,这里得到day=null,这种方法不行,想过!!!!
var date = {"start":start,"end":end,"day":day}
//当点击查询按钮时,
if (this.isPlain[0] === false) {
// console.log('温度测试') //成功
const path = '/api/tem_query';
console.log(path)
axios.post(path,date).then((res) => {
console.log(res.data)
let date = res.data["data"][0]
let max = res.data["data"][1]
let min = res.data["data"][2]
let avg = res.data["data"][3]
draw_date("history_chart",date,max,min,avg,"℃",'温度历史值统计')
})
//这里只写一个if,其他同理
3.后台部分处理前端发送来的post请求,查询到数据中的值返回过去,参考1.的数据库查询逻辑:
@app.route("/tem_query", methods=['POST', 'GET'])
def tem():
if request.method == 'POST':
print('执行')
# 获取vue中传递的值
date = request.get_data() # byte类型
string = date.decode('utf-8', 'ignore') # 转为Str
dic = json.loads(string) # 转为dic
# print(dic)
# dic = {'start': '2022-10-18', 'end': ' 2022-10-24','day':1}
# 现在可以写数据库查询逻辑, dic['day']
get_data = query_three("temperature", dic['start'], dic['day'])
# (['2022-10-20', '2022-10-21'], [24.95, 24.97], [15.39, 15.62], [19.23, 21.1])
print(get_data[0])
res = {"data": get_data}
return res
return "返回值来咯!"
结语
还有一些功能写不下了,下一章再更新,如果觉得思路对你有帮助,请点个赞!