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" href="https://unpkg.byted-static.com/latest/byted/arco-config/assets/favicon.ico"
/> />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>物联网网关系统</title> <title>基于MQTT的IOT设备管理系统</title>
</head> </head>
<body> <body>
<div id="app"></div> <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" :heading="5"
@click="$router.push({ name: 'Workplace' })" @click="$router.push({ name: 'Workplace' })"
> >
物联网网关系统 IOT设备管理系统
</a-typography-title> </a-typography-title>
<icon-menu-fold <icon-menu-fold
v-if="!topMenu && appStore.device === 'mobile'" v-if="!topMenu && appStore.device === 'mobile'"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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