feat(iot): 实现基于 Websocket 的设备管理系统

- 更新系统标题为"基于MQTT的IOT设备管理系统"
- 移除旧的SSE连接,改为使用WebSocket连接
- 新增设备状态、产品状态和报警状态的WebSocket连接及处理逻辑
- 更新面包屑导航和页面标题
- 修改用户编辑页面的标题
- 更新登录页面的标题
This commit is contained in:
Kven 2025-03-25 22:16:36 +08:00
parent 7328db1a23
commit 69a0144ebd
13 changed files with 91 additions and 55 deletions

View File

@ -8,7 +8,7 @@
href="https://unpkg.byted-static.com/latest/byted/arco-config/assets/favicon.ico"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>物联网网关系统</title>
<title>基于MQTT的IOT设备管理系统</title>
</head>
<body>
<div id="app"></div>

View File

@ -79,7 +79,8 @@ export function addAttachments(data: any) {
});
}
// 删除附件
export function deleteAttachment(id: string) {
return axios.delete(`/api/rest/attachment/delete/${id}`);
}
// 删除设备预览图或图标
export function deleteDeviceAttachment(id: string) {
return axios.delete(`/api/rest/device/attachment/${id}`);
}

View File

@ -12,7 +12,7 @@
:heading="5"
@click="$router.push({ name: 'Workplace' })"
>
物联网网关系统
IOT设备管理系统
</a-typography-title>
<icon-menu-fold
v-if="!topMenu && appStore.device === 'mobile'"

View File

