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) { export function queryDeviceDetail(id: number) {
return axios.get(`/api/rest/device/${id}`); return axios.get(`/api/rest/device/${id}`);
} }
// 名称模糊查询 // 名称模糊查询
export function queryDeviceByName(data: any) { export function queryProductByName(data: any) {
return axios({ return axios({
url: `/api/rest/product/fuzzy`, url: `/api/rest/product/fuzzy`,
method: 'get', method: 'get',

View File

@ -67,12 +67,19 @@ export function deleteProduct(id: number) {
return axios.delete(`/api/rest/product/${id}`); return axios.delete(`/api/rest/product/${id}`);
} }
// 文件上传 // 添加附件
export function addAttachments(data: any) { 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) { export function deleteAttachment(id: string) {
return axios.delete(`/api/rest/product/attachments/${id}`); return axios.delete(`/api/rest/attachment/delete/${id}`);
} }

View File

@ -88,3 +88,8 @@ export function deleteEvent(data: any) {
export function queryServeDetail(id: number) { export function queryServeDetail(id: number) {
return axios.get(`/api/rest/tsl/serve/${id}`); 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 fields = ref([]);
const fetchData = async (Id: number) => { const fetchData = async (Id: number) => {
try { try {
const [detailRes, recordRes, serveRes, eventRes] = await Promise.all([ // 1.
queryDeviceDetail(Id), const detailRes = await queryDeviceDetail(Id);
renderData.value = detailRes.data;
// 2. clientId productId
const [recordRes, serveRes, eventRes] = await Promise.all([
queryDeviceRecord({ queryDeviceRecord({
clientId: renderData.value.clientId, clientId: renderData.value.clientId,
size: 10, size: 10,
@ -290,7 +294,7 @@
}), }),
]); ]);
renderData.value = detailRes.data; // 3.
deviceReportData.value = recordRes.data.records; deviceReportData.value = recordRes.data.records;
deviceServerData.value = serveRes.data.records; deviceServerData.value = serveRes.data.records;
deviceEventData.value = eventRes.data.records; deviceEventData.value = eventRes.data.records;

View File

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

View File

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

View File

@ -1,6 +1,17 @@
<template> <template>
<a-layout> <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-row>
<a-col <a-col
:span="24" :span="24"
@ -8,9 +19,54 @@
:key="device.id" :key="device.id"
@click="selectDevice(device)" @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-col>
</a-row> </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-sider>
<a-layout-content> <a-layout-content>
<div id="container"></div> <div id="container"></div>
@ -21,17 +77,47 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import AMapLoader from '@amap/amap-jsapi-loader'; 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 '@amap/amap-jsapi-types';
import router from '@/router'; import router from '@/router';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { debounce } from '@arco-design/web-vue/es/_utils/debounce';
let map: any = null; let map: any = null;
const deviceList = ref<DeviceRecord[]>([]); const deviceList = ref<DeviceRecord[]>([]);
const searchKeyword = ref(''); //
const isLoading = ref(false); //
let selectDevice = (device: DeviceRecord) => { let selectDevice = (device: DeviceRecord) => {
console.log(device); 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({ AMapLoader.load({
key: 'a4e80eed798a56451b226dcfca81b846', // WebKey load key: 'a4e80eed798a56451b226dcfca81b846', // WebKey load
version: '2.0', // JSAPI 1.4.15 version: '2.0', // JSAPI 1.4.15
@ -100,12 +186,9 @@
// AMap then // AMap then
onMounted(() => { onMounted(() => {
// //
queryDeviceList({ queryDeviceByNameList({})
current: 1,
size: 10,
})
.then((response) => { .then((response) => {
deviceList.value = response.data.records; deviceList.value = response.data;
}) })
.catch(() => { .catch(() => {
Message.error('获取设备列表失败'); Message.error('获取设备列表失败');
@ -120,7 +203,7 @@
<style scoped> <style scoped>
#container { #container {
width: 100%; width: 100%;
height: 800px; height: 100vh;
} }
.custom-content-marker img { .custom-content-marker img {

View File

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

View File

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