对接MQTT、APP样式
3515
package-lock.json
generated
137
src/App.vue
@ -1,37 +1,45 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import mqttDataManager from '@/utils/mqttDataManager.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLaunch: function () {
|
onLaunch: function () {
|
||||||
console.log('App Launch')
|
console.log('App Launch')
|
||||||
// 强制设置横屏
|
// 应用启动时的初始化逻辑
|
||||||
this.setOrientation()
|
this.initApp()
|
||||||
},
|
},
|
||||||
onShow: function () {
|
onShow: function () {
|
||||||
console.log('App Show')
|
console.log('App Show')
|
||||||
// 每次显示时确保横屏
|
|
||||||
this.setOrientation()
|
|
||||||
},
|
},
|
||||||
onHide: function () {
|
onHide: function () {
|
||||||
console.log('App Hide')
|
console.log('App Hide')
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setOrientation() {
|
initApp() {
|
||||||
// 设置屏幕方向为横屏
|
// 初始化应用设置
|
||||||
try {
|
console.log('🚀 应用开始初始化...')
|
||||||
// #ifdef APP-PLUS
|
|
||||||
plus.screen.lockOrientation('landscape-primary')
|
// 检查是否是首次启动
|
||||||
// #endif
|
const isFirstLaunch = uni.getStorageSync('isFirstLaunch')
|
||||||
|
if (!isFirstLaunch) {
|
||||||
// #ifdef H5
|
uni.setStorageSync('isFirstLaunch', true)
|
||||||
// H5环境下的横屏设置
|
console.log('✅ 首次启动应用')
|
||||||
if (screen.orientation && screen.orientation.lock) {
|
|
||||||
screen.orientation.lock('landscape').catch(err => {
|
|
||||||
console.log('横屏锁定失败:', err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// #endif
|
|
||||||
} catch (error) {
|
|
||||||
console.log('设置横屏失败:', error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 显示平台信息
|
||||||
|
console.log('📱 当前平台:',
|
||||||
|
// #ifdef H5
|
||||||
|
'H5'
|
||||||
|
// #endif
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
'APP-PLUS'
|
||||||
|
// #endif
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
'MP-WEIXIN'
|
||||||
|
// #endif
|
||||||
|
)
|
||||||
|
|
||||||
|
// MQTT连接已在mqttDataManager中自动初始化
|
||||||
|
console.log('✅ 应用初始化完成')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -47,11 +55,70 @@ page {
|
|||||||
sans-serif;
|
sans-serif;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 确保根元素和页面容器都是100%高度 */
|
/* 确保根元素和页面容器都是100%高度 */
|
||||||
#app {
|
#app {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全局布局容器 */
|
||||||
|
.app-container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 固定头部样式 */
|
||||||
|
.fixed-header {
|
||||||
|
// position: fixed;
|
||||||
|
// top: 0;
|
||||||
|
// left: 0;
|
||||||
|
// right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background-color: #3f51b5;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
color: white;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容区域样式 */
|
||||||
|
.content-area {
|
||||||
|
flex: 1;
|
||||||
|
padding-top: 100rpx; /* 为固定头部留出空间 */
|
||||||
|
padding-bottom: 200rpx; /* 为tabbar留出空间 */
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tabbar页面内容区域 */
|
||||||
|
.tabbar-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20rpx;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-bottom: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 非tabbar页面内容区域 */
|
||||||
|
.non-tabbar-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20rpx;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 修复uni-app中的一些默认样式 */
|
/* 修复uni-app中的一些默认样式 */
|
||||||
@ -70,4 +137,32 @@ button::after {
|
|||||||
border-radius: 10rpx;
|
border-radius: 10rpx;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 卡片样式 */
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮样式 */
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #3f51b5;
|
||||||
|
color: white;
|
||||||
|
padding: 20rpx 40rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: #666;
|
||||||
|
color: white;
|
||||||
|
padding: 20rpx 40rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -166,7 +166,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineEmits, onMounted, onUnmounted, nextTick } from 'vue';
|
import { ref, defineEmits, onMounted, onUnmounted, nextTick } from 'vue';
|
||||||
import { createMqtt, closeMqtt, judgeBeat, getConnectionStatus } from '@/utils/sendMqtt';
|
import { getConnectionStatus } from '@/utils/sendMqtt';
|
||||||
import { DataParser } from '@/config/mqtt';
|
import { DataParser } from '@/config/mqtt';
|
||||||
|
|
||||||
const emit = defineEmits(['openSettings']);
|
const emit = defineEmits(['openSettings']);
|
||||||
@ -238,42 +238,10 @@ const handleDeviceData = (data) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 连接MQTT并订阅数据
|
// 检查MQTT连接状态
|
||||||
const connectMQTT = async () => {
|
const checkMqttStatus = () => {
|
||||||
try {
|
isConnected.value = getConnectionStatus();
|
||||||
console.log('开始连接MQTT...');
|
console.log('MQTT连接状态:', isConnected.value);
|
||||||
createMqtt();
|
|
||||||
|
|
||||||
// 延迟检查连接状态
|
|
||||||
setTimeout(() => {
|
|
||||||
isConnected.value = getConnectionStatus();
|
|
||||||
if (isConnected.value) {
|
|
||||||
console.log('MQTT连接成功,开始监听数据...');
|
|
||||||
uni.showToast({
|
|
||||||
title: 'MQTT连接成功',
|
|
||||||
icon: 'success',
|
|
||||||
duration: 2000
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('MQTT连接失败');
|
|
||||||
uni.showToast({
|
|
||||||
title: 'MQTT连接失败',
|
|
||||||
icon: 'error',
|
|
||||||
duration: 3000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('MQTT连接失败:', error);
|
|
||||||
isConnected.value = false;
|
|
||||||
|
|
||||||
uni.showToast({
|
|
||||||
title: 'MQTT连接失败',
|
|
||||||
icon: 'error',
|
|
||||||
duration: 3000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 开始监听MQTT数据
|
// 开始监听MQTT数据
|
||||||
@ -332,12 +300,12 @@ const getCurrentTime = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 组件挂载时连接MQTT
|
// 组件挂载时开始监听MQTT数据
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
console.log('EnvironmentParams组件已挂载');
|
console.log('EnvironmentParams组件已挂载');
|
||||||
|
|
||||||
// 恢复MQTT连接
|
// 检查MQTT连接状态
|
||||||
connectMQTT();
|
checkMqttStatus();
|
||||||
|
|
||||||
// 等待DOM更新完成
|
// 等待DOM更新完成
|
||||||
await nextTick();
|
await nextTick();
|
||||||
@ -347,7 +315,7 @@ onMounted(async () => {
|
|||||||
console.log('页面加载完成,开始监听MQTT数据...');
|
console.log('页面加载完成,开始监听MQTT数据...');
|
||||||
isPageLoaded.value = true; // 标记页面已加载完成
|
isPageLoaded.value = true; // 标记页面已加载完成
|
||||||
startMqttListener();
|
startMqttListener();
|
||||||
}, 2000); // 2秒后开始监听,确保页面和MQTT连接都已就绪
|
}, 1000); // 1秒后开始监听
|
||||||
});
|
});
|
||||||
|
|
||||||
// 组件卸载时断开连接
|
// 组件卸载时断开连接
|
||||||
|
|||||||
@ -7,7 +7,11 @@ let mqtturl;
|
|||||||
mqtturl = "ws://122.51.194.184:8083/mqtt";
|
mqtturl = "ws://122.51.194.184:8083/mqtt";
|
||||||
// #endif
|
// #endif
|
||||||
|
|
||||||
// #ifdef APP-PLUS || MP-WEIXIN
|
// #ifdef APP-PLUS
|
||||||
|
mqtturl = "ws://122.51.194.184:8083/mqtt";
|
||||||
|
//#endif
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
mqtturl = "wx://122.51.194.184:8083/mqtt";
|
mqtturl = "wx://122.51.194.184:8083/mqtt";
|
||||||
//#endif
|
//#endif
|
||||||
|
|
||||||
@ -52,7 +56,7 @@ export const MQTT_CONFIG = {
|
|||||||
topics: {
|
topics: {
|
||||||
// 设备数据主题
|
// 设备数据主题
|
||||||
// deviceData: 'hdydcj_01_down',
|
// deviceData: 'hdydcj_01_down',
|
||||||
deviceData: 'HDYDCJ_01_DOWN',
|
deviceData: 'HDYDCJ_01_UP',
|
||||||
|
|
||||||
// 设备类型
|
// 设备类型
|
||||||
deviceTypes: {
|
deviceTypes: {
|
||||||
|
|||||||
@ -1,16 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name" : "pad-app",
|
"name" : "上动物联",
|
||||||
"appid" : "__UNI__5F601D3",
|
"appid" : "__UNI__0B541D7",
|
||||||
"description" : "",
|
"description" : "",
|
||||||
"versionName" : "1.0.0",
|
"versionName" : "1.1.0",
|
||||||
"versionCode" : "100",
|
"versionCode" : 110,
|
||||||
"transformPx" : false,
|
"transformPx" : false,
|
||||||
/* 5+App特有相关 */
|
/* 5+App特有相关 */
|
||||||
"app-plus" : {
|
"app-plus" : {
|
||||||
"usingComponents" : true,
|
"usingComponents" : true,
|
||||||
"nvueStyleCompiler" : "uni-app",
|
"nvueStyleCompiler" : "uni-app",
|
||||||
"compilerVersion" : 3,
|
"compilerVersion" : 2,
|
||||||
"orientation" : "landscape",
|
"orientation" : "portrait",
|
||||||
|
"icons" : {
|
||||||
|
"app" : {
|
||||||
|
"hdpi" : "static/app-icon.png",
|
||||||
|
"xhdpi" : "static/app-icon.png",
|
||||||
|
"xxhdpi" : "static/app-icon.png",
|
||||||
|
"xxxhdpi" : "static/app-icon.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
"splashscreen" : {
|
"splashscreen" : {
|
||||||
"alwaysShowBeforeRender" : true,
|
"alwaysShowBeforeRender" : true,
|
||||||
"waiting" : true,
|
"waiting" : true,
|
||||||
@ -19,6 +27,8 @@
|
|||||||
},
|
},
|
||||||
/* 模块配置 */
|
/* 模块配置 */
|
||||||
"modules" : {},
|
"modules" : {},
|
||||||
|
/* 原生插件配置 */
|
||||||
|
"nativePlugins" : {},
|
||||||
/* 应用发布信息 */
|
/* 应用发布信息 */
|
||||||
"distribute" : {
|
"distribute" : {
|
||||||
/* android打包配置 */
|
/* android打包配置 */
|
||||||
@ -38,39 +48,56 @@
|
|||||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>"
|
||||||
],
|
],
|
||||||
"abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ],
|
"abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ],
|
||||||
"screenOrientation" : "landscape"
|
"screenOrientation" : "portrait"
|
||||||
},
|
},
|
||||||
/* ios打包配置 */
|
/* ios打包配置 */
|
||||||
"ios" : {
|
"ios" : {
|
||||||
"dSYMs" : false,
|
"dSYMs" : false,
|
||||||
"screenOrientation" : "landscape"
|
"screenOrientation" : "portrait",
|
||||||
|
"idfa" : false
|
||||||
},
|
},
|
||||||
/* SDK配置 */
|
/* SDK配置 */
|
||||||
"sdkConfigs" : {}
|
"sdkConfigs" : {},
|
||||||
|
"icons" : {
|
||||||
|
"android" : {
|
||||||
|
"hdpi" : "unpackage/res/icons/72x72.png",
|
||||||
|
"xhdpi" : "unpackage/res/icons/96x96.png",
|
||||||
|
"xxhdpi" : "unpackage/res/icons/144x144.png",
|
||||||
|
"xxxhdpi" : "unpackage/res/icons/192x192.png"
|
||||||
|
},
|
||||||
|
"ios" : {
|
||||||
|
"appstore" : "unpackage/res/icons/1024x1024.png",
|
||||||
|
"ipad" : {
|
||||||
|
"app" : "unpackage/res/icons/76x76.png",
|
||||||
|
"app@2x" : "unpackage/res/icons/152x152.png",
|
||||||
|
"notification" : "unpackage/res/icons/20x20.png",
|
||||||
|
"notification@2x" : "unpackage/res/icons/40x40.png",
|
||||||
|
"proapp@2x" : "unpackage/res/icons/167x167.png",
|
||||||
|
"settings" : "unpackage/res/icons/29x29.png",
|
||||||
|
"settings@2x" : "unpackage/res/icons/58x58.png",
|
||||||
|
"spotlight" : "unpackage/res/icons/40x40.png",
|
||||||
|
"spotlight@2x" : "unpackage/res/icons/80x80.png"
|
||||||
|
},
|
||||||
|
"iphone" : {
|
||||||
|
"app@2x" : "unpackage/res/icons/120x120.png",
|
||||||
|
"app@3x" : "unpackage/res/icons/180x180.png",
|
||||||
|
"notification@2x" : "unpackage/res/icons/40x40.png",
|
||||||
|
"notification@3x" : "unpackage/res/icons/60x60.png",
|
||||||
|
"settings@2x" : "unpackage/res/icons/58x58.png",
|
||||||
|
"settings@3x" : "unpackage/res/icons/87x87.png",
|
||||||
|
"spotlight@2x" : "unpackage/res/icons/80x80.png",
|
||||||
|
"spotlight@3x" : "unpackage/res/icons/120x120.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/* 快应用特有相关 */
|
/* 专注于App和H5平台 */
|
||||||
"quickapp" : {},
|
|
||||||
/* 小程序特有相关 */
|
|
||||||
"mp-weixin" : {
|
|
||||||
"appid" : "",
|
|
||||||
"setting" : {
|
|
||||||
"urlCheck" : false
|
|
||||||
},
|
|
||||||
"usingComponents" : true
|
|
||||||
},
|
|
||||||
"mp-alipay" : {
|
|
||||||
"usingComponents" : true
|
|
||||||
},
|
|
||||||
"mp-baidu" : {
|
|
||||||
"usingComponents" : true
|
|
||||||
},
|
|
||||||
"mp-toutiao" : {
|
|
||||||
"usingComponents" : true
|
|
||||||
},
|
|
||||||
"uniStatistics" : {
|
"uniStatistics" : {
|
||||||
"enable" : false
|
"enable" : false
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,15 +1,50 @@
|
|||||||
{
|
{
|
||||||
"pages": [
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "pages/environment/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "移动式检修车间",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/parameter/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "移动式检修车间",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/visual/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "移动式检修车间",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/log/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "移动式检修车间",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/alarm/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "移动式检修车间",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/index/index",
|
"path": "pages/index/index",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "检修系统首页"
|
"navigationBarTitleText": "移动式检修车间"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/system/index",
|
"path": "pages/system/index",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "检修系统",
|
"navigationBarTitleText": "移动式检修车间",
|
||||||
"navigationStyle": "custom",
|
"navigationStyle": "custom",
|
||||||
"orientation": "landscape"
|
"orientation": "landscape"
|
||||||
}
|
}
|
||||||
@ -21,6 +56,34 @@
|
|||||||
"navigationBarBackgroundColor": "#3f51b5",
|
"navigationBarBackgroundColor": "#3f51b5",
|
||||||
"backgroundColor": "#F8F8F8"
|
"backgroundColor": "#F8F8F8"
|
||||||
},
|
},
|
||||||
|
"tabBar": {
|
||||||
|
"color": "#666666",
|
||||||
|
"selectedColor": "#3f51b5",
|
||||||
|
"backgroundColor": "#ffffff",
|
||||||
|
"borderStyle": "black",
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"pagePath": "pages/environment/index",
|
||||||
|
"text": "环境参数"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pagePath": "pages/parameter/index",
|
||||||
|
"text": "参数记录"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pagePath": "pages/visual/index",
|
||||||
|
"text": "视觉监控"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pagePath": "pages/log/index",
|
||||||
|
"text": "系统日志"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pagePath": "pages/alarm/index",
|
||||||
|
"text": "报警记录"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"easycom": {
|
"easycom": {
|
||||||
"autoscan": true,
|
"autoscan": true,
|
||||||
"custom": {
|
"custom": {
|
||||||
|
|||||||
623
src/pages/alarm/index.vue
Normal file
@ -0,0 +1,623 @@
|
|||||||
|
<template>
|
||||||
|
<view class="alarm-record-page">
|
||||||
|
<!-- 固定头部 -->
|
||||||
|
<view class="fixed-header">
|
||||||
|
<text class="header-title">移动式检修车间</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<view class="tabbar-content">
|
||||||
|
|
||||||
|
<!-- 报警统计 -->
|
||||||
|
<view class="alarm-statistics">
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-label">总报警数</text>
|
||||||
|
<text class="stat-value total">{{ totalAlarms }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-label">未处理</text>
|
||||||
|
<text class="stat-value pending">{{ pendingAlarms }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-label">已处理</text>
|
||||||
|
<text class="stat-value resolved">{{ resolvedAlarms }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-label">紧急报警</text>
|
||||||
|
<text class="stat-value urgent">{{ urgentAlarms }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 筛选区域 -->
|
||||||
|
<view class="filter-section">
|
||||||
|
<view class="filter-row">
|
||||||
|
<view class="filter-item">
|
||||||
|
<text class="filter-label">报警级别</text>
|
||||||
|
<picker :value="levelIndex" :range="levelOptions" @change="onLevelChange">
|
||||||
|
<view class="picker-view">
|
||||||
|
<text>{{ levelOptions[levelIndex] }}</text>
|
||||||
|
<text class="picker-arrow">▼</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="filter-item">
|
||||||
|
<text class="filter-label">处理状态</text>
|
||||||
|
<picker :value="statusIndex" :range="statusOptions" @change="onStatusChange">
|
||||||
|
<view class="picker-view">
|
||||||
|
<text>{{ statusOptions[statusIndex] }}</text>
|
||||||
|
<text class="picker-arrow">▼</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="filter-row">
|
||||||
|
<view class="filter-item">
|
||||||
|
<text class="filter-label">时间范围</text>
|
||||||
|
<picker mode="date" :value="alarmDate" @change="onDateChange">
|
||||||
|
<view class="picker-view">
|
||||||
|
<text>{{ alarmDate }}</text>
|
||||||
|
<text class="picker-arrow">▼</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<button class="search-button" @click="searchAlarms">搜索</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 报警列表 -->
|
||||||
|
<view class="alarms-container">
|
||||||
|
<view class="alarms-header">
|
||||||
|
<text class="alarms-title">报警详情</text>
|
||||||
|
<view class="header-actions">
|
||||||
|
<button class="action-button" @click="refreshAlarms">刷新</button>
|
||||||
|
<button class="action-button" @click="exportAlarms">导出</button>
|
||||||
|
<button class="action-button clear" @click="clearAlarms">清空</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view class="alarms-list" scroll-y="true">
|
||||||
|
<view class="alarm-item" v-for="(alarm, index) in alarms" :key="index" :class="alarm.level">
|
||||||
|
<view class="alarm-header">
|
||||||
|
<view class="alarm-level" :class="alarm.level">
|
||||||
|
<text>{{ alarm.levelText }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="alarm-status" :class="alarm.status">
|
||||||
|
<text>{{ alarm.statusText }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="alarm-time">{{ alarm.time }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="alarm-content">
|
||||||
|
<text class="alarm-title">{{ alarm.title }}</text>
|
||||||
|
<text class="alarm-description">{{ alarm.description }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="alarm-details">
|
||||||
|
<view class="detail-item">
|
||||||
|
<text class="detail-label">报警源:</text>
|
||||||
|
<text class="detail-value">{{ alarm.source }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="detail-item">
|
||||||
|
<text class="detail-label">当前值:</text>
|
||||||
|
<text class="detail-value">{{ alarm.currentValue }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="detail-item">
|
||||||
|
<text class="detail-label">阈值:</text>
|
||||||
|
<text class="detail-value">{{ alarm.threshold }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="alarm-actions" v-if="alarm.status === 'pending'">
|
||||||
|
<button class="action-button resolve" @click="resolveAlarm(alarm)">处理</button>
|
||||||
|
<button class="action-button ignore" @click="ignoreAlarm(alarm)">忽略</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="alarm-resolution" v-if="alarm.status === 'resolved'">
|
||||||
|
<text class="resolution-text">处理人: {{ alarm.resolver }}</text>
|
||||||
|
<text class="resolution-time">处理时间: {{ alarm.resolveTime }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
totalAlarms: 28,
|
||||||
|
pendingAlarms: 5,
|
||||||
|
resolvedAlarms: 20,
|
||||||
|
urgentAlarms: 3,
|
||||||
|
levelIndex: 0,
|
||||||
|
levelOptions: ['全部级别', '紧急', '高', '中', '低'],
|
||||||
|
statusIndex: 0,
|
||||||
|
statusOptions: ['全部状态', '未处理', '已处理', '已忽略'],
|
||||||
|
alarmDate: '2025-09-29',
|
||||||
|
alarms: [
|
||||||
|
{
|
||||||
|
level: 'urgent',
|
||||||
|
levelText: '紧急',
|
||||||
|
status: 'pending',
|
||||||
|
statusText: '未处理',
|
||||||
|
time: '2025-09-29 15:45:33',
|
||||||
|
title: '温度超限报警',
|
||||||
|
description: '温度传感器读数超过安全阈值,存在设备损坏风险',
|
||||||
|
source: '温度传感器01',
|
||||||
|
currentValue: '85.2°C',
|
||||||
|
threshold: '80.0°C',
|
||||||
|
resolver: '',
|
||||||
|
resolveTime: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 'high',
|
||||||
|
levelText: '高',
|
||||||
|
status: 'pending',
|
||||||
|
statusText: '未处理',
|
||||||
|
time: '2025-09-29 15:42:15',
|
||||||
|
title: '湿度异常报警',
|
||||||
|
description: '湿度传感器读数异常,可能影响设备正常运行',
|
||||||
|
source: '湿度传感器02',
|
||||||
|
currentValue: '95%',
|
||||||
|
threshold: '90%',
|
||||||
|
resolver: '',
|
||||||
|
resolveTime: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 'medium',
|
||||||
|
levelText: '中',
|
||||||
|
status: 'resolved',
|
||||||
|
statusText: '已处理',
|
||||||
|
time: '2025-09-29 15:38:22',
|
||||||
|
title: '网络连接异常',
|
||||||
|
description: 'MQTT连接中断,数据同步失败',
|
||||||
|
source: '网络模块',
|
||||||
|
currentValue: '离线',
|
||||||
|
threshold: '在线',
|
||||||
|
resolver: '系统管理员',
|
||||||
|
resolveTime: '2025-09-29 15:40:15'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 'low',
|
||||||
|
levelText: '低',
|
||||||
|
status: 'resolved',
|
||||||
|
statusText: '已处理',
|
||||||
|
time: '2025-09-29 15:35:10',
|
||||||
|
title: '存储空间不足',
|
||||||
|
description: '系统存储空间使用率超过80%',
|
||||||
|
source: '存储模块',
|
||||||
|
currentValue: '85%',
|
||||||
|
threshold: '80%',
|
||||||
|
resolver: '系统管理员',
|
||||||
|
resolveTime: '2025-09-29 15:36:45'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 'urgent',
|
||||||
|
levelText: '紧急',
|
||||||
|
status: 'pending',
|
||||||
|
statusText: '未处理',
|
||||||
|
time: '2025-09-29 15:30:55',
|
||||||
|
title: '设备离线报警',
|
||||||
|
description: '关键设备离线,可能影响生产安全',
|
||||||
|
source: '设备监控',
|
||||||
|
currentValue: '离线',
|
||||||
|
threshold: '在线',
|
||||||
|
resolver: '',
|
||||||
|
resolveTime: ''
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
console.log('报警记录页面加载')
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onLevelChange(e) {
|
||||||
|
this.levelIndex = e.detail.value
|
||||||
|
},
|
||||||
|
onStatusChange(e) {
|
||||||
|
this.statusIndex = e.detail.value
|
||||||
|
},
|
||||||
|
onDateChange(e) {
|
||||||
|
this.alarmDate = e.detail.value
|
||||||
|
},
|
||||||
|
searchAlarms() {
|
||||||
|
console.log('搜索报警', {
|
||||||
|
level: this.levelOptions[this.levelIndex],
|
||||||
|
status: this.statusOptions[this.statusIndex],
|
||||||
|
date: this.alarmDate
|
||||||
|
})
|
||||||
|
uni.showToast({
|
||||||
|
title: '搜索中...',
|
||||||
|
icon: 'loading'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
refreshAlarms() {
|
||||||
|
uni.showToast({
|
||||||
|
title: '刷新报警记录',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
exportAlarms() {
|
||||||
|
uni.showToast({
|
||||||
|
title: '导出功能开发中',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
clearAlarms() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认清空',
|
||||||
|
content: '确定要清空所有报警记录吗?此操作不可恢复。',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
this.alarms = []
|
||||||
|
this.totalAlarms = 0
|
||||||
|
this.pendingAlarms = 0
|
||||||
|
this.resolvedAlarms = 0
|
||||||
|
this.urgentAlarms = 0
|
||||||
|
uni.showToast({
|
||||||
|
title: '清空成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
resolveAlarm(alarm) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '处理报警',
|
||||||
|
content: `确定要处理 "${alarm.title}" 吗?`,
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
alarm.status = 'resolved'
|
||||||
|
alarm.statusText = '已处理'
|
||||||
|
alarm.resolver = '当前用户'
|
||||||
|
alarm.resolveTime = new Date().toLocaleString()
|
||||||
|
this.pendingAlarms--
|
||||||
|
this.resolvedAlarms++
|
||||||
|
uni.showToast({
|
||||||
|
title: '处理成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
ignoreAlarm(alarm) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '忽略报警',
|
||||||
|
content: `确定要忽略 "${alarm.title}" 吗?`,
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
alarm.status = 'ignored'
|
||||||
|
alarm.statusText = '已忽略'
|
||||||
|
this.pendingAlarms--
|
||||||
|
uni.showToast({
|
||||||
|
title: '已忽略',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.alarm-record-page {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-statistics {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 25rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&.total {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pending {
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.resolved {
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.urgent {
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 25rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-view {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20rpx;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
border: 2rpx solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-arrow {
|
||||||
|
color: #999;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button {
|
||||||
|
background-color: #3f51b5;
|
||||||
|
color: white;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarms-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15rpx;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarms-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-bottom: 2rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarms-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
background-color: #3f51b5;
|
||||||
|
color: white;
|
||||||
|
padding: 10rpx 20rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
|
||||||
|
&.clear {
|
||||||
|
background-color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.resolve {
|
||||||
|
background-color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ignore {
|
||||||
|
background-color: #ff9800;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarms-list {
|
||||||
|
height: 600rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-item {
|
||||||
|
padding: 30rpx;
|
||||||
|
border-bottom: 2rpx solid #f0f0f0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.urgent {
|
||||||
|
border-left: 8rpx solid #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.high {
|
||||||
|
border-left: 8rpx solid #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.medium {
|
||||||
|
border-left: 8rpx solid #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.low {
|
||||||
|
border-left: 8rpx solid #4caf50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
margin-bottom: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-level {
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&.urgent {
|
||||||
|
background-color: #ffebee;
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.high {
|
||||||
|
background-color: #fff3e0;
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.medium {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
color: #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.low {
|
||||||
|
background-color: #e8f5e8;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-status {
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&.pending {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.resolved {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ignored {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-time {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-content {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-description {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-details {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
padding: 20rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-resolution {
|
||||||
|
background-color: #e8f5e8;
|
||||||
|
padding: 20rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resolution-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resolution-time {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
885
src/pages/environment/index.vue
Normal file
@ -0,0 +1,885 @@
|
|||||||
|
<template>
|
||||||
|
<view class="environment-page">
|
||||||
|
<!-- 固定头部 -->
|
||||||
|
<view class="fixed-header">
|
||||||
|
<text class="header-title">移动式检修车间</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<view class="tabbar-content">
|
||||||
|
<!-- 顶部参数卡片 -->
|
||||||
|
<view class="parameter-cards">
|
||||||
|
<view class="card-item">
|
||||||
|
<view class="card-icon temperature-icon">🌡️</view>
|
||||||
|
<view class="card-content">
|
||||||
|
<text class="card-value">{{ temperature }}°C</text>
|
||||||
|
<text class="card-label">温度</text>
|
||||||
|
</view>
|
||||||
|
<view class="card-status" :class="temperature > 0 ? 'active' : 'inactive'"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-item">
|
||||||
|
<view class="card-icon humidity-icon">💧</view>
|
||||||
|
<view class="card-content">
|
||||||
|
<text class="card-value">{{ humidity }}%</text>
|
||||||
|
<text class="card-label">湿度</text>
|
||||||
|
</view>
|
||||||
|
<view class="card-status" :class="humidity > 0 ? 'active' : 'inactive'"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-item">
|
||||||
|
<view class="card-icon cleanliness-icon">✨</view>
|
||||||
|
<view class="card-content">
|
||||||
|
<text class="card-value">{{ cleanliness > 0 ? cleanliness + '%' : '-%' }}</text>
|
||||||
|
<text class="card-label">洁净度</text>
|
||||||
|
</view>
|
||||||
|
<view class="card-status" :class="cleanliness > 0 ? 'active' : 'inactive'"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 环境参数详情 -->
|
||||||
|
<view class="parameter-details">
|
||||||
|
<view class="parameter-item">
|
||||||
|
<view class="parameter-header">
|
||||||
|
<text class="parameter-label">温度</text>
|
||||||
|
<text class="current-value">{{ temperature }}°C</text>
|
||||||
|
</view>
|
||||||
|
<view class="progress-container">
|
||||||
|
<view class="progress-bar">
|
||||||
|
<view class="progress-fill temperature-progress" :style="{ width: temperatureProgress + '%' }"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="progress-info">
|
||||||
|
<text class="range-value">{{ temperatureRange.min }}°C - {{ temperatureRange.max }}°C</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="parameter-item">
|
||||||
|
<view class="parameter-header">
|
||||||
|
<text class="parameter-label">湿度</text>
|
||||||
|
<text class="current-value">{{ humidity }}%</text>
|
||||||
|
</view>
|
||||||
|
<view class="progress-container">
|
||||||
|
<view class="progress-bar">
|
||||||
|
<view class="progress-fill humidity-progress" :style="{ width: humidityProgress + '%' }"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="progress-info">
|
||||||
|
<text class="range-value">{{ humidityRange.min }}% - {{ humidityRange.max }}%</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="parameter-item">
|
||||||
|
<view class="parameter-header">
|
||||||
|
<text class="parameter-label">洁净度</text>
|
||||||
|
<text class="current-value">{{ cleanliness > 0 ? cleanliness + '%' : '-%' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="progress-container">
|
||||||
|
<view class="progress-bar">
|
||||||
|
<view class="progress-fill cleanliness-progress" :style="{ width: cleanlinessProgress + '%' }"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="progress-info">
|
||||||
|
<text class="range-value">暂无数据</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空调目标参数设置 -->
|
||||||
|
<view class="air-conditioner-settings">
|
||||||
|
<view class="settings-header">
|
||||||
|
<text class="settings-title">空调目标参数设置</text>
|
||||||
|
</view>
|
||||||
|
<view class="temperature-control">
|
||||||
|
<text class="control-label">目标温度</text>
|
||||||
|
<view class="temperature-display">
|
||||||
|
<button class="temp-btn decrease" @click="decreaseTemperature">-</button>
|
||||||
|
<text class="temperature-value">{{ targetTemperature }}°C</text>
|
||||||
|
<button class="temp-btn increase" @click="increaseTemperature">+</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 环境控制区域 -->
|
||||||
|
<view class="environment-control">
|
||||||
|
<view class="control-header">
|
||||||
|
<text class="control-title">环境控制</text>
|
||||||
|
<view class="status-indicator">
|
||||||
|
<view class="status-dot"></view>
|
||||||
|
<text class="status-text">异常</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="control-info">
|
||||||
|
<text class="last-update">最后更新: {{ lastUpdate }}</text>
|
||||||
|
<view class="connection-info">
|
||||||
|
<text class="connection-status" :class="connectionStatus.isConnected ? 'connected' : 'disconnected'">
|
||||||
|
{{ connectionStatus.isConnected ? 'MQTT已连接' : 'MQTT未连接' }}
|
||||||
|
</text>
|
||||||
|
<button v-if="!connectionStatus.isConnected" class="reconnect-btn" @click="manualReconnect">
|
||||||
|
重连
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
<text class="temperature-range">温度控制: {{ temperatureRange.min }}°C - {{ temperatureRange.max }}°C</text>
|
||||||
|
<text class="humidity-range">湿度控制: {{ humidityRange.min }}% - {{ humidityRange.max }}%</text>
|
||||||
|
</view>
|
||||||
|
<button class="settings-button" @click="openSettingsModal">
|
||||||
|
<text class="settings-icon">⚙️</text>
|
||||||
|
<text class="settings-text">参数设定</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 参数设定弹窗 -->
|
||||||
|
<uni-popup ref="settingsPopup" type="center" :mask-click="false">
|
||||||
|
<view class="settings-modal">
|
||||||
|
<view class="modal-header">
|
||||||
|
<text class="modal-title">参数设定</text>
|
||||||
|
<text class="close-btn" @click="closeSettingsModal">✕</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="modal-content">
|
||||||
|
<!-- 温度设定 -->
|
||||||
|
<view class="setting-item">
|
||||||
|
<text class="setting-label">温度控制范围</text>
|
||||||
|
<view class="range-inputs">
|
||||||
|
<input
|
||||||
|
class="range-input"
|
||||||
|
type="number"
|
||||||
|
v-model="tempSettings.min"
|
||||||
|
placeholder="最小值"
|
||||||
|
/>
|
||||||
|
<text class="range-separator">-</text>
|
||||||
|
<input
|
||||||
|
class="range-input"
|
||||||
|
type="number"
|
||||||
|
v-model="tempSettings.max"
|
||||||
|
placeholder="最大值"
|
||||||
|
/>
|
||||||
|
<text class="unit">°C</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 湿度设定 -->
|
||||||
|
<view class="setting-item">
|
||||||
|
<text class="setting-label">湿度控制范围</text>
|
||||||
|
<view class="range-inputs">
|
||||||
|
<input
|
||||||
|
class="range-input"
|
||||||
|
type="number"
|
||||||
|
v-model="humiditySettings.min"
|
||||||
|
placeholder="最小值"
|
||||||
|
/>
|
||||||
|
<text class="range-separator">-</text>
|
||||||
|
<input
|
||||||
|
class="range-input"
|
||||||
|
type="number"
|
||||||
|
v-model="humiditySettings.max"
|
||||||
|
placeholder="最大值"
|
||||||
|
/>
|
||||||
|
<text class="unit">%</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="modal-actions">
|
||||||
|
<button class="cancel-btn" @click="closeSettingsModal">取消</button>
|
||||||
|
<button class="confirm-btn" @click="saveSettings">确定</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</uni-popup>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import mqttDataManager from '@/utils/mqttDataManager.js'
|
||||||
|
import { manualReconnect } from '@/utils/sendMqtt.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
temperature: 0,
|
||||||
|
humidity: 0,
|
||||||
|
cleanliness: 0,
|
||||||
|
temperatureProgress: 0,
|
||||||
|
humidityProgress: 0,
|
||||||
|
cleanlinessProgress: 0,
|
||||||
|
lastUpdate: '暂无数据',
|
||||||
|
temperatureRange: {
|
||||||
|
min: 0,
|
||||||
|
max: 100
|
||||||
|
},
|
||||||
|
humidityRange: {
|
||||||
|
min: 0,
|
||||||
|
max: 100
|
||||||
|
},
|
||||||
|
tempSettings: {
|
||||||
|
min: 25,
|
||||||
|
max: 35
|
||||||
|
},
|
||||||
|
humiditySettings: {
|
||||||
|
min: 0,
|
||||||
|
max: 100
|
||||||
|
},
|
||||||
|
connectionStatus: {
|
||||||
|
isConnected: false,
|
||||||
|
lastUpdate: null
|
||||||
|
},
|
||||||
|
targetTemperature: 40
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
console.log('环境参数页面加载')
|
||||||
|
this.initMqttListener()
|
||||||
|
},
|
||||||
|
onUnload() {
|
||||||
|
console.log('🔌 环境参数页面卸载,清理资源...')
|
||||||
|
|
||||||
|
// 页面卸载时移除监听器
|
||||||
|
if (this.dataUpdateHandler) {
|
||||||
|
mqttDataManager.removeListener('dataUpdate', this.dataUpdateHandler)
|
||||||
|
console.log('✅ 数据更新监听器已移除')
|
||||||
|
}
|
||||||
|
if (this.statusUpdateHandler) {
|
||||||
|
mqttDataManager.removeListener('connectionStatus', this.statusUpdateHandler)
|
||||||
|
console.log('✅ 状态更新监听器已移除')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理调试定时器
|
||||||
|
if (this.debugInterval) {
|
||||||
|
clearInterval(this.debugInterval)
|
||||||
|
console.log('✅ 调试定时器已清理')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ 环境参数页面资源清理完成')
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 初始化MQTT监听
|
||||||
|
initMqttListener() {
|
||||||
|
console.log('🔧 环境参数页面开始初始化MQTT监听...')
|
||||||
|
|
||||||
|
// 监听数据更新
|
||||||
|
this.dataUpdateHandler = (data) => {
|
||||||
|
console.log('📨 环境参数页面收到MQTT数据:', data)
|
||||||
|
this.updateEnvironmentData(data)
|
||||||
|
}
|
||||||
|
mqttDataManager.addListener('dataUpdate', this.dataUpdateHandler)
|
||||||
|
|
||||||
|
// 监听连接状态
|
||||||
|
this.statusUpdateHandler = (status) => {
|
||||||
|
console.log('🔄 环境参数页面连接状态更新:', status)
|
||||||
|
const wasConnected = this.connectionStatus.isConnected
|
||||||
|
this.connectionStatus = status
|
||||||
|
|
||||||
|
// 只在状态发生变化时显示提示(避免重复提示)
|
||||||
|
if (wasConnected !== status.isConnected) {
|
||||||
|
if (status.isConnected) {
|
||||||
|
console.log('✅ MQTT连接状态: 已连接')
|
||||||
|
// 不显示Toast,因为sendMqtt.js中已经显示了
|
||||||
|
} else {
|
||||||
|
console.log('❌ MQTT连接状态: 未连接')
|
||||||
|
// 不显示Toast,因为sendMqtt.js中已经显示了
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mqttDataManager.addListener('connectionStatus', this.statusUpdateHandler)
|
||||||
|
|
||||||
|
// 获取初始数据
|
||||||
|
const lastData = mqttDataManager.getLastData()
|
||||||
|
console.log('📊 获取初始数据:', lastData)
|
||||||
|
if (lastData.timestamp) {
|
||||||
|
this.updateEnvironmentData(lastData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取初始连接状态
|
||||||
|
this.connectionStatus = mqttDataManager.getConnectionStatus()
|
||||||
|
console.log('🔍 初始连接状态:', this.connectionStatus)
|
||||||
|
|
||||||
|
// 定期检查连接状态(用于调试)
|
||||||
|
this.debugInterval = setInterval(() => {
|
||||||
|
const currentStatus = mqttDataManager.getConnectionStatus()
|
||||||
|
console.log('🔍 定期检查连接状态:', currentStatus)
|
||||||
|
}, 10000) // 每10秒检查一次
|
||||||
|
|
||||||
|
console.log('✅ 环境参数页面MQTT监听初始化完成')
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新环境数据
|
||||||
|
updateEnvironmentData(data) {
|
||||||
|
console.log('🌡️ 环境参数页面更新数据:', data)
|
||||||
|
|
||||||
|
// 只处理WSD设备的数据
|
||||||
|
if (data.deviceType === 'WSD') {
|
||||||
|
if (data.temperature !== undefined) {
|
||||||
|
this.temperature = parseFloat(data.temperature.toFixed(1))
|
||||||
|
this.temperatureProgress = Math.min(Math.max(this.temperature, 0), 100)
|
||||||
|
console.log('✅ 温度已更新:', this.temperature)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.humidity !== undefined) {
|
||||||
|
this.humidity = parseFloat(data.humidity.toFixed(1))
|
||||||
|
this.humidityProgress = Math.min(Math.max(this.humidity, 0), 100)
|
||||||
|
console.log('✅ 湿度已更新:', this.humidity)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastUpdate = data.time || new Date().toLocaleString('zh-CN')
|
||||||
|
|
||||||
|
console.log('✅ 环境数据更新完成:', {
|
||||||
|
temperature: this.temperature,
|
||||||
|
humidity: this.humidity,
|
||||||
|
lastUpdate: this.lastUpdate
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ 非WSD设备数据,跳过更新:', data.deviceType)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 降低目标温度
|
||||||
|
decreaseTemperature() {
|
||||||
|
if (this.targetTemperature > 16) {
|
||||||
|
this.targetTemperature--
|
||||||
|
console.log('目标温度降低至:', this.targetTemperature + '°C')
|
||||||
|
this.showTemperatureChangeToast()
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: '温度不能低于16°C',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 提高目标温度
|
||||||
|
increaseTemperature() {
|
||||||
|
if (this.targetTemperature < 30) {
|
||||||
|
this.targetTemperature++
|
||||||
|
console.log('目标温度提高至:', this.targetTemperature + '°C')
|
||||||
|
this.showTemperatureChangeToast()
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: '温度不能高于30°C',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 显示温度变化提示
|
||||||
|
showTemperatureChangeToast() {
|
||||||
|
uni.showToast({
|
||||||
|
title: `目标温度: ${this.targetTemperature}°C`,
|
||||||
|
icon: 'success',
|
||||||
|
duration: 1500
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// 手动重连MQTT
|
||||||
|
manualReconnect() {
|
||||||
|
console.log('🔄 用户手动触发MQTT重连')
|
||||||
|
uni.showToast({
|
||||||
|
title: '正在重连...',
|
||||||
|
icon: 'loading',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
|
||||||
|
// 调用sendMqtt.js中的手动重连函数
|
||||||
|
manualReconnect()
|
||||||
|
},
|
||||||
|
openSettingsModal() {
|
||||||
|
// 打开弹窗前,将当前设置复制到临时变量
|
||||||
|
this.tempSettings = { ...this.temperatureRange }
|
||||||
|
this.humiditySettings = { ...this.humidityRange }
|
||||||
|
this.$refs.settingsPopup.open()
|
||||||
|
},
|
||||||
|
closeSettingsModal() {
|
||||||
|
this.$refs.settingsPopup.close()
|
||||||
|
},
|
||||||
|
saveSettings() {
|
||||||
|
// 保存设置
|
||||||
|
this.temperatureRange = { ...this.tempSettings }
|
||||||
|
this.humidityRange = { ...this.humiditySettings }
|
||||||
|
|
||||||
|
// 更新最后更新时间
|
||||||
|
this.lastUpdate = new Date().toLocaleString()
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
this.closeSettingsModal()
|
||||||
|
|
||||||
|
// 显示保存成功提示
|
||||||
|
uni.showToast({
|
||||||
|
title: '参数设置已保存',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('保存的温度范围:', this.temperatureRange)
|
||||||
|
console.log('保存的湿度范围:', this.humidityRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.environment-page {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部参数卡片样式 */
|
||||||
|
.parameter-cards {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-item {
|
||||||
|
flex: 1;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
|
||||||
|
min-height: 120rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
font-size: 40rpx;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
text-align: center;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 5rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-status {
|
||||||
|
position: absolute;
|
||||||
|
top: 15rpx;
|
||||||
|
right: 15rpx;
|
||||||
|
width: 16rpx;
|
||||||
|
height: 16rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-status.active {
|
||||||
|
background-color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-status.inactive {
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-value {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 20rpx;
|
||||||
|
right: 20rpx;
|
||||||
|
width: 20rpx;
|
||||||
|
height: 20rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperature-indicator {
|
||||||
|
background-color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.humidity-indicator {
|
||||||
|
background-color: #4488ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cleanliness-indicator {
|
||||||
|
background-color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-details {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 25rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-item {
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #4488ff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 16rpx;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperature-progress {
|
||||||
|
background: linear-gradient(90deg, #4488ff, #44ff88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.humidity-progress {
|
||||||
|
background: linear-gradient(90deg, #4488ff, #44ff88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cleanliness-progress {
|
||||||
|
background: linear-gradient(90deg, #4488ff, #44ff88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
min-width: 200rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4488ff;
|
||||||
|
margin-bottom: 5rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-value {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空调目标参数设置样式 */
|
||||||
|
.air-conditioner-settings {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 25rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-header {
|
||||||
|
margin-bottom: 25rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperature-control {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperature-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temp-btn {
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temp-btn.decrease {
|
||||||
|
background-color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temp-btn.increase {
|
||||||
|
background-color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperature-value {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
min-width: 120rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.environment-control {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 25rpx;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 20rpx;
|
||||||
|
height: 20rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #ffaa00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #ffaa00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-update {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #4488ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status.connected {
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status.disconnected {
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reconnect-btn {
|
||||||
|
background-color: #ff4444;
|
||||||
|
color: white;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperature-range {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #4488ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.humidity-range {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #4488ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-button {
|
||||||
|
background-color: #3f51b5;
|
||||||
|
color: white;
|
||||||
|
padding: 20rpx 40rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-icon {
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗样式 */
|
||||||
|
.settings-modal {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
width: 600rpx;
|
||||||
|
max-width: 90vw;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-bottom: 2rpx solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #666;
|
||||||
|
padding: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
padding: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-inputs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20rpx;
|
||||||
|
border: 2rpx solid #e0e0e0;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-separator {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
min-width: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #666;
|
||||||
|
color: white;
|
||||||
|
padding: 20rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #3f51b5;
|
||||||
|
color: white;
|
||||||
|
padding: 20rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,10 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="content">
|
<view class="index-page">
|
||||||
<image class="logo" src="/static/logo.png"></image>
|
<!-- 固定头部 -->
|
||||||
<view class="text-area">
|
<view class="fixed-header">
|
||||||
<text class="title">{{ title }}</text>
|
<text class="header-title">移动式检修车间</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<view class="non-tabbar-content">
|
||||||
|
<view class="content">
|
||||||
|
<image class="logo" src="/static/logo.png"></image>
|
||||||
|
<view class="text-area">
|
||||||
|
<text class="title">{{ title }}</text>
|
||||||
|
</view>
|
||||||
|
<button class="nav-button" @click="navigateToTabBar">进入检修系统</button>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<button class="nav-button" @click="navigateToSystem">进入检修系统</button>
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -17,14 +27,15 @@ export default {
|
|||||||
},
|
},
|
||||||
onLoad() {},
|
onLoad() {},
|
||||||
methods: {
|
methods: {
|
||||||
navigateToSystem() {
|
navigateToTabBar() {
|
||||||
uni.navigateTo({
|
// 跳转到tabbar的第一个页面(环境参数页面)
|
||||||
url: '../system/index',
|
uni.switchTab({
|
||||||
|
url: '/pages/environment/index',
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
console.error('导航失败:', err);
|
console.error('跳转到tabbar失败:', err);
|
||||||
// 尝试使用替代方法
|
// 如果失败,尝试使用redirectTo
|
||||||
uni.redirectTo({
|
uni.redirectTo({
|
||||||
url: '../system/index'
|
url: '/pages/environment/index'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -34,20 +45,26 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.index-page {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 40rpx;
|
padding: 40rpx;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
height: 200rpx;
|
height: 200rpx;
|
||||||
width: 200rpx;
|
width: 200rpx;
|
||||||
margin-top: 200rpx;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
margin-bottom: 50rpx;
|
margin-bottom: 50rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
481
src/pages/log/index.vue
Normal file
@ -0,0 +1,481 @@
|
|||||||
|
<template>
|
||||||
|
<view class="system-log-page">
|
||||||
|
<!-- 固定头部 -->
|
||||||
|
<view class="fixed-header">
|
||||||
|
<text class="header-title">移动式检修车间</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<view class="tabbar-content">
|
||||||
|
|
||||||
|
<!-- 筛选区域 -->
|
||||||
|
<view class="filter-section">
|
||||||
|
<view class="filter-row">
|
||||||
|
<view class="filter-item">
|
||||||
|
<text class="filter-label">日志级别</text>
|
||||||
|
<picker :value="levelIndex" :range="levelOptions" @change="onLevelChange">
|
||||||
|
<view class="picker-view">
|
||||||
|
<text>{{ levelOptions[levelIndex] }}</text>
|
||||||
|
<text class="picker-arrow">▼</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="filter-item">
|
||||||
|
<text class="filter-label">时间范围</text>
|
||||||
|
<picker mode="date" :value="logDate" @change="onDateChange">
|
||||||
|
<view class="picker-view">
|
||||||
|
<text>{{ logDate }}</text>
|
||||||
|
<text class="picker-arrow">▼</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="filter-row">
|
||||||
|
<view class="filter-item">
|
||||||
|
<text class="filter-label">关键词</text>
|
||||||
|
<input class="search-input" v-model="searchKeyword" placeholder="输入关键词搜索" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<button class="search-button" @click="searchLogs">搜索</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 统计信息 -->
|
||||||
|
<view class="statistics-section">
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-label">总日志数</text>
|
||||||
|
<text class="stat-value">{{ totalLogs }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-label">错误日志</text>
|
||||||
|
<text class="stat-value error">{{ errorLogs }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-label">警告日志</text>
|
||||||
|
<text class="stat-value warning">{{ warningLogs }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-label">信息日志</text>
|
||||||
|
<text class="stat-value info">{{ infoLogs }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 日志列表 -->
|
||||||
|
<view class="logs-container">
|
||||||
|
<view class="logs-header">
|
||||||
|
<text class="logs-title">日志详情</text>
|
||||||
|
<view class="header-actions">
|
||||||
|
<button class="action-button" @click="refreshLogs">刷新</button>
|
||||||
|
<button class="action-button" @click="exportLogs">导出</button>
|
||||||
|
<button class="action-button clear" @click="clearLogs">清空</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view class="logs-list" scroll-y="true" @scrolltolower="loadMoreLogs">
|
||||||
|
<view class="log-item" v-for="(log, index) in logs" :key="index" :class="log.level">
|
||||||
|
<view class="log-header">
|
||||||
|
<view class="log-level" :class="log.level">
|
||||||
|
<text>{{ log.levelText }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="log-time">{{ log.time }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="log-content">
|
||||||
|
<text class="log-message">{{ log.message }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="log-details" v-if="log.details">
|
||||||
|
<text class="log-details-text">{{ log.details }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="load-more" v-if="hasMore">
|
||||||
|
<text class="load-more-text">加载更多...</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
levelIndex: 0,
|
||||||
|
levelOptions: ['全部级别', '错误', '警告', '信息', '调试'],
|
||||||
|
logDate: '2025-09-29',
|
||||||
|
searchKeyword: '',
|
||||||
|
totalLogs: 1248,
|
||||||
|
errorLogs: 23,
|
||||||
|
warningLogs: 156,
|
||||||
|
infoLogs: 1069,
|
||||||
|
hasMore: true,
|
||||||
|
logs: [
|
||||||
|
{
|
||||||
|
level: 'error',
|
||||||
|
levelText: 'ERROR',
|
||||||
|
time: '2025-09-29 15:45:33',
|
||||||
|
message: 'MQTT连接失败',
|
||||||
|
details: 'Connection timeout after 5000ms'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 'warning',
|
||||||
|
levelText: 'WARN',
|
||||||
|
time: '2025-09-29 15:44:15',
|
||||||
|
message: '温度传感器读数异常',
|
||||||
|
details: 'Temperature reading: 999.9°C (超出正常范围)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 'info',
|
||||||
|
levelText: 'INFO',
|
||||||
|
time: '2025-09-29 15:43:22',
|
||||||
|
message: '系统启动完成',
|
||||||
|
details: '所有服务已启动,系统运行正常'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 'error',
|
||||||
|
levelText: 'ERROR',
|
||||||
|
time: '2025-09-29 15:42:10',
|
||||||
|
message: '数据库连接失败',
|
||||||
|
details: 'Failed to connect to database: Connection refused'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 'info',
|
||||||
|
levelText: 'INFO',
|
||||||
|
time: '2025-09-29 15:41:55',
|
||||||
|
message: '用户登录成功',
|
||||||
|
details: '用户 admin 登录系统'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 'warning',
|
||||||
|
levelText: 'WARN',
|
||||||
|
time: '2025-09-29 15:40:33',
|
||||||
|
message: '内存使用率过高',
|
||||||
|
details: 'Memory usage: 85% (建议清理缓存)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 'info',
|
||||||
|
levelText: 'INFO',
|
||||||
|
time: '2025-09-29 15:39:18',
|
||||||
|
message: '数据同步完成',
|
||||||
|
details: '同步了 156 条记录到云端'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 'error',
|
||||||
|
levelText: 'ERROR',
|
||||||
|
time: '2025-09-29 15:38:45',
|
||||||
|
message: '文件上传失败',
|
||||||
|
details: 'File size exceeds limit: 50MB > 10MB'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
console.log('系统日志页面加载')
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onLevelChange(e) {
|
||||||
|
this.levelIndex = e.detail.value
|
||||||
|
},
|
||||||
|
onDateChange(e) {
|
||||||
|
this.logDate = e.detail.value
|
||||||
|
},
|
||||||
|
searchLogs() {
|
||||||
|
console.log('搜索日志', {
|
||||||
|
level: this.levelOptions[this.levelIndex],
|
||||||
|
date: this.logDate,
|
||||||
|
keyword: this.searchKeyword
|
||||||
|
})
|
||||||
|
uni.showToast({
|
||||||
|
title: '搜索中...',
|
||||||
|
icon: 'loading'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
refreshLogs() {
|
||||||
|
uni.showToast({
|
||||||
|
title: '刷新日志',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
exportLogs() {
|
||||||
|
uni.showToast({
|
||||||
|
title: '导出功能开发中',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
clearLogs() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认清空',
|
||||||
|
content: '确定要清空所有日志吗?此操作不可恢复。',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
this.logs = []
|
||||||
|
this.totalLogs = 0
|
||||||
|
this.errorLogs = 0
|
||||||
|
this.warningLogs = 0
|
||||||
|
this.infoLogs = 0
|
||||||
|
uni.showToast({
|
||||||
|
title: '清空成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
loadMoreLogs() {
|
||||||
|
if (this.hasMore) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '加载更多...',
|
||||||
|
icon: 'loading'
|
||||||
|
})
|
||||||
|
// 模拟加载更多数据
|
||||||
|
setTimeout(() => {
|
||||||
|
this.hasMore = false
|
||||||
|
uni.hideToast()
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.system-log-page {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 25rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-view {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20rpx;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
border: 2rpx solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-arrow {
|
||||||
|
color: #999;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
padding: 20rpx;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
border: 2rpx solid #e0e0e0;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button {
|
||||||
|
background-color: #3f51b5;
|
||||||
|
color: white;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistics-section {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 25rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.info {
|
||||||
|
color: #2196f3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15rpx;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-bottom: 2rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
background-color: #3f51b5;
|
||||||
|
color: white;
|
||||||
|
padding: 10rpx 20rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
|
||||||
|
&.clear {
|
||||||
|
background-color: #ff4444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-list {
|
||||||
|
height: 600rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-item {
|
||||||
|
padding: 30rpx;
|
||||||
|
border-bottom: 2rpx solid #f0f0f0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
border-left: 8rpx solid #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
border-left: 8rpx solid #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.info {
|
||||||
|
border-left: 8rpx solid #2196f3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-level {
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
background-color: #ffebee;
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
background-color: #fff3e0;
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.info {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
color: #2196f3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-time {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-content {
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-message {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-details {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
padding: 15rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-details-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more {
|
||||||
|
padding: 30rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
383
src/pages/parameter/index.vue
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
<template>
|
||||||
|
<view class="parameter-record-page">
|
||||||
|
<!-- 固定头部 -->
|
||||||
|
<view class="fixed-header">
|
||||||
|
<text class="header-title">移动式检修车间</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<view class="tabbar-content">
|
||||||
|
<!-- 日期选择器 -->
|
||||||
|
<view class="date-selector">
|
||||||
|
<picker mode="date" :value="selectedDate" @change="onDateChange">
|
||||||
|
<view class="date-picker">
|
||||||
|
<text class="date-text">{{ selectedDate }}</text>
|
||||||
|
<text class="picker-arrow">▼</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
<view class="connection-status" :class="connectionStatus.isConnected ? 'connected' : 'disconnected'">
|
||||||
|
{{ connectionStatus.isConnected ? 'MQTT已连接' : 'MQTT未连接' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 温度趋势图表 -->
|
||||||
|
<view class="chart-card">
|
||||||
|
<view class="chart-header">
|
||||||
|
<text class="chart-title">温度趋势</text>
|
||||||
|
<view class="status-indicator">
|
||||||
|
<view class="status-dot temperature-dot"></view>
|
||||||
|
<text class="status-text">正常</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="chart-container">
|
||||||
|
<canvas canvas-id="temperatureChart" class="chart-canvas"></canvas>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 湿度趋势图表 -->
|
||||||
|
<view class="chart-card">
|
||||||
|
<view class="chart-header">
|
||||||
|
<text class="chart-title">湿度趋势</text>
|
||||||
|
<view class="status-indicator">
|
||||||
|
<view class="status-dot humidity-dot"></view>
|
||||||
|
<text class="status-text">正常</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="chart-container">
|
||||||
|
<canvas canvas-id="humidityChart" class="chart-canvas"></canvas>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- PM2.5趋势图表 -->
|
||||||
|
<view class="chart-card">
|
||||||
|
<view class="chart-header">
|
||||||
|
<text class="chart-title">PM2.5趋势</text>
|
||||||
|
<view class="status-indicator">
|
||||||
|
<view class="status-dot pm25-dot"></view>
|
||||||
|
<text class="status-text">正常</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="chart-container">
|
||||||
|
<canvas canvas-id="pm25Chart" class="chart-canvas"></canvas>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import mqttDataManager from '@/utils/mqttDataManager.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedDate: '2025-09-01',
|
||||||
|
// 24小时数据 (0-23点)
|
||||||
|
temperatureData: [22, 25, 28, 32, 35, 38, 40, 38, 35, 32, 28, 25, 22, 20, 18, 20, 22, 25, 28, 30, 32, 30, 28, 25],
|
||||||
|
humidityData: [45, 50, 55, 60, 65, 70, 75, 70, 65, 60, 55, 50, 45, 40, 35, 40, 45, 50, 55, 60, 65, 60, 55, 50],
|
||||||
|
pm25Data: [15, 18, 22, 25, 28, 32, 35, 32, 28, 25, 22, 18, 15, 12, 10, 12, 15, 18, 22, 25, 28, 25, 22, 18],
|
||||||
|
connectionStatus: {
|
||||||
|
isConnected: false,
|
||||||
|
lastUpdate: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
console.log('参数记录页面加载')
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.drawCharts()
|
||||||
|
})
|
||||||
|
this.initMqttListener()
|
||||||
|
},
|
||||||
|
onUnload() {
|
||||||
|
// 页面卸载时移除监听器
|
||||||
|
if (this.dataUpdateHandler) {
|
||||||
|
mqttDataManager.removeListener('dataUpdate', this.dataUpdateHandler)
|
||||||
|
}
|
||||||
|
if (this.statusUpdateHandler) {
|
||||||
|
mqttDataManager.removeListener('connectionStatus', this.statusUpdateHandler)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 初始化MQTT监听
|
||||||
|
initMqttListener() {
|
||||||
|
// 监听数据更新
|
||||||
|
this.dataUpdateHandler = (data) => {
|
||||||
|
console.log('参数记录页面收到MQTT数据:', data)
|
||||||
|
this.updateChartData(data)
|
||||||
|
}
|
||||||
|
mqttDataManager.addListener('dataUpdate', this.dataUpdateHandler)
|
||||||
|
|
||||||
|
// 监听连接状态
|
||||||
|
this.statusUpdateHandler = (status) => {
|
||||||
|
this.connectionStatus = status
|
||||||
|
console.log('参数记录页面连接状态更新:', status)
|
||||||
|
}
|
||||||
|
mqttDataManager.addListener('connectionStatus', this.statusUpdateHandler)
|
||||||
|
|
||||||
|
// 获取初始连接状态
|
||||||
|
this.connectionStatus = mqttDataManager.getConnectionStatus()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新图表数据
|
||||||
|
updateChartData(data) {
|
||||||
|
console.log('📊 参数记录页面更新数据:', data)
|
||||||
|
|
||||||
|
// 只处理WSD设备的数据
|
||||||
|
if (data.deviceType === 'WSD') {
|
||||||
|
const now = new Date()
|
||||||
|
const currentHour = now.getHours()
|
||||||
|
|
||||||
|
// 更新对应小时的数据
|
||||||
|
if (data.temperature !== undefined) {
|
||||||
|
this.temperatureData[currentHour] = Math.round(data.temperature)
|
||||||
|
console.log(`✅ 温度数据已更新 - 小时${currentHour}:`, this.temperatureData[currentHour])
|
||||||
|
}
|
||||||
|
if (data.humidity !== undefined) {
|
||||||
|
this.humidityData[currentHour] = Math.round(data.humidity)
|
||||||
|
console.log(`✅ 湿度数据已更新 - 小时${currentHour}:`, this.humidityData[currentHour])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新绘制图表
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.drawCharts()
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('✅ 图表数据更新完成:', {
|
||||||
|
temperature: this.temperatureData[currentHour],
|
||||||
|
humidity: this.humidityData[currentHour],
|
||||||
|
hour: currentHour
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ 非WSD设备数据,跳过图表更新:', data.deviceType)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDateChange(e) {
|
||||||
|
this.selectedDate = e.detail.value
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.drawCharts()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
drawCharts() {
|
||||||
|
this.drawTemperatureChart()
|
||||||
|
this.drawHumidityChart()
|
||||||
|
this.drawPM25Chart()
|
||||||
|
},
|
||||||
|
drawTemperatureChart() {
|
||||||
|
const ctx = uni.createCanvasContext('temperatureChart', this)
|
||||||
|
this.drawLineChart(ctx, this.temperatureData, '#ff6b35', '°C')
|
||||||
|
},
|
||||||
|
drawHumidityChart() {
|
||||||
|
const ctx = uni.createCanvasContext('humidityChart', this)
|
||||||
|
this.drawLineChart(ctx, this.humidityData, '#4a90e2', '%')
|
||||||
|
},
|
||||||
|
drawPM25Chart() {
|
||||||
|
const ctx = uni.createCanvasContext('pm25Chart', this)
|
||||||
|
this.drawLineChart(ctx, this.pm25Data, '#7ed321', 'μg/m³')
|
||||||
|
},
|
||||||
|
drawLineChart(ctx, data, color, unit) {
|
||||||
|
const canvasWidth = 300
|
||||||
|
const canvasHeight = 200
|
||||||
|
const padding = 40
|
||||||
|
const chartWidth = canvasWidth - padding * 2
|
||||||
|
const chartHeight = canvasHeight - padding * 2
|
||||||
|
|
||||||
|
// 清空画布
|
||||||
|
ctx.clearRect(0, 0, canvasWidth, canvasHeight)
|
||||||
|
|
||||||
|
// 绘制网格
|
||||||
|
ctx.setStrokeStyle('#f0f0f0')
|
||||||
|
ctx.setLineWidth(1)
|
||||||
|
|
||||||
|
// 水平网格线
|
||||||
|
for (let i = 0; i <= 5; i++) {
|
||||||
|
const y = padding + (chartHeight / 5) * i
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(padding, y)
|
||||||
|
ctx.lineTo(padding + chartWidth, y)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 垂直网格线
|
||||||
|
for (let i = 0; i <= 6; i++) {
|
||||||
|
const x = padding + (chartWidth / 6) * i
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(x, padding)
|
||||||
|
ctx.lineTo(x, padding + chartHeight)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制数据线
|
||||||
|
ctx.setStrokeStyle(color)
|
||||||
|
ctx.setLineWidth(3)
|
||||||
|
ctx.beginPath()
|
||||||
|
|
||||||
|
data.forEach((value, index) => {
|
||||||
|
const x = padding + (chartWidth / 23) * index
|
||||||
|
const y = padding + chartHeight - (value / 100) * chartHeight
|
||||||
|
|
||||||
|
if (index === 0) {
|
||||||
|
ctx.moveTo(x, y)
|
||||||
|
} else {
|
||||||
|
ctx.lineTo(x, y)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// 绘制数据点
|
||||||
|
ctx.setFillStyle(color)
|
||||||
|
data.forEach((value, index) => {
|
||||||
|
const x = padding + (chartWidth / 23) * index
|
||||||
|
const y = padding + chartHeight - (value / 100) * chartHeight
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(x, y, 4, 0, 2 * Math.PI)
|
||||||
|
ctx.fill()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 绘制X轴标签
|
||||||
|
ctx.setFillStyle('#666')
|
||||||
|
ctx.setFontSize(12)
|
||||||
|
ctx.setTextAlign('center')
|
||||||
|
|
||||||
|
const timeLabels = ['00', '04', '08', '12', '16', '20', '23']
|
||||||
|
timeLabels.forEach((label, index) => {
|
||||||
|
const x = padding + (chartWidth / 6) * index
|
||||||
|
ctx.fillText(label, x, canvasHeight - 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 绘制Y轴标签
|
||||||
|
ctx.setTextAlign('right')
|
||||||
|
for (let i = 0; i <= 5; i++) {
|
||||||
|
const y = padding + chartHeight - (chartHeight / 5) * i + 5
|
||||||
|
const value = (100 / 5) * i
|
||||||
|
ctx.fillText(value.toString(), padding - 10, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.draw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.parameter-record-page {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-selector {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
padding: 15rpx;
|
||||||
|
margin-bottom: 15rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status.connected {
|
||||||
|
background-color: #e8f5e8;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status.disconnected {
|
||||||
|
background-color: #ffebee;
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-picker {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: 15rpx 20rpx;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-arrow {
|
||||||
|
color: #999;
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
margin-bottom: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-subtitle {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6rpx;
|
||||||
|
background-color: #e8f5e8;
|
||||||
|
padding: 6rpx 12rpx;
|
||||||
|
border-radius: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 8rpx;
|
||||||
|
height: 8rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperature-dot {
|
||||||
|
background-color: #ff6b35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.humidity-dot {
|
||||||
|
background-color: #4a90e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pm25-dot {
|
||||||
|
background-color: #7ed321;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-canvas {
|
||||||
|
width: 280px;
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
504
src/pages/visual/index.vue
Normal file
@ -0,0 +1,504 @@
|
|||||||
|
<template>
|
||||||
|
<view class="visual-monitoring-page">
|
||||||
|
<!-- 固定头部 -->
|
||||||
|
<view class="fixed-header">
|
||||||
|
<text class="header-title">移动式检修车间</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<view class="tabbar-content">
|
||||||
|
|
||||||
|
<!-- 摄像头状态 -->
|
||||||
|
<view class="camera-status">
|
||||||
|
<view class="status-item">
|
||||||
|
<view class="status-icon camera-icon">📹</view>
|
||||||
|
<view class="status-info">
|
||||||
|
<text class="status-label">摄像头状态</text>
|
||||||
|
<text class="status-value" :class="cameraStatus.class">{{ cameraStatus.text }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="status-item">
|
||||||
|
<view class="status-icon recording-icon">🔴</view>
|
||||||
|
<view class="status-info">
|
||||||
|
<text class="status-label">录制状态</text>
|
||||||
|
<text class="status-value" :class="recordingStatus.class">{{ recordingStatus.text }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 视频区域 -->
|
||||||
|
<view class="video-container">
|
||||||
|
<view class="video-placeholder" v-if="!videoLoaded">
|
||||||
|
<image class="placeholder-image" src="/static/camera-placeholder.jpg" mode="aspectFit"></image>
|
||||||
|
<text class="placeholder-text">摄像头未连接</text>
|
||||||
|
<button class="connect-button" @click="connectCamera">连接摄像头</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="video-player" v-else>
|
||||||
|
<text class="video-text">实时视频流</text>
|
||||||
|
<view class="video-controls">
|
||||||
|
<button class="control-button" @click="toggleRecording">
|
||||||
|
{{ isRecording ? '停止录制' : '开始录制' }}
|
||||||
|
</button>
|
||||||
|
<button class="control-button" @click="takeSnapshot">拍照</button>
|
||||||
|
<button class="control-button" @click="toggleFullscreen">全屏</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 监控设置 -->
|
||||||
|
<view class="monitoring-settings">
|
||||||
|
<view class="settings-header">
|
||||||
|
<text class="settings-title">监控设置</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="setting-item">
|
||||||
|
<text class="setting-label">录制质量</text>
|
||||||
|
<picker :value="qualityIndex" :range="qualityOptions" @change="onQualityChange">
|
||||||
|
<view class="picker-view">
|
||||||
|
<text>{{ qualityOptions[qualityIndex] }}</text>
|
||||||
|
<text class="picker-arrow">▼</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="setting-item">
|
||||||
|
<text class="setting-label">录制时长</text>
|
||||||
|
<picker :value="durationIndex" :range="durationOptions" @change="onDurationChange">
|
||||||
|
<view class="picker-view">
|
||||||
|
<text>{{ durationOptions[durationIndex] }}</text>
|
||||||
|
<text class="picker-arrow">▼</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="setting-item">
|
||||||
|
<text class="setting-label">自动保存</text>
|
||||||
|
<switch :checked="autoSave" @change="onAutoSaveChange" color="#3f51b5"/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 录制历史 -->
|
||||||
|
<view class="recording-history">
|
||||||
|
<view class="history-header">
|
||||||
|
<text class="history-title">录制历史</text>
|
||||||
|
<button class="clear-button" @click="clearHistory">清空</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view class="history-list" scroll-y="true">
|
||||||
|
<view class="history-item" v-for="(item, index) in historyList" :key="index">
|
||||||
|
<view class="history-info">
|
||||||
|
<text class="history-time">{{ item.time }}</text>
|
||||||
|
<text class="history-duration">{{ item.duration }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="history-actions">
|
||||||
|
<button class="action-button" @click="playVideo(item)">播放</button>
|
||||||
|
<button class="action-button" @click="downloadVideo(item)">下载</button>
|
||||||
|
<button class="action-button delete" @click="deleteVideo(item)">删除</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
videoLoaded: false,
|
||||||
|
isRecording: false,
|
||||||
|
cameraStatus: {
|
||||||
|
text: '离线',
|
||||||
|
class: 'offline'
|
||||||
|
},
|
||||||
|
recordingStatus: {
|
||||||
|
text: '未录制',
|
||||||
|
class: 'inactive'
|
||||||
|
},
|
||||||
|
qualityIndex: 1,
|
||||||
|
qualityOptions: ['低', '中', '高', '超高清'],
|
||||||
|
durationIndex: 2,
|
||||||
|
durationOptions: ['5分钟', '10分钟', '30分钟', '1小时', '持续录制'],
|
||||||
|
autoSave: true,
|
||||||
|
historyList: [
|
||||||
|
{
|
||||||
|
time: '2025-09-29 15:45:33',
|
||||||
|
duration: '10:30',
|
||||||
|
size: '125MB'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
time: '2025-09-29 14:20:15',
|
||||||
|
duration: '5:45',
|
||||||
|
size: '68MB'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
time: '2025-09-29 13:10:22',
|
||||||
|
duration: '15:20',
|
||||||
|
size: '189MB'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
console.log('视觉监控页面加载')
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
connectCamera() {
|
||||||
|
uni.showLoading({
|
||||||
|
title: '连接中...'
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.hideLoading()
|
||||||
|
this.videoLoaded = true
|
||||||
|
this.cameraStatus = {
|
||||||
|
text: '在线',
|
||||||
|
class: 'online'
|
||||||
|
}
|
||||||
|
uni.showToast({
|
||||||
|
title: '摄像头连接成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}, 2000)
|
||||||
|
},
|
||||||
|
toggleRecording() {
|
||||||
|
this.isRecording = !this.isRecording
|
||||||
|
this.recordingStatus = {
|
||||||
|
text: this.isRecording ? '录制中' : '未录制',
|
||||||
|
class: this.isRecording ? 'recording' : 'inactive'
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: this.isRecording ? '开始录制' : '停止录制',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
takeSnapshot() {
|
||||||
|
uni.showToast({
|
||||||
|
title: '拍照成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
toggleFullscreen() {
|
||||||
|
uni.showToast({
|
||||||
|
title: '全屏功能开发中',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onQualityChange(e) {
|
||||||
|
this.qualityIndex = e.detail.value
|
||||||
|
},
|
||||||
|
onDurationChange(e) {
|
||||||
|
this.durationIndex = e.detail.value
|
||||||
|
},
|
||||||
|
onAutoSaveChange(e) {
|
||||||
|
this.autoSave = e.detail.value
|
||||||
|
},
|
||||||
|
playVideo(item) {
|
||||||
|
uni.showToast({
|
||||||
|
title: `播放 ${item.time}`,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
downloadVideo(item) {
|
||||||
|
uni.showToast({
|
||||||
|
title: `下载 ${item.time}`,
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteVideo(item) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `确定要删除 ${item.time} 的录制文件吗?`,
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
const index = this.historyList.indexOf(item)
|
||||||
|
this.historyList.splice(index, 1)
|
||||||
|
uni.showToast({
|
||||||
|
title: '删除成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
clearHistory() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认清空',
|
||||||
|
content: '确定要清空所有录制历史吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
this.historyList = []
|
||||||
|
uni.showToast({
|
||||||
|
title: '清空成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.visual-monitoring-page {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-status {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 25rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
|
||||||
|
display: flex;
|
||||||
|
gap: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-icon {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recording-icon {
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&.online {
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.offline {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.recording {
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.inactive {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-placeholder {
|
||||||
|
padding: 60rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-image {
|
||||||
|
width: 300rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-button {
|
||||||
|
background-color: #3f51b5;
|
||||||
|
color: white;
|
||||||
|
padding: 20rpx 40rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player {
|
||||||
|
padding: 30rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button {
|
||||||
|
background-color: #3f51b5;
|
||||||
|
color: white;
|
||||||
|
padding: 15rpx 30rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitoring-settings {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-header {
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-view {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
padding: 15rpx 20rpx;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
border: 2rpx solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-arrow {
|
||||||
|
color: #999;
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recording-history {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15rpx;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-bottom: 2rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-button {
|
||||||
|
background-color: #ff4444;
|
||||||
|
color: white;
|
||||||
|
padding: 10rpx 20rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list {
|
||||||
|
height: 400rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-bottom: 2rpx solid #f0f0f0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-time {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-duration {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
background-color: #3f51b5;
|
||||||
|
color: white;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
|
||||||
|
&.delete {
|
||||||
|
background-color: #ff4444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1 +1 @@
|
|||||||
<!-- 这是一个占位符文件,实际项目中需要替换为真实的图片文件 -->
|
data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDMwMCAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIzMDAiIGhlaWdodD0iMjAwIiBmaWxsPSIjRjBGMEYwIi8+CjxjaXJjbGUgY3g9IjE1MCIgY3k9IjEwMCIgcj0iNDAiIGZpbGw9IiNDQ0NDQ0MiLz4KPHBhdGggZD0iTTEzMCA4MEwxNzAgODBMMTY1IDkwTDEzNSA5MEwxMzAgODBaIiBmaWxsPSIjQ0NDQ0NDIi8+Cjx0ZXh0IHg9IjE1MCIgeT0iMTQwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjOTk5OTk5IiBmb250LXNpemU9IjE0Ij7liqDovb3lupTor6XlpLHotKU8L3RleHQ+Cjwvc3ZnPgo=
|
||||||
280
src/utils/mqttDataManager.js
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
// MQTT数据管理器
|
||||||
|
import { createMqtt, closeMqtt, getConnectionStatus } from './sendMqtt.js'
|
||||||
|
|
||||||
|
class MqttDataManager {
|
||||||
|
constructor() {
|
||||||
|
this.listeners = new Map()
|
||||||
|
this.isConnected = false
|
||||||
|
this.lastData = {
|
||||||
|
temperature: null,
|
||||||
|
humidity: null,
|
||||||
|
pm25: null,
|
||||||
|
timestamp: null
|
||||||
|
}
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化MQTT连接
|
||||||
|
init() {
|
||||||
|
try {
|
||||||
|
console.log('🚀 MQTT数据管理器开始初始化...')
|
||||||
|
|
||||||
|
// 监听MQTT数据
|
||||||
|
uni.$on('mqttData', this.handleMqttData.bind(this))
|
||||||
|
console.log('✅ MQTT数据监听器已注册')
|
||||||
|
|
||||||
|
// 立即创建MQTT连接,不使用延迟
|
||||||
|
console.log('🔧 立即创建MQTT连接...')
|
||||||
|
createMqtt()
|
||||||
|
|
||||||
|
// 定期检查连接状态
|
||||||
|
this.statusCheckInterval = setInterval(() => {
|
||||||
|
const wasConnected = this.isConnected
|
||||||
|
this.isConnected = getConnectionStatus()
|
||||||
|
|
||||||
|
// 如果连接状态发生变化,通知监听器
|
||||||
|
if (wasConnected !== this.isConnected) {
|
||||||
|
console.log('🔄 MQTT连接状态变化:', {
|
||||||
|
wasConnected,
|
||||||
|
isConnected: this.isConnected
|
||||||
|
})
|
||||||
|
this.notifyListeners('connectionStatus', {
|
||||||
|
isConnected: this.isConnected,
|
||||||
|
lastUpdate: this.lastData.timestamp ? new Date(this.lastData.timestamp * 1000).toLocaleString('zh-CN') : null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, 3000) // 改为3秒检查一次
|
||||||
|
|
||||||
|
console.log('✅ MQTT数据管理器初始化完成')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ MQTT数据管理器初始化失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理MQTT数据
|
||||||
|
handleMqttData(data) {
|
||||||
|
try {
|
||||||
|
console.log('📨 收到MQTT数据:', data)
|
||||||
|
|
||||||
|
// 更新连接状态
|
||||||
|
this.isConnected = true
|
||||||
|
|
||||||
|
// 检查数据是否为数组
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
console.log('📋 收到数组数据,长度:', data.length)
|
||||||
|
|
||||||
|
// 遍历数组中的每个设备数据
|
||||||
|
data.forEach((deviceData, index) => {
|
||||||
|
console.log(`📦 处理设备数据[${index}]:`, deviceData)
|
||||||
|
this.processDeviceData(deviceData)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 单个设备数据
|
||||||
|
console.log('📦 处理单个设备数据:', data)
|
||||||
|
this.processDeviceData(data)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 处理MQTT数据失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理单个设备数据
|
||||||
|
processDeviceData(deviceData) {
|
||||||
|
try {
|
||||||
|
// 检查数据结构
|
||||||
|
if (!deviceData || !deviceData.Device || !deviceData.Data) {
|
||||||
|
console.warn('⚠️ 设备数据格式不符合预期:', deviceData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceType = deviceData.Device
|
||||||
|
const deviceDataContent = deviceData.Data
|
||||||
|
const timestamp = deviceData.timestamp || Math.floor(Date.now() / 1000)
|
||||||
|
|
||||||
|
console.log(`🔍 处理设备类型: ${deviceType}`)
|
||||||
|
console.log('设备数据:', deviceDataContent)
|
||||||
|
console.log('时间戳:', timestamp)
|
||||||
|
|
||||||
|
// 根据设备类型处理数据
|
||||||
|
if (deviceType === 'WSD') {
|
||||||
|
console.log('✅ 处理WSD设备数据 - 更新环境参数')
|
||||||
|
this.processWSDData(deviceDataContent, timestamp)
|
||||||
|
} else {
|
||||||
|
console.log(`⚠️ 设备类型 ${deviceType} 暂不处理,仅打印到控制台`)
|
||||||
|
console.log('设备详情:', {
|
||||||
|
deviceType,
|
||||||
|
data: deviceDataContent,
|
||||||
|
timestamp: new Date(timestamp * 1000).toLocaleString('zh-CN')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 处理设备数据失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理WSD设备数据
|
||||||
|
processWSDData(data, timestamp) {
|
||||||
|
try {
|
||||||
|
// 解析WSD数据 - 根据您提供的数据结构,WD是温度,SD是湿度
|
||||||
|
const temperature = parseFloat(data.WD) || 0
|
||||||
|
const humidity = parseFloat(data.SD) || 0
|
||||||
|
|
||||||
|
console.log('🌡️ WSD数据解析:')
|
||||||
|
console.log('温度(WD):', temperature)
|
||||||
|
console.log('湿度(SD):', humidity)
|
||||||
|
|
||||||
|
// 构建解析后的数据
|
||||||
|
const parsedData = {
|
||||||
|
deviceType: 'WSD',
|
||||||
|
timestamp,
|
||||||
|
time: new Date(timestamp * 1000).toLocaleString('zh-CN'),
|
||||||
|
temperature,
|
||||||
|
humidity
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新最新数据
|
||||||
|
this.updateLastData(parsedData)
|
||||||
|
|
||||||
|
// 通知所有监听器
|
||||||
|
this.notifyListeners('dataUpdate', parsedData)
|
||||||
|
|
||||||
|
console.log('✅ WSD数据处理完成:', parsedData)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 处理WSD数据失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析设备数据
|
||||||
|
parseDeviceData(rawData) {
|
||||||
|
try {
|
||||||
|
// 如果是数组,取第一个元素
|
||||||
|
if (Array.isArray(rawData) && rawData.length > 0) {
|
||||||
|
rawData = rawData[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查数据结构
|
||||||
|
if (!rawData || !rawData.Device || !rawData.Data) {
|
||||||
|
console.warn('⚠️ 数据格式不符合预期:', rawData)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceType = rawData.Device
|
||||||
|
const deviceData = rawData.Data
|
||||||
|
const timestamp = rawData.timestamp || Math.floor(Date.now() / 1000)
|
||||||
|
|
||||||
|
// 根据设备类型解析数据
|
||||||
|
let parsedData = {
|
||||||
|
deviceType,
|
||||||
|
timestamp,
|
||||||
|
time: new Date(timestamp * 1000).toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (deviceType) {
|
||||||
|
case 'WSD': // 温湿度传感器
|
||||||
|
parsedData.temperature = parseFloat(deviceData.Temperature) || 0
|
||||||
|
parsedData.humidity = parseFloat(deviceData.Humidity) || 0
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'AC': // 空调设备
|
||||||
|
parsedData.temperature = parseFloat(deviceData.Temperature) || 0
|
||||||
|
parsedData.humidity = parseFloat(deviceData.Humidity) || 0
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'PM': // PM2.5传感器
|
||||||
|
parsedData.pm25 = parseFloat(deviceData.PM25) || 0
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn('⚠️ 未知设备类型:', deviceType)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedData
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 解析设备数据失败:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新最新数据
|
||||||
|
updateLastData(parsedData) {
|
||||||
|
if (parsedData.temperature !== undefined) {
|
||||||
|
this.lastData.temperature = parsedData.temperature
|
||||||
|
}
|
||||||
|
if (parsedData.humidity !== undefined) {
|
||||||
|
this.lastData.humidity = parsedData.humidity
|
||||||
|
}
|
||||||
|
if (parsedData.pm25 !== undefined) {
|
||||||
|
this.lastData.pm25 = parsedData.pm25
|
||||||
|
}
|
||||||
|
this.lastData.timestamp = parsedData.timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加数据监听器
|
||||||
|
addListener(key, callback) {
|
||||||
|
if (!this.listeners.has(key)) {
|
||||||
|
this.listeners.set(key, [])
|
||||||
|
}
|
||||||
|
this.listeners.get(key).push(callback)
|
||||||
|
|
||||||
|
// 立即发送当前数据
|
||||||
|
if (key === 'dataUpdate' && this.lastData.timestamp) {
|
||||||
|
callback(this.lastData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除数据监听器
|
||||||
|
removeListener(key, callback) {
|
||||||
|
if (this.listeners.has(key)) {
|
||||||
|
const callbacks = this.listeners.get(key)
|
||||||
|
const index = callbacks.indexOf(callback)
|
||||||
|
if (index > -1) {
|
||||||
|
callbacks.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知监听器
|
||||||
|
notifyListeners(key, data) {
|
||||||
|
if (this.listeners.has(key)) {
|
||||||
|
this.listeners.get(key).forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 监听器回调执行失败:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最新数据
|
||||||
|
getLastData() {
|
||||||
|
return { ...this.lastData }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取连接状态
|
||||||
|
getConnectionStatus() {
|
||||||
|
return {
|
||||||
|
isConnected: this.isConnected,
|
||||||
|
lastUpdate: this.lastData.timestamp ? new Date(this.lastData.timestamp * 1000).toLocaleString('zh-CN') : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 销毁管理器
|
||||||
|
destroy() {
|
||||||
|
uni.$off('mqttData', this.handleMqttData.bind(this))
|
||||||
|
closeMqtt()
|
||||||
|
|
||||||
|
if (this.statusCheckInterval) {
|
||||||
|
clearInterval(this.statusCheckInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listeners.clear()
|
||||||
|
console.log('🔌 MQTT数据管理器已销毁')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建全局实例
|
||||||
|
const mqttDataManager = new MqttDataManager()
|
||||||
|
|
||||||
|
export default mqttDataManager
|
||||||
101
src/utils/mqttTest.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
// MQTT连接测试工具
|
||||||
|
import { createMqtt, getConnectionStatus, closeMqtt } from './sendMqtt.js'
|
||||||
|
|
||||||
|
class MqttTest {
|
||||||
|
constructor() {
|
||||||
|
this.testResults = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行MQTT连接测试
|
||||||
|
async runTest() {
|
||||||
|
console.log('🧪 开始MQTT连接测试...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 测试1: 创建连接
|
||||||
|
console.log('📋 测试1: 创建MQTT连接')
|
||||||
|
createMqtt()
|
||||||
|
|
||||||
|
// 等待连接建立
|
||||||
|
await this.waitForConnection(10000) // 等待10秒
|
||||||
|
|
||||||
|
// 测试2: 检查连接状态
|
||||||
|
console.log('📋 测试2: 检查连接状态')
|
||||||
|
const isConnected = getConnectionStatus()
|
||||||
|
console.log('连接状态:', isConnected)
|
||||||
|
|
||||||
|
// 测试3: 监听数据
|
||||||
|
console.log('📋 测试3: 设置数据监听')
|
||||||
|
uni.$on('mqttData', (data) => {
|
||||||
|
console.log('✅ 收到测试数据:', data)
|
||||||
|
this.testResults.push({
|
||||||
|
type: 'data_received',
|
||||||
|
data: data,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 等待数据
|
||||||
|
console.log('⏳ 等待MQTT数据...')
|
||||||
|
await this.waitForData(30000) // 等待30秒
|
||||||
|
|
||||||
|
// 输出测试结果
|
||||||
|
this.printTestResults()
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ MQTT测试失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待连接建立
|
||||||
|
waitForConnection(timeout = 10000) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const startTime = Date.now()
|
||||||
|
const checkInterval = setInterval(() => {
|
||||||
|
const isConnected = getConnectionStatus()
|
||||||
|
if (isConnected) {
|
||||||
|
clearInterval(checkInterval)
|
||||||
|
console.log('✅ MQTT连接建立成功')
|
||||||
|
resolve(true)
|
||||||
|
} else if (Date.now() - startTime > timeout) {
|
||||||
|
clearInterval(checkInterval)
|
||||||
|
console.log('⏰ MQTT连接超时')
|
||||||
|
reject(new Error('连接超时'))
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待数据接收
|
||||||
|
waitForData(timeout = 30000) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const startTime = Date.now()
|
||||||
|
const checkInterval = setInterval(() => {
|
||||||
|
if (this.testResults.length > 0 || Date.now() - startTime > timeout) {
|
||||||
|
clearInterval(checkInterval)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打印测试结果
|
||||||
|
printTestResults() {
|
||||||
|
console.log('📊 MQTT测试结果:')
|
||||||
|
console.log('连接状态:', getConnectionStatus())
|
||||||
|
console.log('收到数据次数:', this.testResults.length)
|
||||||
|
|
||||||
|
if (this.testResults.length > 0) {
|
||||||
|
console.log('最新数据:', this.testResults[this.testResults.length - 1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理测试
|
||||||
|
cleanup() {
|
||||||
|
uni.$off('mqttData')
|
||||||
|
closeMqtt()
|
||||||
|
console.log('🧹 MQTT测试清理完成')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出测试实例
|
||||||
|
export default new MqttTest()
|
||||||
@ -8,7 +8,12 @@ mqtturl = "ws://122.51.194.184:8083/mqtt";
|
|||||||
import * as mqtt from "mqtt/dist/mqtt.min.js";
|
import * as mqtt from "mqtt/dist/mqtt.min.js";
|
||||||
// #endif
|
// #endif
|
||||||
|
|
||||||
// #ifdef APP-PLUS || MP-WEIXIN
|
// #ifdef APP-PLUS
|
||||||
|
mqtturl = "wx://122.51.194.184:8083/mqtt";
|
||||||
|
import * as mqtt from "mqtt/dist/mqtt.min.js";
|
||||||
|
//#endif
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
mqtturl = "wx://122.51.194.184:8083/mqtt";
|
mqtturl = "wx://122.51.194.184:8083/mqtt";
|
||||||
import * as mqtt from "mqtt/dist/mqtt.min.js";
|
import * as mqtt from "mqtt/dist/mqtt.min.js";
|
||||||
//#endif
|
//#endif
|
||||||
@ -35,9 +40,10 @@ const createMqtt = () => {
|
|||||||
password: "qwer1234",
|
password: "qwer1234",
|
||||||
protocolVersion: 4,
|
protocolVersion: 4,
|
||||||
clean: true,
|
clean: true,
|
||||||
reconnectPeriod: 1000, // reconnectPeriod为1000毫秒,这意味着在连接丢失之后,客户端将在1秒后尝试重新连接。
|
reconnectPeriod: 1000, // 恢复自动重连,1秒重连一次
|
||||||
connectTimeout: 5000, // 5s超时时间 意味着mqtt-reconnect函数5秒钟触发一次
|
connectTimeout: 5000, // 5s超时时间
|
||||||
topic: "HDYDCJ_01_DOWN",
|
// topic: "HDYDCJ_01_DOWN",
|
||||||
|
topic: "HDYDCJ_01_UP",
|
||||||
rejectUnauthorized: false,
|
rejectUnauthorized: false,
|
||||||
// #ifdef MP-ALIPAY
|
// #ifdef MP-ALIPAY
|
||||||
my: my,//注意这里的my
|
my: my,//注意这里的my
|
||||||
@ -49,12 +55,44 @@ const createMqtt = () => {
|
|||||||
console.log('🔧 开始创建MQTT连接...');
|
console.log('🔧 开始创建MQTT连接...');
|
||||||
console.log('🔧 MQTT URL:', mqtturl);
|
console.log('🔧 MQTT URL:', mqtturl);
|
||||||
console.log('🔧 连接选项:', options);
|
console.log('🔧 连接选项:', options);
|
||||||
|
console.log('🔧 当前平台:',
|
||||||
|
// #ifdef H5
|
||||||
|
'H5'
|
||||||
|
// #endif
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
'APP-PLUS'
|
||||||
|
// #endif
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
'MP-WEIXIN'
|
||||||
|
// #endif
|
||||||
|
// #ifdef MP-ALIPAY
|
||||||
|
'MP-ALIPAY'
|
||||||
|
// #endif
|
||||||
|
)
|
||||||
|
|
||||||
|
// 显示连接loading
|
||||||
|
uni.showLoading({
|
||||||
|
title: 'MQTT连接中...',
|
||||||
|
mask: true
|
||||||
|
});
|
||||||
|
|
||||||
client = mqtt.connect(mqtturl, options);
|
client = mqtt.connect(mqtturl, options);
|
||||||
initEventHandleMqtt(options.topic);
|
initEventHandleMqtt(options.topic);
|
||||||
|
} else {
|
||||||
|
console.log('🔧 MQTT客户端已存在,跳过创建');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('❌ MQTT连接创建失败:', e);
|
console.error('❌ MQTT连接创建失败:', e);
|
||||||
|
console.error('❌ 错误详情:', e.message);
|
||||||
|
console.error('❌ 错误堆栈:', e.stack);
|
||||||
|
|
||||||
|
// 连接失败时隐藏loading
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showToast({
|
||||||
|
title: 'MQTT连接失败' + mqtturl,
|
||||||
|
icon: 'error',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -64,10 +102,23 @@ const initEventHandleMqtt = (topicUrl) => {
|
|||||||
client.on("connect", function() {
|
client.on("connect", function() {
|
||||||
uni.hideLoading();
|
uni.hideLoading();
|
||||||
console.log("✅ MQTT连接成功");
|
console.log("✅ MQTT连接成功");
|
||||||
|
|
||||||
|
// 显示连接成功提示
|
||||||
|
uni.showToast({
|
||||||
|
title: 'MQTT连接成功' + mqtturl,
|
||||||
|
icon: 'success',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
|
||||||
//订阅主题
|
//订阅主题
|
||||||
client.subscribe(topicUrl, function(err) {
|
client.subscribe(topicUrl, function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("❌ MQTT订阅主题失败:", err);
|
console.error("❌ MQTT订阅主题失败:", err);
|
||||||
|
uni.showToast({
|
||||||
|
title: '订阅主题失败',
|
||||||
|
icon: 'error',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log("✅ MQTT订阅主题成功:", topicUrl);
|
console.log("✅ MQTT订阅主题成功:", topicUrl);
|
||||||
}
|
}
|
||||||
@ -84,6 +135,18 @@ const initEventHandleMqtt = (topicUrl) => {
|
|||||||
// 获取信息
|
// 获取信息
|
||||||
const mqttData = JSON.parse(message.toString());
|
const mqttData = JSON.parse(message.toString());
|
||||||
console.log('📋 解析后的数据:', mqttData);
|
console.log('📋 解析后的数据:', mqttData);
|
||||||
|
console.log('数据类型:', Array.isArray(mqttData) ? '数组' : '对象');
|
||||||
|
|
||||||
|
// 如果是数组,打印数组信息
|
||||||
|
if (Array.isArray(mqttData)) {
|
||||||
|
console.log('📋 数组长度:', mqttData.length);
|
||||||
|
mqttData.forEach((item, index) => {
|
||||||
|
console.log(`📦 数组[${index}]:`, item);
|
||||||
|
if (item.Device) {
|
||||||
|
console.log(`🔍 设备类型[${index}]: ${item.Device}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 传递信息
|
// 传递信息
|
||||||
uni.$emit("mqttData", mqttData);
|
uni.$emit("mqttData", mqttData);
|
||||||
@ -97,13 +160,14 @@ const initEventHandleMqtt = (topicUrl) => {
|
|||||||
client.on('reconnect', function() {
|
client.on('reconnect', function() {
|
||||||
console.log('🔄 MQTT重新连接中...');
|
console.log('🔄 MQTT重新连接中...');
|
||||||
uni.showLoading({
|
uni.showLoading({
|
||||||
title: "重新连接"
|
title: "重新连接中..."
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 当客户端无法成功连接时或发生解析错误时触发,参数 error 为错误信息
|
// 当客户端无法成功连接时或发生解析错误时触发,参数 error 为错误信息
|
||||||
client.on("error", function(err) {
|
client.on("error", function(err) {
|
||||||
console.error('❌ MQTT连接错误:', err);
|
console.error('❌ MQTT连接错误:', err);
|
||||||
|
uni.hideLoading();
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: 'MQTT连接错误',
|
title: 'MQTT连接错误',
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
@ -114,16 +178,19 @@ const initEventHandleMqtt = (topicUrl) => {
|
|||||||
// 在收到 Broker 发送过来的断开连接的报文时触发
|
// 在收到 Broker 发送过来的断开连接的报文时触发
|
||||||
client.on('disconnect', function() {
|
client.on('disconnect', function() {
|
||||||
console.log('⚠️ MQTT连接断开');
|
console.log('⚠️ MQTT连接断开');
|
||||||
|
uni.hideLoading();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 在断开连接以后触发
|
// 在断开连接以后触发
|
||||||
client.on("close", function() {
|
client.on("close", function() {
|
||||||
console.log('🔌 MQTT连接关闭');
|
console.log('🔌 MQTT连接关闭');
|
||||||
|
uni.hideLoading();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 当客户端下线时触发
|
// 当客户端下线时触发
|
||||||
client.on("offline", function() {
|
client.on("offline", function() {
|
||||||
console.log('📴 MQTT客户端离线');
|
console.log('📴 MQTT客户端离线');
|
||||||
|
uni.hideLoading();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -131,6 +198,7 @@ const initEventHandleMqtt = (topicUrl) => {
|
|||||||
const closeMqtt = () => {
|
const closeMqtt = () => {
|
||||||
if (client) {
|
if (client) {
|
||||||
console.log('🔌 强制断开MQTT连接');
|
console.log('🔌 强制断开MQTT连接');
|
||||||
|
uni.hideLoading();
|
||||||
client.end();
|
client.end();
|
||||||
client = null;
|
client = null;
|
||||||
}
|
}
|
||||||
@ -150,7 +218,28 @@ const judgeBeat = () => {
|
|||||||
|
|
||||||
// 获取连接状态
|
// 获取连接状态
|
||||||
const getConnectionStatus = () => {
|
const getConnectionStatus = () => {
|
||||||
return client && client.connected;
|
if (!client) {
|
||||||
|
console.log('🔍 连接状态检查: 客户端不存在');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isConnected = client.connected;
|
||||||
|
console.log('🔍 连接状态检查:', {
|
||||||
|
clientExists: !!client,
|
||||||
|
connected: isConnected,
|
||||||
|
readyState: client.stream ? client.stream.readyState : 'unknown'
|
||||||
|
});
|
||||||
|
|
||||||
|
return isConnected;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 手动重连函数
|
||||||
|
const manualReconnect = () => {
|
||||||
|
console.log('🔄 手动触发重连');
|
||||||
|
closeMqtt();
|
||||||
|
setTimeout(() => {
|
||||||
|
createMqtt();
|
||||||
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -158,5 +247,6 @@ export {
|
|||||||
closeMqtt,
|
closeMqtt,
|
||||||
judgeBeat,
|
judgeBeat,
|
||||||
getConnectionStatus,
|
getConnectionStatus,
|
||||||
|
manualReconnect,
|
||||||
client,
|
client,
|
||||||
}
|
}
|
||||||
BIN
unpackage/res/icons/1024x1024.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
unpackage/res/icons/120x120.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
unpackage/res/icons/144x144.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
unpackage/res/icons/152x152.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
unpackage/res/icons/167x167.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
unpackage/res/icons/180x180.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
unpackage/res/icons/192x192.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
unpackage/res/icons/20x20.png
Normal file
|
After Width: | Height: | Size: 572 B |
BIN
unpackage/res/icons/29x29.png
Normal file
|
After Width: | Height: | Size: 859 B |
BIN
unpackage/res/icons/40x40.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
unpackage/res/icons/58x58.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
unpackage/res/icons/60x60.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
unpackage/res/icons/72x72.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
unpackage/res/icons/76x76.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
unpackage/res/icons/80x80.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
unpackage/res/icons/87x87.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
unpackage/res/icons/96x96.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |