Compare commits

...

10 Commits

Author SHA1 Message Date
xce 20b808adc7 基础修改 2026-01-08 20:11:32 +08:00
xce 00dd3fe270 小程序调通mq 2026-01-06 22:46:56 +08:00
xce 9b2f94fc5d 开启注册+更换ico 2026-01-06 02:44:57 +08:00
xce d59312b159 微信小程序适配+登录失效直接跳转到首页 2026-01-06 02:19:05 +08:00
xce 2b69bf0d33 mqtt实现全局链接优化 2026-01-06 00:15:34 +08:00
xce cb1e0a4d40 mqtt全局链接 2026-01-05 23:52:27 +08:00
xce f48815d556 代码还原 2026-01-05 21:49:29 +08:00
xce be0492f952 Merge branch 'master' into feasure
# Conflicts:
#	App.vue
2026-01-04 23:58:21 +08:00
lld 77a9977cc6 mqtt 2026-01-01 11:47:14 +08:00
lld 3529b83937 mqtt 2025-12-31 10:53:59 +08:00
16 changed files with 621 additions and 325 deletions

139
App.vue
View File

@ -1,10 +1,71 @@
<script>
import config from './config'
import { getToken } from '@/utils/auth'
import mqttUtil from '@/utils/mqtt'
export default {
globalData: {
config: {},
mqtt: {
hasLogin: false,
token: '',
subscribeList: []
}
},
onLaunch: function() {
// #ifdef H5
// H5/
window.addEventListener('beforeunload', () => {
mqttUtil.disconnectMqtt()
console.log('H5刷新强制断开MQTT连接')
})
// #endif
this.initApp()
mqttUtil.disconnectMqtt()
// ========== H5localStorage ==========
const savedSubscribeList = uni.getStorageSync('mqtt_subscribe_list')
if (savedSubscribeList && savedSubscribeList.length > 0) {
this.globalData.mqtt.subscribeList = savedSubscribeList
}
// ========== 线mqtt ==========
// 2. token
uni.$on('tokenExpired', () => {
console.info("被动token校验")
this.handleTokenExpired()
})
},
onShow() {
//
console.log('小程序切前台/首次显示')
const token = getToken() || this.globalData.mqtt.token
if (token) {
// globalData
if (this.globalData.mqtt.subscribeList.length === 0) {
const savedSubscribeList = uni.getStorageSync('mqtt_subscribe_list')
if (savedSubscribeList && savedSubscribeList.length > 0) {
this.globalData.mqtt.subscribeList = savedSubscribeList
}
}
console.info("clientaasa: ",mqttUtil.getMqttState().client)
if (mqttUtil.getMqttState().client==null) {
console.info("token存在mqtt重新连接中。。")
this.reconnectMqtt()
}
}
},
onHide() {
console.log('小程序切后台')
const mqttState = mqttUtil.getMqttState()
if (mqttState.isConnected) {
this.globalData.mqtt.subscribeList = mqttState.subscribeList
// ========== localStorage ==========
uni.setStorageSync('mqtt_subscribe_list', this.globalData.mqtt.subscribeList)
mqttUtil.disconnectMqtt()
}
},
methods: {
//
@ -15,6 +76,11 @@ export default {
//#ifdef H5
this.checkLogin()
//#endif
const token = getToken()
if (token) {
this.globalData.mqtt.hasLogin = true
this.globalData.mqtt.token = token
}
},
initConfig() {
this.globalData.config = config
@ -23,6 +89,77 @@ export default {
if (!getToken()) {
this.$tab.reLaunch('/pages/login')
}
},
reconnectMqtt() {
const initSuccess = mqttUtil.initMqttConfig()
if (!initSuccess) {
console.error('MQTT配置初始化失败')
return
}
const connectSuccess = mqttUtil.connectMqtt()
if (!connectSuccess) {
console.error('MQTT连接失败')
return
}
const subscribeList = this.globalData.mqtt.subscribeList
if (subscribeList.length > 0) {
mqttUtil.updateSubscribeList(subscribeList)
console.log('恢复MQTT订阅列表', subscribeList)
// ========== localStorage ==========
uni.setStorageSync('mqtt_subscribe_list', subscribeList)
}
},
loginSuccess(token, subscribeList) {
this.globalData.mqtt.hasLogin = true
this.globalData.mqtt.token = token
this.globalData.mqtt.subscribeList = subscribeList || []
// ========== localStorage ==========
uni.setStorageSync('mqtt_subscribe_list', subscribeList || [])
this.reconnectMqtt()
console.log('登录成功MQTT已初始化')
},
logout() {
mqttUtil.disconnectMqtt()
this.globalData.mqtt = {
hasLogin: false,
token: '',
subscribeList: []
}
// ========== localStorage ==========
uni.removeStorageSync('mqtt_subscribe_list')
console.log('登出成功MQTT已断开')
},
// token
handleTokenExpired() {
console.info("被踢下线")
// 1. MQTT
mqttUtil.disconnectMqtt()
// 2. token
this.globalData.mqtt = {
hasLogin: false,
token: '',
subscribeList: []
}
// 3.
uni.showModal({
title: '提示',
content: '登录已过期,请重新登录',
showCancel: false,
success: () => {
//
uni.reLaunch({
url: '/pages/login'
})
}
})
}
}
}
@ -30,4 +167,4 @@ export default {
<style lang="scss">
@import '@/static/scss/index.scss'
</style>
</style>

View File

@ -1,6 +1,6 @@
// 应用全局配置
module.exports = {
baseUrl: process.env.NODE_ENV === "production" ? "/api" : "http://localhost:8088",
baseUrl: process.env.UNI_PLATFORM === 'mp-weixin'?"http://122.51.109.52:8088":'http://localhost:8088',
// 应用信息
appInfo: {
// 应用名称
@ -8,7 +8,7 @@ module.exports = {
// 应用版本
version: "1.2.0",
// 应用logo
logo: "/static/logo.png",
logo: "/static/logo200.png",
// 官方网站
site_url: "http://ruoyi.vip",
// 政策协议

View File

@ -51,6 +51,11 @@
"optimization" : {
"subPackages" : true
},
"lazyCodeLoading" : "requiredComponents",
"networkTimeout" : {
"request" : 60000,
"connectSocket" : 60000
},
"usingComponents" : true
},
"vueVersion" : "2",
@ -65,6 +70,12 @@
"router" : {
"mode" : "hash",
"base" : "/m/"
},
"optimization" : {
"minimize" : true,
"treeShaking" : {
"enable" : true
}
}
}
}

View File

@ -14,47 +14,45 @@
{{ 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_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="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 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">
@ -67,7 +65,11 @@
<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 class="card-sub-wrapper">
<text class="card-sub" v-if="hide">{{ show.jbk }}</text>
<text class="limit-time">运行时间{{ limitTimes.jbk }} s</text>
</view>
</view>
<view class="card-icon" :class="{ active: status.jbk === 1 }">
<uni-icons :type=" (status.jbk === 1)?'circle':'circle-filled'" size="24" color="#fff"/>
@ -78,7 +80,10 @@
<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 class="card-sub-wrapper">
<text class="card-sub" v-if="hide">{{ show.jbg }}</text>
<text class="limit-time">运行时间{{ limitTimes.jbg }} s</text>
</view>
</view>
<view class="card-icon" :class="{ active: status.jbg === 1 }">
<uni-icons :type=" (status.jbg === 1)?'circle':'circle-filled'" size="24" color="#fff"/>
@ -89,7 +94,10 @@
<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 class="card-sub-wrapper">
<text class="card-sub" v-if="hide">{{ show.jm1k }}</text>
<text class="limit-time">运行时间{{ limitTimes.jm1k }} s</text>
</view>
</view>
<view class="card-icon" :class="{ active: status.jm1k === 1 }">
<uni-icons :type="(status.jm1k === 1)?'circle':'circle-filled'" size="24" color="#fff"/>
@ -100,7 +108,10 @@
<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 class="card-sub-wrapper">
<text class="card-sub" v-if="hide">{{ show.jm1g }}</text>
<text class="limit-time">运行时间{{ limitTimes.jm1g }} s</text>
</view>
</view>
<view class="card-icon" :class="{ active: status.jm1g === 1 }">
<uni-icons :type="(status.jm1g === 1)?'circle':'circle-filled'" size="24" color="#fff"/>
@ -111,7 +122,10 @@
<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 class="card-sub-wrapper">
<text class="card-sub" v-if="hide">{{ show.jm2k }}</text>
<text class="limit-time">运行时间{{ limitTimes.jm2k }} s</text>
</view>
</view>
<view class="card-icon" :class="{ active: status.jm2k === 1 }">
<uni-icons :type="(status.jm2k === 1)?'circle':'circle-filled'" size="24" color="#fff"/>
@ -122,7 +136,10 @@
<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 class="card-sub-wrapper">
<text class="card-sub" v-if="hide">{{ show.jm2g }}</text>
<text class="limit-time">运行时间{{ limitTimes.jm2g }} s</text>
</view>
</view>
<view class="card-icon" :class="{ active: status.jm2g === 1 }">
<uni-icons :type="(status.jm2g === 1)?'circle':'circle-filled'" size="24" color="#fff"/>
@ -132,7 +149,10 @@
<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 class="card-sub-wrapper">
<text class="card-sub" v-if="hide">{{ show.jm3k }}</text>
<text class="limit-time">运行时间{{ limitTimes.jm3k }} s</text>
</view>
</view>
<view class="card-icon" :class="{ active: status.jm3k === 1 }">
<uni-icons :type="(status.jm3k === 1)?'circle':'circle-filled'" size="24" color="#fff"/>
@ -143,7 +163,10 @@
<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 class="card-sub-wrapper">
<text class="card-sub" v-if="hide">{{ show.jm3g }}</text>
<text class="limit-time">运行时间{{ limitTimes.jm3g }} s</text>
</view>
</view>
<view class="card-icon" :class="{ active: status.jm3g === 1 }">
<uni-icons :type="(status.jm3g === 1)?'circle':'circle-filled'" size="24" color="#fff"/>
@ -155,12 +178,11 @@
</template>
<script>
import mqtt from 'mqtt'
import UniDatetimePicker
from "../../uni_modules/uni-datetime-picker/components/uni-datetime-picker/uni-datetime-picker.vue";
// ========== mqtt ==========
// import mqtt from 'mqtt' // mqtt
import UniDatetimePicker from "../../uni_modules/uni-datetime-picker/components/uni-datetime-picker/uni-datetime-picker.vue";
import { findDtuDataByInfo } from "@/api/system/data";
import { getMQTTClientInstance, publishMQTT } from '@/utils/mqtt';
import mqttUtil from '@/utils/mqtt'; // MQTT
export default {
dicts: ['sys_data_map'],
@ -169,16 +191,15 @@ export default {
},
data() {
return {
mqttClient: null,
// ========== mqttClient ==========
// mqttClient: null,
temp: "",
// ========== mqttConfig/ ==========
mqttConfig: {
host: '122.51.109.52',
port: 9001,
username: 'admin',
password: 'Admin#12345678',
subscribeTopic:'/up',
},
value: 1,
hide: false,
control: '正在加载中...',
range: [{
"value": '864865085016294',
@ -194,7 +215,8 @@ export default {
publishTopic: '/down',
title:'',
message: {},
connected:false,
// ========== ==========
// connected:false,
liveData: {
temp1: '数据加载中...',
temp2: '数据加载中...',
@ -216,6 +238,17 @@ export default {
jm3k: "暂停",
jm3g: "暂停"
},
//
limitTimes: {
jbk: 30,
jbg: 25,
jm1k: 30,
jm1g: 28,
jm2k: 30,
jm2g: 26,
jm3k: 30,
jm3g: 29
},
deviceType: '',
status: {
jbk: 0,
@ -240,18 +273,25 @@ export default {
['jm3k', 'jm3g']
];
// k=1g=0g=1k=0
// k=1g=0g=1k=0
mutexPairs.forEach(([kKey, gKey]) => {
if (this.status[kKey] === 1) this.status[gKey] = 0;
if (this.status[gKey] === 1) this.status[kKey] = 0;
});
},
onShow() {
this.connectMqtt();
// ========== connectMqtt ==========
// this.connectMqtt();
// MQTT
mqttUtil.setOnMessageCallback(this.ackMessage);
//
this.connected = mqttUtil.getMqttState().isConnected;
},
onUnload() {
this.disconnectMqtt()
// ========== disconnectMqtt ==========
// this.disconnectMqtt()
// MQTT
mqttUtil.removeOnMessageCallback();
},
methods: {
change(e) {
@ -279,17 +319,9 @@ export default {
}
this.reset();
this.style="";
// this.disconnectMqtt();
},
reset() {
/*// 先定义固定的键列表和字面量的键一致
const showKeys = ['jbk', 'jbg', 'jm1k', 'jm1g', 'jm2k', 'jm2g', 'jm3k', 'jm3g'];
// ""
showKeys.forEach(key => {
this.show[key] = "暂停";
});*/
Object.keys(this.show).forEach(key => {
this.show[key] = "暂停";
});
@ -327,13 +359,14 @@ export default {
//
if (status === 1 && this.status[opposite] === 1) {
this.$modal.msgError(`${this.selectedText}${name}在运行状态,不能运行${op}操作!`);
// return
return;
}
// ========== ==========
this.connected = mqttUtil.getMqttState().isConnected;
if (!this.connected) {
// mqtt
this.connectMqtt()
this.$modal.msgError("设备连接异常");
return;
}
if (this.value === 1) {
this.$modal.msgError("设备控制失败!");
@ -347,11 +380,8 @@ export default {
confirmText: '确定',
success: (res) => {
if (res.confirm) {
// console.info(""+type+""+ status)
// //
//
this.message = JSON.stringify({[type]: status})
// console.info(""+this.message+""+this.publishTopic)
//
this.publishMessage();
//
@ -364,83 +394,39 @@ export default {
}
}
})
// 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
}
// ========== connectMqtt ==========
// connectMqtt() { ... },
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连接')
}
},
// ========== disconnectMqtt ==========
// disconnectMqtt() { ... },
// ========== publishMessage ==========
publishMessage() {
// console.info("",this.connected,this.publishTopic,this.message)
if (!this.connected || !this.publishTopic || !this.message) {
uni.showToast({
title: '控制设备失败',
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}]`)
}
})
// MQTT
const publishSuccess = mqttUtil.publishMqtt(this.publishTopic, this.message);
if (publishSuccess) {
this.addMessage(`【指令已发送】imei: ${this.publishTopic},指令: ${this.message}`);
} else {
this.addMessage(`发布失败:设备:[${this.publishTopic}]`)
}
this.message = {};
},
//
ackMessage(topic, payload) {
// console.info(""+topic,payload)
// 1. dtu/xxx/up
if (topic !== this.mqttConfig.subscribeTopic) return;
// console.log(`topic=${topic}message=${payload}`)
// 2.
let msgData = {};
@ -450,24 +436,20 @@ export default {
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)
// jm2ksuc
const commandField = Object.keys(ackData.prop)[0]; // "jm2k"
const commandValue = ackData.prop[commandField]; // 0
@ -479,13 +461,10 @@ export default {
}
this.deviceType = '';
this.$modal.msgSuccess("设备操作成功!")
// /
console.log(`指令[${commandField}=${commandValue}]执行${isSuccess ? "成功" : "失败"}`);
// msgIdUI
},
//
//
handleOtherContent(msgData,payload) {
//
// console.log("", msgData);
@ -496,21 +475,15 @@ export default {
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. msgDatathis.show
Object.keys(msgData).forEach(key => {
const value = msgData[key];
// 01
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 = {
@ -523,11 +496,9 @@ export default {
temp4: div10(msgData["204"]) || "已离线...",
humi4: div10(msgData["104"]) || "已离线...",
}
//
this.temp = "最后更新时间:" + this.getCurrentTime();
this.fontStyle = 'font-size:16px;'
}
}
},
@ -558,15 +529,18 @@ export default {
}
},
onHide() {
this.disconnectMqtt();
// ========== App.vue ==========
// this.disconnectMqtt();
//
mqttUtil.removeOnMessageCallback();
},
beforeDestroy() {
if (this.client) {
this.client.end()
}
// ========== client ==========
// if (this.client) {
// this.client.end()
// }
mqttUtil.removeOnMessageCallback();
},
};
</script>
@ -622,6 +596,23 @@ export default {
color: #999;
}
/* 保留原有样式,仅修改/新增以下部分 */
.card-sub-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
gap: 10rpx;
/* 新增:给容器加最小宽度,确保所有卡片一致 */
min-width: 0;
}
.limit-time {
font-size: 24rpx;
color: #999;
/* 可选:加固定左边距,确保和暂停的间距统一 */
margin-left: 0rpx;
}
/* 卡片图标容器 */
.card-icon {
width: 48rpx;
@ -690,4 +681,4 @@ export default {
/deep/ .uni-section-header__slot-right {
color: green;
}
</style>
</style>

View File

@ -284,6 +284,14 @@ export default {
onUnload() {
//
if (this.client && this.isConnected) {
console.info("mqtt链接已关闭")
this.client.end()
}
},
onHide() {
//
if (this.client && this.isConnected) {
console.info("mqtt链接已关闭")
this.client.end()
}
},

View File

@ -1,6 +1,6 @@
<template>
<view class="content">
<image class="logo" src="@/static/logo.png"></image>
<image class="logo" src="@/static/logo200.png"></image>
<view class="text-area">
<text class="title">Hello Agri</text>
</view>

View File

@ -48,7 +48,7 @@
codeUrl: "",
captchaEnabled: true,
//
register: false,
register: true,
globalConfig: getApp().globalData.config,
loginForm: {
username: "admin",
@ -121,6 +121,21 @@
loginSuccess(result) {
//
this.$store.dispatch('GetInfo').then(res => {
// ========== MQTT ==========
// 1. App
const app = getApp()
// 2. tokenauth
const token = getToken()
// 3.
// /ID
// 864865085016294
// 864536071808560
// 864865085008135
const subscribeList = [`dtu/864865085016294/up`, `dtu/864536071808560/up`,`dtu/864865085008135/up`]
// 4. App.vueloginSuccessMQTT
app.loginSuccess(token, subscribeList)
// ========== ==========
this.$tab.reLaunch('/pages/control/index')
})
}
@ -207,4 +222,4 @@
}
}
</style>
</style>

View File

@ -1,7 +1,7 @@
<template>
<view class="about-container">
<view class="header-section text-center">
<image style="width: 150rpx;height: 150rpx;" src="/static/logo200.png" mode="widthFix">
<image style="width: 160rpx;height: 160rpx;" src="/static/logo200.png" mode="widthFix">
</image>
<uni-title type="h2" title="智能农业移动端"></uni-title>
</view>

View File

@ -49,7 +49,13 @@
},
handleLogout() {
this.$modal.confirm('确定注销并退出系统吗?').then(() => {
this.$store.dispatch('LogOut').then(() => {}).finally(()=>{
this.$store.dispatch('LogOut').then(() => {
// ========== MQTT ==========
// 1. App
const app = getApp()
// 2. App.vuelogoutMQTT+
app.logout()
}).finally(()=>{
this.$tab.reLaunch('/pages/control/index')
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 218 KiB

View File

@ -1,170 +1,288 @@
import mqtt from 'mqtt';
/**
* 智能农业小程序 - MQTT全局工具类uniapp+小程序适配版
* 核心设计
* 1. 配置暂时写死全局后续可替换为登录后从数据字典获取
* 2. clientId仅随机生成无需关联userId保证唯一性即可
* 3. 同步方法为主全局统一管理连接/订阅/发布多页面复用
*/
// 全局变量:封装更完整的状态
let client = {
instance: null, // MQTT客户端实例
connected: false, // 最终连接状态
isConnecting: false, // 连接中状态(防止重复连接)
isManualDisconnect: false // 是否手动断开(区分主动/被动断开)
};
// 引入小程序版MQTT核心库确保mqtt.min.js在utils目录
// 微信小程序端:引入适配小程序的 mqtt 版本
import mqtt from 'mqtt/dist/mqtt'
// ===================== MQTT配置暂时写死TODO后续从数据字典获取=====================
const MQTT_CONFIG = {
server: 'wxs://mq.mj142.cn:443/mqtt', // 替换为你的MQTT服务器地址
username: 'admin', // 替换为通用账号
password: 'Admin#12345678', // 替换为通用密码
clean: true,
host: 'mq.mj142.cn',
port: 443,
reconnectPeriod: 5000, // 重连间隔
connectTimeout: 10000, // 连接超时
keepalive: 60, // 心跳时间
}
// ===================== MQTT全局状态管理 =====================
const mqttState = {
client: null, // MQTT客户端实例
isConnected: false, // 连接状态(同步标记)
subscribeList: [], // 全局订阅列表(登录后赋值)
options: { // MQTT连接选项
clientId: '', // 仅随机生成无需关联userId
...MQTT_CONFIG // 合并基础配置
},
onMessageCallback: null // 全局消息回调(各页面自定义)
}
/**
* 初始化MQTT连接无Promise纯回调
* @param {Object} config 连接配置 {host, port, username, password, clientId, isSSL, keepalive, clean}
* @param {Array} subs 订阅主题列表 [{topic: 'xxx', qos: 0}]
* @param {Function} onSuccess 成功回调 (client) => {}
* @param {Function} onError 失败回调 (err) => {}
* 初始化MQTT配置生成随机clientId无需传入userId
* @returns {Boolean} - 是否配置成功
*/
export function initMQTT(config, subs = [], onSuccess, onError) {
// 1. 已有有效连接:直接返回
if (client.instance && client.connected) {
onSuccess && onSuccess(client.instance);
return client;
export function initMqttConfig() {
if (mqttState.client) {
console.info("重连前强制断开",mqttState.options.clientId)
// 加try-catch避免断开时客户端已异常导致报错
try {
mqttState.client.end(true)
} catch (err) {
console.warn('旧连接断开失败:', err)
}
}
// 2. 正在连接中:防止重复请求
if (client.isConnecting) {
console.info('MQTT已有连接请求在途等待结果...');
// 轮询检查连接状态,直到连接完成/失败
const checkTimer = setInterval(() => {
if (!client.isConnecting) {
clearInterval(checkTimer);
if (client.connected) {
onSuccess && onSuccess(client.instance);
} else {
onError && onError(new Error('MQTT连接请求失败'));
}
}
}, 100);
return;
}
// 3. 参数校验
if (!config.host || !config.port) {
const err = new Error('MQTT配置错误host/port不能为空');
onError && onError(err);
return;
}
// 4. 标记连接中
client.isConnecting = true;
client.isManualDisconnect = false;
// 5. 构建连接参数
const protocol = config.protocol || (config.isSSL ? 'wss' : 'ws');
const url = `${protocol}://${config.host}:${config.port}/mqtt`;
const options = {
clientId: config.clientId || ('uniapp_mqtt_' + Math.random().toString(16).substr(2, 8)),
username: config.username || '',
password: config.password || '',
clean: config.clean !== undefined ? config.clean : true,
connectTimeout: config.connectTimeout || 4000,
reconnectPeriod: config.reconnectPeriod || 1000, // 自动重连间隔(可配置)
keepalive: config.keepalive || 60
};
// 6. 创建客户端并绑定事件
try {
client.instance = mqtt.connect(url, options);
// 连接成功
client.instance.on('connect', () => {
console.info('已连接到MQTT服务器');
client.connected = true;
client.isConnecting = false;
// 订阅主题(带错误处理)
client.instance.subscribe(subs, { qos: 0 }, (err) => {
if (err) {
console.error('MQTT订阅失败', err);
onError && onError(new Error(`订阅失败:${err.message}`));
} else {
console.info(`MQTT订阅成功${subs.map(item => item.topic).join(',')}`);
onSuccess && onSuccess(client.instance);
}
});
});
// 连接错误(核心:处理所有连接失败场景)
client.instance.on('error', (err) => {
console.error('MQTT连接错误', err);
client.connected = false;
client.isConnecting = false;
onError && onError(err);
});
// 连接关闭(被动断开:服务器/网络原因)
client.instance.on('close', () => {
console.info('MQTT连接已关闭');
client.connected = false;
client.isConnecting = false;
// 非手动断开则保留实例(等待自动重连)
if (!client.isManualDisconnect) {
console.info('非手动断开,等待自动重连...');
} else {
client.instance = null; // 手动断开则清空实例
}
});
// 重连中(可选:感知重连状态)
client.instance.on('reconnect', () => {
console.info('MQTT正在重连...');
client.isConnecting = true;
client.connected = false;
});
// 仅随机生成clientId保证唯一性避免连接冲突
mqttState.options.clientId = `wx_mqtt_${Math.random().toString(16).substr(2, 10)}`
// 重置旧状态,避免干扰
mqttState.isConnected = false
mqttState.client = null
console.log('MQTT配置初始化成功clientId', mqttState.options.clientId)
return true
} catch (err) {
console.error('MQTT客户端创建失败', err);
client.isConnecting = false;
client.connected = false;
client.instance = null;
onError && onError(err);
uni.showToast({ title: '设备连接异常', icon: 'none', duration: 2000 })
console.error('MQTT配置初始化失败', err)
return false
}
return client;
}
/**
* 断开MQTT连接无Promise纯回调
* @param {Function} onComplete 完成回调 () => {}
* 建立MQTT连接初始化后调用同步返回触发状态
* @returns {Boolean} - 是否成功触发连接
*/
export function disconnectMQTT(onComplete) {
// 标记为手动断开(避免重连)
client.isManualDisconnect = true;
export function connectMqtt() {
// 前置校验:是否已初始化配置
if (!mqttState.options.clientId) {
uni.showToast({ title: '设备连接异常', icon: 'none' })
console.error('MQTT连接失败请先调用initMqttConfig初始化配置')
return false
}
console.info(mqttState.client!=null,mqttState.client)
// 避免重复连接
if (mqttState.client!=null) {
console.log('MQTT已连接无需重复操作')
return true
}
if (client.instance && client.connected) {
client.instance.end(false, () => { // false不发送will消息
console.info('已断开MQTT连接');
client.connected = false;
client.isConnecting = false;
client.instance = null; // 清空实例
onComplete && onComplete();
});
try {
// 创建客户端实例(同步操作)
// #ifndef MP-WEIXIN
MQTT_CONFIG.server = 'wss://mq.mj142.cn:443/mqtt'
// #endif
console.info("mqttState.connect",mqttState)
mqttState.client = mqtt.connect(MQTT_CONFIG.server, mqttState.options)
console.info("重连中。。")
// 监听核心事件(异步,同步更新状态)
mqttState.client.on('connect', () => {
console.log('MQTT连接成功',mqttState.options.clientId)
mqttState.isConnected = true
// 连接成功后自动订阅全局列表
subscribeAllTopics()
})
mqttState.client.on('close', () => {
console.log('MQTT连接断开')
mqttState.isConnected = false
})
mqttState.client.on('error', (err) => {
uni.showToast({ title: '设备连接异常', icon: 'none' })
console.error('MQTT连接错误', err)
mqttState.isConnected = false
})
// 全局接收消息,转发给页面自定义回调
mqttState.client.on('message', (topic, payload) => {
const message = payload.toString()
// console.log(`收到MQTT消息topic=${topic}message=${message}`)
if (typeof mqttState.onMessageCallback === 'function') {
mqttState.onMessageCallback(topic, message)
}
})
return true
} catch (err) {
uni.showToast({ title: '设备连接异常', icon: 'none' })
console.error('MQTT连接创建失败', err)
mqttState.isConnected = false
return false
}
}
/**
* 设置页面专属消息回调各页面独立处理消息
* @param {Function} callback - (topic, message) => {}
*/
export function setOnMessageCallback(callback) {
if (typeof callback === 'function') {
mqttState.onMessageCallback = callback
console.log('MQTT消息回调已注册')
} else {
// 无有效连接,直接回调
client.connected = false;
client.isConnecting = false;
client.instance = null;
onComplete && onComplete();
console.error('MQTT回调必须是函数类型')
}
}
/**
* 获取MQTT客户端实例
* @returns {Client} MQTT客户端
* 移除消息回调页面卸载时调用避免内存泄漏
*/
export function getMQTTClientInstance() {
return client.instance;
export function removeOnMessageCallback() {
mqttState.onMessageCallback = null
console.log('MQTT消息回调已移除')
}
/**
* 获取MQTT连接状态更精准
* @returns {Object} 完整状态
* 更新全局订阅列表登录后调用自动订阅
* @param {Array} list - 订阅主题列表 ['topic1', 'topic2']
* @returns {Boolean} - 是否更新成功
*/
export function getMQTTStatus() {
export function updateSubscribeList(list) {
if (!Array.isArray(list)) {
uni.showToast({ title: '设备连接异常', icon: 'none' })
console.error('订阅列表必须是数组')
return false
}
mqttState.subscribeList = [...list]
console.log('MQTT订阅列表已更新', list)
// 已连接则立即订阅
if (mqttState.isConnected) {
subscribeAllTopics()
}
return true
}
/**
* 内部方法订阅所有全局主题
*/
function subscribeAllTopics() {
const { isConnected, client, subscribeList } = mqttState
if (!isConnected || !client || subscribeList.length === 0) {
console.warn('MQTT订阅跳过未连接或列表为空')
return
}
client.subscribe(subscribeList, { qos: 0 }, (err) => {
if (err) {
uni.showToast({ title: '设备连接异常', icon: 'none' })
console.error('MQTT订阅失败', err)
} else {
console.log(`MQTT成功订阅${subscribeList.join(', ')}`)
}
})
}
/**
* 发布MQTT消息同步调用
* @param {String} topic - 发布主题
* @param {String/Object} message - 发布内容
* @returns {Boolean} - 是否触发发布成功
*/
export function publishMqtt(topic, message) {
const { isConnected, client } = mqttState
if (!isConnected || !client) {
uni.showToast({ title: '控制异常', icon: 'none' })
console.error('MQTT发布失败未连接')
return false
}
if (!topic) {
uni.showToast({ title: '控制异常', icon: 'none' })
console.error('MQTT发布失败主题为空')
return false
}
try {
const msg = typeof message === 'object' ? JSON.stringify(message) : String(message)
client.publish(topic, msg, (err) => {
if (err) {
uni.showToast({ title: '控制异常', icon: 'none' })
console.error(`MQTT发布失败topic=${topic}err=${err}`)
} else {
console.log(`MQTT发布成功topic=${topic}message=${msg}`)
}
})
return true
} catch (err) {
uni.showToast({ title: '控制异常', icon: 'none' })
console.error('MQTT发布异常', err)
return false
}
}
/**
* 断开MQTT连接登出/小程序切后台时调用
* @returns {Boolean} - 是否断开成功
*/
export function disconnectMqtt() {
if (!mqttState.client) {
console.log('MQTT无客户端实例无需断开')
resetMqttState()
return true
}
try {
mqttState.client.end(true) // 强制断开
resetMqttState()
console.log('MQTT连接已断开')
return true
} catch (err) {
uni.showToast({ title: '设备连接异常', icon: 'none' })
console.error('MQTT断开失败', err)
return false
}
}
/**
* 重置MQTT状态内部方法
*/
function resetMqttState() {
mqttState.isConnected = false
mqttState.client = null
mqttState.subscribeList = []
mqttState.onMessageCallback = null
// 保留clientId直到下一次初始化覆盖
}
/**
* 获取当前MQTT状态同步
* @returns {Object} - 状态副本
*/
export function getMqttState() {
return {
isConnected: client.connected, // 最终连接状态
isConnecting: client.isConnecting, // 连接中/重连中
isManualDisconnect: client.isManualDisconnect // 是否手动断开
};
isConnected: mqttState.isConnected,
subscribeList: [...mqttState.subscribeList],
clientId: mqttState.options.clientId,
client: mqttState.client
}
}
// 导出所有方法(全局调用)
export default {
initMqttConfig,
connectMqtt,
setOnMessageCallback,
removeOnMessageCallback,
updateSubscribeList,
publishMqtt,
disconnectMqtt,
getMqttState
}

1
utils/mqtt.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -38,13 +38,18 @@ const request = config => {
const code = res.data.code || 200
const msg = errorCode[code] || res.data.msg || errorCode['default']
if (code === 401) {
showConfirm('登录状态已过期,您可以继续留在该页面,或者重新登录?').then(res => {
if (res.confirm) {
store.dispatch('LogOut').then(res => {
uni.$emit('tokenExpired')
store.dispatch('LogOut').then(res => {
uni.reLaunch({ url: '/pages/login' })
})
}
})
})
// todo
// showConfirm('登录状态已过期,您可以继续留在该页面,或者重新登录?').then(res => {
// if (res.confirm) {
// store.dispatch('LogOut').then(res => {
// uni.reLaunch({ url: '/pages/login' })
// })
// }
// })
reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 500) {
toast(msg)

View File

@ -35,13 +35,17 @@ const upload = config => {
if (code === 200) {
resolve(result)
} else if (code == 401) {
showConfirm("登录状态已过期,您可以继续留在该页面,或者重新登录?").then(res => {
if (res.confirm) {
store.dispatch('LogOut').then(res => {
uni.reLaunch({ url: '/pages/login/login' })
})
}
})
// todo
// showConfirm("登录状态已过期,您可以继续留在该页面,或者重新登录?").then(res => {
// if (res.confirm) {
// store.dispatch('LogOut').then(res => {
// uni.reLaunch({ url: '/pages/login/login' })
// })
// }
// })
store.dispatch('LogOut').then(res => {
uni.reLaunch({ url: '/pages/login' })
})
reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 500) {
toast(msg)