Compare commits

..

No commits in common. "e5c243b74e6ce8d74381182278c547f6d66421d9" and "8cc77d2852e2ccaa1bec169aaced4ab563345537" have entirely different histories.

5 changed files with 201 additions and 315 deletions

66
App.vue
View File

@ -1,51 +1,10 @@
<script> <script>
import config from './config' import config from './config'
import { getToken } from '@/utils/auth' import { getToken } from '@/utils/auth'
import {
initMQTT,
disconnectMQTT,
getMQTTStatus,
manualReconnect
} from '@/utils/mqtt';
export default {
globalData: {
mqttClient: null, // MQTT
mqttConfig: { // MQTT
host: '1.94.254.176',
port: 9001, // wssEMQ X8084
isSSL: false, // true=wssfalse=ws
username: 'admin',
password: 'Admin#12345678',
keepalive: 60,
clean: true
},
globalSubscribeTopic: [ //
{topic: 'dtu/#', qos: 0},
// {topic: 'system/status', qos: 1}
],
mqttStatus: 'disconnected' // connected/disconnected/reconnecting
},
onLaunch() {
this.initApp()
console.log('应用启动初始化MQTT连接');
this.initGlobalMQTT();
},
onShow() {
console.log('应用切前台检查MQTT连接');
const { isConnected } = getMQTTStatus();
if (!isConnected && this.globalData.mqttStatus !== 'disconnected') {
//
manualReconnect(this.globalData.mqttConfig);
this.globalData.mqttStatus = 'reconnecting';
}
},
onHide() { export default {
console.log('应用切后台断开MQTT连接'); onLaunch: function() {
// this.initApp()
disconnectMQTT();
this.globalData.mqttStatus = 'disconnected';
this.globalData.mqttClient = null;
}, },
methods: { methods: {
// //
@ -64,28 +23,11 @@
if (!getToken()) { if (!getToken()) {
this.$tab.reLaunch('/pages/login') this.$tab.reLaunch('/pages/login')
} }
},
// MQTT
async initGlobalMQTT() {
try {
// +
const client = await initMQTT(
this.globalData.mqttConfig,
this.globalData.globalSubscribeTopic
);
//
this.globalData.mqttClient = client;
this.globalData.mqttStatus = 'connected';
console.log('全局MQTT初始化成功');
} catch (err) {
console.error('全局MQTT初始化失败', err);
this.globalData.mqttStatus = 'disconnected';
}
} }
} }
} }
</script> </script>
<style lang="scss"> <style lang="scss">
@import '@/static/scss/index.scss'; @import '@/static/scss/index.scss'
</style> </style>

View File

@ -1,6 +1,8 @@
// 应用全局配置 // 应用全局配置
module.exports = { module.exports = {
baseUrl: process.env.NODE_ENV === "production" ? "/api" : "http://localhost:8088", // baseUrl: 'https://vue.ruoyi.vip/prod-api',
baseUrl: 'http://172.14.24.109:8088',
// baseUrl: 'http://1.94.254.176:8088',
// 应用信息 // 应用信息
appInfo: { appInfo: {
// 应用名称 // 应用名称

View File

@ -58,9 +58,7 @@
</uni-section> </uni-section>
<uni-section title="设备控制" titleFontSize="16px" type="line" v-if="value!== 1"> <uni-section title="设备控制" titleFontSize="16px" type="line" v-if="value!== 1">
<template v-slot:right >
{{ control }}
</template>
<!-- 卷膜/卷被卡片容器2列栅格布局 --> <!-- 卷膜/卷被卡片容器2列栅格布局 -->
<view class="card-grid"> <view class="card-grid">
<!-- 卷被开卡片 --> <!-- 卷被开卡片 -->
@ -171,12 +169,12 @@ export default {
mqttConfig: { mqttConfig: {
host: '1.94.254.176', host: '1.94.254.176',
port: 9001, port: 9001,
clientId: 'uniapp_mqtt_' + Math.random().toString(16).substr(2, 8),
username: 'admin', username: 'admin',
password: 'Admin#12345678', password: 'Admin#12345678',
subscribeTopic:'/up', subscribeTopic:'/up',
}, },
value: 1, value: 1,
control: '正在加载中...',
range: [{ range: [{
"value": '864865085016294', "value": '864865085016294',
"text": "十方北棚" "text": "十方北棚"
@ -275,7 +273,7 @@ export default {
} }
findDtuDataByInfo(queryParams).then(response => { findDtuDataByInfo(queryParams).then(response => {
this.liveData = { this.liveData = {
temp1: response.data.temp1 || '已离线..', temp1: response.data.temp1 || '已离线...',
temp2: response.data.temp2 || '已离线..', temp2: response.data.temp2 || '已离线..',
temp3: response.data.temp3 || '已离线..', temp3: response.data.temp3 || '已离线..',
temp4: response.data.temp4 || '已离线..', temp4: response.data.temp4 || '已离线..',
@ -319,7 +317,6 @@ export default {
jm3k: 0, jm3k: 0,
jm3g: 0 jm3g: 0
}; };
this.control = '正在加载中...';
this.message = {}; this.message = {};
this.temp = ''; this.temp = '';
this.liveData = { this.liveData = {
@ -396,7 +393,7 @@ export default {
}, },
connectMqtt() { connectMqtt() {
const options = { const options = {
clientId: 'uniapp_mqtt_' + Math.random().toString(16).substr(2, 8), clientId: this.mqttConfig.clientId,
username: this.mqttConfig.username, username: this.mqttConfig.username,
password: this.mqttConfig.password, password: this.mqttConfig.password,
clean: true, clean: true,
@ -412,7 +409,6 @@ export default {
this.connected = true this.connected = true
this.client.subscribe('dtu/+/up', {qos: 0}) this.client.subscribe('dtu/+/up', {qos: 0})
this.addMessage('已连接到MQTT服务器') this.addMessage('已连接到MQTT服务器')
console.info(this.client)
}) })
this.client.on("message", this.ackMessage); this.client.on("message", this.ackMessage);
@ -429,7 +425,6 @@ export default {
this.client.on('close', () => { this.client.on('close', () => {
this.addMessage('连接已关闭') this.addMessage('连接已关闭')
this.connected = false this.connected = false
console.info(this.client)
}) })
}, },
@ -526,13 +521,6 @@ export default {
if (allKeysNumeric) { if (allKeysNumeric) {
// console.info(msgData) // console.info(msgData)
this.status = {...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) // console.info("imei: "+this.publishTopic+"copy: ",this.status)
} }
const allKeysNumeric2 = Object.keys(msgData).every(key => /^\d+$/.test(key)); const allKeysNumeric2 = Object.keys(msgData).every(key => /^\d+$/.test(key));
@ -552,7 +540,7 @@ export default {
humi4: div10(msgData["104"]) || "已离线...", humi4: div10(msgData["104"]) || "已离线...",
} }
// //
this.temp = "最更新时间:" + this.getCurrentTime(); this.temp = "最更新时间:" + this.getCurrentTime();
this.fontStyle = 'font-size:16px;' this.fontStyle = 'font-size:16px;'
} }

View File

@ -238,10 +238,10 @@ export default {
return { return {
// MQTT // MQTT
mqttConfig: { mqttConfig: {
broker: 'ws://1.94.254.176:9001/mqtt', // broker: '', //
clientId: 'uniapp_mqtt_' + Math.random().toString(16).substr(2, 8), clientId: 'uniapp_mqtt_' + Math.random().toString(16).substr(2, 8),
username: 'admin', username: '',
password: 'Admin#12345678', password: '',
timeout: 30 timeout: 30
}, },
// //

View File

@ -1,260 +1,214 @@
/**
* MQTT工具类 - 全局唯一连接自动重连支持自定义配置和订阅列表
* 适配uniapp小程序/APP/H5小程序仅支持ws/wss
*/
// 引入mqtt库需先安装npm install mqtt --save或下载mqtt.min.js到本地引入
import mqtt from 'mqtt' import mqtt from 'mqtt'
// 全局MQTT实例单例避免多页面重复创建
// 全局变量 let mqttInstance = null;
let client = null; // MQTT客户端实例
let subscribeList = []; // 订阅主题列表(重连后自动恢复订阅)
let reconnectTimer = null; // 重连定时器
let maxReconnectTimes = 10; // 最大重连次数
let currentReconnectTimes = 0; // 当前重连次数
let isManualDisconnect = false; // 是否主动断开(用于区分主动/被动断开,避免主动断开后重连)
/** /**
* 初始化MQTT连接 * 初始化MQTT客户端单例模式
* @param {Object} config - MQTT连接配置 * @param {Object} mqttConfig 配置项
* @param {Array} subs - 初始订阅列表格式[{ topic: 'topic1', qos: 0 }, { topic: 'topic2', qos: 1 }] * @param {Array|String} subTopic 初始订阅主题
* @returns {Promise} 连接成功/失败的Promise * @returns {Object} MQTT实例
*/ */
export function initMQTT(config, subs = []) { function createMqttClient(mqttConfig, subTopic) {
return new Promise((resolve, reject) => { // 已存在实例则直接返回
// 1. 校验配置(必填项) if (mqttInstance && mqttInstance.connected) {
if (!config.host || !config.port) { // 若传入新主题,补充订阅
reject(new Error('MQTT配置错误host和port为必填项')); if (subTopic) {
return; mqttInstance.subscribe(subTopic);
}
return mqttInstance;
} }
// 2. 保存订阅列表(用于重连恢复) // 标准化配置(默认值 + 防错)
subscribeList = subs; const config = {
host: mqttConfig.host || '1.94.254.176',
// 3. 避免重复连接 port: mqttConfig.port || 9001,
if (client && client.connected) { clientId: mqttConfig.clientId || `uniapp_mqtt_${Date.now()}${Math.random().toString(16).substr(2, 4)}`, // 时间戳+随机数,减少重复
resolve(client); username: mqttConfig.username || 'admin',
return; password: mqttConfig.password || 'Admin#12345678',
} clean: false, // 关键改为false保持会话缓存消息
reconnectPeriod: mqttConfig.reconnectPeriod || 3000,
// 4. 标记为非主动断开(允许重连) connectTimeout: mqttConfig.connectTimeout || 5000,
isManualDisconnect = false; qos: mqttConfig.qos || 1 // 默认QoS1确保消息不丢
// 5. 构建连接地址小程序仅支持ws/wss优先用wss更安全
const protocol = config.protocol || (config.isSSL ? 'wss' : 'ws');
const connectUrl = `${protocol}://${config.host}:${config.port}/mqtt`;
// 6. 构建MQTT连接选项
const mqttOptions = {
clientId: config.clientId || `uni_mqtt_${Math.random().toString(16).substr(2, 8)}`,
username: config.username || '',
password: config.password || '',
keepalive: config.keepalive || 60, // 心跳间隔(秒)
clean: config.clean !== undefined ? config.clean : true, // 是否清除会话
reconnectPeriod: 0, // 关闭内置重连(自定义重连逻辑)
connectTimeout: config.connectTimeout || 10000, // 连接超时(毫秒)
...config.extraOptions // 额外扩展配置
}; };
try { // 连接选项
// 7. 创建客户端并连接 const options = {
client = mqtt.connect(connectUrl, mqttOptions); clientId: config.clientId,
username: config.username,
// 8. 监听连接成功 password: config.password,
client.on('connect', () => { clean: config.clean,
console.log('MQTT连接成功', connectUrl); connectTimeout: config.connectTimeout,
currentReconnectTimes = 0; // 重置重连次数 reconnectPeriod: config.reconnectPeriod,
// 9. 订阅初始主题 keepalive: 60 // 新增心跳,避免连接被断开
subscribeTopics(subscribeList);
resolve(client);
});
// 10. 监听连接错误
client.on('error', (err) => {
console.error('MQTT连接错误', err);
client.end();
reject(err);
});
// 11. 监听连接断开(被动断开则触发重连)
client.on('close', () => {
console.log('MQTT连接已断开');
client = null;
// 非主动断开 + 未达最大重连次数 → 触发重连
if (!isManualDisconnect && currentReconnectTimes < maxReconnectTimes) {
reconnectMQTT(config);
}
});
// 12. 监听消息全局消息转发页面层通过uni.$on监听
client.on('message', (topic, message) => {
const msg = {
topic,
payload: message.toString(), // 转字符串原始是Buffer
timestamp: Date.now()
}; };
// 全局广播消息,页面层按需监听
uni.$emit('mqtt_message', msg);
});
} catch (err) { // 拼接WS地址兼容配置错误
console.error('MQTT初始化失败', err); const url = `ws://${config.host}:${config.port}/mqtt`;
reject(err);
}
});
}
/** // 创建客户端
* 订阅主题支持单个/多个 const client = mqtt.connect(url, options);
* @param {Array} topics - 订阅列表格式[{ topic: 'topic1', qos: 0 }, { topic: 'topic2', qos: 1 }]
*/ // 实例状态管理
export function subscribeTopics(topics = []) { const instance = {
if (!client || !client.connected) { client: client,
console.warn('MQTT未连接无法订阅主题'); connected: false,
subscribedTopics: new Set(), // 记录已订阅主题
config: config,
messageCallback: null, // 消息接收回调
statusCallback: null, // 状态变更回调
// 订阅主题(支持单个/多个,带重试)
subscribe: function (topics, qos = config.qos) {
if (!this.connected) {
console.warn('MQTT未连接延迟订阅:', topics);
// 连接成功后自动订阅
client.once('connect', () => this.subscribe(topics, qos));
return; return;
} }
// 过滤空主题 const topicList = Array.isArray(topics) ? topics : [topics];
const validTopics = topics.filter(item => item && item.topic); client.subscribe(topicList, { qos }, (err) => {
if (validTopics.length === 0) return;
// 转换为mqtt库要求的格式{ topic1: { qos: 0 }, topic2: { qos: 1 } }
const subscribeMap = {};
validTopics.forEach(item => {
subscribeMap[item.topic] = { qos: item.qos || 0 };
});
client.subscribe(subscribeMap, (err) => {
if (err) { if (err) {
console.error('MQTT订阅失败', err); console.error('订阅失败:', err, '主题:', topicList);
// 订阅失败重试(仅一次)
setTimeout(() => this.subscribe(topics, qos), 1000);
} else { } else {
console.log('MQTT订阅成功', validTopics.map(item => item.topic)); topicList.forEach(t => this.subscribedTopics.add(t));
// 更新订阅列表(用于重连恢复) console.log(`订阅成功${topicList.length > 1 ? '(批量)' : '(单个)'}:`, topicList);
subscribeList = [...new Set([...subscribeList, ...validTopics])]; // 去重 this.statusCallback && this.statusCallback('subscribe_success', topicList);
} }
}); });
} },
/** // 取消订阅
* 取消订阅主题 unsubscribe: function (topics) {
* @param {Array} topics - 取消订阅的主题列表格式['topic1', 'topic2'] if (!this.connected) return;
*/ const topicList = Array.isArray(topics) ? topics : [topics];
export function unsubscribeTopics(topics = []) { client.unsubscribe(topicList, (err) => {
if (!client || !client.connected) {
console.warn('MQTT未连接无法取消订阅');
return;
}
client.unsubscribe(topics, (err) => {
if (err) { if (err) {
console.error('MQTT取消订阅失败', err); console.error('取消订阅失败:', err);
} else { } else {
console.log('MQTT取消订阅成功', topics); topicList.forEach(t => this.subscribedTopics.delete(t));
// 更新订阅列表 console.log('取消订阅成功:', topicList);
subscribeList = subscribeList.filter(item => !topics.includes(item.topic)); this.statusCallback && this.statusCallback('unsubscribe_success', topicList);
} }
}); });
} },
/** // 发布消息(带参数,失败重试)
* 发布消息 publish: function (topic, message, qos = config.qos, retain = false) {
* @param {String} topic - 发布的主题
* @param {String/Buffer} payload - 发布的消息内容
* @param {Object} options - 发布选项{ qos: 0, retain: false }
* @returns {Promise} 发布成功/失败的Promise
*/
export function publishMQTT(topic, payload, options = { qos: 0, retain: false }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!client || !client.connected) { if (!this.connected) {
reject(new Error('MQTT未连接无法发布消息')); reject(new Error('MQTT未连接无法发布消息'));
// 自动重连后发布
this.reconnectAndPublish(topic, message, qos, retain);
return; return;
} }
if (!topic) { // 标准化消息格式对象转JSON
reject(new Error('发布失败:主题不能为空')); const payload = typeof message === 'object' ? JSON.stringify(message) : String(message);
return;
}
client.publish(topic, payload, options, (err) => { client.publish(topic, payload, { qos, retain }, (err) => {
if (err) { if (err) {
console.error(`MQTT发布${topic}失败:`, err); console.error('发布失败:', err, '主题:', topic);
reject(err); reject(err);
// 发布失败重试
setTimeout(() => this.publish(topic, message, qos, retain), 1000);
} else { } else {
console.log(`MQTT发布${topic}成功:`, payload); console.log('发布成功:', topic, '内容:', payload);
resolve(true); resolve({ topic, payload });
} }
}); });
}); });
} },
// 重连后补发消息
reconnectAndPublish: function (topic, message, qos, retain) {
client.once('connect', () => {
this.publish(topic, message, qos, retain);
});
},
/**
* 断开MQTT连接主动断开不会触发重连
*/
export function disconnectMQTT() {
// 标记为主动断开
isManualDisconnect = true;
// 清除重连定时器
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
// 断开连接 // 断开连接
if (client && client.connected) { disconnect: function () {
client.end(false, () => { // false不发送遗嘱消息 if (this.connected && this.client) {
console.log('MQTT主动断开连接'); this.client.end(false, () => { // false等待剩余消息发送完成
client = null; this.connected = false;
subscribeList = []; this.subscribedTopics.clear();
console.log('MQTT连接已断开保留会话');
this.statusCallback && this.statusCallback('disconnect');
}); });
} }
} }
/**
* 重连MQTT内部调用也可外部手动触发
* @param {Object} config - 连接配置同initMQTT的config
*/
export function reconnectMQTT(config) {
// 清除已有定时器,避免重复触发
if (reconnectTimer) {
clearTimeout(reconnectTimer);
}
// 重连间隔1秒/次(可自定义)
reconnectTimer = setTimeout(async () => {
currentReconnectTimes++;
console.log(`MQTT重连中${currentReconnectTimes}/${maxReconnectTimes}`);
try {
await initMQTT(config, subscribeList);
} catch (err) {
// 重连失败,继续尝试(直到达到最大次数)
if (currentReconnectTimes < maxReconnectTimes) {
reconnectMQTT(config);
} else {
console.error('MQTT重连次数已达上限停止重连');
uni.$emit('mqtt_reconnect_fail'); // 全局通知重连失败
}
}
}, 1000);
}
/**
* 获取MQTT客户端状态
* @returns {Object} 状态信息
*/
export function getMQTTStatus() {
return {
isConnected: !!client && client.connected, // 是否连接
client: client, // 客户端实例
subscribeList: [...subscribeList], // 已订阅列表(浅拷贝,避免外部修改)
currentReconnectTimes, // 当前重连次数
maxReconnectTimes // 最大重连次数
}; };
// 绑定客户端事件
client.on('connect', () => {
instance.connected = true;
console.log('MQTT连接成功ClientId:', config.clientId);
instance.statusCallback && instance.statusCallback('connect_success');
// 初始订阅主题
if (subTopic) {
instance.subscribe(subTopic);
}
})
.on('reconnect', (error) => {
instance.connected = false;
console.log('MQTT正在重连...', error);
instance.statusCallback && instance.statusCallback('reconnect', error);
})
.on('error', (error) => {
instance.connected = false;
console.error('MQTT连接错误:', error);
instance.statusCallback && instance.statusCallback('error', error);
})
.on('close', () => {
instance.connected = false;
console.log('MQTT连接关闭');
instance.statusCallback && instance.statusCallback('close');
})
.on('offline', () => {
instance.connected = false;
console.log('MQTT客户端下线');
instance.statusCallback && instance.statusCallback('offline');
})
.on('message', (topic, payload) => {
const msg = payload.toString();
console.log('收到MQTT消息:', topic, msg);
// 消息回调,交给业务层处理
instance.messageCallback && instance.messageCallback(topic, msg);
});
// 赋值单例
mqttInstance = instance;
return instance;
} }
/** /**
* 手动触发重连外部调用比如页面主动刷新连接 * 对外暴露的核心方法
* @param {Object} config - 连接配置
*/ */
export function manualReconnect(config) { export const mqttTool = {
currentReconnectTimes = 0; // 重置重连次数 // 初始化连接
reconnectMQTT(config); connect: function (mqttConfig, subTopic) {
return createMqttClient(mqttConfig, subTopic);
},
// 获取全局实例
getInstance: function () {
return mqttInstance;
},
// 页面切换时的订阅管理(核心解决多页面订阅问题)
switchPageTopic: function (newTopics, oldTopics) {
const instance = mqttInstance;
if (!instance) return;
// 先订阅新主题,再取消旧主题(避免漏消息)
if (newTopics) {
instance.subscribe(newTopics);
} }
if (oldTopics) {
instance.unsubscribe(oldTopics);
}
}
};