网页端使用主流的vue进行搭建。在element和iview之间选择了iview这个前端框架。从个人角度element对开发者更加友好一些,很多内置的方法、组件都可以自定义实现,而且element社区要比iview活跃的多。那么为什么最终选择了iview呢,因为iview的界面ui比element相对丝滑一些。比较符合我的审美。主要原因还是因为懒惰。去年我已经用iview开发过了一套系统。所以这次就直接复用了之前的用户、权限、机构、角色这些大模块。减少了一部分工作量。当然后续如果有机会肯定是优先选择用element翻新一下。其实从本质上,无论是element还是iview区别并不大,他们都是vue的生态圈相对火热的框架,所以无论选择哪一个都可以。这边就先用iview示范如何搭建PC端。

一、框架搭建

1.安装环境

首先下载node环境,安装nodejshttp://nodejs.cn/

然后打开cmd,输入node -v、npm -v确认node环境是否安装成功

然后配置淘宝镜像源

npm config set registry https://registry.npm.taobao.org
npm config get registry

2.获取源码

从git上拉取https://github.com/iview/iview-admin.git

然后执行npm install、npm run dev

就可以看到iview基本展示页面了

iview-admin已经自带了基本的各种组件,可以很大程度上节省我们时间。缺点也很明显就是引用了过多的冗余组件,致使加载速度降低。当然这些我们都可以自行优化。

3.开发前导

在进行iview开发之前,我们要尽量对iview有足够的了解。建议多看看官网https://www.iviewui.com/docs/introduce

基本上都是即粘即用。大部分的组件都是iview已经为我们写好了,极个别没有的,我们也都可以用简单组件自己实现,所以尽量不是我们去创造组件,而是合理运用已经存在的组件,多去官网查找,复制粘贴才是无望而不利的神器。

二、基础开发

1.去除Eslint校验

iview-admin是内置了eslint的,这种校验对于一开始写代码是不太方便的,当然如果一直开着也可以规范我们的代码风格,我个人比较不喜欢这种约束。一般我都是把它关闭了,最后项目成型后,使用

npm run lint-fix

进行代码修复。在view中只需要在根目录的.eslintignore文件中写上*,就会自动忽略所有格式问题。

2.配置后台服务代理

一般服务都是要进行访问真实的后台接口,我们在使用iview后就尽量前后端分离部署。前台使用nginx部署,后台jar包方式部署。这样前台服务访问后台就需要代理进行搭桥。

在项目根目录的vue.config.js中可以找到devServer的属性,这个就是配置代理的地方

 devServer: {
    	host: 'localhost',
    	port: 8010,
        proxy: {
            '/netgate-server': {
	            // target: 'http://localhost:8001/',
                target: 'http://127.0.0.1:8001/',
	            //pathRewrite: {'^/netgate-server':''},
	            changeOrigin: true
            }
        }
    }

这样当我前台使用netgate-server这个属性的 时候就会自动跳转到http://127.0.0.1:8001/这个后台地址

三、核心组件

1.MQTT通讯组件

etcloud采用的是emq作为broker,前台服务作为一个client,后台服务也是一个client,微信端同样是一个client。通过emq中间件进行消息的流通传递。

首先我们需要引入npm的mqtt模块

npm install mqtt@2.0.0

这里有个小坑,最新的mqtt模块和vue不兼容。因为ts的问题。我也没有过多研究,就直接降低了mqtt的版本。

页面上如何使用呢

