feat(iot): 添加设备事件记录功能

- 在设备详情页面新增事件记录标签页
- 实现事件记录的数据加载和展示
- 优化设备地图组件样式
- 更新产品编辑页面的预览图和图标上传逻辑
-调整产品 TSL 页面的样式和功能
- 新增事件日志查询接口
This commit is contained in:
Kven 2025-03-12 20:30:58 +08:00
parent a045b8fc2d
commit 1e95c39a86
7 changed files with 173 additions and 109 deletions

View File

@ -29,3 +29,4 @@ export function deleteLogs(ids: number[]) {
data: ids, data: ids,
}); });
} }

View File

@ -24,6 +24,7 @@ export interface eventRecord extends Record {
level?: string; level?: string;
identifier?: string; identifier?: string;
productId?: number; productId?: number;
clientId?: number;
} }
export function queryServeList(data: ServeRecord) { export function queryServeList(data: ServeRecord) {
@ -50,6 +51,15 @@ export function queryEventList(data: eventRecord) {
}); });
} }
// 事件记录
export function queryEventLog(data: eventRecord) {
return axios({
url: '/api/rest/tsl/event/log',
method: 'get',
params: data,
});
}
export function createServe(data: any) { export function createServe(data: any) {
return axios.post(`/api/rest/tsl/serve`, data); return axios.post(`/api/rest/tsl/serve`, data);
} }

View File

@ -44,7 +44,7 @@ const IOT: AppRouteRecordRaw = {
locale: '设备管理(卡片)', locale: '设备管理(卡片)',
title: '设备管理(卡片)', title: '设备管理(卡片)',
requiresAuth: true, requiresAuth: true,
permissions: ['iot:device'], permissions: ['iot:deviceCard'],
}, },
}, },
{ {

View File

@ -100,15 +100,25 @@
</template> </template>
<template #operations="{ record }"> <template #operations="{ record }">
<a-button type="text" @click="openDetailModal(record)"> <a-button type="text" @click="openDetailModal(record)">
<template #icon><icon-list /></template> <icon-list/>
查看详情 详情
</a-button>
</template>
</a-table>
</a-tab-pane>
<a-tab-pane key="5" tab="事件记录" title="事件记录">
<a-table :columns="deviceEventColumns" :data="deviceEventData" >
<template #operations="{ record }">
<a-button type="text" @click="openDetailModal(record)">
<icon-list />
详情
</a-button> </a-button>
</template> </template>
</a-table> </a-table>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="5" tab="设备地图" title="设备地图"> <a-tab-pane key="6" tab="设备地图" title="设备地图">
<DeviceMap v-if="renderData.id" :renderData=renderData /> <DeviceMap v-if="renderData.id" :renderData=renderData />
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
@ -157,7 +167,7 @@
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { queryDeviceDetail, queryDeviceRecord, sendCommand } from '@/api/device'; import { queryDeviceDetail, queryDeviceRecord, sendCommand } from '@/api/device';
import { queryServeDetail, queryServeList } from '@/api/tsl'; import { queryEventLog, queryServeDetail, queryServeList } from '@/api/tsl';
import useVisible from '@/hooks/visible'; import useVisible from '@/hooks/visible';
import dynamicForm from './dynamic-form.vue'; import dynamicForm from './dynamic-form.vue';
import DeviceMap from './device-map.vue'; import DeviceMap from './device-map.vue';
@ -221,6 +231,18 @@
slotName: 'operations', slotName: 'operations',
}, },
]; ];
const deviceEventColumns = [
{
title: ' 事件名称',
dataIndex: 'name',
slotName: 'name',
},
{
title: '操作',
dataIndex: 'operations',
slotName: 'operations',
},
];;
const activeKey = ref('1'); const activeKey = ref('1');
const detailVisible = ref(false); const detailVisible = ref(false);
@ -243,24 +265,44 @@
} }
); );
const deviceReportData = ref([]); const deviceReportData = ref([]);
const deviceEventData = ref([]);
const deviceServerData = ref([]); const deviceServerData = ref([]);
const fields = ref([]); const fields = ref([]);
const fetchData = async (Id: number) => { const fetchData = async (Id: number) => {
const res = await queryDeviceDetail(Id); try {
renderData.value = res.data; const [detailRes, recordRes, serveRes, eventRes] = await Promise.all([
const res1 = await queryDeviceRecord({ queryDeviceDetail(Id),
clientId: renderData.value.clientId, queryDeviceRecord({
size: 10, clientId: renderData.value.clientId,
current: 1, size: 10,
}); current: 1,
deviceReportData.value = res1.data.records; }),
const res2 = await queryServeList({ queryServeList({
productId: renderData.value.productId, productId: renderData.value.productId,
size: 10, size: 10,
current: 1, current: 1,
}) }),
deviceServerData.value = res2.data.records; queryEventLog({
clientId: renderData.value.clientId,
size: 10,
current: 1,
}),
]);
renderData.value = detailRes.data;
deviceReportData.value = recordRes.data.records;
deviceServerData.value = serveRes.data.records;
deviceEventData.value = eventRes.data.records;
} catch (error) {
console.error('Failed to fetch data:', error);
Message.error({
content: '数据加载失败',
duration: 5 * 1000,
});
}
}; };
const handleMenuClick = (e: any) => { const handleMenuClick = (e: any) => {
activeKey.value = e; activeKey.value = e;
}; };

