This commit is contained in:
xingyu4j
2025-10-10 21:44:08 +08:00
parent 00a25ed1d3
commit 691f9aa764
24 changed files with 689 additions and 732 deletions

View File

@@ -92,5 +92,3 @@ export function getSimpleAlertConfigList() {
'/iot/alert-config/simple-list', '/iot/alert-config/simple-list',
); );
} }
export { AlertConfigApi };

View File

@@ -80,5 +80,3 @@ export function deleteAlertRecordList(ids: number[]) {
params: { ids: ids.join(',') }, params: { ids: ids.join(',') },
}); });
} }
export { AlertRecordApi };

View File

@@ -194,30 +194,3 @@ export function getDeviceMessagePairPage(params: PageParam) {
export function sendDeviceMessage(params: IotDeviceApi.DeviceMessageSendReq) { export function sendDeviceMessage(params: IotDeviceApi.DeviceMessageSendReq) {
return requestClient.post('/iot/device/message/send', params); return requestClient.post('/iot/device/message/send', params);
} }
// Export aliases for compatibility
export const DeviceApi = {
getDevicePage,
getDevice,
createDevice,
updateDevice,
updateDeviceGroup,
deleteDevice,
deleteDeviceList,
exportDeviceExcel,
getDeviceCount,
getSimpleDeviceList,
getDeviceListByProductId,
importDeviceTemplate,
getLatestDeviceProperties,
getHistoryDevicePropertyList,
getDeviceAuthInfo,
getDeviceMessagePage,
getDeviceMessagePairPage,
sendDeviceMessage,
};
export type DeviceVO = IotDeviceApi.Device;
export type IotDeviceAuthInfoVO = IotDeviceApi.DeviceAuthInfo;
export type IotDevicePropertyDetailRespVO = IotDeviceApi.DevicePropertyDetail;
export type IotDevicePropertyRespVO = IotDeviceApi.DeviceProperty;

View File

@@ -96,5 +96,3 @@ export function pauseOtaTask(id: number) {
export function resumeOtaTask(id: number) { export function resumeOtaTask(id: number) {
return requestClient.put(`/iot/ota-task/resume?id=${id}`); return requestClient.put(`/iot/ota-task/resume?id=${id}`);
} }
export { IoTOtaTaskApi };

View File

@@ -99,5 +99,3 @@ export function getOtaTaskRecordStatusStatistics(
{ params: { firmwareId, taskId } }, { params: { firmwareId, taskId } },
); );
} }
export { IoTOtaTaskRecordApi };

View File

@@ -97,18 +97,3 @@ export function getProductByKey(productKey: string) {
params: { productKey }, params: { productKey },
}); });
} }
// Export aliases for compatibility
export const ProductApi = {
getProductPage,
getProduct,
createProduct,
updateProduct,
deleteProduct,
exportProduct,
updateProductStatus,
getSimpleProductList,
getProductByKey,
};
export type ProductVO = IotProductApi.Product;

View File

@@ -146,5 +146,3 @@ export function updateDataSinkStatus(id: number, status: number) {
status, status,
}); });
} }
export { DataSinkApi };

View File

@@ -153,10 +153,3 @@ export function getSimpleRuleSceneList() {
'/iot/scene-rule/simple-list', '/iot/scene-rule/simple-list',
); );
} }
// 别名导出(兼容旧代码)
export {
deleteSceneRule as deleteRuleScene,
getSceneRulePage as getRuleScenePage,
updateSceneRuleStatus as updateRuleSceneStatus,
};

View File

@@ -67,18 +67,3 @@ export function getDeviceMessageSummary(statType: number) {
{ params: { statType } }, { params: { statType } },
); );
} }
// 导出 API 对象(兼容旧代码)
export const StatisticsApi = {
getStatisticsSummary,
getDeviceMessageSummaryByDate,
getDeviceMessageSummary,
};
// 导出类型别名(兼容旧代码)
export type IotStatisticsSummaryRespVO = IotStatisticsApi.StatisticsSummary;
export type IotStatisticsDeviceMessageSummaryRespVO =
IotStatisticsApi.DeviceMessageSummary;
export type IotStatisticsDeviceMessageSummaryByDateRespVO =
IotStatisticsApi.DeviceMessageSummaryByDate;
export type IotStatisticsDeviceMessageReqVO = IotStatisticsApi.DeviceMessageReq;

View File

@@ -189,18 +189,3 @@ export function exportThingModelTSL(productId: number) {
params: { productId }, params: { productId },
}); });
} }
// Add a consolidated API object and getThingModelList alias
export const ThingModelApi = {
getThingModelPage,
getThingModel,
getThingModelList: getThingModelListByProductId, // alias for compatibility
getThingModelListByProductId,
getThingModelListByProductKey,
createThingModel,
updateThingModel,
deleteThingModel,
deleteThingModelList,
importThingModelTSL,
exportThingModelTSL,
};

View File

@@ -4,7 +4,7 @@ import type { AlertConfigApi } from '#/api/iot/alert/config';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue'; import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteAlertConfig, getAlertConfigPage } from '#/api/iot/alert/config'; import { deleteAlertConfig, getAlertConfigPage } from '#/api/iot/alert/config';
@@ -26,7 +26,7 @@ function onRefresh() {
} }
// 获取告警级别文本 // 获取告警级别文本
const getLevelText = (level?: number) => { function getLevelText(level?: number) {
const levelMap: Record<number, string> = { const levelMap: Record<number, string> = {
1: '提示', 1: '提示',
2: '一般', 2: '一般',
@@ -35,10 +35,10 @@ const getLevelText = (level?: number) => {
5: '紧急', 5: '紧急',
}; };
return level ? levelMap[level] || `级别${level}` : '-'; return level ? levelMap[level] || `级别${level}` : '-';
}; }
// 获取告警级别颜色 // 获取告警级别颜色
const getLevelColor = (level?: number) => { function getLevelColor(level?: number) {
const colorMap: Record<number, string> = { const colorMap: Record<number, string> = {
1: 'blue', 1: 'blue',
2: 'green', 2: 'green',
@@ -47,10 +47,10 @@ const getLevelColor = (level?: number) => {
5: 'purple', 5: 'purple',
}; };
return level ? colorMap[level] || 'default' : 'default'; return level ? colorMap[level] || 'default' : 'default';
}; }
// 获取接收类型文本 // 获取接收类型文本
const getReceiveTypeText = (type?: number) => { function getReceiveTypeText(type?: number) {
const typeMap: Record<number, string> = { const typeMap: Record<number, string> = {
1: '站内信', 1: '站内信',
2: '邮箱', 2: '邮箱',
@@ -59,7 +59,7 @@ const getReceiveTypeText = (type?: number) => {
5: '钉钉', 5: '钉钉',
}; };
return type ? typeMap[type] || `类型${type}` : '-'; return type ? typeMap[type] || `类型${type}` : '-';
}; }
/** 创建告警配置 */ /** 创建告警配置 */
function handleCreate() { function handleCreate() {
@@ -138,9 +138,9 @@ const [Grid, gridApi] = useVbenVxeGrid({
<!-- 告警级别列 --> <!-- 告警级别列 -->
<template #level="{ row }"> <template #level="{ row }">
<a-tag :color="getLevelColor(row.level)"> <Tag :color="getLevelColor(row.level)">
{{ getLevelText(row.level) }} {{ getLevelText(row.level) }}
</a-tag> </Tag>
</template> </template>
<!-- 关联场景联动规则列 --> <!-- 关联场景联动规则列 -->
@@ -150,13 +150,13 @@ const [Grid, gridApi] = useVbenVxeGrid({
<!-- 接收类型列 --> <!-- 接收类型列 -->
<template #receiveTypes="{ row }"> <template #receiveTypes="{ row }">
<a-tag <Tag
v-for="(type, index) in row.receiveTypes" v-for="(type, index) in row.receiveTypes"
:key="index" :key="index"
class="mr-1" class="mr-1"
> >
{{ getReceiveTypeText(type) }} {{ getReceiveTypeText(type) }}
</a-tag> </Tag>
</template> </template>
<!-- 操作列 --> <!-- 操作列 -->

View File

@@ -5,8 +5,9 @@ import type { AlertRecord } from '#/api/iot/alert/record';
import { h, onMounted, ref } from 'vue'; import { h, onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { message, Modal } from 'ant-design-vue'; import { Button, message, Modal, Popover, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getAlertRecordPage, processAlertRecord } from '#/api/iot/alert/record'; import { getAlertRecordPage, processAlertRecord } from '#/api/iot/alert/record';
@@ -26,13 +27,13 @@ function onRefresh() {
} }
// 加载产品和设备列表 // 加载产品和设备列表
const loadData = async () => { async function loadData() {
productList.value = await getSimpleProductList(); productList.value = await getSimpleProductList();
deviceList.value = await getSimpleDeviceList(); deviceList.value = await getSimpleDeviceList();
}; }
// 获取告警级别文本 // 获取告警级别文本
const getLevelText = (level?: number) => { function getLevelText(level?: number) {
const levelMap: Record<number, string> = { const levelMap: Record<number, string> = {
1: '提示', 1: '提示',
2: '一般', 2: '一般',
@@ -41,10 +42,10 @@ const getLevelText = (level?: number) => {
5: '紧急', 5: '紧急',
}; };
return level ? levelMap[level] || `级别${level}` : '-'; return level ? levelMap[level] || `级别${level}` : '-';
}; }
// 获取告警级别颜色 // 获取告警级别颜色
const getLevelColor = (level?: number) => { function getLevelColor(level?: number) {
const colorMap: Record<number, string> = { const colorMap: Record<number, string> = {
1: 'blue', 1: 'blue',
2: 'green', 2: 'green',
@@ -53,24 +54,24 @@ const getLevelColor = (level?: number) => {
5: 'purple', 5: 'purple',
}; };
return level ? colorMap[level] || 'default' : 'default'; return level ? colorMap[level] || 'default' : 'default';
}; }
// 获取产品名称 // 获取产品名称
const getProductName = (productId?: number) => { function getProductName(productId?: number) {
if (!productId) return '-'; if (!productId) return '-';
const product = productList.value.find((p: any) => p.id === productId); const product = productList.value.find((p: any) => p.id === productId);
return product?.name || '加载中...'; return product?.name || '加载中...';
}; }
// 获取设备名称 // 获取设备名称
const getDeviceName = (deviceId?: number) => { function getDeviceName(deviceId?: number) {
if (!deviceId) return '-'; if (!deviceId) return '-';
const device = deviceList.value.find((d: any) => d.id === deviceId); const device = deviceList.value.find((d: any) => d.id === deviceId);
return device?.deviceName || '加载中...'; return device?.deviceName || '加载中...';
}; }
// 处理告警记录 // 处理告警记录
const handleProcess = async (row: AlertRecord) => { async function handleProcess(row: AlertRecord) {
Modal.confirm({ Modal.confirm({
title: '处理告警记录', title: '处理告警记录',
content: h('div', [ content: h('div', [
@@ -90,7 +91,7 @@ const handleProcess = async (row: AlertRecord) => {
if (!processRemark) { if (!processRemark) {
message.warning('请输入处理原因'); message.warning('请输入处理原因');
throw undefined; throw new Error('请输入处理原因');
} }
const hideLoading = message.loading({ const hideLoading = message.loading({
@@ -103,16 +104,16 @@ const handleProcess = async (row: AlertRecord) => {
onRefresh(); onRefresh();
} catch (error) { } catch (error) {
console.error('处理失败:', error); console.error('处理失败:', error);
return Promise.reject(); throw error;
} finally { } finally {
hideLoading(); hideLoading();
} }
}, },
}); });
}; }
// 查看告警记录详情 // 查看告警记录详情
const handleView = (row: AlertRecord) => { function handleView(row: AlertRecord) {
Modal.info({ Modal.info({
title: '告警记录详情', title: '告警记录详情',
width: 600, width: 600,
@@ -148,7 +149,7 @@ const handleView = (row: AlertRecord) => {
]), ]),
]), ]),
}); });
}; }
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
formOptions: { formOptions: {
@@ -190,9 +191,9 @@ onMounted(() => {
<Grid table-title="告警记录列表"> <Grid table-title="告警记录列表">
<!-- 告警级别列 --> <!-- 告警级别列 -->
<template #configLevel="{ row }"> <template #configLevel="{ row }">
<a-tag :color="getLevelColor(row.configLevel)"> <Tag :color="getLevelColor(row.configLevel)">
{{ getLevelText(row.configLevel) }} {{ getLevelText(row.configLevel) }}
</a-tag> </Tag>
</template> </template>
<!-- 产品名称列 --> <!-- 产品名称列 -->
@@ -207,7 +208,7 @@ onMounted(() => {
<!-- 设备消息列 --> <!-- 设备消息列 -->
<template #deviceMessage="{ row }"> <template #deviceMessage="{ row }">
<a-popover <Popover
v-if="row.deviceMessage" v-if="row.deviceMessage"
placement="topLeft" placement="topLeft"
trigger="hover" trigger="hover"
@@ -216,11 +217,11 @@ onMounted(() => {
<template #content> <template #content>
<pre class="text-xs">{{ row.deviceMessage }}</pre> <pre class="text-xs">{{ row.deviceMessage }}</pre>
</template> </template>
<VbenButton size="small" type="link"> <Button size="small" type="link">
<Icon icon="ant-design:eye-outlined" class="mr-1" /> <IconifyIcon icon="ant-design:eye-outlined" class="mr-1" />
查看消息 查看消息
</VbenButton> </Button>
</a-popover> </Popover>
<span v-else class="text-gray-400">-</span> <span v-else class="text-gray-400">-</span>
</template> </template>

View File

@@ -6,15 +6,29 @@ import type { IotProductApi } from '#/api/iot/product/product';
import { computed, onMounted, reactive, ref } from 'vue'; import { computed, onMounted, reactive, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants'; import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks'; import { getDictOptions } from '@vben/hooks';
import { IconifyIcon } from '@vben/icons';
import { formatDate } from '@vben/utils'; import { formatDate } from '@vben/utils';
import { message } from 'ant-design-vue'; import {
Button,
Form,
Input,
message,
Modal,
Pagination,
Radio,
Select,
Table,
Tag,
} from 'ant-design-vue';
import { getDevicePage } from '#/api/iot/device/device'; import { getDevicePage } from '#/api/iot/device/device';
import { getSimpleDeviceGroupList } from '#/api/iot/device/group'; import { getSimpleDeviceGroupList } from '#/api/iot/device/group';
import { getSimpleProductList } from '#/api/iot/product/product'; import { getSimpleProductList } from '#/api/iot/product/product';
import { DictTag } from '#/components/dict-tag';
defineOptions({ name: 'IoTDeviceTableSelect' }); defineOptions({ name: 'IoTDeviceTableSelect' });
@@ -33,14 +47,14 @@ const props = defineProps({
const emit = defineEmits(['success']); const emit = defineEmits(['success']);
// 获取字典选项 // 获取字典选项
const getIntDictOptions = (dictType: string) => { function getIntDictOptions(dictType: string) {
return getDictOptions(dictType, 'number'); return getDictOptions(dictType, 'number');
}; }
// 日期格式化 // 日期格式化
const dateFormatter = (_row: any, _column: any, cellValue: any) => { function dateFormatter(_row: any, _column: any, cellValue: any) {
return cellValue ? formatDate(cellValue, 'YYYY-MM-DD HH:mm:ss') : ''; return cellValue ? formatDate(cellValue, 'YYYY-MM-DD HH:mm:ss') : '';
}; }
const dialogVisible = ref(false); const dialogVisible = ref(false);
const dialogTitle = ref('设备选择器'); const dialogTitle = ref('设备选择器');
@@ -73,38 +87,31 @@ const columns = computed(() => {
title: 'DeviceName', title: 'DeviceName',
dataIndex: 'deviceName', dataIndex: 'deviceName',
key: 'deviceName', key: 'deviceName',
align: 'center',
}, },
{ {
title: '备注名称', title: '备注名称',
dataIndex: 'nickname', dataIndex: 'nickname',
key: 'nickname', key: 'nickname',
align: 'center',
}, },
{ {
title: '所属产品', title: '所属产品',
key: 'productId', key: 'productId',
align: 'center',
}, },
{ {
title: '设备类型', title: '设备类型',
key: 'deviceType', key: 'deviceType',
align: 'center',
}, },
{ {
title: '所属分组', title: '所属分组',
key: 'groupIds', key: 'groupIds',
align: 'center',
}, },
{ {
title: '设备状态', title: '设备状态',
key: 'status', key: 'status',
align: 'center',
}, },
{ {
title: '最后上线时间', title: '最后上线时间',
key: 'onlineTime', key: 'onlineTime',
align: 'center',
width: 180, width: 180,
}, },
]; ];
@@ -125,7 +132,7 @@ const columns = computed(() => {
// 多选配置 // 多选配置
const rowSelection = computed(() => ({ const rowSelection = computed(() => ({
selectedRowKeys: selectedRowKeys.value, selectedRowKeys: selectedRowKeys.value,
onChange: (keys: number[], rows: IotDeviceApi.Device[]) => { onChange: (keys: any[], rows: IotDeviceApi.Device[]) => {
selectedRowKeys.value = keys; selectedRowKeys.value = keys;
selectedDevices.value = rows; selectedDevices.value = rows;
}, },
@@ -176,20 +183,20 @@ defineExpose({ open });
/** 处理行点击事件 */ /** 处理行点击事件 */
const tableRef = ref(); const tableRef = ref();
const handleRowClick = (row: IotDeviceApi.Device) => { function handleRowClick(row: IotDeviceApi.Device) {
if (!props.multiple) { if (!props.multiple) {
selectedId.value = row.id; selectedId.value = row.id;
selectedDevices.value = [row]; selectedDevices.value = [row];
} }
}; }
/** 处理单选变更事件 */ /** 处理单选变更事件 */
const handleRadioChange = (row: IotDeviceApi.Device) => { function handleRadioChange(row: IotDeviceApi.Device) {
selectedId.value = row.id; selectedId.value = row.id;
selectedDevices.value = [row]; selectedDevices.value = [row];
}; }
const submitForm = async () => { async function submitForm() {
if (selectedDevices.value.length === 0) { if (selectedDevices.value.length === 0) {
message.warning({ message.warning({
content: props.multiple ? '请至少选择一个设备' : '请选择一个设备', content: props.multiple ? '请至少选择一个设备' : '请选择一个设备',
@@ -201,7 +208,7 @@ const submitForm = async () => {
props.multiple ? selectedDevices.value : selectedDevices.value[0], props.multiple ? selectedDevices.value : selectedDevices.value[0],
); );
dialogVisible.value = false; dialogVisible.value = false;
}; }
/** 初始化 */ /** 初始化 */
onMounted(async () => { onMounted(async () => {
@@ -213,7 +220,7 @@ onMounted(async () => {
</script> </script>
<template> <template>
<a-modal <Modal
:title="dialogTitle" :title="dialogTitle"
v-model:open="dialogVisible" v-model:open="dialogVisible"
width="60%" width="60%"
@@ -221,54 +228,54 @@ onMounted(async () => {
> >
<ContentWrap> <ContentWrap>
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<a-form <Form
ref="queryFormRef" ref="queryFormRef"
layout="inline" layout="inline"
:model="queryParams" :model="queryParams"
class="-mb-15px" class="-mb-15px"
> >
<a-form-item v-if="!props.productId" label="产品" name="productId"> <Form.Item v-if="!props.productId" label="产品" name="productId">
<a-select <Select
v-model:value="queryParams.productId" v-model:value="queryParams.productId"
placeholder="请选择产品" placeholder="请选择产品"
allow-clear allow-clear
style="width: 240px" style="width: 240px"
> >
<a-select-option <Select.Option
v-for="product in products" v-for="product in products"
:key="product.id" :key="product.id"
:value="product.id" :value="product.id"
> >
{{ product.name }} {{ product.name }}
</a-select-option> </Select.Option>
</a-select> </Select>
</a-form-item> </Form.Item>
<a-form-item label="DeviceName" name="deviceName"> <Form.Item label="DeviceName" name="deviceName">
<a-input <Input
v-model:value="queryParams.deviceName" v-model:value="queryParams.deviceName"
placeholder="请输入 DeviceName" placeholder="请输入 DeviceName"
allow-clear allow-clear
@press-enter="handleQuery" @press-enter="handleQuery"
style="width: 240px" style="width: 240px"
/> />
</a-form-item> </Form.Item>
<a-form-item label="备注名称" name="nickname"> <Form.Item label="备注名称" name="nickname">
<a-input <Input
v-model:value="queryParams.nickname" v-model:value="queryParams.nickname"
placeholder="请输入备注名称" placeholder="请输入备注名称"
allow-clear allow-clear
@press-enter="handleQuery" @press-enter="handleQuery"
style="width: 240px" style="width: 240px"
/> />
</a-form-item> </Form.Item>
<a-form-item label="设备类型" name="deviceType"> <Form.Item label="设备类型" name="deviceType">
<a-select <Select
v-model:value="queryParams.deviceType" v-model:value="queryParams.deviceType"
placeholder="请选择设备类型" placeholder="请选择设备类型"
allow-clear allow-clear
style="width: 240px" style="width: 240px"
> >
<a-select-option <Select.Option
v-for="dict in getIntDictOptions( v-for="dict in getIntDictOptions(
DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE, DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE,
)" )"
@@ -276,57 +283,57 @@ onMounted(async () => {
:value="dict.value" :value="dict.value"
> >
{{ dict.label }} {{ dict.label }}
</a-select-option> </Select.Option>
</a-select> </Select>
</a-form-item> </Form.Item>
<a-form-item label="设备状态" name="status"> <Form.Item label="设备状态" name="status">
<a-select <Select
v-model:value="queryParams.status" v-model:value="queryParams.status"
placeholder="请选择设备状态" placeholder="请选择设备状态"
allow-clear allow-clear
style="width: 240px" style="width: 240px"
> >
<a-select-option <Select.Option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DEVICE_STATUS)" v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DEVICE_STATUS)"
:key="dict.value" :key="dict.value"
:value="dict.value" :value="dict.value"
> >
{{ dict.label }} {{ dict.label }}
</a-select-option> </Select.Option>
</a-select> </Select>
</a-form-item> </Form.Item>
<a-form-item label="设备分组" name="groupId"> <Form.Item label="设备分组" name="groupId">
<a-select <Select
v-model:value="queryParams.groupId" v-model:value="queryParams.groupId"
placeholder="请选择设备分组" placeholder="请选择设备分组"
allow-clear allow-clear
style="width: 240px" style="width: 240px"
> >
<a-select-option <Select.Option
v-for="group in deviceGroups" v-for="group in deviceGroups"
:key="group.id" :key="group.id"
:value="group.id" :value="group.id"
> >
{{ group.name }} {{ group.name }}
</a-select-option> </Select.Option>
</a-select> </Select>
</a-form-item> </Form.Item>
<a-form-item> <Form.Item>
<a-button @click="handleQuery"> <Button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" /> <IconifyIcon class="mr-5px" icon="ep:search" />
搜索 搜索
</a-button> </Button>
<a-button @click="resetQuery"> <Button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" /> <IconifyIcon class="mr-5px" icon="ep:refresh" />
重置 重置
</a-button> </Button>
</a-form-item> </Form.Item>
</a-form> </Form>
</ContentWrap> </ContentWrap>
<!-- 列表 --> <!-- 列表 -->
<ContentWrap> <ContentWrap>
<a-table <Table
ref="tableRef" ref="tableRef"
:loading="loading" :loading="loading"
:data-source="list" :data-source="list"
@@ -334,38 +341,38 @@ onMounted(async () => {
:pagination="false" :pagination="false"
:row-selection="multiple ? rowSelection : undefined" :row-selection="multiple ? rowSelection : undefined"
@row-click="handleRowClick" @row-click="handleRowClick"
:row-key="(record: IotDeviceApi.Device) => record.id" :row-key="(record: IotDeviceApi.Device) => record.id?.toString() ?? ''"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'radio'"> <template v-if="column.key === 'radio'">
<a-radio <Radio
:checked="selectedId === record.id" :checked="selectedId === record.id"
@click="() => handleRadioChange(record)" @click="() => handleRadioChange(record as IotDeviceApi.Device)"
/> />
</template> </template>
<template v-else-if="column.key === 'productId'"> <template v-else-if="column.key === 'productId'">
{{ products.find((p) => p.id === record.productId)?.name || '-' }} {{ products.find((p) => p.id === record.productId)?.name || '-' }}
</template> </template>
<template v-else-if="column.key === 'deviceType'"> <template v-else-if="column.key === 'deviceType'">
<dict-tag <DictTag
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
:value="record.deviceType" :value="record.deviceType"
/> />
</template> </template>
<template v-else-if="column.key === 'groupIds'"> <template v-else-if="column.key === 'groupIds'">
<template v-if="record.groupIds?.length"> <template v-if="record.groupIds?.length">
<a-tag <Tag
v-for="id in record.groupIds" v-for="id in record.groupIds"
:key="id" :key="id"
class="ml-5px" class="ml-5px"
size="small" size="small"
> >
{{ deviceGroups.find((g) => g.id === id)?.name }} {{ deviceGroups.find((g) => g.id === id)?.name }}
</a-tag> </Tag>
</template> </template>
</template> </template>
<template v-else-if="column.key === 'status'"> <template v-else-if="column.key === 'status'">
<dict-tag <DictTag
:type="DICT_TYPE.IOT_DEVICE_STATUS" :type="DICT_TYPE.IOT_DEVICE_STATUS"
:value="record.status" :value="record.status"
/> />
@@ -374,7 +381,7 @@ onMounted(async () => {
{{ dateFormatter(null, null, record.onlineTime) }} {{ dateFormatter(null, null, record.onlineTime) }}
</template> </template>
</template> </template>
</a-table> </Table>
<!-- 分页 --> <!-- 分页 -->
<Pagination <Pagination
@@ -386,10 +393,10 @@ onMounted(async () => {
</ContentWrap> </ContentWrap>
<template #footer> <template #footer>
<a-button @click="submitForm" type="primary" :disabled="formLoading"> <Button @click="submitForm" type="primary" :disabled="formLoading">
确 定 确 定
</a-button> </Button>
<a-button @click="dialogVisible = false"> </a-button> <Button @click="dialogVisible = false"> </Button>
</template> </template>
</a-modal> </Modal>
</template> </template>

View File

@@ -1,18 +1,18 @@
<!-- 设备配置 --> <!-- 设备配置 -->
<script lang="ts" setup> <script lang="ts" setup>
import type { DeviceVO } from '#/api/iot/device/device'; import type { IotDeviceApi } from '#/api/iot/device/device';
import { ref, watchEffect } from 'vue'; import { ref, watchEffect } from 'vue';
import { message } from 'ant-design-vue'; import { Alert, Button, message } from 'ant-design-vue';
import { DeviceApi } from '#/api/iot/device/device'; import { sendDeviceMessage, updateDevice } from '#/api/iot/device/device';
import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants'; import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants';
defineOptions({ name: 'DeviceDetailConfig' }); defineOptions({ name: 'DeviceDetailConfig' });
const props = defineProps<{ const props = defineProps<{
device: DeviceVO; device: IotDeviceApi.Device;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@@ -35,13 +35,13 @@ watchEffect(() => {
const isEditing = ref(false); // 编辑状态 const isEditing = ref(false); // 编辑状态
/** 启用编辑模式的函数 */ /** 启用编辑模式的函数 */
const enableEdit = () => { function enableEdit() {
isEditing.value = true; isEditing.value = true;
hasJsonError.value = false; // 重置错误状态 hasJsonError.value = false; // 重置错误状态
}; }
/** 取消编辑的函数 */ /** 取消编辑的函数 */
const cancelEdit = () => { function cancelEdit() {
try { try {
config.value = props.device.config ? JSON.parse(props.device.config) : {}; config.value = props.device.config ? JSON.parse(props.device.config) : {};
} catch { } catch {
@@ -49,25 +49,25 @@ const cancelEdit = () => {
} }
isEditing.value = false; isEditing.value = false;
hasJsonError.value = false; // 重置错误状态 hasJsonError.value = false; // 重置错误状态
}; }
/** 保存配置的函数 */ /** 保存配置的函数 */
const saveConfig = async () => { async function saveConfig() {
if (hasJsonError.value) { if (hasJsonError.value) {
message.error({ content: 'JSON格式错误请修正后再提交' }); message.error({ content: 'JSON格式错误请修正后再提交' });
return; return;
} }
await updateDeviceConfig(); await updateDeviceConfig();
isEditing.value = false; isEditing.value = false;
}; }
/** 配置推送处理函数 */ /** 配置推送处理函数 */
const handleConfigPush = async () => { async function handleConfigPush() {
try { try {
pushLoading.value = true; pushLoading.value = true;
// 调用配置推送接口 // 调用配置推送接口
await DeviceApi.sendDeviceMessage({ await sendDeviceMessage({
deviceId: props.device.id!, deviceId: props.device.id!,
method: IotDeviceMessageMethodEnum.CONFIG_PUSH.method, method: IotDeviceMessageMethodEnum.CONFIG_PUSH.method,
params: config.value, params: config.value,
@@ -82,17 +82,17 @@ const handleConfigPush = async () => {
} finally { } finally {
pushLoading.value = false; pushLoading.value = false;
} }
}; }
/** 更新设备配置 */ /** 更新设备配置 */
const updateDeviceConfig = async () => { async function updateDeviceConfig() {
try { try {
// 提交请求 // 提交请求
loading.value = true; loading.value = true;
await DeviceApi.updateDevice({ await updateDevice({
id: props.device.id, id: props.device.id,
config: JSON.stringify(config.value), config: JSON.stringify(config.value),
} as DeviceVO); } as IotDeviceApi.Device);
message.success({ content: '更新成功!' }); message.success({ content: '更新成功!' });
// 触发 success 事件 // 触发 success 事件
emit('success'); emit('success');
@@ -101,21 +101,21 @@ const updateDeviceConfig = async () => {
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; }
/** 处理 JSON 编辑器错误的函数 */ /** 处理 JSON 编辑器错误的函数 */
const onError = (errors: any) => { function onError(errors: any) {
if (!errors || (Array.isArray(errors) && errors.length === 0)) { if (!errors || (Array.isArray(errors) && errors.length === 0)) {
hasJsonError.value = false; hasJsonError.value = false;
return; return;
} }
hasJsonError.value = true; hasJsonError.value = true;
}; }
</script> </script>
<template> <template>
<div> <div>
<a-alert <Alert
message="支持远程更新设备的配置文件(JSON 格式),可以在下方编辑配置模板,对设备的系统参数、网络参数等进行远程配置。配置完成后,需点击「下发」按钮,设备即可进行远程配置。" message="支持远程更新设备的配置文件(JSON 格式),可以在下方编辑配置模板,对设备的系统参数、网络参数等进行远程配置。配置完成后,需点击「下发」按钮,设备即可进行远程配置。"
type="info" type="info"
show-icon show-icon
@@ -129,24 +129,24 @@ const onError = (errors: any) => {
@error="onError" @error="onError"
/> />
<div class="mt-5 text-center"> <div class="mt-5 text-center">
<a-button v-if="isEditing" @click="cancelEdit">取消</a-button> <Button v-if="isEditing" @click="cancelEdit">取消</Button>
<a-button <Button
v-if="isEditing" v-if="isEditing"
type="primary" type="primary"
@click="saveConfig" @click="saveConfig"
:disabled="hasJsonError" :disabled="hasJsonError"
> >
保存 保存
</a-button> </Button>
<a-button v-else @click="enableEdit">编辑</a-button> <Button v-else @click="enableEdit">编辑</Button>
<a-button <Button
v-if="!isEditing" v-if="!isEditing"
type="primary" type="primary"
@click="handleConfigPush" @click="handleConfigPush"
:loading="pushLoading" :loading="pushLoading"
> >
配置推送 配置推送
</a-button> </Button>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,18 +1,18 @@
<!-- 设备信息头部 --> <!-- 设备信息头部 -->
<script setup lang="ts"> <script setup lang="ts">
import type { DeviceVO } from '#/api/iot/device/device'; import type { IotDeviceApi } from '#/api/iot/device/device';
import type { ProductVO } from '#/api/iot/product/product'; import type { IotProductApi } from '#/api/iot/product/product';
import { ref } from 'vue'; import { ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { message } from 'ant-design-vue'; import { Button, Card, Descriptions, message } from 'ant-design-vue';
import DeviceForm from '../DeviceForm.vue'; import DeviceForm from '../DeviceForm.vue';
interface Props { interface Props {
product: ProductVO; product: IotProductApi.Product;
device: DeviceVO; device: IotDeviceApi.Device;
loading?: boolean; loading?: boolean;
} }
@@ -28,12 +28,12 @@ const router = useRouter();
/** 操作修改 */ /** 操作修改 */
const formRef = ref(); const formRef = ref();
const openForm = (type: string, id?: number) => { function openForm(type: string, id?: number) {
formRef.value.open(type, id); formRef.value.open(type, id);
}; }
/** 复制到剪贴板方法 */ /** 复制到剪贴板方法 */
const copyToClipboard = async (text: string | undefined) => { async function copyToClipboard(text: string | undefined) {
if (!text) return; if (!text) return;
try { try {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
@@ -41,14 +41,14 @@ const copyToClipboard = async (text: string | undefined) => {
} catch { } catch {
message.error({ content: '复制失败' }); message.error({ content: '复制失败' });
} }
}; }
/** 跳转到产品详情页面 */ /** 跳转到产品详情页面 */
const goToProductDetail = (productId: number | undefined) => { function goToProductDetail(productId: number | undefined) {
if (productId) { if (productId) {
router.push({ name: 'IoTProductDetail', params: { id: productId } }); router.push({ name: 'IoTProductDetail', params: { id: productId } });
} }
}; }
</script> </script>
<template> <template>
<div class="mb-4"> <div class="mb-4">
@@ -58,38 +58,38 @@ const goToProductDetail = (productId: number | undefined) => {
</div> </div>
<div class="space-x-2"> <div class="space-x-2">
<!-- 右上按钮 --> <!-- 右上按钮 -->
<a-button <Button
v-if="product.status === 0" v-if="product.status === 0"
v-hasPermi="['iot:device:update']" v-hasPermi="['iot:device:update']"
@click="openForm('update', device.id)" @click="openForm('update', device.id)"
> >
编辑 编辑
</a-button> </Button>
</div> </div>
</div> </div>
<a-card class="mt-4"> <Card class="mt-4">
<a-descriptions :column="1"> <Descriptions :column="1">
<a-descriptions-item label="产品"> <Descriptions.Item label="产品">
<a <a
@click="goToProductDetail(product.id)" @click="goToProductDetail(product.id)"
class="cursor-pointer text-blue-600" class="cursor-pointer text-blue-600"
> >
{{ product.name }} {{ product.name }}
</a> </a>
</a-descriptions-item> </Descriptions.Item>
<a-descriptions-item label="ProductKey"> <Descriptions.Item label="ProductKey">
{{ product.productKey }} {{ product.productKey }}
<a-button <Button
size="small" size="small"
class="ml-2" class="ml-2"
@click="copyToClipboard(product.productKey)" @click="copyToClipboard(product.productKey)"
> >
复制 复制
</a-button> </Button>
</a-descriptions-item> </Descriptions.Item>
</a-descriptions> </Descriptions>
</a-card> </Card>
<!-- 表单弹窗添加/修改 --> <!-- 表单弹窗添加/修改 -->
<DeviceForm ref="formRef" @success="emit('refresh')" /> <DeviceForm ref="formRef" @success="emit('refresh')" />

View File

@@ -1,28 +1,42 @@
<!-- 设备信息 --> <!-- 设备信息 -->
<script setup lang="ts"> <script setup lang="ts">
import type { DeviceVO, IotDeviceAuthInfoVO } from '#/api/iot/device/device'; import type { IotDeviceApi } from '#/api/iot/device/device';
import type { ProductVO } from '#/api/iot/product/product'; import type { IotProductApi } from '#/api/iot/product/product';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants'; import { DICT_TYPE } from '@vben/constants';
import { IconifyIcon } from '@vben/icons';
import { formatDate } from '@vben/utils'; import { formatDate } from '@vben/utils';
import { message } from 'ant-design-vue'; import {
Button,
Card,
Col,
Descriptions,
Form,
Input,
message,
Modal,
Row,
} from 'ant-design-vue';
import { DeviceApi } from '#/api/iot/device/device'; import { getDeviceAuthInfo } from '#/api/iot/device/device';
import { DictTag } from '#/components/dict-tag';
// 消息提示 // 消息提示
const { product, device } = defineProps<{ const { product, device } = defineProps<{
device: DeviceVO; device: IotDeviceApi.Device;
product: ProductVO; product: IotProductApi.Product;
}>(); // 定义 Props }>(); // 定义 Props
const emit = defineEmits(['refresh']); // 定义 Emits // const emit = defineEmits(['refresh']); // 定义 Emits
const authDialogVisible = ref(false); // 定义设备认证信息弹框的可见性 const authDialogVisible = ref(false); // 定义设备认证信息弹框的可见性
const authPasswordVisible = ref(false); // 定义密码可见性状态 const authPasswordVisible = ref(false); // 定义密码可见性状态
const authInfo = ref<IotDeviceAuthInfoVO>({} as IotDeviceAuthInfoVO); // 定义设备认证信息对象 const authInfo = ref<IotDeviceApi.DeviceAuthInfo>(
{} as IotDeviceApi.DeviceAuthInfo,
); // 定义设备认证信息对象
/** 控制地图显示的标志 */ /** 控制地图显示的标志 */
const showMap = computed(() => { const showMap = computed(() => {
@@ -30,20 +44,20 @@ const showMap = computed(() => {
}); });
/** 复制到剪贴板方法 */ /** 复制到剪贴板方法 */
const copyToClipboard = async (text: string) => { async function copyToClipboard(text: string) {
try { try {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
message.success({ content: '复制成功' }); message.success({ content: '复制成功' });
} catch { } catch {
message.error({ content: '复制失败' }); message.error({ content: '复制失败' });
} }
}; }
/** 打开设备认证信息弹框的方法 */ /** 打开设备认证信息弹框的方法 */
const handleAuthInfoDialogOpen = async () => { async function handleAuthInfoDialogOpen() {
if (!device.id) return; if (!device.id) return;
try { try {
authInfo.value = await DeviceApi.getDeviceAuthInfo(device.id); authInfo.value = await getDeviceAuthInfo(device.id);
// 显示设备认证信息弹框 // 显示设备认证信息弹框
authDialogVisible.value = true; authDialogVisible.value = true;
} catch (error) { } catch (error) {
@@ -52,82 +66,82 @@ const handleAuthInfoDialogOpen = async () => {
content: '获取设备认证信息失败,请检查网络连接或联系管理员', content: '获取设备认证信息失败,请检查网络连接或联系管理员',
}); });
} }
}; }
/** 关闭设备认证信息弹框的方法 */ /** 关闭设备认证信息弹框的方法 */
const handleAuthInfoDialogClose = () => { function handleAuthInfoDialogClose() {
authDialogVisible.value = false; authDialogVisible.value = false;
}; }
</script> </script>
<template> <template>
<div> <div>
<a-row :gutter="16"> <Row :gutter="16">
<!-- 左侧设备信息 --> <!-- 左侧设备信息 -->
<a-col :span="12"> <Col :span="12">
<a-card class="h-full"> <Card class="h-full">
<template #title> <template #title>
<div class="flex items-center"> <div class="flex items-center">
<Icon icon="ep:info-filled" class="text-primary mr-2" /> <IconifyIcon icon="ep:info-filled" class="text-primary mr-2" />
<span>设备信息</span> <span>设备信息</span>
</div> </div>
</template> </template>
<a-descriptions :column="1" bordered> <Descriptions :column="1" bordered>
<a-descriptions-item label="产品名称"> <Descriptions.Item label="产品名称">
{{ product.name }} {{ product.name }}
</a-descriptions-item> </Descriptions.Item>
<a-descriptions-item label="ProductKey"> <Descriptions.Item label="ProductKey">
{{ product.productKey }} {{ product.productKey }}
</a-descriptions-item> </Descriptions.Item>
<a-descriptions-item label="设备类型"> <Descriptions.Item label="设备类型">
<dict-tag <DictTag
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
:value="product.deviceType" :value="product.deviceType"
/> />
</a-descriptions-item> </Descriptions.Item>
<a-descriptions-item label="DeviceName"> <Descriptions.Item label="DeviceName">
{{ device.deviceName }} {{ device.deviceName }}
</a-descriptions-item> </Descriptions.Item>
<a-descriptions-item label="备注名称"> <Descriptions.Item label="备注名称">
{{ device.nickname || '--' }} {{ device.nickname || '--' }}
</a-descriptions-item> </Descriptions.Item>
<a-descriptions-item label="当前状态"> <Descriptions.Item label="当前状态">
<dict-tag <DictTag
:type="DICT_TYPE.IOT_DEVICE_STATUS" :type="DICT_TYPE.IOT_DEVICE_STATUS"
:value="device.state" :value="device.state"
/> />
</a-descriptions-item> </Descriptions.Item>
<a-descriptions-item label="创建时间"> <Descriptions.Item label="创建时间">
{{ formatDate(device.createTime) }} {{ formatDate(device.createTime) }}
</a-descriptions-item> </Descriptions.Item>
<a-descriptions-item label="激活时间"> <Descriptions.Item label="激活时间">
{{ formatDate(device.activeTime) }} {{ formatDate(device.activeTime) }}
</a-descriptions-item> </Descriptions.Item>
<a-descriptions-item label="最后上线时间"> <Descriptions.Item label="最后上线时间">
{{ formatDate(device.onlineTime) }} {{ formatDate(device.onlineTime) }}
</a-descriptions-item> </Descriptions.Item>
<a-descriptions-item label="最后离线时间"> <Descriptions.Item label="最后离线时间">
{{ formatDate(device.offlineTime) }} {{ formatDate(device.offlineTime) }}
</a-descriptions-item> </Descriptions.Item>
<a-descriptions-item label="MQTT 连接参数"> <Descriptions.Item label="MQTT 连接参数">
<a-button <Button
type="link" type="link"
@click="handleAuthInfoDialogOpen" @click="handleAuthInfoDialogOpen"
size="small" size="small"
> >
查看 查看
</a-button> </Button>
</a-descriptions-item> </Descriptions.Item>
</a-descriptions> </Descriptions>
</a-card> </Card>
</a-col> </Col>
<!-- 右侧地图 --> <!-- 右侧地图 -->
<a-col :span="12"> <Col :span="12">
<a-card class="h-full"> <Card class="h-full">
<template #title> <template #title>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center"> <div class="flex items-center">
<Icon icon="ep:location" class="text-primary mr-2" /> <IconifyIcon icon="ep:location" class="text-primary mr-2" />
<span>设备位置</span> <span>设备位置</span>
</div> </div>
</div> </div>
@@ -143,78 +157,71 @@ const handleAuthInfoDialogClose = () => {
v-else v-else
class="flex h-full w-full items-center justify-center rounded bg-gray-50 text-gray-400" class="flex h-full w-full items-center justify-center rounded bg-gray-50 text-gray-400"
> >
<Icon icon="ep:warning" class="mr-2" /> <IconifyIcon icon="ep:warning" class="mr-2" />
<span>暂无位置信息</span> <span>暂无位置信息</span>
</div> </div>
</div> </div>
</a-card> </Card>
</a-col> </Col>
</a-row> </Row>
<!-- 认证信息弹框 --> <!-- 认证信息弹框 -->
<a-modal <Modal
v-model:open="authDialogVisible" v-model:open="authDialogVisible"
title="MQTT 连接参数" title="MQTT 连接参数"
width="640px" width="640px"
:footer="null" :footer="null"
> >
<a-form :label-col="{ span: 6 }"> <Form :label-col="{ span: 6 }">
<a-form-item label="clientId"> <Form.Item label="clientId">
<a-input-group compact> <Input.Group compact>
<a-input <Input
v-model:value="authInfo.clientId" v-model:value="authInfo.clientId"
readonly readonly
style="width: calc(100% - 80px)" style="width: calc(100% - 80px)"
/> />
<a-button <Button @click="copyToClipboard(authInfo.clientId)" type="primary">
@click="copyToClipboard(authInfo.clientId)" <IconifyIcon icon="ph:copy" />
type="primary" </Button>
> </Input.Group>
<Icon icon="ph:copy" /> </Form.Item>
</a-button> <Form.Item label="username">
</a-input-group> <Input.Group compact>
</a-form-item> <Input
<a-form-item label="username">
<a-input-group compact>
<a-input
v-model:value="authInfo.username" v-model:value="authInfo.username"
readonly readonly
style="width: calc(100% - 80px)" style="width: calc(100% - 80px)"
/> />
<a-button <Button @click="copyToClipboard(authInfo.username)" type="primary">
@click="copyToClipboard(authInfo.username)" <IconifyIcon icon="ph:copy" />
type="primary" </Button>
> </Input.Group>
<Icon icon="ph:copy" /> </Form.Item>
</a-button> <Form.Item label="password">
</a-input-group> <Input.Group compact>
</a-form-item> <Input
<a-form-item label="password">
<a-input-group compact>
<a-input
v-model:value="authInfo.password" v-model:value="authInfo.password"
readonly readonly
:type="authPasswordVisible ? 'text' : 'password'" :type="authPasswordVisible ? 'text' : 'password'"
style="width: calc(100% - 160px)" style="width: calc(100% - 160px)"
/> />
<a-button <Button
@click="authPasswordVisible = !authPasswordVisible" @click="authPasswordVisible = !authPasswordVisible"
type="primary" type="primary"
> >
<Icon :icon="authPasswordVisible ? 'ph:eye-slash' : 'ph:eye'" /> <IconifyIcon
</a-button> :icon="authPasswordVisible ? 'ph:eye-slash' : 'ph:eye'"
<a-button />
@click="copyToClipboard(authInfo.password)" </Button>
type="primary" <Button @click="copyToClipboard(authInfo.password)" type="primary">
> <IconifyIcon icon="ph:copy" />
<Icon icon="ph:copy" /> </Button>
</a-button> </Input.Group>
</a-input-group> </Form.Item>
</a-form-item> </Form>
</a-form>
<div class="mt-4 text-right"> <div class="mt-4 text-right">
<a-button @click="handleAuthInfoDialogClose">关闭</a-button> <Button @click="handleAuthInfoDialogClose">关闭</Button>
</div> </div>
</a-modal> </Modal>
</div> </div>
</template> </template>

View File

@@ -9,10 +9,23 @@ import {
watch, watch,
} from 'vue'; } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants'; import { DICT_TYPE } from '@vben/constants';
import { IconifyIcon } from '@vben/icons';
import { formatDate } from '@vben/utils'; import { formatDate } from '@vben/utils';
import { DeviceApi } from '#/api/iot/device/device'; import {
Button,
Form,
Pagination,
Select,
Switch,
Table,
Tag,
} from 'ant-design-vue';
import { getDeviceMessagePage } from '#/api/iot/device/device';
import { DictTag } from '#/components/dict-tag';
import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants'; import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants';
const props = defineProps<{ const props = defineProps<{
@@ -49,64 +62,58 @@ const columns = [
title: '时间', title: '时间',
dataIndex: 'ts', dataIndex: 'ts',
key: 'ts', key: 'ts',
align: 'center',
width: 180, width: 180,
}, },
{ {
title: '上行/下行', title: '上行/下行',
dataIndex: 'upstream', dataIndex: 'upstream',
key: 'upstream', key: 'upstream',
align: 'center',
width: 140, width: 140,
}, },
{ {
title: '是否回复', title: '是否回复',
dataIndex: 'reply', dataIndex: 'reply',
key: 'reply', key: 'reply',
align: 'center',
width: 140, width: 140,
}, },
{ {
title: '请求编号', title: '请求编号',
dataIndex: 'requestId', dataIndex: 'requestId',
key: 'requestId', key: 'requestId',
align: 'center',
width: 300, width: 300,
}, },
{ {
title: '请求方法', title: '请求方法',
dataIndex: 'method', dataIndex: 'method',
key: 'method', key: 'method',
align: 'center',
width: 140, width: 140,
}, },
{ {
title: '请求/响应数据', title: '请求/响应数据',
dataIndex: 'params', dataIndex: 'params',
key: 'params', key: 'params',
align: 'center',
ellipsis: true, ellipsis: true,
}, },
]; ];
/** 查询消息列表 */ /** 查询消息列表 */
const getMessageList = async () => { async function getMessageList() {
if (!props.deviceId) return; if (!props.deviceId) return;
loading.value = true; loading.value = true;
try { try {
const data = await DeviceApi.getDeviceMessagePage(queryParams); const data = await getDeviceMessagePage(queryParams);
total.value = data.total; total.value = data.total;
list.value = data.list; list.value = data.list;
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; }
/** 搜索操作 */ /** 搜索操作 */
const handleQuery = () => { function handleQuery() {
queryParams.pageNo = 1; queryParams.pageNo = 1;
getMessageList(); getMessageList();
}; }
/** 监听自动刷新 */ /** 监听自动刷新 */
watch(autoRefresh, (newValue) => { watch(autoRefresh, (newValue) => {
@@ -146,7 +153,7 @@ onMounted(() => {
}); });
/** 刷新消息列表 */ /** 刷新消息列表 */
const refresh = (delay = 0) => { function refresh(delay = 0) {
if (delay > 0) { if (delay > 0) {
setTimeout(() => { setTimeout(() => {
handleQuery(); handleQuery();
@@ -154,7 +161,7 @@ const refresh = (delay = 0) => {
} else { } else {
handleQuery(); handleQuery();
} }
}; }
/** 暴露方法给父组件 */ /** 暴露方法给父组件 */
defineExpose({ defineExpose({
@@ -165,54 +172,55 @@ defineExpose({
<template> <template>
<ContentWrap> <ContentWrap>
<!-- 搜索区域 --> <!-- 搜索区域 -->
<a-form :model="queryParams" layout="inline"> <Form :model="queryParams" layout="inline">
<a-form-item> <Form.Item>
<a-select <Select
v-model:value="queryParams.method" v-model:value="queryParams.method"
placeholder="所有方法" placeholder="所有方法"
style="width: 160px" style="width: 160px"
allow-clear allow-clear
> >
<a-select-option <Select.Option
v-for="item in methodOptions" v-for="item in methodOptions"
:key="item.value" :key="item.value"
:label="item.label" :label="item.label"
:value="item.value" :value="item.value"
> >
{{ item.label }} {{ item.label }}
</a-select-option> </Select.Option>
</a-select> </Select>
</a-form-item> </Form.Item>
<a-form-item> <Form.Item>
<a-select <Select
v-model:value="queryParams.upstream" v-model:value="queryParams.upstream"
placeholder="上行/下行" placeholder="上行/下行"
style="width: 160px" style="width: 160px"
allow-clear allow-clear
> >
<a-select-option label="上行" value="true">上行</a-select-option> <Select.Option label="上行" value="true">上行</Select.Option>
<a-select-option label="下行" value="false">下行</a-select-option> <Select.Option label="下行" value="false">下行</Select.Option>
</a-select> </Select>
</a-form-item> </Form.Item>
<a-form-item> <Form.Item>
<a-button type="primary" @click="handleQuery"> <Button type="primary" @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索 <IconifyIcon icon="ep:search" class="mr-5px" /> 搜索
</a-button> </Button>
<a-switch <Switch
v-model:checked="autoRefresh" v-model:checked="autoRefresh"
class="ml-20px" class="ml-20px"
checked-children="定时刷新" checked-children="定时刷新"
un-checked-children="定时刷新" un-checked-children="定时刷新"
/> />
</a-form-item> </Form.Item>
</a-form> </Form>
<!-- 消息列表 --> <!-- 消息列表 -->
<a-table <Table
:loading="loading" :loading="loading"
:data-source="list" :data-source="list"
:columns="columns" :columns="columns"
:pagination="false" :pagination="false"
align="center"
class="whitespace-nowrap" class="whitespace-nowrap"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
@@ -220,12 +228,12 @@ defineExpose({
{{ formatDate(record.ts) }} {{ formatDate(record.ts) }}
</template> </template>
<template v-else-if="column.key === 'upstream'"> <template v-else-if="column.key === 'upstream'">
<a-tag :color="record.upstream ? 'blue' : 'green'"> <Tag :color="record.upstream ? 'blue' : 'green'">
{{ record.upstream ? '上行' : '下行' }} {{ record.upstream ? '上行' : '下行' }}
</a-tag> </Tag>
</template> </template>
<template v-else-if="column.key === 'reply'"> <template v-else-if="column.key === 'reply'">
<dict-tag <DictTag
:type="DICT_TYPE.INFRA_BOOLEAN_STRING" :type="DICT_TYPE.INFRA_BOOLEAN_STRING"
:value="record.reply" :value="record.reply"
/> />
@@ -244,7 +252,7 @@ defineExpose({
<span v-else>{{ record.params }}</span> <span v-else>{{ record.params }}</span>
</template> </template>
</template> </template>
</a-table> </Table>
<!-- 分页 --> <!-- 分页 -->
<div class="mt-10px flex justify-end"> <div class="mt-10px flex justify-end">

View File

@@ -1,14 +1,26 @@
<!-- 模拟设备 --> <!-- 模拟设备 -->
<script lang="ts" setup> <script lang="ts" setup>
import type { DeviceVO } from '#/api/iot/device/device'; import type { TableColumnType } from 'ant-design-vue';
import type { ProductVO } from '#/api/iot/product/product';
import type { IotDeviceApi } from '#/api/iot/device/device';
import type { IotProductApi } from '#/api/iot/product/product';
import type { ThingModelData } from '#/api/iot/thingmodel'; import type { ThingModelData } from '#/api/iot/thingmodel';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { message } from 'ant-design-vue'; import {
Button,
Card,
Col,
Input,
message,
Row,
Table,
Tabs,
Textarea,
} from 'ant-design-vue';
import { DeviceApi, DeviceStateEnum } from '#/api/iot/device/device'; import { DeviceStateEnum, sendDeviceMessage } from '#/api/iot/device/device';
import { import {
IotDeviceMessageMethodEnum, IotDeviceMessageMethodEnum,
IoTThingModelTypeEnum, IoTThingModelTypeEnum,
@@ -17,8 +29,8 @@ import {
import DeviceDetailsMessage from './DeviceDetailsMessage.vue'; import DeviceDetailsMessage from './DeviceDetailsMessage.vue';
const props = defineProps<{ const props = defineProps<{
device: DeviceVO; device: IotDeviceApi.Device;
product: ProductVO; product: IotProductApi.Product;
thingModelList: ThingModelData[]; thingModelList: ThingModelData[];
}>(); }>();
@@ -55,41 +67,36 @@ const serviceList = computed(() =>
); );
// 属性表格列定义 // 属性表格列定义
const propertyColumns = [ const propertyColumns: TableColumnType[] = [
{ {
title: '功能名称', title: '功能名称',
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
width: 120, width: 120,
align: 'center', fixed: 'left' as any,
fixed: 'left',
}, },
{ {
title: '标识符', title: '标识符',
dataIndex: 'identifier', dataIndex: 'identifier',
key: 'identifier', key: 'identifier',
width: 120, width: 120,
align: 'center', fixed: 'left' as any,
fixed: 'left',
}, },
{ {
title: '数据类型', title: '数据类型',
key: 'dataType', key: 'dataType',
width: 100, width: 100,
align: 'center',
}, },
{ {
title: '数据定义', title: '数据定义',
key: 'dataDefinition', key: 'dataDefinition',
minWidth: 200, minWidth: 200,
align: 'left',
}, },
{ {
title: '值', title: '值',
key: 'value', key: 'value',
width: 150, width: 150,
align: 'center', fixed: 'right' as any,
fixed: 'right',
}, },
]; ];
@@ -100,41 +107,35 @@ const eventColumns = [
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
width: 120, width: 120,
align: 'center', fixed: 'left' as any,
fixed: 'left',
}, },
{ {
title: '标识符', title: '标识符',
dataIndex: 'identifier', dataIndex: 'identifier',
key: 'identifier', key: 'identifier',
width: 120, width: 120,
align: 'center', fixed: 'left' as any,
fixed: 'left',
}, },
{ {
title: '数据类型', title: '数据类型',
key: 'dataType', key: 'dataType',
width: 100, width: 100,
align: 'center',
}, },
{ {
title: '数据定义', title: '数据定义',
key: 'dataDefinition', key: 'dataDefinition',
minWidth: 200, minWidth: 200,
align: 'left',
}, },
{ {
title: '值', title: '值',
key: 'value', key: 'value',
width: 200, width: 200,
align: 'center',
}, },
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
width: 100, width: 100,
align: 'center', fixed: 'right' as any,
fixed: 'right',
}, },
]; ];
@@ -145,50 +146,45 @@ const serviceColumns = [
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
width: 120, width: 120,
align: 'center', fixed: 'left' as any,
fixed: 'left',
}, },
{ {
title: '标识符', title: '标识符',
dataIndex: 'identifier', dataIndex: 'identifier',
key: 'identifier', key: 'identifier',
width: 120, width: 120,
align: 'center', fixed: 'left' as any,
fixed: 'left',
}, },
{ {
title: '输入参数', title: '输入参数',
key: 'dataDefinition', key: 'dataDefinition',
minWidth: 200, minWidth: 200,
align: 'left',
}, },
{ {
title: '参数值', title: '参数值',
key: 'value', key: 'value',
width: 200, width: 200,
align: 'center',
}, },
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
width: 100, width: 100,
align: 'center', fixed: 'right' as any,
fixed: 'right',
}, },
]; ];
// 获取表单值 // 获取表单值
const getFormValue = (identifier: string) => { function getFormValue(identifier: string) {
return formData.value[identifier] || ''; return formData.value[identifier] || '';
}; }
// 设置表单值 // 设置表单值
const setFormValue = (identifier: string, value: string) => { function setFormValue(identifier: string, value: string) {
formData.value[identifier] = value; formData.value[identifier] = value;
}; }
// 属性上报 // 属性上报
const handlePropertyPost = async () => { async function handlePropertyPost() {
try { try {
const params: Record<string, any> = {}; const params: Record<string, any> = {};
propertyList.value.forEach((item) => { propertyList.value.forEach((item) => {
@@ -203,7 +199,7 @@ const handlePropertyPost = async () => {
return; return;
} }
await DeviceApi.sendDeviceMessage({ await sendDeviceMessage({
deviceId: props.device.id!, deviceId: props.device.id!,
method: IotDeviceMessageMethodEnum.PROPERTY_POST.method, method: IotDeviceMessageMethodEnum.PROPERTY_POST.method,
params, params,
@@ -216,10 +212,10 @@ const handlePropertyPost = async () => {
message.error({ content: '属性上报失败' }); message.error({ content: '属性上报失败' });
console.error(error); console.error(error);
} }
}; }
// 事件上报 // 事件上报
const handleEventPost = async (row: ThingModelData) => { async function handleEventPost(row: ThingModelData) {
try { try {
const valueStr = formData.value[row.identifier!]; const valueStr = formData.value[row.identifier!];
let params: any = {}; let params: any = {};
@@ -233,7 +229,7 @@ const handleEventPost = async (row: ThingModelData) => {
} }
} }
await DeviceApi.sendDeviceMessage({ await sendDeviceMessage({
deviceId: props.device.id!, deviceId: props.device.id!,
method: IotDeviceMessageMethodEnum.EVENT_POST.method, method: IotDeviceMessageMethodEnum.EVENT_POST.method,
params: { params: {
@@ -249,12 +245,12 @@ const handleEventPost = async (row: ThingModelData) => {
message.error({ content: '事件上报失败' }); message.error({ content: '事件上报失败' });
console.error(error); console.error(error);
} }
}; }
// 状态变更 // 状态变更
const handleDeviceState = async (state: number) => { async function handleDeviceState(state: number) {
try { try {
await DeviceApi.sendDeviceMessage({ await sendDeviceMessage({
deviceId: props.device.id!, deviceId: props.device.id!,
method: IotDeviceMessageMethodEnum.STATE_UPDATE.method, method: IotDeviceMessageMethodEnum.STATE_UPDATE.method,
params: { state }, params: { state },
@@ -267,10 +263,10 @@ const handleDeviceState = async (state: number) => {
message.error({ content: '状态变更失败' }); message.error({ content: '状态变更失败' });
console.error(error); console.error(error);
} }
}; }
// 属性设置 // 属性设置
const handlePropertySet = async () => { async function handlePropertySet() {
try { try {
const params: Record<string, any> = {}; const params: Record<string, any> = {};
propertyList.value.forEach((item) => { propertyList.value.forEach((item) => {
@@ -285,7 +281,7 @@ const handlePropertySet = async () => {
return; return;
} }
await DeviceApi.sendDeviceMessage({ await sendDeviceMessage({
deviceId: props.device.id!, deviceId: props.device.id!,
method: IotDeviceMessageMethodEnum.PROPERTY_SET.method, method: IotDeviceMessageMethodEnum.PROPERTY_SET.method,
params, params,
@@ -298,10 +294,10 @@ const handlePropertySet = async () => {
message.error({ content: '属性设置失败' }); message.error({ content: '属性设置失败' });
console.error(error); console.error(error);
} }
}; }
// 服务调用 // 服务调用
const handleServiceInvoke = async (row: ThingModelData) => { async function handleServiceInvoke(row: ThingModelData) {
try { try {
const valueStr = formData.value[row.identifier!]; const valueStr = formData.value[row.identifier!];
let params: any = {}; let params: any = {};
@@ -315,7 +311,7 @@ const handleServiceInvoke = async (row: ThingModelData) => {
} }
} }
await DeviceApi.sendDeviceMessage({ await sendDeviceMessage({
deviceId: props.device.id!, deviceId: props.device.id!,
method: IotDeviceMessageMethodEnum.SERVICE_INVOKE.method, method: IotDeviceMessageMethodEnum.SERVICE_INVOKE.method,
params: { params: {
@@ -331,30 +327,31 @@ const handleServiceInvoke = async (row: ThingModelData) => {
message.error({ content: '服务调用失败' }); message.error({ content: '服务调用失败' });
console.error(error); console.error(error);
} }
}; }
</script> </script>
<template> <template>
<ContentWrap> <ContentWrap>
<a-row :gutter="20"> <Row :gutter="20">
<!-- 左侧指令调试区域 --> <!-- 左侧指令调试区域 -->
<a-col :span="12"> <Col :span="12">
<a-card> <Card>
<a-tabs v-model:active-key="activeTab"> <Tabs v-model:active-key="activeTab">
<!-- 上行指令调试 --> <!-- 上行指令调试 -->
<a-tab-pane key="upstream" tab="上行指令调试"> <Tabs.Pane key="upstream" tab="上行指令调试">
<a-tabs <Tabs
v-if="activeTab === 'upstream'" v-if="activeTab === 'upstream'"
v-model:active-key="upstreamTab" v-model:active-key="upstreamTab"
> >
<!-- 属性上报 --> <!-- 属性上报 -->
<a-tab-pane <Tabs.Pane
:key="IotDeviceMessageMethodEnum.PROPERTY_POST.method" :key="IotDeviceMessageMethodEnum.PROPERTY_POST.method"
tab="属性上报" tab="属性上报"
> >
<ContentWrap> <ContentWrap>
<a-table <Table
:data-source="propertyList" :data-source="propertyList"
align="center"
:columns="propertyColumns" :columns="propertyColumns"
:pagination="false" :pagination="false"
> >
@@ -366,7 +363,7 @@ const handleServiceInvoke = async (row: ThingModelData) => {
<DataDefinition :data="record" /> <DataDefinition :data="record" />
</template> </template>
<template v-else-if="column.key === 'value'"> <template v-else-if="column.key === 'value'">
<a-input <Input
:value="getFormValue(record.identifier)" :value="getFormValue(record.identifier)"
@update:value=" @update:value="
setFormValue(record.identifier, $event) setFormValue(record.identifier, $event)
@@ -376,26 +373,27 @@ const handleServiceInvoke = async (row: ThingModelData) => {
/> />
</template> </template>
</template> </template>
</a-table> </Table>
<div class="mt-4 flex items-center justify-between"> <div class="mt-4 flex items-center justify-between">
<span class="text-sm text-gray-600"> <span class="text-sm text-gray-600">
设置属性值后点击发送属性上报按钮 设置属性值后点击发送属性上报按钮
</span> </span>
<a-button type="primary" @click="handlePropertyPost"> <Button type="primary" @click="handlePropertyPost">
发送属性上报 发送属性上报
</a-button> </Button>
</div> </div>
</ContentWrap> </ContentWrap>
</a-tab-pane> </Tabs.Pane>
<!-- 事件上报 --> <!-- 事件上报 -->
<a-tab-pane <Tabs.Pane
:key="IotDeviceMessageMethodEnum.EVENT_POST.method" :key="IotDeviceMessageMethodEnum.EVENT_POST.method"
tab="事件上报" tab="事件上报"
> >
<ContentWrap> <ContentWrap>
<a-table <Table
:data-source="eventList" :data-source="eventList"
align="center"
:columns="eventColumns" :columns="eventColumns"
:pagination="false" :pagination="false"
> >
@@ -407,7 +405,7 @@ const handleServiceInvoke = async (row: ThingModelData) => {
<DataDefinition :data="record" /> <DataDefinition :data="record" />
</template> </template>
<template v-else-if="column.key === 'value'"> <template v-else-if="column.key === 'value'">
<a-textarea <Textarea
:value="getFormValue(record.identifier)" :value="getFormValue(record.identifier)"
@update:value=" @update:value="
setFormValue(record.identifier, $event) setFormValue(record.identifier, $event)
@@ -418,58 +416,59 @@ const handleServiceInvoke = async (row: ThingModelData) => {
/> />
</template> </template>
<template v-else-if="column.key === 'action'"> <template v-else-if="column.key === 'action'">
<a-button <Button
type="primary" type="primary"
size="small" size="small"
@click="handleEventPost(record)" @click="handleEventPost(record)"
> >
上报事件 上报事件
</a-button> </Button>
</template> </template>
</template> </template>
</a-table> </Table>
</ContentWrap> </ContentWrap>
</a-tab-pane> </Tabs.Pane>
<!-- 状态变更 --> <!-- 状态变更 -->
<a-tab-pane <Tabs.Pane
:key="IotDeviceMessageMethodEnum.STATE_UPDATE.method" :key="IotDeviceMessageMethodEnum.STATE_UPDATE.method"
tab="状态变更" tab="状态变更"
> >
<ContentWrap> <ContentWrap>
<div class="flex gap-4"> <div class="flex gap-4">
<a-button <Button
type="primary" type="primary"
@click="handleDeviceState(DeviceStateEnum.ONLINE)" @click="handleDeviceState(DeviceStateEnum.ONLINE)"
> >
设备上线 设备上线
</a-button> </Button>
<a-button <Button
danger danger
@click="handleDeviceState(DeviceStateEnum.OFFLINE)" @click="handleDeviceState(DeviceStateEnum.OFFLINE)"
> >
设备下线 设备下线
</a-button> </Button>
</div> </div>
</ContentWrap> </ContentWrap>
</a-tab-pane> </Tabs.Pane>
</a-tabs> </Tabs>
</a-tab-pane> </Tabs.Pane>
<!-- 下行指令调试 --> <!-- 下行指令调试 -->
<a-tab-pane key="downstream" tab="下行指令调试"> <Tabs.Pane key="downstream" tab="下行指令调试">
<a-tabs <Tabs
v-if="activeTab === 'downstream'" v-if="activeTab === 'downstream'"
v-model:active-key="downstreamTab" v-model:active-key="downstreamTab"
> >
<!-- 属性调试 --> <!-- 属性调试 -->
<a-tab-pane <Tabs.Pane
:key="IotDeviceMessageMethodEnum.PROPERTY_SET.method" :key="IotDeviceMessageMethodEnum.PROPERTY_SET.method"
tab="属性设置" tab="属性设置"
> >
<ContentWrap> <ContentWrap>
<a-table <Table
:data-source="propertyList" :data-source="propertyList"
align="center"
:columns="propertyColumns" :columns="propertyColumns"
:pagination="false" :pagination="false"
> >
@@ -481,7 +480,7 @@ const handleServiceInvoke = async (row: ThingModelData) => {
<DataDefinition :data="record" /> <DataDefinition :data="record" />
</template> </template>
<template v-else-if="column.key === 'value'"> <template v-else-if="column.key === 'value'">
<a-input <Input
:value="getFormValue(record.identifier)" :value="getFormValue(record.identifier)"
@update:value=" @update:value="
setFormValue(record.identifier, $event) setFormValue(record.identifier, $event)
@@ -491,26 +490,27 @@ const handleServiceInvoke = async (row: ThingModelData) => {
/> />
</template> </template>
</template> </template>
</a-table> </Table>
<div class="mt-4 flex items-center justify-between"> <div class="mt-4 flex items-center justify-between">
<span class="text-sm text-gray-600"> <span class="text-sm text-gray-600">
设置属性值后点击发送属性设置按钮 设置属性值后点击发送属性设置按钮
</span> </span>
<a-button type="primary" @click="handlePropertySet"> <Button type="primary" @click="handlePropertySet">
发送属性设置 发送属性设置
</a-button> </Button>
</div> </div>
</ContentWrap> </ContentWrap>
</a-tab-pane> </Tabs.Pane>
<!-- 服务调用 --> <!-- 服务调用 -->
<a-tab-pane <Tabs.Pane
:key="IotDeviceMessageMethodEnum.SERVICE_INVOKE.method" :key="IotDeviceMessageMethodEnum.SERVICE_INVOKE.method"
tab="设备服务调用" tab="设备服务调用"
> >
<ContentWrap> <ContentWrap>
<a-table <Table
:data-source="serviceList" :data-source="serviceList"
align="center"
:columns="serviceColumns" :columns="serviceColumns"
:pagination="false" :pagination="false"
> >
@@ -519,7 +519,7 @@ const handleServiceInvoke = async (row: ThingModelData) => {
<DataDefinition :data="record" /> <DataDefinition :data="record" />
</template> </template>
<template v-else-if="column.key === 'value'"> <template v-else-if="column.key === 'value'">
<a-textarea <Textarea
:value="getFormValue(record.identifier)" :value="getFormValue(record.identifier)"
@update:value=" @update:value="
setFormValue(record.identifier, $event) setFormValue(record.identifier, $event)
@@ -530,26 +530,26 @@ const handleServiceInvoke = async (row: ThingModelData) => {
/> />
</template> </template>
<template v-else-if="column.key === 'action'"> <template v-else-if="column.key === 'action'">
<a-button <Button
type="primary" type="primary"
size="small" size="small"
@click="handleServiceInvoke(record)" @click="handleServiceInvoke(record)"
> >
服务调用 服务调用
</a-button> </Button>
</template> </template>
</template> </template>
</a-table> </Table>
</ContentWrap> </ContentWrap>
</a-tab-pane> </Tabs.Pane>
</a-tabs> </Tabs>
</a-tab-pane> </Tabs.Pane>
</a-tabs> </Tabs>
</a-card> </Card>
</a-col> </Col>
<!-- 右侧设备日志区域 --> <!-- 右侧设备日志区域 -->
<a-col :span="12"> <Col :span="12">
<ContentWrap title="设备消息"> <ContentWrap title="设备消息">
<DeviceDetailsMessage <DeviceDetailsMessage
v-if="device.id" v-if="device.id"
@@ -557,7 +557,7 @@ const handleServiceInvoke = async (row: ThingModelData) => {
:device-id="device.id" :device-id="device.id"
/> />
</ContentWrap> </ContentWrap>
</a-col> </Col>
</a-row> </Row>
</ContentWrap> </ContentWrap>
</template> </template>

View File

@@ -6,6 +6,8 @@ import { ref } from 'vue';
import { ContentWrap } from '@vben/common-ui'; import { ContentWrap } from '@vben/common-ui';
import { Tabs } from 'ant-design-vue';
import DeviceDetailsThingModelEvent from './DeviceDetailsThingModelEvent.vue'; import DeviceDetailsThingModelEvent from './DeviceDetailsThingModelEvent.vue';
import DeviceDetailsThingModelProperty from './DeviceDetailsThingModelProperty.vue'; import DeviceDetailsThingModelProperty from './DeviceDetailsThingModelProperty.vue';
import DeviceDetailsThingModelService from './DeviceDetailsThingModelService.vue'; import DeviceDetailsThingModelService from './DeviceDetailsThingModelService.vue';
@@ -19,32 +21,22 @@ const activeTab = ref('property'); // 默认选中设备属性
</script> </script>
<template> <template>
<ContentWrap> <ContentWrap>
<a-tabs v-model:active-key="activeTab" class="thing-model-tabs"> <Tabs v-model:active-key="activeTab" class="!h-auto !p-0">
<a-tab-pane key="property" tab="设备属性(运行状态)"> <Tabs.Pane key="property" tab="设备属性(运行状态)">
<DeviceDetailsThingModelProperty :device-id="deviceId" /> <DeviceDetailsThingModelProperty :device-id="deviceId" />
</a-tab-pane> </Tabs.Pane>
<a-tab-pane key="event" tab="设备事件上报"> <Tabs.Pane key="event" tab="设备事件上报">
<DeviceDetailsThingModelEvent <DeviceDetailsThingModelEvent
:device-id="props.deviceId" :device-id="props.deviceId"
:thing-model-list="props.thingModelList" :thing-model-list="props.thingModelList"
/> />
</a-tab-pane> </Tabs.Pane>
<a-tab-pane key="service" tab="设备服务调用"> <Tabs.Pane key="service" tab="设备服务调用">
<DeviceDetailsThingModelService <DeviceDetailsThingModelService
:device-id="deviceId" :device-id="deviceId"
:thing-model-list="props.thingModelList" :thing-model-list="props.thingModelList"
/> />
</a-tab-pane> </Tabs.Pane>
</a-tabs> </Tabs>
</ContentWrap> </ContentWrap>
</template> </template>
<style scoped>
.thing-model-tabs :deep(.ant-tabs-content) {
height: auto !important;
}
.thing-model-tabs :deep(.ant-tabs-tabpane) {
padding: 0 !important;
}
</style>

View File

@@ -8,9 +8,18 @@ import { ContentWrap } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { formatDate } from '@vben/utils'; import { formatDate } from '@vben/utils';
import { Pagination } from 'ant-design-vue'; import {
Button,
Divider,
Form,
Pagination,
RangePicker,
Select,
Table,
Tag,
} from 'ant-design-vue';
import { DeviceApi } from '#/api/iot/device/device'; import { getDeviceMessagePairPage } from '#/api/iot/device/device';
import { import {
getEventTypeLabel, getEventTypeLabel,
IotDeviceMessageMethodEnum, IotDeviceMessageMethodEnum,
@@ -29,7 +38,7 @@ const queryParams = reactive({
deviceId: props.deviceId, deviceId: props.deviceId,
method: IotDeviceMessageMethodEnum.EVENT_POST.method, // 固定筛选事件消息 method: IotDeviceMessageMethodEnum.EVENT_POST.method, // 固定筛选事件消息
identifier: '', identifier: '',
times: [] as any[], times: undefined,
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
}); });
@@ -44,53 +53,53 @@ const eventThingModels = computed(() => {
}); });
/** 查询列表 */ /** 查询列表 */
const getList = async () => { async function getList() {
if (!props.deviceId) return; if (!props.deviceId) return;
loading.value = true; loading.value = true;
try { try {
const data = await DeviceApi.getDeviceMessagePairPage(queryParams); const data = await getDeviceMessagePairPage(queryParams);
list.value = data.list || []; list.value = data.list || [];
total.value = data.total || 0; total.value = data.total || 0;
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; }
/** 搜索按钮操作 */ /** 搜索按钮操作 */
const handleQuery = () => { function handleQuery() {
queryParams.pageNo = 1; queryParams.pageNo = 1;
getList(); getList();
}; }
/** 重置按钮操作 */ /** 重置按钮操作 */
const resetQuery = () => { function resetQuery() {
queryFormRef.value?.resetFields(); queryFormRef.value?.resetFields();
queryParams.identifier = ''; queryParams.identifier = '';
queryParams.times = []; queryParams.times = undefined;
handleQuery(); handleQuery();
}; }
/** 获取事件名称 */ /** 获取事件名称 */
const getEventName = (identifier: string | undefined) => { function getEventName(identifier: string | undefined) {
if (!identifier) return '-'; if (!identifier) return '-';
const event = eventThingModels.value.find( const event = eventThingModels.value.find(
(item: ThingModelData) => item.identifier === identifier, (item: ThingModelData) => item.identifier === identifier,
); );
return event?.name || identifier; return event?.name || identifier;
}; }
/** 获取事件类型 */ /** 获取事件类型 */
const getEventType = (identifier: string | undefined) => { function getEventType(identifier: string | undefined) {
if (!identifier) return '-'; if (!identifier) return '-';
const event = eventThingModels.value.find( const event = eventThingModels.value.find(
(item: ThingModelData) => item.identifier === identifier, (item: ThingModelData) => item.identifier === identifier,
); );
if (!event?.event?.type) return '-'; if (!event?.event?.type) return '-';
return getEventTypeLabel(event.event.type) || '-'; return getEventTypeLabel(event.event.type) || '-';
}; }
/** 解析参数 */ /** 解析参数 */
const parseParams = (params: string) => { function parseParams(params: string) {
try { try {
const parsed = JSON.parse(params); const parsed = JSON.parse(params);
if (parsed.params) { if (parsed.params) {
@@ -100,7 +109,7 @@ const parseParams = (params: string) => {
} catch { } catch {
return {}; return {};
} }
}; }
/** 初始化 */ /** 初始化 */
onMounted(() => { onMounted(() => {
@@ -111,58 +120,58 @@ onMounted(() => {
<template> <template>
<ContentWrap> <ContentWrap>
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<a-form <Form
:model="queryParams" :model="queryParams"
ref="queryFormRef" ref="queryFormRef"
layout="inline" layout="inline"
@submit.prevent @submit.prevent
style="margin-bottom: 16px" style="margin-bottom: 16px"
> >
<a-form-item label="标识符" name="identifier"> <Form.Item label="标识符" name="identifier">
<a-select <Select
v-model:value="queryParams.identifier" v-model:value="queryParams.identifier"
placeholder="请选择事件标识符" placeholder="请选择事件标识符"
allow-clear allow-clear
style="width: 240px" style="width: 240px"
> >
<a-select-option <Select.Option
v-for="event in eventThingModels" v-for="event in eventThingModels"
:key="event.identifier" :key="event.identifier"
:value="event.identifier!" :value="event.identifier!"
> >
{{ event.name }}({{ event.identifier }}) {{ event.name }}({{ event.identifier }})
</a-select-option> </Select.Option>
</a-select> </Select>
</a-form-item> </Form.Item>
<a-form-item label="时间范围" name="times"> <Form.Item label="时间范围" name="times">
<a-range-picker <RangePicker
v-model:value="queryParams.times" v-model:value="queryParams.times"
show-time show-time
format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss"
style="width: 360px" style="width: 360px"
/> />
</a-form-item> </Form.Item>
<a-form-item> <Form.Item>
<a-button type="primary" @click="handleQuery"> <Button type="primary" @click="handleQuery">
<template #icon> <template #icon>
<IconifyIcon icon="ep:search" /> <IconifyIcon icon="ep:search" />
</template> </template>
搜索 搜索
</a-button> </Button>
<a-button @click="resetQuery" style="margin-left: 8px"> <Button @click="resetQuery" style="margin-left: 8px">
<template #icon> <template #icon>
<IconifyIcon icon="ep:refresh" /> <IconifyIcon icon="ep:refresh" />
</template> </template>
重置 重置
</a-button> </Button>
</a-form-item> </Form.Item>
</a-form> </Form>
<a-divider style="margin: 16px 0" /> <Divider style="margin: 16px 0" />
<!-- 事件列表 --> <!-- 事件列表 -->
<a-table v-loading="loading" :data-source="list" :pagination="false"> <Table v-loading="loading" :data-source="list" :pagination="false">
<a-table-column <Table.Column
title="上报时间" title="上报时间"
align="center" align="center"
data-index="reportTime" data-index="reportTime"
@@ -175,20 +184,20 @@ onMounted(() => {
: '-' : '-'
}} }}
</template> </template>
</a-table-column> </Table.Column>
<a-table-column <Table.Column
title="标识符" title="标识符"
align="center" align="center"
data-index="identifier" data-index="identifier"
:width="160" :width="160"
> >
<template #default="{ record }"> <template #default="{ record }">
<a-tag color="blue" size="small"> <Tag color="blue" size="small">
{{ record.request?.identifier }} {{ record.request?.identifier }}
</a-tag> </Tag>
</template> </template>
</a-table-column> </Table.Column>
<a-table-column <Table.Column
title="事件名称" title="事件名称"
align="center" align="center"
data-index="eventName" data-index="eventName"
@@ -197,8 +206,8 @@ onMounted(() => {
<template #default="{ record }"> <template #default="{ record }">
{{ getEventName(record.request?.identifier) }} {{ getEventName(record.request?.identifier) }}
</template> </template>
</a-table-column> </Table.Column>
<a-table-column <Table.Column
title="事件类型" title="事件类型"
align="center" align="center"
data-index="eventType" data-index="eventType"
@@ -207,13 +216,13 @@ onMounted(() => {
<template #default="{ record }"> <template #default="{ record }">
{{ getEventType(record.request?.identifier) }} {{ getEventType(record.request?.identifier) }}
</template> </template>
</a-table-column> </Table.Column>
<a-table-column title="输入参数" align="center" data-index="params"> <Table.Column title="输入参数" align="center" data-index="params">
<template #default="{ record }"> <template #default="{ record }">
{{ parseParams(record.request.params) }} {{ parseParams(record.request.params) }}
</template> </template>
</a-table-column> </Table.Column>
</a-table> </Table>
<!-- 分页 --> <!-- 分页 -->
<Pagination <Pagination

View File

@@ -1,6 +1,6 @@
<!-- 设备属性管理 --> <!-- 设备属性管理 -->
<script setup lang="ts"> <script setup lang="ts">
import type { IotDevicePropertyDetailRespVO } from '#/api/iot/device/device'; import type { IotDeviceApi } from '#/api/iot/device/device';
import { onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'; import { onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
@@ -8,15 +8,27 @@ import { ContentWrap } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { formatDate } from '@vben/utils'; import { formatDate } from '@vben/utils';
import { DeviceApi } from '#/api/iot/device/device'; import {
Button,
Card,
Col,
Divider,
Input,
Row,
Switch,
Table,
Tag,
} from 'ant-design-vue';
import { getLatestDeviceProperties } from '#/api/iot/device/device';
import DeviceDetailsThingModelPropertyHistory from './DeviceDetailsThingModelPropertyHistory.vue'; import DeviceDetailsThingModelPropertyHistory from './DeviceDetailsThingModelPropertyHistory.vue';
const props = defineProps<{ deviceId: number }>(); const props = defineProps<{ deviceId: number }>();
const loading = ref(true); // 列表的加载中 const loading = ref(true); // 列表的加载中
const list = ref<IotDevicePropertyDetailRespVO[]>([]); // 显示的列表数据 const list = ref<IotDeviceApi.DevicePropertyDetail[]>([]); // 显示的列表数据
const filterList = ref<IotDevicePropertyDetailRespVO[]>([]); // 完整的数据列表 const filterList = ref<IotDeviceApi.DevicePropertyDetail[]>([]); // 完整的数据列表
const queryParams = reactive({ const queryParams = reactive({
keyword: '' as string, keyword: '' as string,
}); });
@@ -25,7 +37,7 @@ let autoRefreshTimer: any = null; // 定时器
const viewMode = ref<'card' | 'list'>('card'); // 视图模式状态 const viewMode = ref<'card' | 'list'>('card'); // 视图模式状态
/** 查询列表 */ /** 查询列表 */
const getList = async () => { async function getList() {
loading.value = true; loading.value = true;
try { try {
const params = { const params = {
@@ -33,50 +45,46 @@ const getList = async () => {
identifier: undefined as string | undefined, identifier: undefined as string | undefined,
name: undefined as string | undefined, name: undefined as string | undefined,
}; };
filterList.value = await DeviceApi.getLatestDeviceProperties(params); filterList.value = await getLatestDeviceProperties(params);
handleFilter(); handleFilter();
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; }
/** 前端筛选数据 */ /** 前端筛选数据 */
const handleFilter = () => { function handleFilter() {
if (queryParams.keyword.trim()) { if (queryParams.keyword.trim()) {
const keyword = queryParams.keyword.toLowerCase(); const keyword = queryParams.keyword.toLowerCase();
list.value = filterList.value.filter( list.value = filterList.value.filter(
(item: IotDevicePropertyDetailRespVO) => (item: IotDeviceApi.DevicePropertyDetail) =>
item.identifier?.toLowerCase().includes(keyword) || item.identifier?.toLowerCase().includes(keyword) ||
item.name?.toLowerCase().includes(keyword), item.name?.toLowerCase().includes(keyword),
); );
} else { } else {
list.value = filterList.value; list.value = filterList.value;
} }
}; }
/** 搜索按钮操作 */ /** 搜索按钮操作 */
const handleQuery = () => { function handleQuery() {
handleFilter(); handleFilter();
}; }
/** 历史操作 */ /** 历史操作 */
const historyRef = ref(); const historyRef = ref();
const openHistory = ( function openHistory(deviceId: number, identifier: string, dataType: string) {
deviceId: number,
identifier: string,
dataType: string,
) => {
historyRef.value.open(deviceId, identifier, dataType); historyRef.value.open(deviceId, identifier, dataType);
}; }
/** 格式化属性值和单位 */ /** 格式化属性值和单位 */
const formatValueWithUnit = (item: IotDevicePropertyDetailRespVO) => { function formatValueWithUnit(item: IotDeviceApi.DevicePropertyDetail) {
if (item.value === null || item.value === undefined || item.value === '') { if (item.value === null || item.value === undefined || item.value === '') {
return '-'; return '-';
} }
const unitName = item.dataSpecs?.unitName; const unitName = item.dataSpecs?.unitName;
return unitName ? `${item.value} ${unitName}` : item.value; return unitName ? `${item.value} ${unitName}` : item.value;
}; }
/** 监听自动刷新 */ /** 监听自动刷新 */
watch(autoRefresh, (newValue) => { watch(autoRefresh, (newValue) => {
@@ -108,7 +116,7 @@ onMounted(() => {
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<div class="flex items-center justify-between" style="margin-bottom: 16px"> <div class="flex items-center justify-between" style="margin-bottom: 16px">
<div class="flex items-center" style="gap: 16px"> <div class="flex items-center" style="gap: 16px">
<a-input <Input
v-model:value="queryParams.keyword" v-model:value="queryParams.keyword"
placeholder="请输入属性名称、标识符" placeholder="请输入属性名称、标识符"
allow-clear allow-clear
@@ -117,32 +125,32 @@ onMounted(() => {
/> />
<div class="flex items-center" style="gap: 8px"> <div class="flex items-center" style="gap: 8px">
<span style="font-size: 14px; color: #666">自动刷新</span> <span style="font-size: 14px; color: #666">自动刷新</span>
<a-switch v-model:checked="autoRefresh" size="small" /> <Switch v-model:checked="autoRefresh" size="small" />
</div> </div>
</div> </div>
<a-button-group> <Button.Group>
<a-button <Button
:type="viewMode === 'card' ? 'primary' : 'default'" :type="viewMode === 'card' ? 'primary' : 'default'"
@click="viewMode = 'card'" @click="viewMode = 'card'"
> >
<IconifyIcon icon="ep:grid" /> <IconifyIcon icon="ep:grid" />
</a-button> </Button>
<a-button <Button
:type="viewMode === 'list' ? 'primary' : 'default'" :type="viewMode === 'list' ? 'primary' : 'default'"
@click="viewMode = 'list'" @click="viewMode = 'list'"
> >
<IconifyIcon icon="ep:list" /> <IconifyIcon icon="ep:list" />
</a-button> </Button>
</a-button-group> </Button.Group>
</div> </div>
<!-- 分隔线 --> <!-- 分隔线 -->
<a-divider style="margin: 16px 0" /> <Divider style="margin: 16px 0" />
<!-- 卡片视图 --> <!-- 卡片视图 -->
<template v-if="viewMode === 'card'"> <template v-if="viewMode === 'card'">
<a-row :gutter="16" v-loading="loading"> <Row :gutter="16" v-loading="loading">
<a-col <Col
v-for="item in list" v-for="item in list"
:key="item.identifier" :key="item.identifier"
:xs="24" :xs="24"
@@ -151,7 +159,7 @@ onMounted(() => {
:lg="6" :lg="6"
class="mb-4" class="mb-4"
> >
<a-card <Card
class="relative h-full overflow-hidden transition-colors" class="relative h-full overflow-hidden transition-colors"
:body-style="{ padding: '0' }" :body-style="{ padding: '0' }"
> >
@@ -171,15 +179,15 @@ onMounted(() => {
<div class="font-600 flex-1 text-[16px]">{{ item.name }}</div> <div class="font-600 flex-1 text-[16px]">{{ item.name }}</div>
<!-- 标识符 --> <!-- 标识符 -->
<div class="mr-2 inline-flex items-center"> <div class="mr-2 inline-flex items-center">
<a-tag size="small" color="blue"> <Tag size="small" color="blue">
{{ item.identifier }} {{ item.identifier }}
</a-tag> </Tag>
</div> </div>
<!-- 数据类型标签 --> <!-- 数据类型标签 -->
<div class="mr-2 inline-flex items-center"> <div class="mr-2 inline-flex items-center">
<a-tag size="small"> <Tag size="small">
{{ item.dataType }} {{ item.dataType }}
</a-tag> </Tag>
</div> </div>
<!-- 数据图标 - 可点击 --> <!-- 数据图标 - 可点击 -->
<div <div
@@ -211,26 +219,22 @@ onMounted(() => {
</div> </div>
</div> </div>
</div> </div>
</a-card> </Card>
</a-col> </Col>
</a-row> </Row>
</template> </template>
<!-- 列表视图 --> <!-- 列表视图 -->
<a-table v-else v-loading="loading" :data-source="list" :pagination="false"> <Table v-else v-loading="loading" :data-source="list" :pagination="false">
<a-table-column <Table.Column title="属性标识符" align="center" data-index="identifier" />
title="属性标识符" <Table.Column title="属性名称" align="center" data-index="name" />
align="center" <Table.Column title="数据类型" align="center" data-index="dataType" />
data-index="identifier" <Table.Column title="属性值" align="center" data-index="value">
/>
<a-table-column title="属性名称" align="center" data-index="name" />
<a-table-column title="数据类型" align="center" data-index="dataType" />
<a-table-column title="属性值" align="center" data-index="value">
<template #default="{ record }"> <template #default="{ record }">
{{ formatValueWithUnit(record) }} {{ formatValueWithUnit(record) }}
</template> </template>
</a-table-column> </Table.Column>
<a-table-column <Table.Column
title="更新时间" title="更新时间"
align="center" align="center"
data-index="updateTime" data-index="updateTime"
@@ -239,20 +243,20 @@ onMounted(() => {
<template #default="{ record }"> <template #default="{ record }">
{{ record.updateTime ? formatDate(record.updateTime) : '-' }} {{ record.updateTime ? formatDate(record.updateTime) : '-' }}
</template> </template>
</a-table-column> </Table.Column>
<a-table-column title="操作" align="center"> <Table.Column title="操作" align="center">
<template #default="{ record }"> <template #default="{ record }">
<a-button <Button
type="link" type="link"
@click=" @click="
openHistory(props.deviceId, record.identifier, record.dataType) openHistory(props.deviceId, record.identifier, record.dataType)
" "
> >
查看数据 查看数据
</a-button> </Button>
</template> </template>
</a-table-column> </Table.Column>
</a-table> </Table>
<!-- 表单弹窗添加/修改 --> <!-- 表单弹窗添加/修改 -->
<DeviceDetailsThingModelPropertyHistory <DeviceDetailsThingModelPropertyHistory
@@ -264,7 +268,7 @@ onMounted(() => {
<style scoped> <style scoped>
/* 移除 a-row 的额外边距 */ /* 移除 a-row 的额外边距 */
:deep(.ant-row) { :deep(.ant-row) {
margin-left: -8px !important;
margin-right: -8px !important; margin-right: -8px !important;
margin-left: -8px !important;
} }
</style> </style>

View File

@@ -4,17 +4,27 @@ import type { Dayjs } from 'dayjs';
import type { EchartsUIType } from '@vben/plugins/echarts'; import type { EchartsUIType } from '@vben/plugins/echarts';
import type { IotDevicePropertyRespVO } from '#/api/iot/device/device'; import type { IotDeviceApi } from '#/api/iot/device/device';
import { computed, nextTick, reactive, ref, watch } from 'vue'; import { computed, nextTick, reactive, ref, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { beginOfDay, endOfDay, formatDate } from '@vben/utils'; import { beginOfDay, endOfDay, formatDate } from '@vben/utils';
import { Empty, message, Modal } from 'ant-design-vue'; import {
Button,
Empty,
message,
Modal,
RangePicker,
Space,
Spin,
Tag,
} from 'ant-design-vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { DeviceApi } from '#/api/iot/device/device'; import { getHistoryDevicePropertyList } from '#/api/iot/device/device';
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants'; import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
/** IoT 设备属性历史数据详情 */ /** IoT 设备属性历史数据详情 */
@@ -26,7 +36,7 @@ const dialogVisible = ref(false); // 弹窗的是否展示
const loading = ref(false); const loading = ref(false);
const exporting = ref(false); const exporting = ref(false);
const viewMode = ref<'chart' | 'list'>('chart'); // 视图模式状态 const viewMode = ref<'chart' | 'list'>('chart'); // 视图模式状态
const list = ref<IotDevicePropertyRespVO[]>([]); // 列表的数据 const list = ref<IotDeviceApi.DevicePropertyDetail[]>([]); // 列表的数据
const total = ref(0); // 总数据量 const total = ref(0); // 总数据量
const thingModelDataType = ref<string>(''); // 物模型数据类型 const thingModelDataType = ref<string>(''); // 物模型数据类型
const propertyIdentifier = ref<string>(''); // 属性标识符 const propertyIdentifier = ref<string>(''); // 属性标识符
@@ -62,7 +72,7 @@ const maxValue = computed(() => {
if (isComplexDataType.value || list.value.length === 0) return '-'; if (isComplexDataType.value || list.value.length === 0) return '-';
const values = list.value const values = list.value
.map((item) => Number(item.value)) .map((item) => Number(item.value))
.filter((v) => !isNaN(v)); .filter((v) => !Number.isNaN(v));
return values.length > 0 ? Math.max(...values).toFixed(2) : '-'; return values.length > 0 ? Math.max(...values).toFixed(2) : '-';
}); });
@@ -70,7 +80,7 @@ const minValue = computed(() => {
if (isComplexDataType.value || list.value.length === 0) return '-'; if (isComplexDataType.value || list.value.length === 0) return '-';
const values = list.value const values = list.value
.map((item) => Number(item.value)) .map((item) => Number(item.value))
.filter((v) => !isNaN(v)); .filter((v) => !Number.isNaN(v));
return values.length > 0 ? Math.min(...values).toFixed(2) : '-'; return values.length > 0 ? Math.min(...values).toFixed(2) : '-';
}); });
@@ -78,7 +88,7 @@ const avgValue = computed(() => {
if (isComplexDataType.value || list.value.length === 0) return '-'; if (isComplexDataType.value || list.value.length === 0) return '-';
const values = list.value const values = list.value
.map((item) => Number(item.value)) .map((item) => Number(item.value))
.filter((v) => !isNaN(v)); .filter((v) => !Number.isNaN(v));
if (values.length === 0) return '-'; if (values.length === 0) return '-';
const sum = values.reduce((acc, val) => acc + val, 0); const sum = values.reduce((acc, val) => acc + val, 0);
return (sum / values.length).toFixed(2); return (sum / values.length).toFixed(2);
@@ -120,11 +130,11 @@ const paginationConfig = computed(() => ({
})); }));
/** 获得设备历史数据 */ /** 获得设备历史数据 */
const getList = async () => { async function getList() {
loading.value = true; loading.value = true;
try { try {
const data = await DeviceApi.getHistoryDevicePropertyList(queryParams); const data = await getHistoryDevicePropertyList(queryParams);
list.value = data?.list || []; list.value = (data?.list as IotDeviceApi.DevicePropertyDetail[]) || [];
total.value = list.value.length; total.value = list.value.length;
// 如果是图表模式且不是复杂数据类型,渲染图表 // 如果是图表模式且不是复杂数据类型,渲染图表
@@ -143,10 +153,10 @@ const getList = async () => {
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; }
/** 渲染图表 */ /** 渲染图表 */
const renderChart = () => { function renderChart() {
if (!list.value || list.value.length === 0) return; if (!list.value || list.value.length === 0) return;
const chartData = list.value.map((item) => [item.updateTime, item.value]); const chartData = list.value.map((item) => [item.updateTime, item.value]);
@@ -255,10 +265,10 @@ const renderChart = () => {
}, },
], ],
}); });
}; }
/** 打开弹窗 */ /** 打开弹窗 */
const open = async (deviceId: number, identifier: string, dataType: string) => { async function open(deviceId: number, identifier: string, dataType: string) {
dialogVisible.value = true; dialogVisible.value = true;
queryParams.deviceId = deviceId; queryParams.deviceId = deviceId;
queryParams.identifier = identifier; queryParams.identifier = identifier;
@@ -271,10 +281,10 @@ const open = async (deviceId: number, identifier: string, dataType: string) => {
// 等待弹窗完全渲染后再获取数据 // 等待弹窗完全渲染后再获取数据
await nextTick(); await nextTick();
await getList(); await getList();
}; }
/** 时间变化处理 */ /** 时间变化处理 */
const handleTimeChange = () => { function handleTimeChange() {
if (!dateRange.value || dateRange.value.length !== 2) { if (!dateRange.value || dateRange.value.length !== 2) {
return; return;
} }
@@ -285,15 +295,15 @@ const handleTimeChange = () => {
]; ];
getList(); getList();
}; }
/** 刷新数据 */ /** 刷新数据 */
const handleRefresh = () => { function handleRefresh() {
getList(); getList();
}; }
/** 导出数据 */ /** 导出数据 */
const handleExport = async () => { async function handleExport() {
if (list.value.length === 0) { if (list.value.length === 0) {
message.warning('暂无数据可导出'); message.warning('暂无数据可导出');
return; return;
@@ -338,22 +348,22 @@ const handleExport = async () => {
} finally { } finally {
exporting.value = false; exporting.value = false;
} }
}; }
/** 关闭弹窗 */ /** 关闭弹窗 */
const handleClose = () => { function handleClose() {
dialogVisible.value = false; dialogVisible.value = false;
list.value = []; list.value = [];
total.value = 0; total.value = 0;
}; }
/** 格式化复杂数据类型 */ /** 格式化复杂数据类型 */
const formatComplexValue = (value: any) => { function formatComplexValue(value: any) {
if (typeof value === 'object') { if (typeof value === 'object') {
return JSON.stringify(value); return JSON.stringify(value);
} }
return String(value); return String(value);
}; }
/** 监听视图模式变化,重新渲染图表 */ /** 监听视图模式变化,重新渲染图表 */
watch(viewMode, async (newMode) => { watch(viewMode, async (newMode) => {
@@ -380,78 +390,78 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
<div class="property-history-container"> <div class="property-history-container">
<!-- 工具栏 --> <!-- 工具栏 -->
<div class="toolbar-wrapper mb-4"> <div class="toolbar-wrapper mb-4">
<a-space :size="12" class="w-full" wrap> <Space :size="12" class="w-full" wrap>
<!-- 时间选择 --> <!-- 时间选择 -->
<a-range-picker <RangePicker
v-model:value="dateRange" v-model:value="dateRange"
:show-time="{ format: 'HH:mm:ss' }" :show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss"
:placeholder="['开始时间', '结束时间']" :placeholder="['开始时间', '结束时间']"
class="!w-[400px]" class="!w-[400px]"
@change="handleTimeChange" @press-enter="handleTimeChange"
/> />
<!-- 刷新按钮 --> <!-- 刷新按钮 -->
<a-button @click="handleRefresh" :loading="loading"> <Button @click="handleRefresh" :loading="loading">
<template #icon> <template #icon>
<Icon icon="ant-design:reload-outlined" /> <IconifyIcon icon="ant-design:reload-outlined" />
</template> </template>
刷新 刷新
</a-button> </Button>
<!-- 导出按钮 --> <!-- 导出按钮 -->
<a-button <Button
@click="handleExport" @click="handleExport"
:loading="exporting" :loading="exporting"
:disabled="list.length === 0" :disabled="list.length === 0"
> >
<template #icon> <template #icon>
<Icon icon="ant-design:export-outlined" /> <IconifyIcon icon="ant-design:export-outlined" />
</template> </template>
导出 导出
</a-button> </Button>
<!-- 视图切换 --> <!-- 视图切换 -->
<a-button-group class="ml-auto"> <Button.Group class="ml-auto">
<a-button <Button
:type="viewMode === 'chart' ? 'primary' : 'default'" :type="viewMode === 'chart' ? 'primary' : 'default'"
@click="viewMode = 'chart'" @click="viewMode = 'chart'"
:disabled="isComplexDataType" :disabled="isComplexDataType"
> >
<template #icon> <template #icon>
<Icon icon="ant-design:line-chart-outlined" /> <IconifyIcon icon="ant-design:line-chart-outlined" />
</template> </template>
图表 图表
</a-button> </Button>
<a-button <Button
:type="viewMode === 'list' ? 'primary' : 'default'" :type="viewMode === 'list' ? 'primary' : 'default'"
@click="viewMode = 'list'" @click="viewMode = 'list'"
> >
<template #icon> <template #icon>
<Icon icon="ant-design:table-outlined" /> <IconifyIcon icon="ant-design:table-outlined" />
</template> </template>
列表 列表
</a-button> </Button>
</a-button-group> </Button.Group>
</a-space> </Space>
<!-- 数据统计信息 --> <!-- 数据统计信息 -->
<div v-if="list.length > 0" class="mt-3 text-sm text-gray-600"> <div v-if="list.length > 0" class="mt-3 text-sm text-gray-600">
<a-space :size="16"> <Space :size="16">
<span> {{ total }} 条数据</span> <span> {{ total }} 条数据</span>
<span v-if="viewMode === 'chart' && !isComplexDataType"> <span v-if="viewMode === 'chart' && !isComplexDataType">
最大值: {{ maxValue }} | 最小值: {{ minValue }} | 平均值: 最大值: {{ maxValue }} | 最小值: {{ minValue }} | 平均值:
{{ avgValue }} {{ avgValue }}
</span> </span>
</a-space> </Space>
</div> </div>
</div> </div>
<!-- 数据展示区域 --> <!-- 数据展示区域 -->
<a-spin :spinning="loading" :delay="200"> <Spin :spinning="loading" :delay="200">
<!-- 图表模式 --> <!-- 图表模式 -->
<div v-if="viewMode === 'chart'" class="chart-container"> <div v-if="viewMode === 'chart'" class="chart-container">
<a-empty <Empty
v-if="list.length === 0" v-if="list.length === 0"
:image="Empty.PRESENTED_IMAGE_SIMPLE" :image="Empty.PRESENTED_IMAGE_SIMPLE"
description="暂无数据" description="暂无数据"
@@ -462,7 +472,7 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
<!-- 表格模式 --> <!-- 表格模式 -->
<div v-else class="table-container"> <div v-else class="table-container">
<a-table <Table
:data-source="list" :data-source="list"
:columns="tableColumns" :columns="tableColumns"
:pagination="paginationConfig" :pagination="paginationConfig"
@@ -475,19 +485,19 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
{{ formatDate(new Date(record.updateTime)) }} {{ formatDate(new Date(record.updateTime)) }}
</template> </template>
<template v-else-if="column.key === 'value'"> <template v-else-if="column.key === 'value'">
<a-tag v-if="isComplexDataType" color="processing"> <Tag v-if="isComplexDataType" color="processing">
{{ formatComplexValue(record.value) }} {{ formatComplexValue(record.value) }}
</a-tag> </Tag>
<span v-else class="font-medium">{{ record.value }}</span> <span v-else class="font-medium">{{ record.value }}</span>
</template> </template>
</template> </template>
</a-table> </Table>
</div> </div>
</a-spin> </Spin>
</div> </div>
<template #footer> <template #footer>
<a-button @click="handleClose">关闭</a-button> <Button @click="handleClose">关闭</Button>
</template> </template>
</Modal> </Modal>
</template> </template>

View File

@@ -8,9 +8,17 @@ import { ContentWrap } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { formatDate } from '@vben/utils'; import { formatDate } from '@vben/utils';
import { Pagination } from 'ant-design-vue'; import {
Button,
Divider,
Form,
Pagination,
Select,
Table,
Tag,
} from 'ant-design-vue';
import { DeviceApi } from '#/api/iot/device/device'; import { getDeviceMessagePairPage } from '#/api/iot/device/device';
import { import {
getThingModelServiceCallTypeLabel, getThingModelServiceCallTypeLabel,
IotDeviceMessageMethodEnum, IotDeviceMessageMethodEnum,
@@ -48,8 +56,8 @@ const getList = async () => {
if (!props.deviceId) return; if (!props.deviceId) return;
loading.value = true; loading.value = true;
try { try {
const data = await DeviceApi.getDeviceMessagePairPage(queryParams); const data = await getDeviceMessagePairPage(queryParams);
list.value = data.list; list.value = data.list || [];
total.value = data.total; total.value = data.total;
} finally { } finally {
loading.value = false; loading.value = false;
@@ -112,58 +120,58 @@ onMounted(() => {
<template> <template>
<ContentWrap> <ContentWrap>
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<a-form <Form
:model="queryParams" :model="queryParams"
ref="queryFormRef" ref="queryFormRef"
layout="inline" layout="inline"
@submit.prevent @submit.prevent
style="margin-bottom: 16px" style="margin-bottom: 16px"
> >
<a-form-item label="标识符" name="identifier"> <Form.Item label="标识符" name="identifier">
<a-select <Select
v-model:value="queryParams.identifier" v-model:value="queryParams.identifier"
placeholder="请选择服务标识符" placeholder="请选择服务标识符"
allow-clear allow-clear
style="width: 240px" style="width: 240px"
> >
<a-select-option <Select.Option
v-for="service in serviceThingModels" v-for="service in serviceThingModels"
:key="service.identifier" :key="service.identifier"
:value="service.identifier!" :value="service.identifier!"
> >
{{ service.name }}({{ service.identifier }}) {{ service.name }}({{ service.identifier }})
</a-select-option> </Select.Option>
</a-select> </Select>
</a-form-item> </Form.Item>
<a-form-item label="时间范围" name="times"> <Form.Item label="时间范围" name="times">
<a-range-picker <RangePicker
v-model:value="queryParams.times" v-model:value="queryParams.times"
show-time show-time
format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss"
style="width: 360px" style="width: 360px"
/> />
</a-form-item> </Form.Item>
<a-form-item> <Form.Item>
<a-button type="primary" @click="handleQuery"> <Button type="primary" @click="handleQuery">
<template #icon> <template #icon>
<IconifyIcon icon="ep:search" /> <IconifyIcon icon="ep:search" />
</template> </template>
搜索 搜索
</a-button> </Button>
<a-button @click="resetQuery" style="margin-left: 8px"> <Button @click="resetQuery" style="margin-left: 8px">
<template #icon> <template #icon>
<IconifyIcon icon="ep:refresh" /> <IconifyIcon icon="ep:refresh" />
</template> </template>
重置 重置
</a-button> </Button>
</a-form-item> </Form.Item>
</a-form> </Form>
<a-divider style="margin: 16px 0" /> <Divider style="margin: 16px 0" />
<!-- 服务调用列表 --> <!-- 服务调用列表 -->
<a-table v-loading="loading" :data-source="list" :pagination="false"> <Table v-loading="loading" :data-source="list" :pagination="false">
<a-table-column <Table.Column
title="调用时间" title="调用时间"
align="center" align="center"
data-index="requestTime" data-index="requestTime"
@@ -176,8 +184,8 @@ onMounted(() => {
: '-' : '-'
}} }}
</template> </template>
</a-table-column> </Table.Column>
<a-table-column <Table.Column
title="响应时间" title="响应时间"
align="center" align="center"
data-index="responseTime" data-index="responseTime"
@@ -188,20 +196,20 @@ onMounted(() => {
record.reply?.reportTime ? formatDate(record.reply.reportTime) : '-' record.reply?.reportTime ? formatDate(record.reply.reportTime) : '-'
}} }}
</template> </template>
</a-table-column> </Table.Column>
<a-table-column <Table.Column
title="标识符" title="标识符"
align="center" align="center"
data-index="identifier" data-index="identifier"
:width="160" :width="160"
> >
<template #default="{ record }"> <template #default="{ record }">
<a-tag color="blue" size="small"> <Tag color="blue" size="small">
{{ record.request?.identifier }} {{ record.request?.identifier }}
</a-tag> </Tag>
</template> </template>
</a-table-column> </Table.Column>
<a-table-column <Table.Column
title="服务名称" title="服务名称"
align="center" align="center"
data-index="serviceName" data-index="serviceName"
@@ -210,8 +218,8 @@ onMounted(() => {
<template #default="{ record }"> <template #default="{ record }">
{{ getServiceName(record.request?.identifier) }} {{ getServiceName(record.request?.identifier) }}
</template> </template>
</a-table-column> </Table.Column>
<a-table-column <Table.Column
title="调用方式" title="调用方式"
align="center" align="center"
data-index="callType" data-index="callType"
@@ -220,13 +228,13 @@ onMounted(() => {
<template #default="{ record }"> <template #default="{ record }">
{{ getCallType(record.request?.identifier) }} {{ getCallType(record.request?.identifier) }}
</template> </template>
</a-table-column> </Table.Column>
<a-table-column title="输入参数" align="center" data-index="inputParams"> <Table.Column title="输入参数" align="center" data-index="inputParams">
<template #default="{ record }"> <template #default="{ record }">
{{ parseParams(record.request?.params) }} {{ parseParams(record.request?.params) }}
</template> </template>
</a-table-column> </Table.Column>
<a-table-column title="输出参数" align="center" data-index="outputParams"> <Table.Column title="输出参数" align="center" data-index="outputParams">
<template #default="{ record }"> <template #default="{ record }">
<span v-if="record.reply"> <span v-if="record.reply">
{{ {{
@@ -235,8 +243,8 @@ onMounted(() => {
</span> </span>
<span v-else>-</span> <span v-else>-</span>
</template> </template>
</a-table-column> </Table.Column>
</a-table> </Table>
<!-- 分页 --> <!-- 分页 -->
<Pagination <Pagination

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { DeviceVO } from '#/api/iot/device/device'; import type { IotDeviceApi } from '#/api/iot/device/device';
import type { ProductVO } from '#/api/iot/product/product'; import type { IotProductApi } from '#/api/iot/product/product';
import type { ThingModelData } from '#/api/iot/thingmodel'; import type { ThingModelData } from '#/api/iot/thingmodel';
import { onMounted, ref, unref } from 'vue'; import { onMounted, ref, unref } from 'vue';
@@ -9,11 +9,11 @@ import { useRoute, useRouter } from 'vue-router';
import { Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
import { useTabbarStore } from '@vben/stores'; import { useTabbarStore } from '@vben/stores';
import { message } from 'ant-design-vue'; import { message, Tabs } from 'ant-design-vue';
import { DeviceApi } from '#/api/iot/device/device'; import { getDevice } from '#/api/iot/device/device';
import { DeviceTypeEnum, ProductApi } from '#/api/iot/product/product'; import { DeviceTypeEnum, getProduct } from '#/api/iot/product/product';
import { ThingModelApi } from '#/api/iot/thingmodel'; import { getThingModelListByProductId } from '#/api/iot/thingmodel';
import DeviceDetailConfig from './DeviceDetailConfig.vue'; import DeviceDetailConfig from './DeviceDetailConfig.vue';
import DeviceDetailsHeader from './DeviceDetailsHeader.vue'; import DeviceDetailsHeader from './DeviceDetailsHeader.vue';
@@ -27,38 +27,38 @@ defineOptions({ name: 'IoTDeviceDetail' });
const route = useRoute(); const route = useRoute();
const id = Number(route.params.id); // 将字符串转换为数字 const id = Number(route.params.id); // 将字符串转换为数字
const loading = ref(true); // 加载中 const loading = ref(true); // 加载中
const product = ref<ProductVO>({} as ProductVO); // 产品详情 const product = ref<IotProductApi.Product>({} as IotProductApi.Product); // 产品详情
const device = ref<DeviceVO>({} as DeviceVO); // 设备详情 const device = ref<IotDeviceApi.Device>({} as IotDeviceApi.Device); // 设备详情
const activeTab = ref('info'); // 默认激活的标签页 const activeTab = ref('info'); // 默认激活的标签页
const thingModelList = ref<ThingModelData[]>([]); // 物模型列表数据 const thingModelList = ref<ThingModelData[]>([]); // 物模型列表数据
/** 获取设备详情 */ /** 获取设备详情 */
const getDeviceData = async () => { async function getDeviceData() {
loading.value = true; loading.value = true;
try { try {
device.value = await DeviceApi.getDevice(id); device.value = await getDevice(id);
await getProductData(device.value.productId); await getProductData(device.value.productId);
await getThingModelList(device.value.productId); await getThingModelList(device.value.productId);
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; }
/** 获取产品详情 */ /** 获取产品详情 */
const getProductData = async (id: number) => { async function getProductData(id: number) {
product.value = await ProductApi.getProduct(id); product.value = await getProduct(id);
}; }
/** 获取物模型列表 */ /** 获取物模型列表 */
const getThingModelList = async (productId: number) => { async function getThingModelList(productId: number) {
try { try {
const data = await ThingModelApi.getThingModelList(productId); const data = await getThingModelListByProductId(productId);
thingModelList.value = data || []; thingModelList.value = data || [];
} catch (error) { } catch (error) {
console.error('获取物模型列表失败:', error); console.error('获取物模型列表失败:', error);
thingModelList.value = []; thingModelList.value = [];
} }
}; }
/** 初始化 */ /** 初始化 */
const tabbarStore = useTabbarStore(); // 视图操作 const tabbarStore = useTabbarStore(); // 视图操作
@@ -83,47 +83,47 @@ onMounted(async () => {
@refresh="getDeviceData" @refresh="getDeviceData"
/> />
<a-tabs v-model:active-key="activeTab" class="device-detail-tabs mt-4"> <Tabs v-model:active-key="activeTab" class="device-detail-tabs mt-4">
<a-tab-pane key="info" tab="设备信息"> <Tabs.Pane key="info" tab="设备信息">
<DeviceDetailsInfo <DeviceDetailsInfo
v-if="activeTab === 'info'" v-if="activeTab === 'info'"
:product="product" :product="product"
:device="device" :device="device"
/> />
</a-tab-pane> </Tabs.Pane>
<a-tab-pane key="model" tab="物模型数据"> <Tabs.Pane key="model" tab="物模型数据">
<DeviceDetailsThingModel <DeviceDetailsThingModel
v-if="activeTab === 'model' && device.id" v-if="activeTab === 'model' && device.id"
:device-id="device.id" :device-id="device.id"
:thing-model-list="thingModelList" :thing-model-list="thingModelList"
/> />
</a-tab-pane> </Tabs.Pane>
<a-tab-pane <Tabs.Pane
v-if="product.deviceType === DeviceTypeEnum.GATEWAY" v-if="product.deviceType === DeviceTypeEnum.GATEWAY"
key="sub-device" key="sub-device"
tab="子设备管理" tab="子设备管理"
/> />
<a-tab-pane key="log" tab="设备消息"> <Tabs.Pane key="log" tab="设备消息">
<DeviceDetailsMessage <DeviceDetailsMessage
v-if="activeTab === 'log' && device.id" v-if="activeTab === 'log' && device.id"
:device-id="device.id" :device-id="device.id"
/> />
</a-tab-pane> </Tabs.Pane>
<a-tab-pane key="simulator" tab="模拟设备"> <Tabs.Pane key="simulator" tab="模拟设备">
<DeviceDetailsSimulator <DeviceDetailsSimulator
v-if="activeTab === 'simulator'" v-if="activeTab === 'simulator'"
:product="product" :product="product"
:device="device" :device="device"
:thing-model-list="thingModelList" :thing-model-list="thingModelList"
/> />
</a-tab-pane> </Tabs.Pane>
<a-tab-pane key="config" tab="设备配置"> <Tabs.Pane key="config" tab="设备配置">
<DeviceDetailConfig <DeviceDetailConfig
v-if="activeTab === 'config'" v-if="activeTab === 'config'"
:device="device" :device="device"
@success="getDeviceData" @success="getDeviceData"
/> />
</a-tab-pane> </Tabs.Pane>
</a-tabs> </Tabs>
</Page> </Page>
</template> </template>