feat(iot): 优化设备和产品相关功能

- 添加设备名称模糊查询功能
- 更新设备详情和列表展示样式
- 修改产品编辑界面,移除不必要的上传功能
- 优化产品 TSL界面,增加事件状态切换功能
- 重构部分 API 接口命名
This commit is contained in:
Kven 2025-03-19 17:53:25 +08:00
parent 1e95c39a86
commit bdd99287cf
9 changed files with 219 additions and 78 deletions

View File

@ -50,12 +50,21 @@ export function queryDeviceList(data: DeviceRecord) {
});
}
// 名称模糊查询
export function queryDeviceByNameList(data: any) {
return axios({
url: `/api/rest/device/list`,
method: 'get',
params: data,
});
}
// 查看详情
export function queryDeviceDetail(id: number) {
return axios.get(`/api/rest/device/${id}`);
}
// 名称模糊查询
export function queryDeviceByName(data: any) {
export function queryProductByName(data: any) {
return axios({
url: `/api/rest/product/fuzzy`,
method: 'get',

View File

@ -67,12 +67,19 @@ 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);
return axios({
method: 'post',
url: '/api/rest/attachment',
headers: {
'Content-Type': 'multipart/form-data',
},
data,
});
}
// 文件删除
export function deleteAttachment(id: number) {
return axios.delete(`/api/rest/product/attachments/${id}`);
// 删除附件
export function deleteAttachment(id: string) {
return axios.delete(`/api/rest/attachment/delete/${id}`);
}

View File

@ -88,3 +88,8 @@ export function deleteEvent(data: any) {
export function queryServeDetail(id: number) {
return axios.get(`/api/rest/tsl/serve/${id}`);
}
// 事件启用
export function enableEvent(id: number) {
return axios.patch(`/api/rest/tsl/event/toggle/${id}`);
}

View File

@ -271,8 +271,12 @@
const fields = ref([]);
const fetchData = async (Id: number) => {
try {
const [detailRes, recordRes, serveRes, eventRes] = await Promise.all([
queryDeviceDetail(Id),
// 1.
const detailRes = await queryDeviceDetail(Id);
renderData.value = detailRes.data;
// 2. clientId productId
const [recordRes, serveRes, eventRes] = await Promise.all([
queryDeviceRecord({
clientId: renderData.value.clientId,
size: 10,
@ -290,7 +294,7 @@
}),
]);
renderData.value = detailRes.data;
// 3.
deviceReportData.value = recordRes.data.records;
deviceServerData.value = serveRes.data.records;
deviceEventData.value = eventRes.data.records;

View File

@ -158,8 +158,8 @@
import {
createDevice,
queryDeviceByName,
queryDeviceDetail,
updateDevice,
queryDeviceDetail, queryProductByName,
updateDevice
} from '@/api/device';
import type { SelectOptionData } from '@arco-design/web-vue/es/select/interface';
import { queryProductDetail } from '@/api/product';
@ -243,7 +243,7 @@
loading.value = true;
options.value = [];
window.setTimeout(async () => {
const res = await queryDeviceByName({
const res = await queryProductByName({
name: value,
});
options.value = res.data.map((item: any) => {

View File

@ -235,7 +235,7 @@
import useTableOption from '@/hooks/table-option';
import { Message } from '@arco-design/web-vue';
import { useRouter } from 'vue-router';
import { deleteDevice, DeviceRecord, queryDeviceByName, queryDeviceList } from '@/api/device';
import { deleteDevice, DeviceRecord, queryDeviceList, queryProductByName } from '@/api/device';
import DeviceEdit from '@/views/iot/device/components/device-edit.vue';
const generateFormModel = () => {
@ -342,7 +342,7 @@
loading.value = true;
options.value = [];
window.setTimeout(async () => {
const res = await queryDeviceByName({
const res = await queryProductByName({
name: value,
});
options.value = res.data.map((item: any) => {

View File

@ -1,6 +1,17 @@
<template>
<a-layout>
<a-layout-sider :resize-directions="['right']">
<a-layout-sider style="width:300px;" :resize-directions="['right']">
<!-- 搜索框 -->
<a-card>
<a-input-search
v-model="searchKeyword"
placeholder="设备名称"
@change="handleSearch"
@search="handleSearch"
style="margin-bottom: 16px"
/>
</a-card>
<!-- 设备列表 -->
<a-row>
<a-col
:span="24"
@ -8,9 +19,54 @@
:key="device.id"
@click="selectDevice(device)"
>
{{ device.name }}
<a-card hoverable :style="{ margin: '5px', height: '100px' }">
<div
:style="{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}"
>
<span
:style="{ display: 'flex', alignItems: 'center', color: '#1D2129' }"
>
<a-avatar
shape="square"
:size="70"
>
<a-image
:src=device.previewUrl
@error="handleImageError"
/>
</a-avatar>
<a-descriptions layout="inline-horizontal" :style="{ marginLeft: '10px'}">
<a-descriptions-item label="设备名称" :span="3">{{ device.name }}</a-descriptions-item>
<!-- <a-descriptions-item label="硬件版本">{{ device.hardwareVersion }}</a-descriptions-item>-->
<!-- <a-descriptions-item label="固件版本">{{ device.firmwareVersion }}</a-descriptions-item>-->
<a-descriptions-item label="所属产品">{{ device.productName }}</a-descriptions-item>
</a-descriptions>
</span>
</div>
</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>
@ -21,17 +77,47 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import AMapLoader from '@amap/amap-jsapi-loader';
import { DeviceRecord, queryDeviceList } from '@/api/device';
import { DeviceRecord, queryDeviceByNameList, queryDeviceList } from '@/api/device';
import '@amap/amap-jsapi-types';
import router from '@/router';
import { Message } from '@arco-design/web-vue';
import { debounce } from '@arco-design/web-vue/es/_utils/debounce';
let map: any = null;
const deviceList = ref<DeviceRecord[]>([]);
const searchKeyword = ref(''); //
const isLoading = ref(false); //
let selectDevice = (device: DeviceRecord) => {
console.log(device);
};
//
const handleSearch = debounce(async (value: string) => {
try {
isLoading.value = true; //
if (value) {
const res = await queryDeviceByNameList({
name: value,
});
deviceList.value = res.data;
} else {
//
const response = await queryDeviceByNameList({});
deviceList.value = response.data;
}
} catch (error) {
Message.error('搜索失败,请稍后重试');
} finally {
isLoading.value = false; //
}
}, 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'; //
};
AMapLoader.load({
key: 'a4e80eed798a56451b226dcfca81b846', // WebKey load
version: '2.0', // JSAPI 1.4.15
@ -100,12 +186,9 @@
// AMap then
onMounted(() => {
//
queryDeviceList({
current: 1,
size: 10,
})
queryDeviceByNameList({})
.then((response) => {
deviceList.value = response.data.records;
deviceList.value = response.data;
})
.catch(() => {
Message.error('获取设备列表失败');
@ -120,7 +203,7 @@
<style scoped>
#container {
width: 100%;
height: 800px;
height: 100vh;
}
.custom-content-marker img {

View File

@ -77,33 +77,31 @@
<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.preview"
show-file-list
:file-list="previewList"
:limit="1"
list-type="picture-card"
:custom-request="(option: any) => customRequest(option, 'preview')"
@before-remove="beforeRemove"
/>
</a-form-item>
<!-- 上传预览图 -->
<a-form-item
label="图标"
>
<a-upload
v-model="formData.icon"
show-file-list
:file-list="iconList"
:limit="1"
:custom-request="(option: any) => customRequest(option, 'icon')"
@before-remove="beforeRemove"
/>
</a-form-item>
<!-- &lt;!&ndash; 上传预览图 &ndash;&gt;-->
<!-- <a-form-item-->
<!-- label="预览图"-->
<!-- >-->
<!-- <a-upload-->
<!-- show-file-list-->
<!-- :file-list="previewList"-->
<!-- :limit="1"-->
<!-- list-type="picture-card"-->
<!-- :custom-request="(option: any) => customRequest(option, 'preview')"-->
<!-- @before-remove="beforeRemove"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- &lt;!&ndash; 上传图标 &ndash;&gt;-->
<!-- <a-form-item-->
<!-- label="图标"-->
<!-- >-->
<!-- <a-upload-->
<!-- show-file-list-->
<!-- :file-list="iconList"-->
<!-- :limit="1"-->
<!-- :custom-request="(option: any) => customRequest(option, 'icon')"-->
<!-- @before-remove="beforeRemove"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- 参数 -->
<a-form-item field="params" label="参数">
<div style="width: 100%">
@ -257,26 +255,22 @@
paramsData.value = res.data.params;
};
//
const previewList = computed(() => {
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 {
name: item.fileName,
url: item.url,
uid: item.id,
};
});
});
// //
// const previewList = computed(() => {
// return {
// name: formData.value.preview?.fileName,
// url: formData.value.preview?.url,
// uid: formData.value.preview?.id,
// }
// });
// //
// const iconList = computed(() => {
// return {
// name: formData.value.icon?.fileName,
// url: formData.value.icon?.url,
// uid: formData.value.icon?.id,
// }
// });
//
const customRequest = async (option: any,type: string) => {
const { fileItem, onSuccess, onError } = option;
@ -289,9 +283,11 @@
if (type === 'preview') {
formData.value.preview = formData.value.preview || [];
formData.value.preview.push(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.iconId = res.data.id;
}
} else {
onError(res.data);

View File

@ -86,6 +86,12 @@
<template #type="{ record }">
<div>{{ record.type === 'PASSIVE' ? '主动' : '被动' }}</div>
</template>
<template #enabled="{ record }">
<a-switch
v-model="record.enabled"
@change="handleToggleEventStatus(record)"
/>
</template>
<template #operation="{ record }">
<a-button
v-permission="['iot:event:delete']"
@ -124,7 +130,7 @@
>
<a-input
v-model="propertyAddData.name"
placeholder="请输入设备名称"
placeholder="请输入属性名称"
/>
</a-form-item>
<!-- 标识 -->
@ -393,14 +399,14 @@
style="width: 140px"
/>
<a-select
v-model="param.comparisonType"
v-model="param.compareType"
:options="comparisonTypeOptions"
allow-search
placeholder="比较类型"
style="width: 140px"
/>
<a-input
v-model="param.default"
v-model="param.defaultValue"
placeholder="默认值"
allow-clear
style="width: 140px"
@ -446,10 +452,10 @@
createServe,
deleteEvent,
deleteProperty,
deleteServe,
deleteServe, enableEvent,
queryEventList,
queryPropertyList,
queryServeList,
queryServeList
} from '@/api/tsl';
import type { SelectOptionData } from '@arco-design/web-vue/es/select/interface';
import { FormInstance } from '@arco-design/web-vue/es/form';
@ -524,6 +530,11 @@
dataIndex: 'type',
slotName: 'type',
},
{
title: '状态',
dataIndex: 'enabled',
slotName: 'enabled',
},
{
title: '备注',
dataIndex: 'remark',
@ -586,16 +597,24 @@
const comparisonTypeOptions = computed<SelectOptionData[]>(() => [
{
label: '大于',
value: 'GREATER',
value: 'GT',
},
{
label: '小于',
value: 'LESS',
value: 'LT',
},
{
label: '等于',
value: 'EQUAL',
value: 'EQ',
},
{
label: '大于等于',
value: 'GE',
},
{
label: '小于等于',
value: 'LE',
}
]);
const eventTypeOptions = computed<SelectOptionData[]>(() => [
{
@ -670,6 +689,7 @@
identifier: '',
dataType: '',
type: '',
defaultValue: '',
},
]);
const propertyCreateRef = ref<FormInstance>();
@ -814,6 +834,23 @@
await fetchData(id);
}
};
//
const handleToggleEventStatus = async (record: any) => {
try {
const res = await enableEvent(record.id);
if (res.status === 200) {
Message.success({
content: '状态更新成功',
duration: 5 * 1000,
});
await fetchData(id);
}
} catch (error) {
console.error('状态更新失败:', error);
}
};
//
const handleDeleteEvent = async (record: any) => {
const res = await deleteEvent(record.id);