agri-app/pages/control/index.vue

722 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="container">
<!-- 控制设置标题 -->
<view class="control-title">控制设置</view>
<uni-section title="请选择大棚:" titleFontSize="18px" type="line">
<view class="uni-px-5 uni-pb-5">
<uni-data-select v-model="value" :localdata="range" @change="change"></uni-data-select>
</view>
</uni-section>
<uni-section title="实时温湿度" titleFontSize="16px" type="line" v-if="value!== 1">
<template v-slot:right >
{{ temp }}
</template>
<view>
<view class="uni-flex_control uni-row" >
<view class="text uni-flex_control_one uni-view">
<text class="data" :style="fontStyle">{{ liveData.temp1 }}<p v-if=(testNumber(liveData.temp1)) class="tempStyle">℃</p></text>
<text class="data">温度1</text>
</view>
<view class="text uni-flex_control_one uni-view">
<text class="data" :style="fontStyle">{{ liveData.temp2 }}<p v-if=(testNumber(liveData.temp2)) class="tempStyle">℃</p></text>
<text class="data">温度2</text>
</view>
<view class="text uni-flex_control_one uni-view">
<text class="data" :style="fontStyle">{{ liveData.temp3 }}<p v-if=(testNumber(liveData.temp3)) class="tempStyle">℃</p></text>
<text class="data">温度3</text>
</view>
<view class="text uni-flex_control_one uni-view">
<text class="data" :style="fontStyle">{{ liveData.temp4 }}<p v-if=(testNumber(liveData.temp4)) class="tempStyle">℃</p></text>
<text class="data">温度4</text>
</view>
</view>
<view class="uni-flex_control uni-row" >
<view class="text uni-flex_control_two uni-view">
<text class="data" :style="fontStyle">{{ liveData.humi1 }}<p v-if=(testNumber(liveData.humi1)) class="humiStyle"> %RH</p></text>
<text class="data">湿度1</text>
</view>
<view class="text uni-flex_control_two uni-view">
<text class="data" :style="fontStyle">{{ liveData.humi2 }}<p v-if=(testNumber(liveData.humi2)) class="humiStyle"> %RH</p></text>
<text class="data">湿度2</text>
</view>
<view class="text uni-flex_control_two uni-view">
<text class="data" :style="fontStyle">{{ liveData.humi3 }}<p v-if=(testNumber(liveData.humi3)) class="humiStyle"> %RH</p></text>
<text class="data">湿度3</text>
</view>
<view class="text uni-flex_control_two uni-view">
<text class="data" :style="fontStyle">{{ liveData.humi4 }}<p v-if=(testNumber(liveData.humi4)) class="humiStyle"> %RH</p></text>
<text class="data">湿度4</text>
</view>
</view>
</view>
</uni-section>
<uni-section title="设备控制" titleFontSize="16px" type="line" v-if="value!== 1">
<template v-slot:right >
{{ control }}
</template>
<!-- 卷膜/卷被卡片容器2列栅格布局 -->
<view class="card-grid">
<!-- 卷被开卡片 -->
<view class="control-card" @click="handleCardClick(1-status.jbk, 'jbk')">
<view class="card-text">
<text class="card-main">卷被开</text>
<text class="card-sub">{{ show.jbk }}</text>
</view>
<view class="card-icon" :class="{ active: status.jbk === 1 }">
<uni-icons :type=" (status.jbk === 1)?'circle':'circle-filled'" size="24" color="#fff"/>
</view>
</view>
<!-- 卷被关卡片 -->
<view class="control-card" @click="handleCardClick(1-status.jbg,'jbg')">
<view class="card-text">
<text class="card-main">卷被关</text>
<text class="card-sub">{{ show.jbg }}</text>
</view>
<view class="card-icon" :class="{ active: status.jbg === 1 }">
<uni-icons :type=" (status.jbg === 1)?'circle':'circle-filled'" size="24" color="#fff"/>
</view>
</view>
<!-- 卷膜1开卡片 -->
<view class="control-card" @click="handleCardClick(1-status.jm1k, 'jm1k')">
<view class="card-text">
<text class="card-main">卷膜1开</text>
<text class="card-sub">{{ show.jm1k }}</text>
</view>
<view class="card-icon" :class="{ active: status.jm1k === 1 }">
<uni-icons :type="(status.jm1k === 1)?'circle':'circle-filled'" size="24" color="#fff"/>
</view>
</view>
<!-- 卷膜1关卡片 -->
<view class="control-card" @click="handleCardClick(1-status.jm1g, 'jm1g')">
<view class="card-text">
<text class="card-main">卷膜1关</text>
<text class="card-sub">{{ show.jm1g }}</text>
</view>
<view class="card-icon" :class="{ active: status.jm1g === 1 }">
<uni-icons :type="(status.jm1g === 1)?'circle':'circle-filled'" size="24" color="#fff"/>
</view>
</view>
<!-- 卷膜2卡片 -->
<view class="control-card" @click="handleCardClick(1-status.jm2k, 'jm2k')">
<view class="card-text">
<text class="card-main">卷膜2开</text>
<text class="card-sub">{{ show.jm2k }}</text>
</view>
<view class="card-icon" :class="{ active: status.jm2k === 1 }">
<uni-icons :type="(status.jm2k === 1)?'circle':'circle-filled'" size="24" color="#fff"/>
</view>
</view>
<!-- 卷膜2关卡片 -->
<view class="control-card" @click="handleCardClick(1-status.jm2g, 'jm2g')">
<view class="card-text">
<text class="card-main">卷膜2关</text>
<text class="card-sub">{{ show.jm2g }}</text>
</view>
<view class="card-icon" :class="{ active: status.jm2g === 1 }">
<uni-icons :type="(status.jm2g === 1)?'circle':'circle-filled'" size="24" color="#fff"/>
</view>
</view>
<!-- 卷膜3开卡片 -->
<view class="control-card" @click="handleCardClick(1-status.jm3k, 'jm3k')">
<view class="card-text">
<text class="card-main">卷膜3开</text>
<text class="card-sub">{{ show.jm3k }}</text>
</view>
<view class="card-icon" :class="{ active: status.jm3k === 1 }">
<uni-icons :type="(status.jm3k === 1)?'circle':'circle-filled'" size="24" color="#fff"/>
</view>
</view>
<!-- 卷膜3关卡片 -->
<view class="control-card" @click="handleCardClick(1-status.jm3g, 'jm3g')">
<view class="card-text">
<text class="card-main">卷膜3关</text>
<text class="card-sub">{{ show.jm3g }}</text>
</view>
<view class="card-icon" :class="{ active: status.jm3g === 1 }">
<uni-icons :type="(status.jm3g === 1)?'circle':'circle-filled'" size="24" color="#fff"/>
</view>
</view>
</view>
</uni-section>
</view>
</template>
<script>
import mqtt from 'mqtt'
import UniDatetimePicker
from "../../uni_modules/uni-datetime-picker/components/uni-datetime-picker/uni-datetime-picker.vue";
import { findDtuDataByInfo } from "@/api/system/data";
export default {
dicts: ['sys_data_map'],
components: {
UniDatetimePicker
},
data() {
return {
temp: "",
mqttConfig: {
host: '1.94.254.176',
port: 9001,
username: 'admin',
password: 'Admin#12345678',
subscribeTopic:'/up',
},
value: 1,
control: '正在加载中...',
range: [{
"value": '864865085016294',
"text": "十方北棚"
}, {
"value": '864536071808560',
"text": "七方北棚",
}, {
"value": '864865085008135',
"text": "八方北棚"
}],
imei:'',
publishTopic: '/down',
title:'',
message: {},
connected:false,
liveData: {
temp1: '数据加载中...',
temp2: '数据加载中...',
temp3: '数据加载中...',
temp4: '数据加载中...',
humi1: '数据加载中...',
humi2: '数据加载中...',
humi3: '数据加载中...',
humi4: '数据加载中...'
},
// 卡片状态(模拟后端返回数据)
show: {
jbk: "暂停",
jbg: "暂停",
jm1k: "暂停",
jm1g: "暂停",
jm2k: "暂停",
jm2g: "暂停",
jm3k: "暂停",
jm3g: "暂停"
},
deviceType: '',
status: {
jbk: 0,
jbg: 0,
jm1k: 0,
jm1g: 0,
jm2k: 0,
jm2g: 0,
jm3k: 0,
jm3g: 0
},
fontStyle: ''
};
},
onLoad() {
this.title="";
if (this.status.jbk===1) {
this.status.jbg = 0;
}
if (this.status.jbg===1) {
this.status.jbk = 0;
}
if (this.status.jm1k===1) {
this.status.jm1g = 0;
}
if (this.status.jm2k===1) {
this.status.jm2g = 0;
}
if (this.status.jm3k===1) {
this.status.jm3g = 0;
}
if (this.status.jm1g===1) {
this.status.jm1k = 0;
}
if (this.status.jm2g===1) {
this.status.jm2k = 0;
}
if (this.status.jm2g===1) {
this.status.jm3k = 0;
}
},
onShow() {
this.connectMqtt();
},
onUnload() {
this.disconnectMqtt()
},
methods: {
change(e) {
this.imei = e;
this.publishTopic = "dtu/"+this.imei+"/down";
this.mqttConfig.subscribeTopic = "dtu/"+this.imei+"/up";
const selectedItem = this.range.find(item => item.value === e);
if (selectedItem) {
this.selectedText = selectedItem.text; // 获取展示文本
this.title= this.selectedText;
var queryParams = {
imei: this.imei
}
findDtuDataByInfo(queryParams).then(response => {
this.liveData = {
temp1: response.data.temp1 || '已离线..',
temp2: response.data.temp2 || '已离线..',
temp3: response.data.temp3 || '已离线..',
temp4: response.data.temp4 || '已离线..',
humi1: response.data.humi1 || '已离线..',
humi2: response.data.humi2 || '已离线..',
humi3: response.data.humi3 || '已离线..',
humi4: response.data.humi4 || '已离线..'
}
this.temp = "最后更新时间:"+response.data.time;
this.fontStyle = 'font-size:16px;'
})
} else {
this.selectedText = ''; // 无匹配项时清空
this.title='';
this.value=1;
}
this.reset();
this.style="";
// this.disconnectMqtt();
},
reset() {
this.show = {
jbk: "暂停",
jbg: "暂停",
jm1k: "暂停",
jm1g: "暂停",
jm2k: "暂停",
jm2g: "暂停",
jm3k: "暂停",
jm3g: "暂停"
}
this.deviceType = '';
this.status = {
jbk: 0,
jbg: 0,
jm1k: 0,
jm1g: 0,
jm2k: 0,
jm2g: 0,
jm3k: 0,
jm3g: 0
};
this.control = '正在加载中...';
this.message = {};
this.temp = '';
this.liveData = {
temp1: '数据加载中...',
temp2: '数据加载中...',
temp3: '数据加载中...',
temp4: '数据加载中...',
humi1: '数据加载中...',
humi2: '数据加载中...',
humi3: '数据加载中...',
humi4: '数据加载中...'
}
},
// 卡片点击事件(实际项目中调用接口修改状态) 功能标识
handleCardClick(status, type) {
// 校验
// 定义类型与提示文案的映射关系,减少重复代码
const tipMap = {
'jbk': {opposite: 'jbg', name: '卷被关', op: '卷被开'},
'jbg': {opposite: 'jbk', name: '卷被开', op: '卷被关'},
'jm1k': {opposite: 'jm1g', name: '卷膜1关', op: '卷膜1开'},
'jm1g': {opposite: 'jm1k', name: '卷膜1开', op: '卷膜1关'},
'jm2k': {opposite: 'jm2g', name: '卷膜2关', op: '卷膜2开'},
'jm2g': {opposite: 'jm2k', name: '卷膜2开', op: '卷膜2关'},
'jm3k': {opposite: 'jm3g', name: '卷膜3关', op: '卷膜3开'},
'jm3g': {opposite: 'jm3k', name: '卷膜3开', op: '卷膜3关'}
};
// 先判断类型是否在映射表中避免无效case
if (!tipMap[type]) return;
const {opposite, name, op} = tipMap[type];
// 核心校验逻辑(只写一次,无需重复)
if (status === 1 && this.status[opposite] === 1) {
this.$modal.msgError(`${this.selectedText}${name}在运行状态,不能运行${op}操作!`);
// 此处return可保留也可去掉因为函数执行到这里已完成核心逻辑
return;
}
if (!this.connected) {
// 链接mqtt
this.connectMqtt()
}
if (this.value === 1) {
this.$modal.msgError("设备控制失败!");
console.info("大棚选取失败!")
return;
}
uni.showModal({
title: '操作提示:',
content: '确定' + (status === 1 ? "运行" : "暂停") + '【' + this.selectedText + '】设备?',
cancelText: '取消',
confirmText: '确定',
success: (res) => {
if (res.confirm) {
// console.info("操作功能:【"+type+"】,变更状态为:"+ status)
// // 组装消息
this.message = JSON.stringify({[type]: status})
// console.info("指令:"+this.message+";大棚:"+this.publishTopic)
// 控制设备
this.publishMessage();
// 设备回执
this.deviceType = type;
//todo
// this.status[type] = this.status[type] === 0 ? 1 : 0;
// this.show[type] = this.status[type] === 0 ? "运行" : "暂停";
}
}
})
// console.info(this.status)
},
connectMqtt() {
const options = {
clientId: 'uniapp_mqtt_' + Math.random().toString(16).substr(2, 8),
username: this.mqttConfig.username,
password: this.mqttConfig.password,
clean: true,
connectTimeout: 4000,
reconnectPeriod: 1000
}
const url = `ws://${this.mqttConfig.host}:${this.mqttConfig.port}/mqtt`
this.client = mqtt.connect(url, options)
this.client.on('connect', () => {
this.connected = true
this.client.subscribe('dtu/+/up', {qos: 0})
this.addMessage('已连接到MQTT服务器')
console.info(this.client)
})
this.client.on("message", this.ackMessage);
this.client.on('error', (err) => {
this.addMessage(`控制失败: ${err.message}`)
this.connected = false
})
this.client.on('reconnect', () => {
this.addMessage('正在重新连接...')
})
this.client.on('close', () => {
this.addMessage('连接已关闭')
this.connected = false
console.info(this.client)
})
},
disconnectMqtt() {
if (this.client && this.connected) {
this.client.end()
this.connected = false
this.addMessage('已断开MQTT连接')
}
},
publishMessage() {
// console.info("开始发布消息",this.connected,this.publishTopic,this.message)
if (!this.connected || !this.publishTopic || !this.message) {
uni.showToast({
title: '控制设备失败',
icon: 'none'
})
return
}
this.client.publish(this.publishTopic, this.message, (err) => {
if (!err) {
this.addMessage(`【指令已发送】imei: ${this.publishTopic},指令: ${this.message}`);
} else {
this.addMessage(`发布失败: ${err.message},设备:[${this.publishTopic}]`)
}
})
this.message = {};
},
ackMessage(topic, payload) {
// console.info("消息解析:"+topic,payload)
// 1. 先判断是否是目标订阅主题如dtu/xxx/up
if (topic !== this.mqttConfig.subscribeTopic) return;
// 2. 解析消息体(注意异常捕获)
let msgData = {};
try {
msgData = JSON.parse(payload);
} catch (e) {
console.error("消息解析失败:", e);
return;
}
// console.info("回执消息:"+msgData)
// 3. 区分“回执”和“其他内容”
if (msgData.prop && "suc" in msgData) {
// console.info("处理回执消息")
// 👉 这是“指令回执”
this.handleCommandAck(msgData, this.deviceType);
} else {
this.handleOtherContent(msgData,payload)
}
},
addMessage(content) {
console.info("提示消息:" + content)
},
// 处理指令回执的函数
handleCommandAck(ackData, type) {
// console.info("回执消息11111"+ackData)
// 拿到指令字段如jm2k和执行状态suc
const commandField = Object.keys(ackData.prop)[0]; // 这里是"jm2k"
const commandValue = ackData.prop[commandField]; // 这里是0
const isSuccess = ackData.suc; // 这里是true
if (isSuccess) {
this.status[type] = this.status[type] === 0 ? 1 : 0;
this.show[type] = this.status[type] === 0 ? "运行" : "暂停";
}
this.deviceType = '';
this.$modal.msgSuccess("设备操作成功!")
// 业务逻辑:提示“指令执行成功/失败”
console.log(`指令[${commandField}=${commandValue}]执行${isSuccess ? "成功" : "失败"}`);
// 可匹配之前发布的指令msgId更新UI状态
},
// 处理其他内容的函数
handleOtherContent(msgData,payload) {
// 业务逻辑:处理传感器数据、设备状态等
// console.log("收到其他内容:", msgData);
// 例如:更新温湿度显示、设备在线状态等
// 设备状态展示
if (this.value !== 1) {
var arr = ['jbk', "jbg", "jm1k", "jm1g", "jm2k", "jm2g", "jm3k", "jm3g"]
const allKeysNumeric = Object.keys(msgData).some(key => arr.includes(key));
if (allKeysNumeric) {
// console.info(msgData)
this.status = {...msgData}
// 3. 遍历msgData的所有键根据值设置this.show的对应文本
Object.keys(msgData).forEach(key => {
const value = msgData[key];
// 判断值0→暂停1→运行其他值可补充默认值可选
this.show[key] = value === 0 ? '暂停' : '运行';
});
this.control = '最后更新时间:' + this.getCurrentTime();
// console.info("imei: "+this.publishTopic+"copy: ",this.status)
}
const allKeysNumeric2 = Object.keys(msgData).every(key => /^\d+$/.test(key));
if (Object.keys(msgData).length > 0 && allKeysNumeric2) {
// 你的键温度101~104、湿度201~204按你之前约定温度/10、湿度/10
const div10 = (v) => (v == null ? null : Math.round((Number(v)/10)*10)/10)
this.liveData = {
temp1: div10(msgData["201"]) || "已离线...",
humi1: div10(msgData["101"]) || "已离线...",
temp2: div10(msgData["202"]) || "已离线...",
humi2: div10(msgData["102"]) || "已离线...",
temp3: div10(msgData["203"]) || "已离线...",
humi3: div10(msgData["103"]) || "已离线...",
temp4: div10(msgData["204"]) || "已离线...",
humi4: div10(msgData["104"]) || "已离线...",
}
// 调用函数获取并输出格式化后的当前时间
this.temp = "最后更新时间:" + this.getCurrentTime();
this.fontStyle = 'font-size:16px;'
}
}
},
/**
* 获取格式化后的当前时间
* @returns {string} 格式为 YYYY-MM-DD HH:mm:ss 的当前时间
*/
getCurrentTime() {
const now = new Date();
// 获取年、月、日(补零确保两位数)
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0'); // 月份从 0 开始,需 +1
const day = String(now.getDate()).padStart(2, '0');
// 获取时、分、秒(补零确保两位数)
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
// 拼接成标准格式
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
},
testNumber(data) {
const reg = /^-?\d+(\.\d+)?$/;
return reg.test(String(data).trim());
}
},
onHide() {
this.disconnectMqtt();
},
beforeDestroy() {
if (this.client) {
this.client.end()
}
},
};
</script>
<style scoped>
.container {
padding: 20rpx;
background-color: #f5f5f5;
}
/* 控制设置标题 */
.control-title {
font-size: 32rpx;
font-weight: 500;
text-align: center;
margin-bottom: 30rpx;
color: #333;
}
/* 2列栅格布局 */
.card-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20rpx;
padding: 20rpx;
}
/* 卡片样式 */
.control-card {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2rpx 8rpx #bfbec1;
}
/* 卡片文字区域 */
.card-text {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.card-main {
font-size: 30rpx;
color: #333;
font-weight: 500;
}
.card-sub {
font-size: 24rpx;
color: #999;
}
/* 卡片图标容器 */
.card-icon {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background-color: #e5e5e5;
display: flex;
align-items: center;
justify-content: center;
}
/* 激活状态(运行) */
.card-icon.active {
background-color: #007aff;
}
.uni-px-5 {
padding-left: 20rpx;
padding-right: 20rpx;
}
.uni-pb-5 {
padding-bottom: 40rpx;
}
.text {
width: 50rpx;
margin: 10rpx 10rpx 8rpx 0;
padding: 0;
height: 70rpx;
line-height: 70rpx;
text-align: center;
font-size: 26rpx;
box-shadow: 0 2rpx 8rpx #bfbec1
}
.tempStyle,.humiStyle {
display: inline-block;
font-size: 12px;
}
.text:first-child {
margin-left: 10rpx;
}
.uni-view {
-webkit-flex: 1;
flex: 1;
height: 150rpx;
-webkit-justify-content: center;
justify-content: center;
-webkit-align-items: center;
align-items: center;
}
.data {
font-size: 13px;
display: inline-block;
line-height: 23px;
color: #3a3a3a;
}
.data:nth-child(even) {
font-size: 12px;
margin-top: 3px;
color: #9c9c9c;
}
/deep/ .uni-section-header__slot-right {
color: green;
}
</style>