feat(iot): 优化设备和产品详情页面布局

-调整设备和产品详情页面的布局结构
- 添加设备和产品预览图片
- 优化事件记录展示方式
- 增加设备编辑功能
- 重构事件记录查询接口
This commit is contained in:
Kven 2025-03-24 20:47:06 +08:00
parent eed0d9c99a
commit 7328db1a23
9 changed files with 214 additions and 114 deletions

View File

@ -55,6 +55,7 @@
"vue": "^3.2.40",
"vue-echarts": "^6.2.3",
"vue-i18n": "^9.2.2",
"vue-json-pretty": "^2.4.0",
"vue-quill-editor": "^3.0.6",
"vue-router": "^4.0.14",
"xlsx": "^0.18.5"

View File

@ -11,11 +11,13 @@ export interface DeviceRecord {
pageable?: string;
longitude?: number;
latitude?: number;
icon?: string;
icon?: object;
hardwareVersion?: string;
firmwareVersion?: string;
productName?: string;
id?: number;
warning?: string;
iconId?: number;
}
export interface DeviceCreateRecord {

View File

@ -52,12 +52,8 @@ export function queryEventList(data: eventRecord) {
}
// 事件记录
export function queryEventLog(data: eventRecord) {
return axios({
url: '/api/rest/tsl/event/log',
method: 'get',
params: data,
});
export function queryEventLog(clientId: number) {
return axios.get(`/api/rest/tsl/event/${clientId}/log`)
}
export function createServe(data: any) {

View File

@ -2,9 +2,11 @@
<div class="container">
<Breadcrumb :items="['物联网管理', '设备管理', '设备详情']" />
<a-card class="general-card" title=" ">
<a-descriptions v-model="renderData" size="large">
<template #title
><h3 style="margin-top: -15px"
<a-row :gutter="16">
<a-col :span="16">
<a-descriptions v-model="renderData" size="large">
<template #title
><h3 style="margin-top: -15px"
><a-button
class="nav-btn"
type="outline"
@ -13,26 +15,31 @@
>
<icon-undo />
</a-button>
设备详情</h3
>
</template>
<a-descriptions-item label="设备名称">{{
renderData.name
}}</a-descriptions-item>
<a-descriptions-item label="硬件版本">{{
renderData.hardwareVersion
}}</a-descriptions-item>
<a-descriptions-item label="固件版本">{{
renderData.firmwareVersion
}}</a-descriptions-item>
<a-descriptions-item label="所属产品">{{
renderData.productName
}}</a-descriptions-item>
<!-- <a-descriptions-item label="备注">{{renderData.remark }}</a-descriptions-item>-->
<a-descriptions-item label="创建时间">{{
dayjs(renderData.createTime).format('YYYY-MM-DD HH:mm:ss')
}}</a-descriptions-item>
</a-descriptions>
设备详情</h3
>
</template>
<a-descriptions-item label="设备名称">{{
renderData.name
}}</a-descriptions-item>
<a-descriptions-item label="硬件版本">{{
renderData.hardwareVersion
}}</a-descriptions-item>
<a-descriptions-item label="固件版本">{{
renderData.firmwareVersion
}}</a-descriptions-item>
<a-descriptions-item label="所属产品">{{
renderData.productName
}}</a-descriptions-item>
<!-- <a-descriptions-item label="备注">{{renderData.remark }}</a-descriptions-item>-->
<a-descriptions-item label="创建时间">{{
dayjs(renderData.createTime).format('YYYY-MM-DD HH:mm:ss')
}}</a-descriptions-item>
</a-descriptions>
</a-col>
<a-col :span="4">
<a-image :src="previewUrl" width="100px" />
</a-col>
</a-row>
</a-card>
<a-card class="general-card" style="margin-top: 10px">
<a-tabs
@ -89,14 +96,8 @@
<div>{{ dayjs(record.recordTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
</template>
<template #content="{ record }">
<div v-if="record.content">
<span v-for="(value, key, index) in record.content" :key="key">
<template v-if="index === 0">
<strong>{{ key }}:</strong> {{ value }}
</template>
</span>
<span v-if="Object.keys(record.content).length > 1">. . . . . .</span>
</div>
<vue-json-pretty v-if="record.content" :data="record.content" :deep="0" />
<div v-else>/</div>
</template>
<template #operations="{ record }">
<a-button type="text" @click="openDetailModal(record)">
@ -108,8 +109,16 @@
</a-tab-pane>
<a-tab-pane key="5" tab="事件记录" title="事件记录">
<a-table :columns="deviceEventColumns" :data="deviceEventData" >
<template #recordTime="{ record }">
<div>{{ dayjs(record.recordData.recordTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
</template>
<template #content="{ record }">
<vue-json-pretty v-if="record.recordData.content" :data="record.recordData.content" :deep="0" >
</vue-json-pretty>
<div v-else>/</div>
</template>
<template #operations="{ record }">
<a-button type="text" @click="openDetailModal(record)">
<a-button type="text" @click="openEventDetailModal(record)">
<icon-list />
详情
</a-button>
@ -150,11 +159,33 @@
<a-button @click="closeDetailModal">关闭</a-button>
</template>
<div v-if="selectedRecord">
<p><strong>上报时间:</strong> {{ dayjs(selectedRecord.recordTime).format('YYYY-MM-DD HH:mm:ss') }}</p>
<div v-if="selectedRecord.content">
<div v-for="(value, key) in selectedRecord.content" :key="key">
<strong>{{ key }}:</strong> {{ value }}
</div>
<p>
<strong>上报时间:</strong> {{ dayjs(selectedRecord.recordTime).format('YYYY-MM-DD HH:mm:ss') }}
</p>
<strong>内容:</strong>
<div style="border: 1px solid #e5e5e5; border-radius: 4px; padding: 8px; margin-top: 8px;">
<vue-json-pretty :data="selectedRecord.content" :deep="1" />
</div>
</div>
</a-modal>
<!-- 事件记录详情模态框 -->
<a-modal
:visible="eventDetailVisible"
@cancel="closeEventDetailModal"
title="事件记录详情"
>
<template #footer>
<a-button @click="closeEventDetailModal">关闭</a-button>
</template>
<div v-if="selectedEventRecord">
<strong>事件名称:</strong> {{ selectedEventRecord.name }}
<p>
<strong>发生时间:</strong> {{ dayjs(selectedEventRecord.recordData.recordTime).format('YYYY-MM-DD HH:mm:ss') }}
</p>
<strong>内容:</strong>
<div style="border: 1px solid #e5e5e5; border-radius: 4px; padding: 8px; margin-top: 8px;">
<vue-json-pretty :data="selectedEventRecord.recordData.content" :deep="1" />
</div>
</div>
</a-modal>
@ -164,13 +195,15 @@
<script lang="ts" setup>
import dayjs from 'dayjs';
import { useRoute } from 'vue-router';
import { onMounted, ref } from 'vue';
import { onMounted, reactive, ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import { queryDeviceDetail, queryDeviceRecord, sendCommand } from '@/api/device';
import { queryEventLog, queryServeDetail, queryServeList } from '@/api/tsl';
import useVisible from '@/hooks/visible';
import VueJsonPretty from 'vue-json-pretty';
import dynamicForm from './dynamic-form.vue';
import DeviceMap from './device-map.vue';
import 'vue-json-pretty/lib/styles.css';
const route = useRoute();
const { visible, setVisible } = useVisible();
@ -207,6 +240,7 @@
title: '内容',
dataIndex: 'content',
slotName: 'content',
ellipsis: true,
},
{
title: '操作',
@ -237,6 +271,16 @@
dataIndex: 'name',
slotName: 'name',
},
{
title: '发生时间',
dataIndex: 'recordData.recordTime',
slotName: 'recordTime',
},
{
title: '内容',
dataIndex: 'recordData.content',
slotName: 'content',
},
{
title: '操作',
dataIndex: 'operations',
@ -244,10 +288,15 @@
},
];;
const activeKey = ref('1');
const previewUrl = ref('');
const detailVisible = ref(false);
const selectedRecord = ref<any>(null);
//
const eventDetailVisible = ref(false);
const selectedEventRecord = ref<any>(null);
const openDetailModal = (record: any) => {
selectedRecord.value = record;
detailVisible.value = true;
@ -262,6 +311,9 @@
{
clientId: 0,
productId: 0,
preview: {
url: '',
},
}
);
const deviceReportData = ref([]);
@ -274,6 +326,7 @@
// 1.
const detailRes = await queryDeviceDetail(Id);
renderData.value = detailRes.data;
previewUrl.value = renderData.value.preview.url;
// 2. clientId productId
const [recordRes, serveRes, eventRes] = await Promise.all([
@ -287,11 +340,7 @@
size: 10,
current: 1,
}),
queryEventLog({
clientId: renderData.value.clientId,
size: 10,
current: 1,
}),
queryEventLog(renderData.value.clientId),
]);
// 3.
@ -355,6 +404,18 @@
}
};
//
const openEventDetailModal = (record: any) => {
selectedEventRecord.value = record;
eventDetailVisible.value = true;
};
//
const closeEventDetailModal = () => {
eventDetailVisible.value = false;
selectedEventRecord.value = null;
};
onMounted(() => {
fetchData(id);
});

View File

@ -61,6 +61,21 @@
>
<a-input v-model="formData.name" placeholder="请输入设备名称" />
</a-form-item>
<!-- 客户机序列号 -->
<a-form-item
field="clientId"
label="客户机ID"
:rules="[
{ max: 12, message: '客户机ID不能超过12位' }
]"
:validate-trigger="['change']"
>
<a-input
v-model="formData.clientId"
placeholder="请输入客户机ID"
:max-length="12"
/>
</a-form-item>
<!-- 硬件版本 -->
<a-form-item
field="hardwareVersion"
@ -85,6 +100,29 @@
placeholder="请输入固件版本"
/>
</a-form-item>
<!-- 纬度 -->
<a-form-item
field="latitude"
label="纬度"
:validate-trigger="['change']"
>
<a-input
v-model="formData.latitude"
placeholder="请输入纬度"
/>
</a-form-item>
<!-- 经度 -->
<a-form-item
field="longitude"
label="经度"
:validate-trigger="['change']"
>
<a-input
v-model="formData.longitude"
placeholder="请输入经度"
/>
</a-form-item>
<a-form-item field="remark" label="备注">
<a-textarea v-model="formData.remark" placeholder="请输入描述" />
</a-form-item>
@ -157,7 +195,6 @@
import '@wangeditor/editor/dist/css/style.css';
import {
createDevice,
queryDeviceByName,
queryDeviceDetail, queryProductByName,
updateDevice
} from '@/api/device';

View File

@ -47,7 +47,7 @@
</a-form-item>
</a-col>
<a-col :span="10">
<a-form-item field="isOnline" label="在线">
<a-form-item field="online" label="在线">
<a-select
v-model="formModel.isOnline"
style="width: 360px"

View File

@ -1,6 +1,6 @@
<template>
<a-layout>
<a-layout-sider style="width:300px;" :resize-directions="['right']">
<a-layout-sider style="width:300px;background-color: transparent;" :resize-directions="['right']">
<!-- 搜索框 -->
<a-card>
<a-input-search
@ -35,8 +35,7 @@
:size="70"
>
<a-image
:src=device.previewUrl
@error="handleImageError"
:src="getImageUrl(device.previewId)"
/>
</a-avatar>
<a-descriptions layout="inline-horizontal" :style="{ marginLeft: '10px'}">
@ -50,23 +49,6 @@
</a-card>
</a-col>
</a-row>
<!-- <a-list>-->
<!-- <a-list-item v-for="device in deviceList" :key="device.id" @click="selectDevice(device)">-->
<!-- <a-list-item-meta-->
<!-- :title="device.name"-->
<!-- :description="device.name"-->
<!-- >-->
<!-- <template #avatar>-->
<!-- <a-avatar shape="square">-->
<!-- <img-->
<!-- alt="avatar"-->
<!-- src="https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp"-->
<!-- />-->
<!-- </a-avatar>-->
<!-- </template>-->
<!-- </a-list-item-meta>-->
<!-- </a-list-item>-->
<!-- </a-list>-->
</a-layout-sider>
<a-layout-content>
<div id="container"></div>
@ -92,6 +74,15 @@
};
// URL
const getImageUrl = (previewId: string) => {
if (!previewId) {
return 'https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp';
}
return `/api/rest/attachment/${previewId}`;
};
//
const handleSearch = debounce(async (value: string) => {
try {
@ -113,10 +104,10 @@
}
}, 300); // 300ms
const handleImageError = (event: Event) => {
const img = event.target as HTMLImageElement;
img.src = 'https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp'; //
};
// const handleImageError = (event: Event) => {
// const img = event.target as HTMLImageElement;
// img.src = 'https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp'; //
// };
AMapLoader.load({
key: 'a4e80eed798a56451b226dcfca81b846', // WebKey load
@ -136,9 +127,11 @@
const lngLat = [device.longitude, device.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 markerIcon = device.warning === '1'
? `/api/rest/attachment/${device.icon?.url}` //
: `/api/rest/attachment/${device.icon?.url}`; //
content.innerHTML = `<img src="${markerIcon}" alt="标记点" />`;
//
const marker = new AMap.Marker({
position: lngLat,

View File

@ -2,39 +2,45 @@
<div class="container">
<Breadcrumb :items="['物联网管理', '产品管理', '产品详情']" />
<a-card class="general-card" title=" ">
<a-descriptions v-model="renderData" size="large">
<template #title
><h3 style="margin-top: -15px">
<a-button
class="nav-btn"
type="outline"
:shape="'circle'"
@click="() => $router.go(-1)"
>
<icon-undo />
</a-button>
产品详情</h3
>
</template>
<a-descriptions-item label="产品名称">{{
renderData.name
}}</a-descriptions-item>
<a-descriptions-item label="产品类型">{{
renderData.productType
}}</a-descriptions-item>
<a-descriptions-item label="产品型号">{{
renderData.model
}}</a-descriptions-item>
<a-descriptions-item label="通讯协议">{{
renderData.link
}}</a-descriptions-item>
<a-descriptions-item label="备注">{{
renderData.remark
}}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{
dayjs(renderData.createTime).format('YYYY-MM-DD HH:mm:ss')
}}</a-descriptions-item>
</a-descriptions>
<a-row :gutter="10">
<a-col :span="16">
<a-descriptions v-model="renderData" size="large">
<template #title
><h3 style="margin-top: -15px">
<a-button
class="nav-btn"
type="outline"
:shape="'circle'"
@click="() => $router.go(-1)"
>
<icon-undo />
</a-button>
产品详情</h3>
</template>
<a-descriptions-item label="产品名称">{{
renderData.name
}}</a-descriptions-item>
<a-descriptions-item label="产品类型">{{
renderData.productType
}}</a-descriptions-item>
<a-descriptions-item label="产品型号">{{
renderData.model
}}</a-descriptions-item>
<a-descriptions-item label="通讯协议">{{
renderData.link
}}</a-descriptions-item>
<a-descriptions-item label="备注">{{
renderData.remark
}}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{
dayjs(renderData.createTime).format('YYYY-MM-DD HH:mm:ss')
}}</a-descriptions-item>
</a-descriptions>
</a-col>
<a-col :span="4">
<a-image :src="previewUrl" width="100px" />
</a-col>
</a-row>
</a-card>
<a-card class="general-card" style="margin-top: 10px">
<a-tabs
@ -88,9 +94,11 @@
];
const activeKey = ref('1');
const renderData = ref([]);
const previewUrl = ref('');
const fetchData = async (Id: number) => {
const res = await queryProductDetail(Id);
renderData.value = res.data;
previewUrl.value = res.data.preview.url;
};
const handleMenuClick = (e: any) => {
activeKey.value = e;

View File

@ -84,6 +84,7 @@
<a-upload
show-file-list
:file-list="previewList"
image-preview
:limit="1"
list-type="picture-card"
:custom-request="(option: any) => customRequest(option, 'preview')"
@ -96,6 +97,7 @@
>
<a-upload
show-file-list
list-type="picture"
:file-list="iconList"
:limit="1"
:custom-request="(option: any) => customRequest(option, 'icon')"
@ -275,11 +277,11 @@
onSuccess(res.data);
if (type === 'preview') {
formData.value.preview = formData.value.preview || [];
formData.value.preview.push(res.data);
formData.value.preview = res.data;
formData.value.previewId = res.data.id;
} else if (type === 'icon') {
formData.value.icon = formData.value.icon || [];
formData.value.icon.push(res.data);
formData.value.icon = res.data;
formData.value.iconId = res.data.id;
}
} else {
@ -295,7 +297,7 @@
}
//
const res = await deleteAttachment(fileId);
if (res.status === 200) {
if (res.data === true) {
//
Message.success('删除成功');
} else {