View File

@ -1,7 +1,7 @@
<template> <template>
<a-layout> <a-layout>
<a-layout-content> <a-layout-content>
<div id="container" style="height: 600px"></div> <div id="container" style="height: 500px"></div>
</a-layout-content> </a-layout-content>
</a-layout> </a-layout>
</template> </template>

View File

@ -82,12 +82,12 @@
label="预览图" label="预览图"
> >
<a-upload <a-upload
v-model="formData.attachment" v-model="formData.preview"
placeholder="请选择预览图"
show-file-list show-file-list
:file-list="fileList" :file-list="previewList"
:custom-request="customRequest" :limit="1"
list-type="picture-card"
:custom-request="(option: any) => customRequest(option, 'preview')"
@before-remove="beforeRemove" @before-remove="beforeRemove"
/> />
</a-form-item> </a-form-item>
@ -97,11 +97,10 @@
> >
<a-upload <a-upload
v-model="formData.icon" v-model="formData.icon"
placeholder="请选择图标"
show-file-list show-file-list
:file-list="fileList" :file-list="iconList"
:custom-request="customRequest" :limit="1"
:custom-request="(option: any) => customRequest(option, 'icon')"
@before-remove="beforeRemove" @before-remove="beforeRemove"
/> />
</a-form-item> </a-form-item>
@ -153,81 +152,11 @@
> >
</template> </template>
</a-modal> </a-modal>
<!-- <a-modal-->
<!-- width="900px"-->
<!-- :visible="paramsVisible"-->
<!-- @cancel="paramsCancel"-->
<!-- >-->
<!-- <template #title>添加参数</template>-->
<!-- <a-form-->
<!-- ref="CreateRef"-->
<!-- :model="paramsData"-->
<!-- :style="{ width: '650px' }"-->
<!-- >-->
<!-- &lt;!&ndash; 添加参数 &ndash;&gt;-->
<!-- <a-form-item-->
<!-- field="name"-->
<!-- label="参数名称"-->
<!-- :rules="[{ required: true, message: '参数名称不能为空' }]"-->
<!-- :validate-trigger="['change']"-->
<!-- >-->
<!-- <a-input-->
<!-- v-model="paramsData.name"-->
<!-- placeholder='请选择参数名称'-->
<!-- />-->
<!-- </a-form-item>-->
<!-- &lt;!&ndash; 参数标识 &ndash;&gt;-->
<!-- <a-form-item-->
<!-- field="identifier"-->
<!-- label="参数标识"-->
<!-- :rules="[{ required: true, message: '参数标识不能为空' }]"-->
<!-- :validate-trigger="['change']"-->
<!-- >-->
<!-- <a-input-->
<!-- v-model="paramsData.identifier"-->
<!-- placeholder='请选择参数标识'-->
<!-- />-->
<!-- </a-form-item>-->
<!-- <a-form-item-->
<!-- field="dataType"-->
<!-- label="数据类型"-->
<!-- :rules="[{ required: true, message: '数据类型不能为空' }]"-->
<!-- :validate-trigger="['change']"-->
<!-- >-->
<!-- <a-select-->
<!-- v-model="paramsData.dataType"-->
<!-- placeholder='请输入数据类型'-->
<!-- :options="dataTypeOptions"-->
<!-- allow-search-->
<!-- />-->
<!-- </a-form-item>-->
<!-- <a-form-item-->
<!-- field="type"-->
<!-- label="参数类型"-->
<!-- :rules="[{ required: true, message: '参数类型不能为空' }]"-->
<!-- :validate-trigger="['change']"-->
<!-- >-->
<!-- <a-select-->
<!-- v-model="paramsData.type"-->
<!-- placeholder='请输入参数类型'-->
<!-- :options="typeOptions"-->
<!-- allow-search-->
<!-- />-->
<!-- </a-form-item>-->
<!-- </a-form>-->
<!-- <template #footer>-->
<!-- <a-button class="editor-button" @click="paramsCancel">取消</a-button>-->
<!-- <a-button class="editor-button" type="primary" @click="paramsSubmit">确定</a-button>-->
<!-- </template>-->
<!-- </a-modal>-->
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import useVisible from '@/hooks/visible'; import useVisible from '@/hooks/visible';
import { computed, defineEmits, onMounted, PropType, ref } from 'vue'; import { computed, defineEmits, PropType, ref } from 'vue';
import { FormInstance } from '@arco-design/web-vue/es/form'; import { FormInstance } from '@arco-design/web-vue/es/form';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import '@wangeditor/editor/dist/css/style.css'; import '@wangeditor/editor/dist/css/style.css';
@ -329,8 +258,18 @@
}; };
// //
const fileList = computed(() => { const previewList = computed(() => {
return formData.value.attachments?.map((item: any) => { return formData.value.preview?.map((item: any) => {
return {
name: item.fileName,
url: item.url,
uid: item.id,
};
});
});
//
const iconList = computed(() => {
return formData.value.icon?.map((item: any) => {
return { return {
name: item.fileName, name: item.fileName,
url: item.url, url: item.url,
@ -339,7 +278,7 @@
}); });
}); });
// //
const customRequest = async (option: any) => { const customRequest = async (option: any,type: string) => {
const { fileItem, onSuccess, onError } = option; const { fileItem, onSuccess, onError } = option;
const formDataFile = new FormData(); const formDataFile = new FormData();
formDataFile.append('file', fileItem.file); formDataFile.append('file', fileItem.file);
@ -347,7 +286,13 @@
const res = await addAttachments(formDataFile); const res = await addAttachments(formDataFile);
if (res.status === 200) { if (res.status === 200) {
onSuccess(res.data); onSuccess(res.data);
formData.value.attachmentIds?.push(res.data.id); if (type === 'preview') {
formData.value.preview = formData.value.preview || [];
formData.value.preview.push(res.data);
} else if (type === 'icon') {
formData.value.icon = formData.value.icon || [];
formData.value.icon.push(res.data);
}
} else { } else {
onError(res.data); onError(res.data);
} }

View File

@ -24,7 +24,7 @@
新建 新建
</a-button> </a-button>
<a-table <a-table
:columns="columns" :columns="propertyColumns"
:data="propertyData" :data="propertyData"
:pagination="true" :pagination="true"
> >
@ -56,7 +56,7 @@
<template #icon><icon-plus /></template> <template #icon><icon-plus /></template>
新建 新建
</a-button> </a-button>
<a-table :columns="columns" :data="serveData"> <a-table :columns="serveColumns" :data="serveData">
<template #operation="{ record }"> <template #operation="{ record }">
<a-button <a-button
v-permission="['iot:serve:delete']" v-permission="['iot:serve:delete']"
@ -82,7 +82,10 @@
<template #icon><icon-plus /></template> <template #icon><icon-plus /></template>
新建 新建
</a-button> </a-button>
<a-table :columns="columns" :data="eventData"> <a-table :columns="eventColumns" :data="eventData">
<template #type="{ record }">
<div>{{ record.type === 'PASSIVE' ? '主动' : '被动' }}</div>
</template>
<template #operation="{ record }"> <template #operation="{ record }">
<a-button <a-button
v-permission="['iot:event:delete']" v-permission="['iot:event:delete']"
@ -362,7 +365,13 @@
:key="index" :key="index"
style="margin-bottom: 5px" style="margin-bottom: 5px"
> >
<a-input v-model="param.name" placeholder="属性" allow-clear style="width: 140px" /> <a-select
v-model="param.name"
:options="propertyOptions"
allow-search
placeholder="属性"
style="width: 140px"
/>
<a-input <a-input
v-model="param.identifier" v-model="param.identifier"
placeholder="标识" placeholder="标识"
@ -450,7 +459,29 @@
const { visible, setVisible } = useVisible(); const { visible, setVisible } = useVisible();
const route = useRoute(); const route = useRoute();
const id = Number(route.params.id); const id = Number(route.params.id);
const columns = [ const serveColumns = [
{
title: '名称',
dataIndex: 'name',
slotName: 'name',
},
{
title: '标识',
dataIndex: 'identifier',
slotName: 'identifier',
},
{
title: '备注',
dataIndex: 'remark',
slotName: 'remark',
},
{
title: '操作',
dataIndex: 'operation',
slotName: 'operation',
},
];
const propertyColumns = [
{ {
title: '名称', title: '名称',
dataIndex: 'name', dataIndex: 'name',
@ -477,6 +508,33 @@
slotName: 'operation', slotName: 'operation',
}, },
]; ];
const eventColumns = [
{
title: '名称',
dataIndex: 'name',
slotName: 'name',
},
{
title: '标识',
dataIndex: 'identifier',
slotName: 'identifier',
},
{
title: '类型',
dataIndex: 'type',
slotName: 'type',
},
{
title: '备注',
dataIndex: 'remark',
slotName: 'remark',
},
{
title: '操作',
dataIndex: 'operation',
slotName: 'operation',
},
];
const serveData = ref([]); const serveData = ref([]);
const propertyData = ref([]); const propertyData = ref([]);
const eventData = ref([]); const eventData = ref([]);
@ -549,6 +607,14 @@
value: 'PASSIVE', value: 'PASSIVE',
}, },
]); ]);
const propertyOptions = computed(() => {
return propertyData.value.map((item) => {
return {
label: item.name,
value: item.identifier,
};
});
});
const propertyAddData = ref({ const propertyAddData = ref({
productId: id, productId: id,
}); });