首页 前端知识 三维交互可视化平台(智慧海上牧场平台)学习开发Flask Vue Echarts Mysql websocket 实战(四)

三维交互可视化平台(智慧海上牧场平台)学习开发Flask Vue Echarts Mysql websocket 实战(四)

2024-05-10 08:05:15 前端知识 前端哥 56 241 我要收藏

前言

三维交互可视化平台(智慧海上牧场平台)学习开发之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个,&nbsp; &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 "返回值来咯!"

结语

还有一些功能写不下了,下一章再更新,如果觉得思路对你有帮助,请点个赞!

转载请注明出处或者链接地址:https://www.qianduange.cn//article/7825.html
评论
发布的文章

JSON加密的看法

2024-05-14 23:05:47

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!