<template>
    <Row>
		<Card>
			<p slot="title">
				<Icon type="android-person"></Icon> 设备客户端测试
			</p>
			<div>
				<Row>
					<i-col span="8">
						<Select style="width:200px" placeholder="请选择设备">
							<Option v-for="(item,index) in deviceList" :value="item.sn" :key="index" @click.native="choseDevice(item)">{{ item.sn }}</Option>
						</Select>
						<Button type="success" @click="connectMq">连接</Button>
						<Button type="error"  @click="disConnectMq">断开</Button>
					</i-col>
					<i-col span="16">
						<Row>
							<i-col span="12">
								产品ID:{{curDevice.pid}}
							</i-col>
							<i-col span="12">
								产品TOKEN:{{curDevice.token}}
							</i-col>
						</Row>
						<Row>
							<i-col span="12">
								设备序列号:{{curDevice.sn}}
							</i-col>
							<i-col span="12">
								产品名称:{{curDevice.pname}}
							</i-col>
						</Row>
					</i-col>
				</Row>
			</div>
		</Card>
</template>
<script>
	const uuidv1 = require('uuid/v1');
	import mqtt from 'mqtt'

	const moment = require('moment');
	// 连接选项
	let options = {
		clean: true, // 保留回话
		connectTimeout: 4000, // 超时时间
		// 认证信息
		clientId: 'test_client_'+Math.random(),
		username: 'test',
		password: 'test',
	}
	const connectUrl = 'wss://www.etcloud.club:8084/mqtt'
    export default {
		name: 'mqtt-test',
        data() {
            return {
            	client:{},//mqtt客户端
            }
        },
     created(){
            //页面刚进入时开启长连接
		 	//this.getDeviceList();
        },
     destroyed: function() {
    	 //页面销毁时关闭长连接
			this.client.end();
		 	this.$Message.success('断开成功');
     },
     methods: {
			 connectMq(){
			 	if(JSON.stringify(this.curDevice) == "{}"){
					this.$Message.info('请先选择设备');
			 		return;
				}
			 	if(this.client.connected){
			 		return
				}
			 	options.clientId = this.curDevice.sn;
			 	options.username = this.curDevice.pid;
			 	options.password = this.curDevice.token;
				 this.client = mqtt.connect(connectUrl, options)
				 console.log(this.client)
				 this.client.on('connect', (e) => {
					 this.$Message.success('连接成功');
				 })
				 // this.client.subscribe('/World1234', { qos: 1 }, (error) => {
					//  if (!error) {
					// 	 console.log('订阅成功')
					//  } else {
					// 	 console.log('订阅失败')
					//  }
				 // })
				 // 接收消息处理
				 this.client.on('message', (topic, message) => {
					 console.log('收到来自', topic, '的消息', message.toString())
					 var msgObj = {};
					 new Promise((resolve, reject) => {/* executor函数 */
						 msgObj = JSON.parse(message.toString());
						 resolve(msgObj);
					 }).catch(function (value) {
						 console.log('JSON转化异常')
						 return;
					 });
					 this.sdData.push({time:moment().format("YYYY-MM-DD HH:mm:ss"),topic:topic,content:msgObj})
				 })
				 // 断开发起重连
				 this.client.on('reconnect', (error) => {
					 console.log('正在重连:', error)
				 })
				 // 链接异常处理
				 this.client.on('error', (error) => {
					 console.log('连接失败:', error)
				 })
			 },
			 disConnectMq(){
				 this.sbData = [];
				 this.sdData = [];
				 this.curMsg={
					 topic:'',
					 obj:{}
				 },
				 this.subTopic={};
				 this.subTopicList=[];
				 if(this.client.connected){
					 this.client.end();
				 }
				 // this.curMsg.topic = item.pid+'/'+item.sn+'/client'
				 // this.subTopic.topic =  item.pid+'/'+item.sn+'/server'
				 // this.subTopic.qos = 0
				 this.$Message.success('断开成功');
			 },
		 	//订阅主题
		 	subTopicHandle(){
			 	console.log(this.subTopic)
			 	console.log(this.subTopic.qos)
			 	console.log(this.subTopic.qos=='')
				 if(JSON.stringify(this.curDevice) == "{}"){
					 this.$Message.info('请先选择设备');
					 return;
				 }else if(!this.client.connected){
					 this.$Message.info('请先选连接设备');
					 return;
				 }else if(this.subTopic.topic==''){
					 this.$Message.info('订阅主题不能为空');
					 return;
				 }
				 for(let i=0;i<this.subTopicList.length;i++){
				 	if(this.subTopic.topic ==  this.subTopicList[i].topic){
						this.$Message.info('相同的主题无法订阅两次');
						return;
					}
				 }
				 // else if(this.subTopic.qos==''){
					//  this.$Message.info('订阅消息质量不能为空');
					//  return;
				 // }
				this.client.subscribe(this.subTopic.topic, { qos: this.subTopic.qos }, (error) => {
					if (!error) {
						this.$Message.success('订阅成功');
						this.subTopicList.push({time:moment().format("YYYY-MM-DD HH:mm:ss"),topic:this.subTopic.topic,qos:this.subTopic.qos})
					} else {
						this.$Message.error('订阅失败');
					}
				})

			 },
			
	  },
	}
</script>

2.JsonEditor

可以格式化数据为json格式,因为系统中所有的消息都是采用json格式进行通讯的,所以引入这个组件可以有效的减少json出错率,十分好用

首先我们先引入JsonEditor

npm install jsoneditor --save

接下来我们把JsonEditor封装成一个组件

<!--codemirror-json格式化-->
<template>
  <div class="json-editor">
    <textarea ref="textarea"/>
  </div>
</template>

<script>
  import CodeMirror from 'codemirror'
  import 'codemirror/addon/lint/lint.css'
  import 'codemirror/lib/codemirror.css'
  import 'codemirror/theme/rubyblue.css'
  // require('script-loader!jsonlint')
  import 'codemirror/mode/javascript/javascript'
  // import 'codemirror/addon/lint/lint'
  import 'codemirror/addon/lint/json-lint'

  export default {
    name: 'JsonEditor',
    /* eslint-disable vue/require-prop-types */
    props: ['value'],
    data () {
      return {
        jsonEditor: false
      }
    },
    watch: {
      value (value) {
        const editor_value = this.jsonEditor.getValue()
        if (value !== editor_value) {
          this.jsonEditor.setValue(JSON.stringify(this.value, null, 2))
        }
      }
    },
    mounted () {
      this.jsonEditor = CodeMirror.fromTextArea(this.$refs.textarea, {
        lineNumbers: true,
        mode: 'application/json',
        gutters: ['CodeMirror-lint-markers'],
        theme: 'rubyblue',
        autoRefresh: true,
        lint: true
      })

      this.jsonEditor.setValue(JSON.stringify(this.value, null, 2))
      this.jsonEditor.on('change', cm => {
        this.$emit('changed', cm.getValue())
        this.$emit('input', cm.getValue())
      })
    },
    methods: {
      getValue () {
        return this.jsonEditor.getValue()
      },
      refresh() {
        this.jsonEditor && this.jsonEditor.refresh();
      }
    }
  }
</script>

<style scoped>
  .json-editor {
    height: 100%;
    position: relative;
  }
  .json-editor >>> .CodeMirror {
    height: auto;
    min-height: 180px;
  }
  .json-editor >>> .CodeMirror-scroll {
    min-height: 180px;
  }
  .json-editor >>> .cm-s-rubyblue span.cm-string {
    color: #f08047;
  }
</style>

在页面上调用组件,传入参数即可

import JsonEditor from '@/views/main-components/CodeEditor'

记得声明组件

components: { JsonEditor },

 在页面中调用组件,然后对curMsg.obj传参即可使用

<json-editor style="width: 98%" ref="jsonEditor" v-model="curMsg.obj"/>

效果图

 3.粘贴板组件Clipboard

Clipboard可以实现点击复制某段文字,在我们输入密钥、各种ID的时候十分方便,点击即可复制某个字符串。

安装 

npm install clipboard --save

 引入

import Clipboard from 'clipboard';

页面中使用 

 <Tooltip placement="top">
	<span style="display:block;cursor:pointer;width:100%;overflow: hidden;text-overflow:ellipsis;white-space: nowrap;height:12px;line-height: 15px;" class="tag-read2" :data-clipboard-text="productInfo.token" @click="copy(2)">{{productInfo.token}}</span>
	<div slot="content">
		点击复制
	</div>