@ -83,7 +83,7 @@ const SYSTEM: AppRouteRecordRaw = {
name: 'Log',
component: () => import('@/views/system/log/index.vue'),
meta: {
title: '操作日志管理',
title: '操作日志',
requiresAuth: true,
permissions: ['system:menu'],
},

View File

@ -3,7 +3,7 @@
<div class="left-side">
<div class="panel">
<Banner />
<DataPanel :deviceInfo="computedDeviceInfo" :alarmInfo='alarmInfo' :productInfo='productInfo' />
<DataPanel v-if="computedDeviceInfo && alarmInfo && productInfo" :deviceInfo="computedDeviceInfo" :alarmInfo='alarmInfo' :productInfo='productInfo' />
<ContentPublishingSource v-if="computedDeviceInfo && alarmInfo && productInfo" :deviceInfo="computedDeviceInfo" :alarmInfo='alarmInfo' :productInfo='productInfo' />
</div>
<a-grid :cols="24" :col-gap="16" :row-gap="16" style="margin-top: 16px">
@ -22,8 +22,7 @@
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { getAlarmInfo, getDeviceInfo, getProductInfo } from '@/api/dashboard';
import { EventSourcePolyfill } from 'event-source-polyfill';
import { Message } from '@arco-design/web-vue';
import { getToken } from '@/utils/auth';
import Banner from './components/banner.vue';
@ -37,55 +36,91 @@
const alarmInfo = ref<any>();
const productInfo = ref<any>();
// const fetchData = async () => {
// const res = await getDeviceInfo();
// deviceInfo.value = res.data;
// const res1 = await getAlarmInfo();
// alarmInfo.value = res1.data;
// const res2 = await getProductInfo();
// productInfo.value = res2.data;
// }
// onMounted(() => {
// fetchData();
// })
let deviceWebSocket: WebSocket | null = null;
let productWebSocket: WebSocket | null = null;
let alarmWebSocket: WebSocket | null = null;
let eventSource: EventSourcePolyfill | null = null;
const token = getToken();
const startSSE = () => {
// EventSourcePolyfill SSE
eventSource = new EventSourcePolyfill(`http://127.0.0.1:8081/api/rest/device/sse/status`, {
headers: {
'X-CSRF-TOKEN': token
}
});
//
eventSource.onmessage = (event:any) => {
const startWebSockets = () => {
// WebSocket
deviceWebSocket = new WebSocket('ws://127.0.0.1:8081/api/rest/ws/device/status');
deviceWebSocket.onopen = () => {
console.log('Device WebSocket connected');
};
deviceWebSocket.onmessage = (event) => {
const data = JSON.parse(event.data); // JSON
console.log('Received data:', data);
//
console.log('Received device data:', data);
deviceInfo.value = data; //
};
deviceWebSocket.onerror = (error) => {
console.error('Device WebSocket error:', error);
Message.error({
content: '设备状态 WebSocket 连接错误',
duration: 5 * 1000,
});
};
deviceWebSocket.onclose = () => {
console.log('Device WebSocket closed');
};
//
eventSource.onerror = (error:any) => {
console.error('SSE error:', error);
//
// WebSocket
productWebSocket = new WebSocket('ws://127.0.0.1:8081/api/rest/ws/product/status');
productWebSocket.onopen = () => {
console.log('Product WebSocket connected');
};
productWebSocket.onmessage = (event) => {
const data = JSON.parse(event.data); // JSON
console.log('Received product data:', data);
productInfo.value = data; //
};
productWebSocket.onerror = (error) => {
console.error('Product WebSocket error:', error);
Message.error({
content: '产品状态 WebSocket 连接错误',
duration: 5 * 1000,
});
};
productWebSocket.onclose = () => {
console.log('Product WebSocket closed');
};
// WebSocket
alarmWebSocket = new WebSocket('ws://127.0.0.1:8081/api/rest/ws/record/status');
alarmWebSocket.onopen = () => {
console.log('Alarm WebSocket connected');
};
alarmWebSocket.onmessage = (event) => {
const data = JSON.parse(event.data); // JSON
console.log('Received alarm data:', data);
alarmInfo.value = data; //
};
alarmWebSocket.onerror = (error) => {
console.error('Alarm WebSocket error:', error);
Message.error({
content: '报警状态 WebSocket 连接错误',
duration: 5 * 1000,
});
};
alarmWebSocket.onclose = () => {
console.log('Alarm WebSocket closed');
};
};
onMounted(() => {
startSSE();
startWebSockets();
});
onUnmounted(() => {
// SSE
if (eventSource) {
eventSource.close();
// WebSocket
if (deviceWebSocket) {
deviceWebSocket.close();
}
if (productWebSocket) {
productWebSocket.close();
}
if (alarmWebSocket) {
alarmWebSocket.close();
}
});
</script>
<script lang="ts">

View File

@ -1,6 +1,6 @@
<template>
<div class="container">
<Breadcrumb :items="['系统管理', '公告设置']" />
<Breadcrumb :items="['系统管理', '设备管理']" />
<a-card class="general-card" title=" ">
<a-row>
<a-col :flex="1">

View File

@ -1,6 +1,6 @@
<template>
<div class="container">
<Breadcrumb :items="['系统管理', '公告设置']" />
<Breadcrumb :items="['系统管理', '设备管理']" />
<a-card class="general-card" title=" ">
<a-row>
<a-col :flex="1">

View File

@ -164,7 +164,7 @@
ProductCreateRecord,
queryProductDetail,
updateProduct,
addAttachments, deleteAttachment
addAttachments, deleteDeviceAttachment
} from '@/api/product';
const props = defineProps({
@ -296,7 +296,7 @@
Message.error('无法获取图片 ID');
}
//
const res = await deleteAttachment(fileId);
const res = await deleteDeviceAttachment(fileId);
if (res.data === true) {
//
Message.success('删除成功');

View File

@ -1,7 +1,7 @@
<template>
<div class="container">
<div class="logo">
<div class="logo-text">Hello Ticket!</div>
<div class="logo-text">Hello</div>
</div>
<LoginBanner />
<div class="content">

View File

@ -1,5 +1,5 @@
export default {
'login.form.title': '票据管理系统',
'login.form.title': 'IOT设备管理系统',
'login.form.userName.errMsg': '账号不能为空',
'login.form.password.errMsg': '密码不能为空',
'login.form.login.errMsg': '登录出错,请刷新重试',

View File

@ -1,6 +1,6 @@
<template>
<div class="container">
<Breadcrumb :items="['通知管理', '消息设置']" />
<Breadcrumb :items="['通知管理', '消息管理']" />
<a-card class="general-card" title=" ">
<a-row>
<a-col :flex="1">

View File

@ -147,7 +147,7 @@
});
const emit = defineEmits(['refresh']);
const modalTitle = computed(() => {
return props.isCreate ? '新用户' : '编辑用户';
return props.isCreate ? '新用户' : '编辑用户';
});
const { visible, setVisible } = useVisible(false);
const checkKeys = ref<number[]>([]);

View File

@ -37,7 +37,7 @@
</a-form-item>
</a-col>
<a-col :span="9">
<a-col :span="10">
<a-form-item field="phone" label="电话号码">
<a-input
v-model="formModel.phone"