feat(device): 增加设备详情和地图功能

- 在 Device 接口中添加了 hardwareVersion、firmwareVersion、productName 和 id 字段
- 修改了 sendCommand函数,增加了 deviceId 和 qos 参数- 更新了设备详情页面,添加了新的设备信息字段
- 在地图页面添加了设备列表和设备信息窗口
- 优化了表单组件,增加了 Qos等级选择和字段校验- 统一了删除按钮的样式
This commit is contained in:
Kven 2025-03-07 19:16:59 +08:00
parent 39fca49ea6
commit 085d0ae690
8 changed files with 142 additions and 46 deletions

View File

@ -13,6 +13,10 @@ export interface DeviceRecord {
longitude?: number; longitude?: number;
latitude?: number; latitude?: number;
icon?: string; icon?: string;
hardwareVersion?: string;
firmwareVersion?: string;
productName?: string;
id?: number;
} }
export interface DeviceCreateRecord { export interface DeviceCreateRecord {
@ -108,10 +112,15 @@ export function triggerEvent(data: DeviceEventRecord) {
// 下发命令 // 下发命令
export function sendCommand(data: any) { export function sendCommand(data: any) {
const { deviceId, qos, ...rest } = data;
return axios({ return axios({
url: `/api/rest/device/send`, url: `/api/rest/device/send`,
method: 'post', method: 'post',
data, params: {
deviceId,
qos,
},
data: rest,
}); });
} }

View File

@ -78,7 +78,7 @@
status="success" status="success"
@click="openServeForm(record)" @click="openServeForm(record)"
> >
执行 <icon-plus />执行
</a-button> </a-button>
</template> </template>
</a-table> </a-table>
@ -90,7 +90,7 @@
</a-card> </a-card>
<a-modal <a-modal
width="900px" width="1100px"
height="500px" height="500px"
:visible="visible" :visible="visible"
@cancel="handleCancel" @cancel="handleCancel"
@ -111,7 +111,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { onMounted, ref } from 'vue'; import { onMounted, reactive, 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 { queryServeDetail, queryServeList } from '@/api/tsl';
@ -174,6 +174,7 @@
]; ];
const activeKey = ref('1'); const activeKey = ref('1');
const renderData = ref({ const renderData = ref({
deviceId: 0,
clientId: 1, clientId: 1,
productId: 1 productId: 1
}); });
@ -200,6 +201,7 @@
activeKey.value = e; activeKey.value = e;
}; };
const dynamicFormData = ref(); const dynamicFormData = ref();
const formRef = ref();
// //
const handleCancel = async () => { const handleCancel = async () => {
setVisible(false); setVisible(false);
@ -211,21 +213,35 @@
fields.value = res.data.inputs; fields.value = res.data.inputs;
}; };
const handleFormDataUpdate = (newFormData) => { const handleFormDataUpdate = (newFormData: any) => {
dynamicFormData.value = newFormData; // formData dynamicFormData.value = newFormData; // formData
}; };
// //
const handleSubmit = async () => { const handleSubmit = async () => {
const res = await sendCommand(dynamicFormData.value); try {
if (res.status === 200) { const { qos, ...rest } = dynamicFormData.value;
Message.success({ const params = {
content: '执行成功', deviceId: id,
duration: 5 * 1000, qos,
}); paras: rest,
setVisible(false); };
} else { console.log(params);
const res = await sendCommand(params);
if (res.status === 200) {
Message.success({
content: '执行成功',
duration: 5 * 1000,
});
setVisible(false);
} else {
Message.error({
content: '执行失败',
duration: 5 * 1000,
});
}
} catch (error) {
Message.error({ Message.error({
content: '执行失败', content: '表单不能为空',
duration: 5 * 1000, duration: 5 * 1000,
}); });
} }

View File

@ -122,7 +122,7 @@
style="width: 140px" style="width: 140px"
/> />
<a-button type="text" @click="handleDeleteParams(index)" <a-button type="text" @click="handleDeleteParams(index)"
><icon-minus-circle ><icon-minus-circle style="color: red;"
/></a-button> /></a-button>
</a-space> </a-space>
</div> </div>

View File

@ -1,10 +1,22 @@
<template> <template>
<a-form <a-form
:model="formData" :model="formData"
:style="{ width: '800px' }" ref="formRef"
:style="{ width: '1000px'}"
> >
<a-form-item label="Qos等级" :rules="rules.qos">
<a-select
v-model="formData.qos"
placeholder="Qos等级"
:options="[
{ value: 0, label: '最多一次' },
{ value: 1, label: '至少一次' },
{ value: 2, label: '仅一次' },
]"
/>
</a-form-item>
<template v-for="(field, index) in fields" :key="index"> <template v-for="(field, index) in fields" :key="index">
<a-form-item :label="field.name"> <a-form-item :label="field.name" :rules="rules[field.identifier]">
<a-input <a-input
v-model="formData[field.identifier]" v-model="formData[field.identifier]"
:placeholder="field.name" :placeholder="field.name"
@ -26,13 +38,25 @@
const formData = ref({}); const formData = ref({});
const emit = defineEmits(['update:formData']); const emit = defineEmits(['update:formData']);
const formRef = ref(null); // formRef
//
const rules = {
qos: [{ required: true, message: '请选择 Qos 等级', trigger: 'change' }],
...props.fields.reduce((acc, field) => {
acc[field.identifier] = [
{ required: true, message: `${field.name}不能为空`, trigger: 'blur' },
];
return acc;
}, {}),
};
// formData // formData
watch(formData, (newValue) => { watch(formData, (newValue) => {
emit('update:formData', newValue); emit('update:formData', newValue);
}, { deep: true }); }, { deep: true });
</script> </script>
<style scoped> <style scoped>
/* ... existing code ... */ /* ... existing code ... */
</style> </style>

View File

@ -140,7 +140,7 @@
style="width: 140px" style="width: 140px"
/> />
<a-button type="text" @click="handleDeleteParams(index)" <a-button type="text" @click="handleDeleteParams(index)"
><icon-minus-circle ><icon-minus-circle style="color: red;"
/></a-button> /></a-button>
</a-space> </a-space>
</div> </div>

View File

@ -2,6 +2,9 @@
<a-layout> <a-layout>
<a-layout-sider :resize-directions="['right']"> <a-layout-sider :resize-directions="['right']">
<a-row> <a-row>
<a-col :span="24" v-for="device in deviceList" :key="device.id" @click="selectDevice(device)">
{{ device.name }}
</a-col>
</a-row> </a-row>
</a-layout-sider> </a-layout-sider>
<a-layout-content> <a-layout-content>
@ -11,13 +14,18 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, 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, queryDeviceList } from '@/api/device';
import "@amap/amap-jsapi-types"; import "@amap/amap-jsapi-types";
import router from '@/router';
let map: any = null; let map: any = null;
const deviceList = ref<DeviceRecord[]>([]); const deviceList = ref<DeviceRecord[]>([]);
let selectDevice = (device: DeviceRecord) => {
console.log(device);
}
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
@ -30,32 +38,71 @@
zoom: 11, // zoom: 11, //
center: [116.397428, 39.90923], // center: [116.397428, 39.90923], //
}); });
map.on('click', (e: any) => {
// selectDevice = (device: DeviceRecord) => {
const lngLat = e.lnglat; if (device.longitude && device.latitude) {
const content = document.createElement('div'); const lngLat = [device.longitude, device.latitude];
content.className = 'custom-content-marker'; const content = document.createElement('div');
content.innerHTML = ` content.className = 'custom-content-marker';
<img src="https://webapi.amap.com/theme/v1.3/markers/n/mark_b.png" alt="标记点" /> content.innerHTML = `
<div class="close-btn">×</div>`; <img src="https://webapi.amap.com/theme/v1.3/markers/n/mark_b.png" alt="标记点" />`;
//
const marker = new AMap.Marker({ //
position: lngLat, const marker = new AMap.Marker({
content, position: lngLat,
}); content,
// });
map.add(marker);
// //
const closeBtn = marker.getContent().querySelector('.close-btn'); map.setCenter(lngLat);
closeBtn.addEventListener('click', () => { map.setZoom(16);
map.remove(marker); // 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>设备名称: ${device.name}</b>`,
`<span style="font-size: 16px; color: #333;">硬件版本: ${device.hardwareVersion}</span>`,
`<span style="font-size: 16px; color: #333;">固件版本: ${device.firmwareVersion}</span>`,
`<span style="font-size: 16px; color: #333;">所属产品: ${device.productName}</span>`,
`<a-button style="margin-left: 200px;color: #0960bd" onclick="handleViewDetail('${device.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('设备缺少经纬度信息', device);
}
};
}) })
.catch((e) => { .catch((e) => {
console.log(e); console.log(e);
}); });
// AMap then
onMounted(() => { onMounted(() => {
// //
queryDeviceList({ queryDeviceList({

View File

@ -107,7 +107,7 @@
style="width: 140px" style="width: 140px"
/> />
<a-button type="text" @click="handleDeleteParams(index)" <a-button type="text" @click="handleDeleteParams(index)"
><icon-minus-circle ><icon-minus-circle style="color: red;"
/></a-button> /></a-button>
</a-space> </a-space>
</div> </div>

View File

@ -235,7 +235,7 @@
<a-button <a-button
type="text" type="text"
@click="handleDeleteParams(index, 'serveInputData')" @click="handleDeleteParams(index, 'serveInputData')"
><icon-minus-circle ><icon-minus-circle style="color: red;"
/></a-button> /></a-button>
</a-space> </a-space>
</div> </div>
@ -280,7 +280,7 @@
<a-button <a-button
type="text" type="text"
@click="handleDeleteParams(index, 'serveOutputData')" @click="handleDeleteParams(index, 'serveOutputData')"
><icon-minus-circle ><icon-minus-circle style="color: red;"
/></a-button> /></a-button>
</a-space> </a-space>
</div> </div>
@ -385,7 +385,7 @@
<a-button <a-button
type="text" type="text"
@click="handleDeleteParams(index, 'eventOutputData')" @click="handleDeleteParams(index, 'eventOutputData')"
><icon-minus-circle ><icon-minus-circle style="color: red;"
/></a-button> /></a-button>
</a-space> </a-space>
</div> </div>