</Tooltip>

方法

copy(type) {
	var clipboard = new Clipboard('.tag-read'+type)
	clipboard.on('success', e => {
		this.$Message.success('复制成功');
		// 释放内存
		clipboard.destroy()
	})
	clipboard.on('error', e => {
		// 不支持复制 释放内存
		this.$Message.error('不支持复制');
		clipboard.destroy()
	})
},

效果图

 4.设备树

设备树可以直观的个人所拥有的产品、设备的概况、在线情况、连接日志、指令日志、功能日志……设备树是整个系统的门面和入口,当然后续我也打算做一些Echarts的报表图。

首先在vue的data中声明

gatewaydata: [
	{
		title: '设备列表',
		expand: true,
		render: (h, { root, node, data }) => {
			return h('span', {
				style: {
					display: 'inline-block',
					width: '100%'
				}
			}, [
				h('span', [
					h('Icon', {
						props: {
							type: 'network',
							color: '#2d8cf0',
							size: '15'
						},
						style: {
							marginRight: '8px'
						}
					}),
					h('span', data.title)
				]),

			]);
		},
		children: []
	}
],
buttonProps: {
	type: 'ghost',
	size: 'small',
},

然后再页面中写入树

 <Tree :data="gatewaydata" :render="renderContent" style="overflow-y: auto;overflow-x: hidden " :style="treestyle"></Tree>

 

然后在methods中写明render对树进行渲染

renderContent (h, { root, node, data }) {
	return h('span', {
		style: {
			display: 'inline-block',
			width: '100%'
		}
	}, [
		h('span', [
			h('Icon', {
				props: {
					type: 'record',
					size: '12',
					color: data.color
				},
				style: {
					marginRight: '8px'
				}
			}),
			h('Button', {
				props: Object.assign({}, this.buttonProps, {
					type: data.buttontype,
					inner: data.title
				}),
				style: {
					margin: '-4px 0px 0px 0px'
				},
				on: {
					click: () => {
						this.handleTreeClick(data)
					},
					hover: () => {
						//console.log(11)
					}
				}
			}, data.title)
		])
	]);
},

对于树节点的设备的上线下线只需要更改data中gateawaydata子元素的color即可

if(topic.startsWith('$SYS/brokers/')){
	let status = topicArr[5]
	if(status=='connected'){
		for(let j=0;j<self.gatewaydata[0].children.length;j++){
			if(self.gatewaydata[0].children[j].id==msgObj.username){
				for(let k=0;k<self.gatewaydata[0].children[j].children.length;k++){
					if(self.gatewaydata[0].children[j].children[k].sn==msgObj.clientid){
						self.gatewaydata[0].children[j].children[k].color = '#19be6b';
						return;
					}
				}
			}
		}
	}else if(status=='disconnected'){
		for(let j=0;j<self.gatewaydata[0].children.length;j++){
			if(self.gatewaydata[0].children[j].id==msgObj.username){
				for(let k=0;k<self.gatewaydata[0].children[j].children.length;k++){
					if(self.gatewaydata[0].children[j].children[k].sn==msgObj.clientid){
						self.gatewaydata[0].children[j].children[k].color = '#bbbec4';
						return;
					}
				}
			}
		}
	}
} 

 

四、总结

       其实引用的组件远不止这些,vue还有各种很好玩的组件只要往往给人眼前一亮的感觉,所以要勇于尝试,不过结局如何,过程也会其乐无穷。PC端其实我并没有引用很复杂的东西,大部分功能都是在iview的组件基础上完成的,比如树形结构、比如扩展列、父子组件的传参监听、权限控制路由显示……。但是总体上PC端并不难,这篇文章更多的是一个引子,你可以用不同的方式去实现更酷的界面,更好用的功能。前端的路很精彩!

Logo

前往低代码交流专区

更多推荐