feat(iot): 新增设备地图功能并优化产品编辑界面
- 新增设备地图组件,用于展示设备在地图上的位置 -优化产品编辑界面,增加预览图和图标上传功能 - 修改 API 调用路径,使用 SSE (Server-Sent Events) 实现实时数据推送 - 更新表格展示,增加设备上报记录的详细信息
This commit is contained in:
parent
085d0ae690
commit
3113bac668
@ -34,12 +34,15 @@
|
||||
"@amap/amap-jsapi-loader": "^1.0.1",
|
||||
"@amap/amap-jsapi-types": "^0.0.15",
|
||||
"@arco-design/web-vue": "^2.44.7",
|
||||
"@commitlint/load": "^19.8.0",
|
||||
"@vueuse/core": "^9.3.0",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"arco-design-pro-vue": "^2.7.2",
|
||||
"axios": "^0.24.0",
|
||||
"cosmiconfig": "^9.0.0",
|
||||
"dayjs": "^1.11.5",
|
||||
"echarts": "^5.4.0",
|
||||
"event-source-polyfill": "^1.0.31",
|
||||
"exceljs": "^4.4.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
|
@ -3,20 +3,20 @@ import axios from 'axios';
|
||||
|
||||
// 获取设备信息
|
||||
export function getDeviceInfo() {
|
||||
return axios.get('/api/rest/device/status');
|
||||
return axios.get('/api/rest/device/sse/status');
|
||||
}
|
||||
|
||||
// 获取消息信息
|
||||
export function getMessageInfo() {
|
||||
return axios.get('/api/rest/device/record/status');
|
||||
return axios.get('/api/rest/device/sse/record/status');
|
||||
}
|
||||
|
||||
// 获取告警信息
|
||||
export function getAlarmInfo() {
|
||||
return axios.get('/api/rest/device/data/status');
|
||||
return axios.get('/api/rest/device/sse/data/status');
|
||||
}
|
||||
|
||||
// 获取产品信息
|
||||
export function getProductInfo() {
|
||||
return axios.get('/api/rest/product/status');
|
||||
return axios.get('/api/rest/product/sse/status');
|
||||
}
|
@ -66,3 +66,13 @@ export function updateProduct(data: ProductUpdateRecord) {
|
||||
export function deleteProduct(id: number) {
|
||||
return axios.delete(`/api/rest/product/${id}`);
|
||||
}
|
||||
|
||||
// 文件上传
|
||||
export function addAttachments(data: any) {
|
||||
return axios.post(`/api/rest/product/upload`, data);
|
||||
}
|
||||
|
||||
// 文件删除
|
||||
export function deleteAttachment(id: number) {
|
||||
return axios.delete(`/api/rest/product/attachments/${id}`);
|
||||
}
|
||||
|
BIN
src/assets/images/device.png
Normal file
BIN
src/assets/images/device.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
@ -3,7 +3,7 @@
|
||||
<div class="left-side">
|
||||
<div class="panel">
|
||||
<Banner />
|
||||
<DataPanel v-if="computedDeviceInfo && alarmInfo && productInfo" :deviceInfo="computedDeviceInfo" :alarmInfo='alarmInfo' :productInfo='productInfo' />
|
||||
<DataPanel :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">
|
||||
@ -21,8 +21,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { getAlarmInfo, getDeviceInfo, getProductInfo } from '@/api/dashboard';
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
import { getToken } from '@/utils/auth';
|
||||
|
||||
import Banner from './components/banner.vue';
|
||||
import DataPanel from './components/data-panel.vue';
|
||||
import ContentPublishingSource from './components/content-publishing-source.vue';
|
||||
@ -34,19 +37,53 @@
|
||||
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;
|
||||
console.log(computedDeviceInfo.value);
|
||||
console.log(deviceInfo.value);
|
||||
// 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 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 data = JSON.parse(event.data); // 假设服务器发送的是 JSON 数据
|
||||
console.log('Received data:', data);
|
||||
// 在这里处理接收到的数据
|
||||
};
|
||||
|
||||
// 监听错误事件
|
||||
eventSource.onerror = (error:any) => {
|
||||
console.error('SSE error:', error);
|
||||
// 在这里处理错误
|
||||
};
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
})
|
||||
startSSE();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 组件卸载时关闭 SSE 连接
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
@ -84,7 +84,22 @@
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="4" tab="上报记录" title="上报记录">
|
||||
<a-table :columns="deviceReportColumns" :data="deviceReportData" />
|
||||
<a-table :columns="deviceReportColumns" :data="deviceReportData" >
|
||||
<template #recordTime="{ record }">
|
||||
<div>{{ dayjs(record.recordTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
|
||||
</template>
|
||||
<template #content="{ record }">
|
||||
<div v-for="(value, key) in record.content" :key="key">
|
||||
<strong>{{ key }}:</strong> {{ value }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="5" tab="设备地图" title="设备地图">
|
||||
<DeviceMap :renderData=renderData />
|
||||
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
@ -116,7 +131,8 @@
|
||||
import { queryDeviceDetail, queryDeviceRecord, sendCommand } from '@/api/device';
|
||||
import { queryServeDetail, queryServeList } from '@/api/tsl';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import dynamicForm from './dynamic-form.vue'
|
||||
import dynamicForm from './dynamic-form.vue';
|
||||
import DeviceMap from './device-map.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const { visible, setVisible } = useVisible();
|
||||
|
117
src/views/iot/device/components/device-map.vue
Normal file
117
src/views/iot/device/components/device-map.vue
Normal file
@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<a-layout>
|
||||
<a-layout-content>
|
||||
<div id="container"></div>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import AMapLoader from '@amap/amap-jsapi-loader';
|
||||
import { DeviceRecord, queryDeviceList } from '@/api/device';
|
||||
import "@amap/amap-jsapi-types";
|
||||
import router from '@/router';
|
||||
|
||||
let map: any = null;
|
||||
const porps = defineProps({
|
||||
renderData: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
AMapLoader.load({
|
||||
key: 'a4e80eed798a56451b226dcfca81b846', // 申请好的Web端开发者Key,首次调用 load 时必填
|
||||
version: '2.0', // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
|
||||
plugins: [], // 需要使用的的插件列表,如比例尺'AMap.Scale'等
|
||||
})
|
||||
.then((AMap) => {
|
||||
map = new AMap.Map('container', {
|
||||
// 设置地图容器id
|
||||
viewMode: '3D', // 是否为3D地图模式
|
||||
zoom: 11, // 初始化地图级别
|
||||
center: [porps.renderData.longitude, porps.renderData.latitude], // 初始化地图中心点位置
|
||||
});
|
||||
|
||||
if (porps.renderData.longitude && porps.renderData.latitude) {
|
||||
const lngLat = [porps.renderData.longitude, porps.renderData.latitude];
|
||||
const content = document.createElement('div');
|
||||
content.className = 'custom-content-marker';
|
||||
content.innerHTML = `
|
||||
<img src="https://webapi.amap.com/theme/v1.3/markers/n/mark_b.png" alt="标记点" />`;
|
||||
|
||||
// 创建标记点
|
||||
const marker = new AMap.Marker({
|
||||
position: lngLat,
|
||||
content,
|
||||
});
|
||||
|
||||
// 将标记点添加到地图上
|
||||
map.setCenter(lngLat);
|
||||
map.setZoom(20);
|
||||
map.add(marker);
|
||||
|
||||
// const infoContent = `
|
||||
// <div style="width: 220px; padding: 10px; background: #fff; border-radius: 4px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);">
|
||||
// <h4 style="margin: 0 0 10px; font-size: 16px; color: #333;">设备信息</h4>
|
||||
// <p style="margin: 0; font-size: 14px; color: #555;"><strong>设备名称:</strong> ${device.name}</p>
|
||||
// <p style="margin: 0; font-size: 14px; color: #555;"><strong>硬件版本:</strong> ${device.hardwareVersion}</p>
|
||||
// <p style="margin: 0; font-size: 14px; color: #555;"><strong>固件版本:</strong> ${device.firmwareVersion}</p>
|
||||
// <p style="margin: 0; font-size: 14px; color: #555;"><strong>所属产品:</strong> ${device.productId}</p>
|
||||
// </div>
|
||||
// `;
|
||||
const infoContent = [
|
||||
`<br/><div style="width: 300px;margin-left: 20px;padding-bottom: 10px"><b>设备名称: ${porps.renderData.name}</b>`,
|
||||
`<span style="font-size: 16px; color: #333;">硬件版本: ${porps.renderData.hardwareVersion}</span>`,
|
||||
`<span style="font-size: 16px; color: #333;">固件版本: ${porps.renderData.firmwareVersion}</span>`,
|
||||
`<span style="font-size: 16px; color: #333;">所属产品: ${porps.renderData.productName}</span>`,
|
||||
`<a-button style="margin-left: 200px;color: #0960bd" onclick="handleViewDetail('${porps.renderData.id}')">查看详情
|
||||
</a-button></div>`
|
||||
]
|
||||
|
||||
const handleViewDetail = (deviceId: number) => {
|
||||
router.push({ name: 'deviceDetail', params: { id: deviceId } });
|
||||
};
|
||||
window.handleViewDetail = handleViewDetail;
|
||||
|
||||
|
||||
// 初始化信息窗体
|
||||
const infoWindow = new AMap.InfoWindow({
|
||||
content: infoContent.join('<br>'),
|
||||
offset: new AMap.Pixel(10, 10), // 调整信息窗体的偏移量
|
||||
});
|
||||
|
||||
// 绑定标记点点击事件
|
||||
marker.on('click', () => {
|
||||
infoWindow.open(map, lngLat); // 打开信息窗体
|
||||
});
|
||||
} else {
|
||||
console.warn('设备缺少经纬度信息', porps.renderData);
|
||||
}
|
||||
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
|
||||
|
||||
// onUnmounted(() => {
|
||||
// map?.destroy();
|
||||
// });
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#container {
|
||||
width: 100%;
|
||||
height: 800px;
|
||||
}
|
||||
|
||||
.custom-content-marker img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
</style>
|
@ -55,7 +55,7 @@
|
||||
|
||||
// 将标记点添加到地图上
|
||||
map.setCenter(lngLat);
|
||||
map.setZoom(16);
|
||||
map.setZoom(20);
|
||||
map.add(marker);
|
||||
|
||||
// const infoContent = `
|
||||
|
@ -77,6 +77,34 @@
|
||||
<a-form-item field="remark" label="备注">
|
||||
<a-textarea v-model="formData.remark" placeholder="请输入备注" />
|
||||
</a-form-item>
|
||||
<!-- 上传预览图 -->
|
||||
<a-form-item
|
||||
label="预览图"
|
||||
>
|
||||
<a-upload
|
||||
v-model="formData.attachment"
|
||||
placeholder="请选择预览图"
|
||||
|
||||
show-file-list
|
||||
:file-list="fileList"
|
||||
:custom-request="customRequest"
|
||||
@before-remove="beforeRemove"
|
||||
/>
|
||||
</a-form-item>
|
||||
<!-- 上传预览图 -->
|
||||
<a-form-item
|
||||
label="图标"
|
||||
>
|
||||
<a-upload
|
||||
v-model="formData.icon"
|
||||
placeholder="请选择图标"
|
||||
|
||||
show-file-list
|
||||
:file-list="fileList"
|
||||
:custom-request="customRequest"
|
||||
@before-remove="beforeRemove"
|
||||
/>
|
||||
</a-form-item>
|
||||
<!-- 参数 -->
|
||||
<a-form-item field="params" label="参数">
|
||||
<div style="width: 100%">
|
||||
@ -209,6 +237,7 @@
|
||||
ProductCreateRecord,
|
||||
queryProductDetail,
|
||||
updateProduct,
|
||||
addAttachments, deleteAttachment
|
||||
} from '@/api/product';
|
||||
|
||||
const props = defineProps({
|
||||
@ -298,6 +327,42 @@
|
||||
formData.value = res.data;
|
||||
paramsData.value = res.data.params;
|
||||
};
|
||||
|
||||
// 预览图列表
|
||||
const fileList = computed(() => {
|
||||
return formData.value.attachments?.map((item: any) => {
|
||||
return {
|
||||
name: item.fileName,
|
||||
url: item.url,
|
||||
uid: item.id,
|
||||
};
|
||||
});
|
||||
});
|
||||
// 自定义预览图上传
|
||||
const customRequest = async (option: any) => {
|
||||
const { fileItem, onSuccess, onError } = option;
|
||||
const formDataFile = new FormData();
|
||||
formDataFile.append('file', fileItem.file);
|
||||
formDataFile.append('type', '其他');
|
||||
const res = await addAttachments(formDataFile);
|
||||
if (res.status === 200) {
|
||||
onSuccess(res.data);
|
||||
formData.value.attachmentIds?.push(res.data.id);
|
||||
console.log(formData.value);
|
||||
} else {
|
||||
onError(res.data);
|
||||
}
|
||||
};
|
||||
// 删除图片
|
||||
const beforeRemove = async (file: any) => {
|
||||
if (!file.uid) {
|
||||
const res = await deleteAttachment(file.response.id);
|
||||
return res.status === 200;
|
||||
}
|
||||
const res = await deleteAttachment(file.uid);
|
||||
return res.status === 200;
|
||||
};
|
||||
|
||||
// 组件被点击
|
||||
const handleClick = () => {
|
||||
setVisible(true);
|
||||
|
@ -310,7 +310,7 @@
|
||||
</a-modal>
|
||||
|
||||
<a-modal
|
||||
width="900px"
|
||||
width="1300px"
|
||||
height="500px"
|
||||
:visible="visible && keyValue === '3'"
|
||||
@cancel="handleCancel"
|
||||
@ -319,7 +319,7 @@
|
||||
<a-form
|
||||
ref="eventCreateRef"
|
||||
:model="eventAddData"
|
||||
:style="{ width: '800px', height: '420px' }"
|
||||
:style="{ width: '1110px', height: '420px' }"
|
||||
>
|
||||
<!-- 事件名称 -->
|
||||
<a-form-item
|
||||
@ -362,11 +362,12 @@
|
||||
:key="index"
|
||||
style="margin-bottom: 5px"
|
||||
>
|
||||
<a-input v-model="param.name" placeholder="名称" allow-clear />
|
||||
<a-input v-model="param.name" placeholder="属性" allow-clear style="width: 140px" />
|
||||
<a-input
|
||||
v-model="param.identifier"
|
||||
placeholder="标识"
|
||||
allow-clear
|
||||
style="width: 140px"
|
||||
/>
|
||||
<a-select
|
||||
v-model="param.dataType"
|
||||
@ -382,6 +383,19 @@
|
||||
placeholder="类型"
|
||||
style="width: 140px"
|
||||
/>
|
||||
<a-select
|
||||
v-model="param.comparisonType"
|
||||
:options="comparisonTypeOptions"
|
||||
allow-search
|
||||
placeholder="比较类型"
|
||||
style="width: 140px"
|
||||
/>
|
||||
<a-input
|
||||
v-model="param.default"
|
||||
placeholder="默认值"
|
||||
allow-clear
|
||||
style="width: 140px"
|
||||
/>
|
||||
<a-button
|
||||
type="text"
|
||||
@click="handleDeleteParams(index, 'eventOutputData')"
|
||||
@ -511,6 +525,20 @@
|
||||
value: 'RW',
|
||||
},
|
||||
]);
|
||||
const comparisonTypeOptions = computed<SelectOptionData[]>(() => [
|
||||
{
|
||||
label: '大于',
|
||||
value: 'GREATER',
|
||||
},
|
||||
{
|
||||
label: '小于',
|
||||
value: 'LESS',
|
||||
},
|
||||
{
|
||||
label: '等于',
|
||||
value: 'EQUAL',
|
||||
},
|
||||
]);
|
||||
const eventTypeOptions = computed<SelectOptionData[]>(() => [
|
||||
{
|
||||
label: '主动',
|
||||
|
Loading…
Reference in New Issue
Block a user