fix: iot
This commit is contained in:
@@ -92,5 +92,3 @@ export function getSimpleAlertConfigList() {
|
||||
'/iot/alert-config/simple-list',
|
||||
);
|
||||
}
|
||||
|
||||
export { AlertConfigApi };
|
||||
|
||||
@@ -80,5 +80,3 @@ export function deleteAlertRecordList(ids: number[]) {
|
||||
params: { ids: ids.join(',') },
|
||||
});
|
||||
}
|
||||
|
||||
export { AlertRecordApi };
|
||||
|
||||
@@ -194,30 +194,3 @@ export function getDeviceMessagePairPage(params: PageParam) {
|
||||
export function sendDeviceMessage(params: IotDeviceApi.DeviceMessageSendReq) {
|
||||
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;
|
||||
|
||||
@@ -96,5 +96,3 @@ export function pauseOtaTask(id: number) {
|
||||
export function resumeOtaTask(id: number) {
|
||||
return requestClient.put(`/iot/ota-task/resume?id=${id}`);
|
||||
}
|
||||
|
||||
export { IoTOtaTaskApi };
|
||||
|
||||
@@ -99,5 +99,3 @@ export function getOtaTaskRecordStatusStatistics(
|
||||
{ params: { firmwareId, taskId } },
|
||||
);
|
||||
}
|
||||
|
||||
export { IoTOtaTaskRecordApi };
|
||||
|
||||
@@ -97,18 +97,3 @@ export function getProductByKey(productKey: string) {
|
||||
params: { productKey },
|
||||
});
|
||||
}
|
||||
|
||||
// Export aliases for compatibility
|
||||
export const ProductApi = {
|
||||
getProductPage,
|
||||
getProduct,
|
||||
createProduct,
|
||||
updateProduct,
|
||||
deleteProduct,
|
||||
exportProduct,
|
||||
updateProductStatus,
|
||||
getSimpleProductList,
|
||||
getProductByKey,
|
||||
};
|
||||
|
||||
export type ProductVO = IotProductApi.Product;
|
||||
|
||||
@@ -146,5 +146,3 @@ export function updateDataSinkStatus(id: number, status: number) {
|
||||
status,
|
||||
});
|
||||
}
|
||||
|
||||
export { DataSinkApi };
|
||||
|
||||
@@ -153,10 +153,3 @@ export function getSimpleRuleSceneList() {
|
||||
'/iot/scene-rule/simple-list',
|
||||
);
|
||||
}
|
||||
|
||||
// 别名导出(兼容旧代码)
|
||||
export {
|
||||
deleteSceneRule as deleteRuleScene,
|
||||
getSceneRulePage as getRuleScenePage,
|
||||
updateSceneRuleStatus as updateRuleSceneStatus,
|
||||
};
|
||||
|
||||
@@ -67,18 +67,3 @@ export function getDeviceMessageSummary(statType: number) {
|
||||
{ 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;
|
||||
|
||||
@@ -189,18 +189,3 @@ export function exportThingModelTSL(productId: number) {
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { AlertConfigApi } from '#/api/iot/alert/config';
|
||||
|
||||
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 { 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> = {
|
||||
1: '提示',
|
||||
2: '一般',
|
||||
@@ -35,10 +35,10 @@ const getLevelText = (level?: number) => {
|
||||
5: '紧急',
|
||||
};
|
||||
return level ? levelMap[level] || `级别${level}` : '-';
|
||||
};
|
||||
}
|
||||
|
||||
// 获取告警级别颜色
|
||||
const getLevelColor = (level?: number) => {
|
||||
function getLevelColor(level?: number) {
|
||||
const colorMap: Record<number, string> = {
|
||||
1: 'blue',
|
||||
2: 'green',
|
||||
@@ -47,10 +47,10 @@ const getLevelColor = (level?: number) => {
|
||||
5: 'purple',
|
||||
};
|
||||
return level ? colorMap[level] || 'default' : 'default';
|
||||
};
|
||||
}
|
||||
|
||||
// 获取接收类型文本
|
||||
const getReceiveTypeText = (type?: number) => {
|
||||
function getReceiveTypeText(type?: number) {
|
||||
const typeMap: Record<number, string> = {
|
||||
1: '站内信',
|
||||
2: '邮箱',
|
||||
@@ -59,7 +59,7 @@ const getReceiveTypeText = (type?: number) => {
|
||||
5: '钉钉',
|
||||
};
|
||||
return type ? typeMap[type] || `类型${type}` : '-';
|
||||
};
|
||||
}
|
||||
|
||||
/** 创建告警配置 */
|
||||
function handleCreate() {
|
||||
@@ -138,9 +138,9 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
|
||||
<!-- 告警级别列 -->
|
||||
<template #level="{ row }">
|
||||
<a-tag :color="getLevelColor(row.level)">
|
||||
<Tag :color="getLevelColor(row.level)">
|
||||
{{ getLevelText(row.level) }}
|
||||
</a-tag>
|
||||
</Tag>
|
||||
</template>
|
||||
|
||||
<!-- 关联场景联动规则列 -->
|
||||
@@ -150,13 +150,13 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
|
||||
<!-- 接收类型列 -->
|
||||
<template #receiveTypes="{ row }">
|
||||
<a-tag
|
||||
<Tag
|
||||
v-for="(type, index) in row.receiveTypes"
|
||||
:key="index"
|
||||
class="mr-1"
|
||||
>
|
||||
{{ getReceiveTypeText(type) }}
|
||||
</a-tag>
|
||||
</Tag>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
|
||||
@@ -5,8 +5,9 @@ import type { AlertRecord } from '#/api/iot/alert/record';
|
||||
import { h, onMounted, ref } from 'vue';
|
||||
|
||||
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 { getAlertRecordPage, processAlertRecord } from '#/api/iot/alert/record';
|
||||
@@ -26,13 +27,13 @@ function onRefresh() {
|
||||
}
|
||||
|
||||
// 加载产品和设备列表
|
||||
const loadData = async () => {
|
||||
async function loadData() {
|
||||
productList.value = await getSimpleProductList();
|
||||
deviceList.value = await getSimpleDeviceList();
|
||||
};
|
||||
}
|
||||
|
||||
// 获取告警级别文本
|
||||
const getLevelText = (level?: number) => {
|
||||
function getLevelText(level?: number) {
|
||||
const levelMap: Record<number, string> = {
|
||||
1: '提示',
|
||||
2: '一般',
|
||||
@@ -41,10 +42,10 @@ const getLevelText = (level?: number) => {
|
||||
5: '紧急',
|
||||
};
|
||||
return level ? levelMap[level] || `级别${level}` : '-';
|
||||
};
|
||||
}
|
||||
|
||||
// 获取告警级别颜色
|
||||
const getLevelColor = (level?: number) => {
|
||||
function getLevelColor(level?: number) {
|
||||
const colorMap: Record<number, string> = {
|
||||
1: 'blue',
|
||||
2: 'green',
|
||||
@@ -53,24 +54,24 @@ const getLevelColor = (level?: number) => {
|
||||
5: 'purple',
|
||||
};
|
||||
return level ? colorMap[level] || 'default' : 'default';
|
||||
};
|
||||
}
|
||||
|
||||
// 获取产品名称
|
||||
const getProductName = (productId?: number) => {
|
||||
function getProductName(productId?: number) {
|
||||
if (!productId) return '-';
|
||||
const product = productList.value.find((p: any) => p.id === productId);
|
||||
return product?.name || '加载中...';
|
||||
};
|
||||
}
|
||||
|
||||
// 获取设备名称
|
||||
const getDeviceName = (deviceId?: number) => {
|
||||
function getDeviceName(deviceId?: number) {
|
||||
if (!deviceId) return '-';
|
||||
const device = deviceList.value.find((d: any) => d.id === deviceId);
|
||||
return device?.deviceName || '加载中...';
|
||||
};
|
||||
}
|
||||
|
||||
// 处理告警记录
|
||||
const handleProcess = async (row: AlertRecord) => {
|
||||
async function handleProcess(row: AlertRecord) {
|
||||
Modal.confirm({
|
||||
title: '处理告警记录',
|
||||
content: h('div', [
|
||||
@@ -90,7 +91,7 @@ const handleProcess = async (row: AlertRecord) => {
|
||||
|
||||
if (!processRemark) {
|
||||
message.warning('请输入处理原因');
|
||||
throw undefined;
|
||||
throw new Error('请输入处理原因');
|
||||
}
|
||||
|
||||
const hideLoading = message.loading({
|
||||
@@ -103,16 +104,16 @@ const handleProcess = async (row: AlertRecord) => {
|
||||
onRefresh();
|
||||
} catch (error) {
|
||||
console.error('处理失败:', error);
|
||||
return Promise.reject();
|
||||
throw error;
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// 查看告警记录详情
|
||||
const handleView = (row: AlertRecord) => {
|
||||
function handleView(row: AlertRecord) {
|
||||
Modal.info({
|
||||
title: '告警记录详情',
|
||||
width: 600,
|
||||
@@ -148,7 +149,7 @@ const handleView = (row: AlertRecord) => {
|
||||
]),
|
||||
]),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
@@ -190,9 +191,9 @@ onMounted(() => {
|
||||
<Grid table-title="告警记录列表">
|
||||
<!-- 告警级别列 -->
|
||||
<template #configLevel="{ row }">
|
||||
<a-tag :color="getLevelColor(row.configLevel)">
|
||||
<Tag :color="getLevelColor(row.configLevel)">
|
||||
{{ getLevelText(row.configLevel) }}
|
||||
</a-tag>
|
||||
</Tag>
|
||||
</template>
|
||||
|
||||
<!-- 产品名称列 -->
|
||||
@@ -207,7 +208,7 @@ onMounted(() => {
|
||||
|
||||
<!-- 设备消息列 -->
|
||||
<template #deviceMessage="{ row }">
|
||||
<a-popover
|
||||
<Popover
|
||||
v-if="row.deviceMessage"
|
||||
placement="topLeft"
|
||||
trigger="hover"
|
||||
@@ -216,11 +217,11 @@ onMounted(() => {
|
||||
<template #content>
|
||||
<pre class="text-xs">{{ row.deviceMessage }}</pre>
|
||||
</template>
|
||||
<VbenButton size="small" type="link">
|
||||
<Icon icon="ant-design:eye-outlined" class="mr-1" />
|
||||
<Button size="small" type="link">
|
||||
<IconifyIcon icon="ant-design:eye-outlined" class="mr-1" />
|
||||
查看消息
|
||||
</VbenButton>
|
||||
</a-popover>
|
||||
</Button>
|
||||
</Popover>
|
||||
<span v-else class="text-gray-400">-</span>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -6,15 +6,29 @@ import type { IotProductApi } from '#/api/iot/product/product';
|
||||
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { ContentWrap } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
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 { getSimpleDeviceGroupList } from '#/api/iot/device/group';
|
||||
import { getSimpleProductList } from '#/api/iot/product/product';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
|
||||
defineOptions({ name: 'IoTDeviceTableSelect' });
|
||||
|
||||
@@ -33,14 +47,14 @@ const props = defineProps({
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
// 获取字典选项
|
||||
const getIntDictOptions = (dictType: string) => {
|
||||
function getIntDictOptions(dictType: string) {
|
||||
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') : '';
|
||||
};
|
||||
}
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref('设备选择器');
|
||||
@@ -73,38 +87,31 @@ const columns = computed(() => {
|
||||
title: 'DeviceName',
|
||||
dataIndex: 'deviceName',
|
||||
key: 'deviceName',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '备注名称',
|
||||
dataIndex: 'nickname',
|
||||
key: 'nickname',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '所属产品',
|
||||
key: 'productId',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '设备类型',
|
||||
key: 'deviceType',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '所属分组',
|
||||
key: 'groupIds',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '设备状态',
|
||||
key: 'status',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '最后上线时间',
|
||||
key: 'onlineTime',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
},
|
||||
];
|
||||
@@ -125,7 +132,7 @@ const columns = computed(() => {
|
||||
// 多选配置
|
||||
const rowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (keys: number[], rows: IotDeviceApi.Device[]) => {
|
||||
onChange: (keys: any[], rows: IotDeviceApi.Device[]) => {
|
||||
selectedRowKeys.value = keys;
|
||||
selectedDevices.value = rows;
|
||||
},
|
||||
@@ -176,20 +183,20 @@ defineExpose({ open });
|
||||
|
||||
/** 处理行点击事件 */
|
||||
const tableRef = ref();
|
||||
const handleRowClick = (row: IotDeviceApi.Device) => {
|
||||
function handleRowClick(row: IotDeviceApi.Device) {
|
||||
if (!props.multiple) {
|
||||
selectedId.value = row.id;
|
||||
selectedDevices.value = [row];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 处理单选变更事件 */
|
||||
const handleRadioChange = (row: IotDeviceApi.Device) => {
|
||||
function handleRadioChange(row: IotDeviceApi.Device) {
|
||||
selectedId.value = row.id;
|
||||
selectedDevices.value = [row];
|
||||
};
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
async function submitForm() {
|
||||
if (selectedDevices.value.length === 0) {
|
||||
message.warning({
|
||||
content: props.multiple ? '请至少选择一个设备' : '请选择一个设备',
|
||||
@@ -201,7 +208,7 @@ const submitForm = async () => {
|
||||
props.multiple ? selectedDevices.value : selectedDevices.value[0],
|
||||
);
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
@@ -213,7 +220,7 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal
|
||||
<Modal
|
||||
:title="dialogTitle"
|
||||
v-model:open="dialogVisible"
|
||||
width="60%"
|
||||
@@ -221,54 +228,54 @@ onMounted(async () => {
|
||||
>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<a-form
|
||||
<Form
|
||||
ref="queryFormRef"
|
||||
layout="inline"
|
||||
:model="queryParams"
|
||||
class="-mb-15px"
|
||||
>
|
||||
<a-form-item v-if="!props.productId" label="产品" name="productId">
|
||||
<a-select
|
||||
<Form.Item v-if="!props.productId" label="产品" name="productId">
|
||||
<Select
|
||||
v-model:value="queryParams.productId"
|
||||
placeholder="请选择产品"
|
||||
allow-clear
|
||||
style="width: 240px"
|
||||
>
|
||||
<a-select-option
|
||||
<Select.Option
|
||||
v-for="product in products"
|
||||
:key="product.id"
|
||||
:value="product.id"
|
||||
>
|
||||
{{ product.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="DeviceName" name="deviceName">
|
||||
<a-input
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="DeviceName" name="deviceName">
|
||||
<Input
|
||||
v-model:value="queryParams.deviceName"
|
||||
placeholder="请输入 DeviceName"
|
||||
allow-clear
|
||||
@press-enter="handleQuery"
|
||||
style="width: 240px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注名称" name="nickname">
|
||||
<a-input
|
||||
</Form.Item>
|
||||
<Form.Item label="备注名称" name="nickname">
|
||||
<Input
|
||||
v-model:value="queryParams.nickname"
|
||||
placeholder="请输入备注名称"
|
||||
allow-clear
|
||||
@press-enter="handleQuery"
|
||||
style="width: 240px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="设备类型" name="deviceType">
|
||||
<a-select
|
||||
</Form.Item>
|
||||
<Form.Item label="设备类型" name="deviceType">
|
||||
<Select
|
||||
v-model:value="queryParams.deviceType"
|
||||
placeholder="请选择设备类型"
|
||||
allow-clear
|
||||
style="width: 240px"
|
||||
>
|
||||
<a-select-option
|
||||
<Select.Option
|
||||
v-for="dict in getIntDictOptions(
|
||||
DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE,
|
||||
)"
|
||||
@@ -276,57 +283,57 @@ onMounted(async () => {
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="设备状态" name="status">
|
||||
<a-select
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="设备状态" name="status">
|
||||
<Select
|
||||
v-model:value="queryParams.status"
|
||||
placeholder="请选择设备状态"
|
||||
allow-clear
|
||||
style="width: 240px"
|
||||
>
|
||||
<a-select-option
|
||||
<Select.Option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DEVICE_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="设备分组" name="groupId">
|
||||
<a-select
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="设备分组" name="groupId">
|
||||
<Select
|
||||
v-model:value="queryParams.groupId"
|
||||
placeholder="请选择设备分组"
|
||||
allow-clear
|
||||
style="width: 240px"
|
||||
>
|
||||
<a-select-option
|
||||
<Select.Option
|
||||
v-for="group in deviceGroups"
|
||||
:key="group.id"
|
||||
:value="group.id"
|
||||
>
|
||||
{{ group.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button @click="handleQuery">
|
||||
<Icon class="mr-5px" icon="ep:search" />
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button @click="handleQuery">
|
||||
<IconifyIcon class="mr-5px" icon="ep:search" />
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button @click="resetQuery">
|
||||
<Icon class="mr-5px" icon="ep:refresh" />
|
||||
</Button>
|
||||
<Button @click="resetQuery">
|
||||
<IconifyIcon class="mr-5px" icon="ep:refresh" />
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<a-table
|
||||
<Table
|
||||
ref="tableRef"
|
||||
:loading="loading"
|
||||
:data-source="list"
|
||||
@@ -334,38 +341,38 @@ onMounted(async () => {
|
||||
:pagination="false"
|
||||
:row-selection="multiple ? rowSelection : undefined"
|
||||
@row-click="handleRowClick"
|
||||
:row-key="(record: IotDeviceApi.Device) => record.id"
|
||||
:row-key="(record: IotDeviceApi.Device) => record.id?.toString() ?? ''"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'radio'">
|
||||
<a-radio
|
||||
<Radio
|
||||
:checked="selectedId === record.id"
|
||||
@click="() => handleRadioChange(record)"
|
||||
@click="() => handleRadioChange(record as IotDeviceApi.Device)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'productId'">
|
||||
{{ products.find((p) => p.id === record.productId)?.name || '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'deviceType'">
|
||||
<dict-tag
|
||||
<DictTag
|
||||
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
|
||||
:value="record.deviceType"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'groupIds'">
|
||||
<template v-if="record.groupIds?.length">
|
||||
<a-tag
|
||||
<Tag
|
||||
v-for="id in record.groupIds"
|
||||
:key="id"
|
||||
class="ml-5px"
|
||||
size="small"
|
||||
>
|
||||
{{ deviceGroups.find((g) => g.id === id)?.name }}
|
||||
</a-tag>
|
||||
</Tag>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<dict-tag
|
||||
<DictTag
|
||||
:type="DICT_TYPE.IOT_DEVICE_STATUS"
|
||||
:value="record.status"
|
||||
/>
|
||||
@@ -374,7 +381,7 @@ onMounted(async () => {
|
||||
{{ dateFormatter(null, null, record.onlineTime) }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</Table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
@@ -386,10 +393,10 @@ onMounted(async () => {
|
||||
</ContentWrap>
|
||||
|
||||
<template #footer>
|
||||
<a-button @click="submitForm" type="primary" :disabled="formLoading">
|
||||
<Button @click="submitForm" type="primary" :disabled="formLoading">
|
||||
确 定
|
||||
</a-button>
|
||||
<a-button @click="dialogVisible = false">取 消</a-button>
|
||||
</Button>
|
||||
<Button @click="dialogVisible = false">取 消</Button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<!-- 设备配置 -->
|
||||
<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 { 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';
|
||||
|
||||
defineOptions({ name: 'DeviceDetailConfig' });
|
||||
|
||||
const props = defineProps<{
|
||||
device: DeviceVO;
|
||||
device: IotDeviceApi.Device;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -35,13 +35,13 @@ watchEffect(() => {
|
||||
|
||||
const isEditing = ref(false); // 编辑状态
|
||||
/** 启用编辑模式的函数 */
|
||||
const enableEdit = () => {
|
||||
function enableEdit() {
|
||||
isEditing.value = true;
|
||||
hasJsonError.value = false; // 重置错误状态
|
||||
};
|
||||
}
|
||||
|
||||
/** 取消编辑的函数 */
|
||||
const cancelEdit = () => {
|
||||
function cancelEdit() {
|
||||
try {
|
||||
config.value = props.device.config ? JSON.parse(props.device.config) : {};
|
||||
} catch {
|
||||
@@ -49,25 +49,25 @@ const cancelEdit = () => {
|
||||
}
|
||||
isEditing.value = false;
|
||||
hasJsonError.value = false; // 重置错误状态
|
||||
};
|
||||
}
|
||||
|
||||
/** 保存配置的函数 */
|
||||
const saveConfig = async () => {
|
||||
async function saveConfig() {
|
||||
if (hasJsonError.value) {
|
||||
message.error({ content: 'JSON格式错误,请修正后再提交!' });
|
||||
return;
|
||||
}
|
||||
await updateDeviceConfig();
|
||||
isEditing.value = false;
|
||||
};
|
||||
}
|
||||
|
||||
/** 配置推送处理函数 */
|
||||
const handleConfigPush = async () => {
|
||||
async function handleConfigPush() {
|
||||
try {
|
||||
pushLoading.value = true;
|
||||
|
||||
// 调用配置推送接口
|
||||
await DeviceApi.sendDeviceMessage({
|
||||
await sendDeviceMessage({
|
||||
deviceId: props.device.id!,
|
||||
method: IotDeviceMessageMethodEnum.CONFIG_PUSH.method,
|
||||
params: config.value,
|
||||
@@ -82,17 +82,17 @@ const handleConfigPush = async () => {
|
||||
} finally {
|
||||
pushLoading.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 更新设备配置 */
|
||||
const updateDeviceConfig = async () => {
|
||||
async function updateDeviceConfig() {
|
||||
try {
|
||||
// 提交请求
|
||||
loading.value = true;
|
||||
await DeviceApi.updateDevice({
|
||||
await updateDevice({
|
||||
id: props.device.id,
|
||||
config: JSON.stringify(config.value),
|
||||
} as DeviceVO);
|
||||
} as IotDeviceApi.Device);
|
||||
message.success({ content: '更新成功!' });
|
||||
// 触发 success 事件
|
||||
emit('success');
|
||||
@@ -101,21 +101,21 @@ const updateDeviceConfig = async () => {
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 处理 JSON 编辑器错误的函数 */
|
||||
const onError = (errors: any) => {
|
||||
function onError(errors: any) {
|
||||
if (!errors || (Array.isArray(errors) && errors.length === 0)) {
|
||||
hasJsonError.value = false;
|
||||
return;
|
||||
}
|
||||
hasJsonError.value = true;
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a-alert
|
||||
<Alert
|
||||
message="支持远程更新设备的配置文件(JSON 格式),可以在下方编辑配置模板,对设备的系统参数、网络参数等进行远程配置。配置完成后,需点击「下发」按钮,设备即可进行远程配置。"
|
||||
type="info"
|
||||
show-icon
|
||||
@@ -129,24 +129,24 @@ const onError = (errors: any) => {
|
||||
@error="onError"
|
||||
/>
|
||||
<div class="mt-5 text-center">
|
||||
<a-button v-if="isEditing" @click="cancelEdit">取消</a-button>
|
||||
<a-button
|
||||
<Button v-if="isEditing" @click="cancelEdit">取消</Button>
|
||||
<Button
|
||||
v-if="isEditing"
|
||||
type="primary"
|
||||
@click="saveConfig"
|
||||
:disabled="hasJsonError"
|
||||
>
|
||||
保存
|
||||
</a-button>
|
||||
<a-button v-else @click="enableEdit">编辑</a-button>
|
||||
<a-button
|
||||
</Button>
|
||||
<Button v-else @click="enableEdit">编辑</Button>
|
||||
<Button
|
||||
v-if="!isEditing"
|
||||
type="primary"
|
||||
@click="handleConfigPush"
|
||||
:loading="pushLoading"
|
||||
>
|
||||
配置推送
|
||||
</a-button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<!-- 设备信息(头部) -->
|
||||
<script setup lang="ts">
|
||||
import type { DeviceVO } from '#/api/iot/device/device';
|
||||
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 { ref } from 'vue';
|
||||
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';
|
||||
|
||||
interface Props {
|
||||
product: ProductVO;
|
||||
device: DeviceVO;
|
||||
product: IotProductApi.Product;
|
||||
device: IotDeviceApi.Device;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
@@ -28,12 +28,12 @@ const router = useRouter();
|
||||
|
||||
/** 操作修改 */
|
||||
const formRef = ref();
|
||||
const openForm = (type: string, id?: number) => {
|
||||
function openForm(type: string, id?: number) {
|
||||
formRef.value.open(type, id);
|
||||
};
|
||||
}
|
||||
|
||||
/** 复制到剪贴板方法 */
|
||||
const copyToClipboard = async (text: string | undefined) => {
|
||||
async function copyToClipboard(text: string | undefined) {
|
||||
if (!text) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
@@ -41,14 +41,14 @@ const copyToClipboard = async (text: string | undefined) => {
|
||||
} catch {
|
||||
message.error({ content: '复制失败' });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 跳转到产品详情页面 */
|
||||
const goToProductDetail = (productId: number | undefined) => {
|
||||
function goToProductDetail(productId: number | undefined) {
|
||||
if (productId) {
|
||||
router.push({ name: 'IoTProductDetail', params: { id: productId } });
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="mb-4">
|
||||
@@ -58,38 +58,38 @@ const goToProductDetail = (productId: number | undefined) => {
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<!-- 右上:按钮 -->
|
||||
<a-button
|
||||
<Button
|
||||
v-if="product.status === 0"
|
||||
v-hasPermi="['iot:device:update']"
|
||||
@click="openForm('update', device.id)"
|
||||
>
|
||||
编辑
|
||||
</a-button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-card class="mt-4">
|
||||
<a-descriptions :column="1">
|
||||
<a-descriptions-item label="产品">
|
||||
<Card class="mt-4">
|
||||
<Descriptions :column="1">
|
||||
<Descriptions.Item label="产品">
|
||||
<a
|
||||
@click="goToProductDetail(product.id)"
|
||||
class="cursor-pointer text-blue-600"
|
||||
>
|
||||
{{ product.name }}
|
||||
</a>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="ProductKey">
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="ProductKey">
|
||||
{{ product.productKey }}
|
||||
<a-button
|
||||
<Button
|
||||
size="small"
|
||||
class="ml-2"
|
||||
@click="copyToClipboard(product.productKey)"
|
||||
>
|
||||
复制
|
||||
</a-button>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</Button>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<DeviceForm ref="formRef" @success="emit('refresh')" />
|
||||
|
||||
@@ -1,28 +1,42 @@
|
||||
<!-- 设备信息 -->
|
||||
<script setup lang="ts">
|
||||
import type { DeviceVO, IotDeviceAuthInfoVO } from '#/api/iot/device/device';
|
||||
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 { computed, ref } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
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<{
|
||||
device: DeviceVO;
|
||||
product: ProductVO;
|
||||
device: IotDeviceApi.Device;
|
||||
product: IotProductApi.Product;
|
||||
}>(); // 定义 Props
|
||||
const emit = defineEmits(['refresh']); // 定义 Emits
|
||||
// const emit = defineEmits(['refresh']); // 定义 Emits
|
||||
|
||||
const authDialogVisible = ref(false); // 定义设备认证信息弹框的可见性
|
||||
const authPasswordVisible = ref(false); // 定义密码可见性状态
|
||||
const authInfo = ref<IotDeviceAuthInfoVO>({} as IotDeviceAuthInfoVO); // 定义设备认证信息对象
|
||||
const authInfo = ref<IotDeviceApi.DeviceAuthInfo>(
|
||||
{} as IotDeviceApi.DeviceAuthInfo,
|
||||
); // 定义设备认证信息对象
|
||||
|
||||
/** 控制地图显示的标志 */
|
||||
const showMap = computed(() => {
|
||||
@@ -30,20 +44,20 @@ const showMap = computed(() => {
|
||||
});
|
||||
|
||||
/** 复制到剪贴板方法 */
|
||||
const copyToClipboard = async (text: string) => {
|
||||
async function copyToClipboard(text: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
message.success({ content: '复制成功' });
|
||||
} catch {
|
||||
message.error({ content: '复制失败' });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 打开设备认证信息弹框的方法 */
|
||||
const handleAuthInfoDialogOpen = async () => {
|
||||
async function handleAuthInfoDialogOpen() {
|
||||
if (!device.id) return;
|
||||
try {
|
||||
authInfo.value = await DeviceApi.getDeviceAuthInfo(device.id);
|
||||
authInfo.value = await getDeviceAuthInfo(device.id);
|
||||
// 显示设备认证信息弹框
|
||||
authDialogVisible.value = true;
|
||||
} catch (error) {
|
||||
@@ -52,82 +66,82 @@ const handleAuthInfoDialogOpen = async () => {
|
||||
content: '获取设备认证信息失败,请检查网络连接或联系管理员',
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 关闭设备认证信息弹框的方法 */
|
||||
const handleAuthInfoDialogClose = () => {
|
||||
function handleAuthInfoDialogClose() {
|
||||
authDialogVisible.value = false;
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<a-row :gutter="16">
|
||||
<Row :gutter="16">
|
||||
<!-- 左侧设备信息 -->
|
||||
<a-col :span="12">
|
||||
<a-card class="h-full">
|
||||
<Col :span="12">
|
||||
<Card class="h-full">
|
||||
<template #title>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
<a-descriptions :column="1" bordered>
|
||||
<a-descriptions-item label="产品名称">
|
||||
<Descriptions :column="1" bordered>
|
||||
<Descriptions.Item label="产品名称">
|
||||
{{ product.name }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="ProductKey">
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="ProductKey">
|
||||
{{ product.productKey }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="设备类型">
|
||||
<dict-tag
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="设备类型">
|
||||
<DictTag
|
||||
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
|
||||
:value="product.deviceType"
|
||||
/>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="DeviceName">
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="DeviceName">
|
||||
{{ device.deviceName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="备注名称">
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="备注名称">
|
||||
{{ device.nickname || '--' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="当前状态">
|
||||
<dict-tag
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="当前状态">
|
||||
<DictTag
|
||||
:type="DICT_TYPE.IOT_DEVICE_STATUS"
|
||||
:value="device.state"
|
||||
/>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">
|
||||
{{ formatDate(device.createTime) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="激活时间">
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="激活时间">
|
||||
{{ formatDate(device.activeTime) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="最后上线时间">
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最后上线时间">
|
||||
{{ formatDate(device.onlineTime) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="最后离线时间">
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最后离线时间">
|
||||
{{ formatDate(device.offlineTime) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="MQTT 连接参数">
|
||||
<a-button
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="MQTT 连接参数">
|
||||
<Button
|
||||
type="link"
|
||||
@click="handleAuthInfoDialogOpen"
|
||||
size="small"
|
||||
>
|
||||
查看
|
||||
</a-button>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</Button>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<!-- 右侧地图 -->
|
||||
<a-col :span="12">
|
||||
<a-card class="h-full">
|
||||
<Col :span="12">
|
||||
<Card class="h-full">
|
||||
<template #title>
|
||||
<div class="flex items-center justify-between">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,78 +157,71 @@ const handleAuthInfoDialogClose = () => {
|
||||
v-else
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 认证信息弹框 -->
|
||||
<a-modal
|
||||
<Modal
|
||||
v-model:open="authDialogVisible"
|
||||
title="MQTT 连接参数"
|
||||
width="640px"
|
||||
:footer="null"
|
||||
>
|
||||
<a-form :label-col="{ span: 6 }">
|
||||
<a-form-item label="clientId">
|
||||
<a-input-group compact>
|
||||
<a-input
|
||||
<Form :label-col="{ span: 6 }">
|
||||
<Form.Item label="clientId">
|
||||
<Input.Group compact>
|
||||
<Input
|
||||
v-model:value="authInfo.clientId"
|
||||
readonly
|
||||
style="width: calc(100% - 80px)"
|
||||
/>
|
||||
<a-button
|
||||
@click="copyToClipboard(authInfo.clientId)"
|
||||
type="primary"
|
||||
>
|
||||
<Icon icon="ph:copy" />
|
||||
</a-button>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="username">
|
||||
<a-input-group compact>
|
||||
<a-input
|
||||
<Button @click="copyToClipboard(authInfo.clientId)" type="primary">
|
||||
<IconifyIcon icon="ph:copy" />
|
||||
</Button>
|
||||
</Input.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label="username">
|
||||
<Input.Group compact>
|
||||
<Input
|
||||
v-model:value="authInfo.username"
|
||||
readonly
|
||||
style="width: calc(100% - 80px)"
|
||||
/>
|
||||
<a-button
|
||||
@click="copyToClipboard(authInfo.username)"
|
||||
type="primary"
|
||||
>
|
||||
<Icon icon="ph:copy" />
|
||||
</a-button>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="password">
|
||||
<a-input-group compact>
|
||||
<a-input
|
||||
<Button @click="copyToClipboard(authInfo.username)" type="primary">
|
||||
<IconifyIcon icon="ph:copy" />
|
||||
</Button>
|
||||
</Input.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label="password">
|
||||
<Input.Group compact>
|
||||
<Input
|
||||
v-model:value="authInfo.password"
|
||||
readonly
|
||||
:type="authPasswordVisible ? 'text' : 'password'"
|
||||
style="width: calc(100% - 160px)"
|
||||
/>
|
||||
<a-button
|
||||
<Button
|
||||
@click="authPasswordVisible = !authPasswordVisible"
|
||||
type="primary"
|
||||
>
|
||||
<Icon :icon="authPasswordVisible ? 'ph:eye-slash' : 'ph:eye'" />
|
||||
</a-button>
|
||||
<a-button
|
||||
@click="copyToClipboard(authInfo.password)"
|
||||
type="primary"
|
||||
>
|
||||
<Icon icon="ph:copy" />
|
||||
</a-button>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<IconifyIcon
|
||||
:icon="authPasswordVisible ? 'ph:eye-slash' : 'ph:eye'"
|
||||
/>
|
||||
</Button>
|
||||
<Button @click="copyToClipboard(authInfo.password)" type="primary">
|
||||
<IconifyIcon icon="ph:copy" />
|
||||
</Button>
|
||||
</Input.Group>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<div class="mt-4 text-right">
|
||||
<a-button @click="handleAuthInfoDialogClose">关闭</a-button>
|
||||
<Button @click="handleAuthInfoDialogClose">关闭</Button>
|
||||
</div>
|
||||
</a-modal>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,10 +9,23 @@ import {
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
import { ContentWrap } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
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';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -49,64 +62,58 @@ const columns = [
|
||||
title: '时间',
|
||||
dataIndex: 'ts',
|
||||
key: 'ts',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '上行/下行',
|
||||
dataIndex: 'upstream',
|
||||
key: 'upstream',
|
||||
align: 'center',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: '是否回复',
|
||||
dataIndex: 'reply',
|
||||
key: 'reply',
|
||||
align: 'center',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: '请求编号',
|
||||
dataIndex: 'requestId',
|
||||
key: 'requestId',
|
||||
align: 'center',
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
title: '请求方法',
|
||||
dataIndex: 'method',
|
||||
key: 'method',
|
||||
align: 'center',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: '请求/响应数据',
|
||||
dataIndex: 'params',
|
||||
key: 'params',
|
||||
align: 'center',
|
||||
ellipsis: true,
|
||||
},
|
||||
];
|
||||
|
||||
/** 查询消息列表 */
|
||||
const getMessageList = async () => {
|
||||
async function getMessageList() {
|
||||
if (!props.deviceId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await DeviceApi.getDeviceMessagePage(queryParams);
|
||||
const data = await getDeviceMessagePage(queryParams);
|
||||
total.value = data.total;
|
||||
list.value = data.list;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 搜索操作 */
|
||||
const handleQuery = () => {
|
||||
function handleQuery() {
|
||||
queryParams.pageNo = 1;
|
||||
getMessageList();
|
||||
};
|
||||
}
|
||||
|
||||
/** 监听自动刷新 */
|
||||
watch(autoRefresh, (newValue) => {
|
||||
@@ -146,7 +153,7 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
/** 刷新消息列表 */
|
||||
const refresh = (delay = 0) => {
|
||||
function refresh(delay = 0) {
|
||||
if (delay > 0) {
|
||||
setTimeout(() => {
|
||||
handleQuery();
|
||||
@@ -154,7 +161,7 @@ const refresh = (delay = 0) => {
|
||||
} else {
|
||||
handleQuery();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 暴露方法给父组件 */
|
||||
defineExpose({
|
||||
@@ -165,54 +172,55 @@ defineExpose({
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索区域 -->
|
||||
<a-form :model="queryParams" layout="inline">
|
||||
<a-form-item>
|
||||
<a-select
|
||||
<Form :model="queryParams" layout="inline">
|
||||
<Form.Item>
|
||||
<Select
|
||||
v-model:value="queryParams.method"
|
||||
placeholder="所有方法"
|
||||
style="width: 160px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option
|
||||
<Select.Option
|
||||
v-for="item in methodOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-select
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Select
|
||||
v-model:value="queryParams.upstream"
|
||||
placeholder="上行/下行"
|
||||
style="width: 160px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option label="上行" value="true">上行</a-select-option>
|
||||
<a-select-option label="下行" value="false">下行</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleQuery">
|
||||
<Icon icon="ep:search" class="mr-5px" /> 搜索
|
||||
</a-button>
|
||||
<a-switch
|
||||
<Select.Option label="上行" value="true">上行</Select.Option>
|
||||
<Select.Option label="下行" value="false">下行</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" @click="handleQuery">
|
||||
<IconifyIcon icon="ep:search" class="mr-5px" /> 搜索
|
||||
</Button>
|
||||
<Switch
|
||||
v-model:checked="autoRefresh"
|
||||
class="ml-20px"
|
||||
checked-children="定时刷新"
|
||||
un-checked-children="定时刷新"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<a-table
|
||||
<Table
|
||||
:loading="loading"
|
||||
:data-source="list"
|
||||
:columns="columns"
|
||||
:pagination="false"
|
||||
align="center"
|
||||
class="whitespace-nowrap"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
@@ -220,12 +228,12 @@ defineExpose({
|
||||
{{ formatDate(record.ts) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'upstream'">
|
||||
<a-tag :color="record.upstream ? 'blue' : 'green'">
|
||||
<Tag :color="record.upstream ? 'blue' : 'green'">
|
||||
{{ record.upstream ? '上行' : '下行' }}
|
||||
</a-tag>
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'reply'">
|
||||
<dict-tag
|
||||
<DictTag
|
||||
:type="DICT_TYPE.INFRA_BOOLEAN_STRING"
|
||||
:value="record.reply"
|
||||
/>
|
||||
@@ -244,7 +252,7 @@ defineExpose({
|
||||
<span v-else>{{ record.params }}</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</Table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="mt-10px flex justify-end">
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
<!-- 模拟设备 -->
|
||||
<script lang="ts" setup>
|
||||
import type { DeviceVO } from '#/api/iot/device/device';
|
||||
import type { ProductVO } from '#/api/iot/product/product';
|
||||
import type { TableColumnType } from 'ant-design-vue';
|
||||
|
||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||
|
||||
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 {
|
||||
IotDeviceMessageMethodEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
@@ -17,8 +29,8 @@ import {
|
||||
import DeviceDetailsMessage from './DeviceDetailsMessage.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
device: DeviceVO;
|
||||
product: ProductVO;
|
||||
device: IotDeviceApi.Device;
|
||||
product: IotProductApi.Product;
|
||||
thingModelList: ThingModelData[];
|
||||
}>();
|
||||
|
||||
@@ -55,41 +67,36 @@ const serviceList = computed(() =>
|
||||
);
|
||||
|
||||
// 属性表格列定义
|
||||
const propertyColumns = [
|
||||
const propertyColumns: TableColumnType[] = [
|
||||
{
|
||||
title: '功能名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
fixed: 'left',
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '标识符',
|
||||
dataIndex: 'identifier',
|
||||
key: 'identifier',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
fixed: 'left',
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '数据类型',
|
||||
key: 'dataType',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '数据定义',
|
||||
key: 'dataDefinition',
|
||||
minWidth: 200,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: '值',
|
||||
key: 'value',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
fixed: 'right' as any,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -100,41 +107,35 @@ const eventColumns = [
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
fixed: 'left',
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '标识符',
|
||||
dataIndex: 'identifier',
|
||||
key: 'identifier',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
fixed: 'left',
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '数据类型',
|
||||
key: 'dataType',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '数据定义',
|
||||
key: 'dataDefinition',
|
||||
minWidth: 200,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: '值',
|
||||
key: 'value',
|
||||
width: 200,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
fixed: 'right' as any,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -145,50 +146,45 @@ const serviceColumns = [
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
fixed: 'left',
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '标识符',
|
||||
dataIndex: 'identifier',
|
||||
key: 'identifier',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
fixed: 'left',
|
||||
fixed: 'left' as any,
|
||||
},
|
||||
{
|
||||
title: '输入参数',
|
||||
key: 'dataDefinition',
|
||||
minWidth: 200,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: '参数值',
|
||||
key: 'value',
|
||||
width: 200,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
fixed: 'right' as any,
|
||||
},
|
||||
];
|
||||
|
||||
// 获取表单值
|
||||
const getFormValue = (identifier: string) => {
|
||||
function getFormValue(identifier: string) {
|
||||
return formData.value[identifier] || '';
|
||||
};
|
||||
}
|
||||
|
||||
// 设置表单值
|
||||
const setFormValue = (identifier: string, value: string) => {
|
||||
function setFormValue(identifier: string, value: string) {
|
||||
formData.value[identifier] = value;
|
||||
};
|
||||
}
|
||||
|
||||
// 属性上报
|
||||
const handlePropertyPost = async () => {
|
||||
async function handlePropertyPost() {
|
||||
try {
|
||||
const params: Record<string, any> = {};
|
||||
propertyList.value.forEach((item) => {
|
||||
@@ -203,7 +199,7 @@ const handlePropertyPost = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
await DeviceApi.sendDeviceMessage({
|
||||
await sendDeviceMessage({
|
||||
deviceId: props.device.id!,
|
||||
method: IotDeviceMessageMethodEnum.PROPERTY_POST.method,
|
||||
params,
|
||||
@@ -216,10 +212,10 @@ const handlePropertyPost = async () => {
|
||||
message.error({ content: '属性上报失败' });
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 事件上报
|
||||
const handleEventPost = async (row: ThingModelData) => {
|
||||
async function handleEventPost(row: ThingModelData) {
|
||||
try {
|
||||
const valueStr = formData.value[row.identifier!];
|
||||
let params: any = {};
|
||||
@@ -233,7 +229,7 @@ const handleEventPost = async (row: ThingModelData) => {
|
||||
}
|
||||
}
|
||||
|
||||
await DeviceApi.sendDeviceMessage({
|
||||
await sendDeviceMessage({
|
||||
deviceId: props.device.id!,
|
||||
method: IotDeviceMessageMethodEnum.EVENT_POST.method,
|
||||
params: {
|
||||
@@ -249,12 +245,12 @@ const handleEventPost = async (row: ThingModelData) => {
|
||||
message.error({ content: '事件上报失败' });
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 状态变更
|
||||
const handleDeviceState = async (state: number) => {
|
||||
async function handleDeviceState(state: number) {
|
||||
try {
|
||||
await DeviceApi.sendDeviceMessage({
|
||||
await sendDeviceMessage({
|
||||
deviceId: props.device.id!,
|
||||
method: IotDeviceMessageMethodEnum.STATE_UPDATE.method,
|
||||
params: { state },
|
||||
@@ -267,10 +263,10 @@ const handleDeviceState = async (state: number) => {
|
||||
message.error({ content: '状态变更失败' });
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 属性设置
|
||||
const handlePropertySet = async () => {
|
||||
async function handlePropertySet() {
|
||||
try {
|
||||
const params: Record<string, any> = {};
|
||||
propertyList.value.forEach((item) => {
|
||||
@@ -285,7 +281,7 @@ const handlePropertySet = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
await DeviceApi.sendDeviceMessage({
|
||||
await sendDeviceMessage({
|
||||
deviceId: props.device.id!,
|
||||
method: IotDeviceMessageMethodEnum.PROPERTY_SET.method,
|
||||
params,
|
||||
@@ -298,10 +294,10 @@ const handlePropertySet = async () => {
|
||||
message.error({ content: '属性设置失败' });
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 服务调用
|
||||
const handleServiceInvoke = async (row: ThingModelData) => {
|
||||
async function handleServiceInvoke(row: ThingModelData) {
|
||||
try {
|
||||
const valueStr = formData.value[row.identifier!];
|
||||
let params: any = {};
|
||||
@@ -315,7 +311,7 @@ const handleServiceInvoke = async (row: ThingModelData) => {
|
||||
}
|
||||
}
|
||||
|
||||
await DeviceApi.sendDeviceMessage({
|
||||
await sendDeviceMessage({
|
||||
deviceId: props.device.id!,
|
||||
method: IotDeviceMessageMethodEnum.SERVICE_INVOKE.method,
|
||||
params: {
|
||||
@@ -331,30 +327,31 @@ const handleServiceInvoke = async (row: ThingModelData) => {
|
||||
message.error({ content: '服务调用失败' });
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<a-row :gutter="20">
|
||||
<Row :gutter="20">
|
||||
<!-- 左侧指令调试区域 -->
|
||||
<a-col :span="12">
|
||||
<a-card>
|
||||
<a-tabs v-model:active-key="activeTab">
|
||||
<Col :span="12">
|
||||
<Card>
|
||||
<Tabs v-model:active-key="activeTab">
|
||||
<!-- 上行指令调试 -->
|
||||
<a-tab-pane key="upstream" tab="上行指令调试">
|
||||
<a-tabs
|
||||
<Tabs.Pane key="upstream" tab="上行指令调试">
|
||||
<Tabs
|
||||
v-if="activeTab === 'upstream'"
|
||||
v-model:active-key="upstreamTab"
|
||||
>
|
||||
<!-- 属性上报 -->
|
||||
<a-tab-pane
|
||||
<Tabs.Pane
|
||||
:key="IotDeviceMessageMethodEnum.PROPERTY_POST.method"
|
||||
tab="属性上报"
|
||||
>
|
||||
<ContentWrap>
|
||||
<a-table
|
||||
<Table
|
||||
:data-source="propertyList"
|
||||
align="center"
|
||||
:columns="propertyColumns"
|
||||
:pagination="false"
|
||||
>
|
||||
@@ -366,7 +363,7 @@ const handleServiceInvoke = async (row: ThingModelData) => {
|
||||
<DataDefinition :data="record" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<a-input
|
||||
<Input
|
||||
:value="getFormValue(record.identifier)"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
@@ -376,26 +373,27 @@ const handleServiceInvoke = async (row: ThingModelData) => {
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</Table>
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">
|
||||
设置属性值后,点击「发送属性上报」按钮
|
||||
</span>
|
||||
<a-button type="primary" @click="handlePropertyPost">
|
||||
<Button type="primary" @click="handlePropertyPost">
|
||||
发送属性上报
|
||||
</a-button>
|
||||
</Button>
|
||||
</div>
|
||||
</ContentWrap>
|
||||
</a-tab-pane>
|
||||
</Tabs.Pane>
|
||||
|
||||
<!-- 事件上报 -->
|
||||
<a-tab-pane
|
||||
<Tabs.Pane
|
||||
:key="IotDeviceMessageMethodEnum.EVENT_POST.method"
|
||||
tab="事件上报"
|
||||
>
|
||||
<ContentWrap>
|
||||
<a-table
|
||||
<Table
|
||||
:data-source="eventList"
|
||||
align="center"
|
||||
:columns="eventColumns"
|
||||
:pagination="false"
|
||||
>
|
||||
@@ -407,7 +405,7 @@ const handleServiceInvoke = async (row: ThingModelData) => {
|
||||
<DataDefinition :data="record" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<a-textarea
|
||||
<Textarea
|
||||
:value="getFormValue(record.identifier)"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
@@ -418,58 +416,59 @@ const handleServiceInvoke = async (row: ThingModelData) => {
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-button
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleEventPost(record)"
|
||||
>
|
||||
上报事件
|
||||
</a-button>
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</Table>
|
||||
</ContentWrap>
|
||||
</a-tab-pane>
|
||||
</Tabs.Pane>
|
||||
|
||||
<!-- 状态变更 -->
|
||||
<a-tab-pane
|
||||
<Tabs.Pane
|
||||
:key="IotDeviceMessageMethodEnum.STATE_UPDATE.method"
|
||||
tab="状态变更"
|
||||
>
|
||||
<ContentWrap>
|
||||
<div class="flex gap-4">
|
||||
<a-button
|
||||
<Button
|
||||
type="primary"
|
||||
@click="handleDeviceState(DeviceStateEnum.ONLINE)"
|
||||
>
|
||||
设备上线
|
||||
</a-button>
|
||||
<a-button
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
@click="handleDeviceState(DeviceStateEnum.OFFLINE)"
|
||||
>
|
||||
设备下线
|
||||
</a-button>
|
||||
</Button>
|
||||
</div>
|
||||
</ContentWrap>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-tab-pane>
|
||||
</Tabs.Pane>
|
||||
</Tabs>
|
||||
</Tabs.Pane>
|
||||
|
||||
<!-- 下行指令调试 -->
|
||||
<a-tab-pane key="downstream" tab="下行指令调试">
|
||||
<a-tabs
|
||||
<Tabs.Pane key="downstream" tab="下行指令调试">
|
||||
<Tabs
|
||||
v-if="activeTab === 'downstream'"
|
||||
v-model:active-key="downstreamTab"
|
||||
>
|
||||
<!-- 属性调试 -->
|
||||
<a-tab-pane
|
||||
<Tabs.Pane
|
||||
:key="IotDeviceMessageMethodEnum.PROPERTY_SET.method"
|
||||
tab="属性设置"
|
||||
>
|
||||
<ContentWrap>
|
||||
<a-table
|
||||
<Table
|
||||
:data-source="propertyList"
|
||||
align="center"
|
||||
:columns="propertyColumns"
|
||||
:pagination="false"
|
||||
>
|
||||
@@ -481,7 +480,7 @@ const handleServiceInvoke = async (row: ThingModelData) => {
|
||||
<DataDefinition :data="record" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<a-input
|
||||
<Input
|
||||
:value="getFormValue(record.identifier)"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
@@ -491,26 +490,27 @@ const handleServiceInvoke = async (row: ThingModelData) => {
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</Table>
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">
|
||||
设置属性值后,点击「发送属性设置」按钮
|
||||
</span>
|
||||
<a-button type="primary" @click="handlePropertySet">
|
||||
<Button type="primary" @click="handlePropertySet">
|
||||
发送属性设置
|
||||
</a-button>
|
||||
</Button>
|
||||
</div>
|
||||
</ContentWrap>
|
||||
</a-tab-pane>
|
||||
</Tabs.Pane>
|
||||
|
||||
<!-- 服务调用 -->
|
||||
<a-tab-pane
|
||||
<Tabs.Pane
|
||||
:key="IotDeviceMessageMethodEnum.SERVICE_INVOKE.method"
|
||||
tab="设备服务调用"
|
||||
>
|
||||
<ContentWrap>
|
||||
<a-table
|
||||
<Table
|
||||
:data-source="serviceList"
|
||||
align="center"
|
||||
:columns="serviceColumns"
|
||||
:pagination="false"
|
||||
>
|
||||
@@ -519,7 +519,7 @@ const handleServiceInvoke = async (row: ThingModelData) => {
|
||||
<DataDefinition :data="record" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<a-textarea
|
||||
<Textarea
|
||||
:value="getFormValue(record.identifier)"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
@@ -530,26 +530,26 @@ const handleServiceInvoke = async (row: ThingModelData) => {
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-button
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleServiceInvoke(record)"
|
||||
>
|
||||
服务调用
|
||||
</a-button>
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</Table>
|
||||
</ContentWrap>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</Tabs.Pane>
|
||||
</Tabs>
|
||||
</Tabs.Pane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<!-- 右侧设备日志区域 -->
|
||||
<a-col :span="12">
|
||||
<Col :span="12">
|
||||
<ContentWrap title="设备消息">
|
||||
<DeviceDetailsMessage
|
||||
v-if="device.id"
|
||||
@@ -557,7 +557,7 @@ const handleServiceInvoke = async (row: ThingModelData) => {
|
||||
:device-id="device.id"
|
||||
/>
|
||||
</ContentWrap>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</Col>
|
||||
</Row>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
@@ -6,6 +6,8 @@ import { ref } from 'vue';
|
||||
|
||||
import { ContentWrap } from '@vben/common-ui';
|
||||
|
||||
import { Tabs } from 'ant-design-vue';
|
||||
|
||||
import DeviceDetailsThingModelEvent from './DeviceDetailsThingModelEvent.vue';
|
||||
import DeviceDetailsThingModelProperty from './DeviceDetailsThingModelProperty.vue';
|
||||
import DeviceDetailsThingModelService from './DeviceDetailsThingModelService.vue';
|
||||
@@ -19,32 +21,22 @@ const activeTab = ref('property'); // 默认选中设备属性
|
||||
</script>
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<a-tabs v-model:active-key="activeTab" class="thing-model-tabs">
|
||||
<a-tab-pane key="property" tab="设备属性(运行状态)">
|
||||
<Tabs v-model:active-key="activeTab" class="!h-auto !p-0">
|
||||
<Tabs.Pane key="property" tab="设备属性(运行状态)">
|
||||
<DeviceDetailsThingModelProperty :device-id="deviceId" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="event" tab="设备事件上报">
|
||||
</Tabs.Pane>
|
||||
<Tabs.Pane key="event" tab="设备事件上报">
|
||||
<DeviceDetailsThingModelEvent
|
||||
:device-id="props.deviceId"
|
||||
:thing-model-list="props.thingModelList"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="service" tab="设备服务调用">
|
||||
</Tabs.Pane>
|
||||
<Tabs.Pane key="service" tab="设备服务调用">
|
||||
<DeviceDetailsThingModelService
|
||||
:device-id="deviceId"
|
||||
:thing-model-list="props.thingModelList"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</Tabs.Pane>
|
||||
</Tabs>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.thing-model-tabs :deep(.ant-tabs-content) {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.thing-model-tabs :deep(.ant-tabs-tabpane) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,9 +8,18 @@ import { ContentWrap } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
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 {
|
||||
getEventTypeLabel,
|
||||
IotDeviceMessageMethodEnum,
|
||||
@@ -29,7 +38,7 @@ const queryParams = reactive({
|
||||
deviceId: props.deviceId,
|
||||
method: IotDeviceMessageMethodEnum.EVENT_POST.method, // 固定筛选事件消息
|
||||
identifier: '',
|
||||
times: [] as any[],
|
||||
times: undefined,
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
@@ -44,53 +53,53 @@ const eventThingModels = computed(() => {
|
||||
});
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
async function getList() {
|
||||
if (!props.deviceId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await DeviceApi.getDeviceMessagePairPage(queryParams);
|
||||
const data = await getDeviceMessagePairPage(queryParams);
|
||||
list.value = data.list || [];
|
||||
total.value = data.total || 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
function handleQuery() {
|
||||
queryParams.pageNo = 1;
|
||||
getList();
|
||||
};
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
function resetQuery() {
|
||||
queryFormRef.value?.resetFields();
|
||||
queryParams.identifier = '';
|
||||
queryParams.times = [];
|
||||
queryParams.times = undefined;
|
||||
handleQuery();
|
||||
};
|
||||
}
|
||||
|
||||
/** 获取事件名称 */
|
||||
const getEventName = (identifier: string | undefined) => {
|
||||
function getEventName(identifier: string | undefined) {
|
||||
if (!identifier) return '-';
|
||||
const event = eventThingModels.value.find(
|
||||
(item: ThingModelData) => item.identifier === identifier,
|
||||
);
|
||||
return event?.name || identifier;
|
||||
};
|
||||
}
|
||||
|
||||
/** 获取事件类型 */
|
||||
const getEventType = (identifier: string | undefined) => {
|
||||
function getEventType(identifier: string | undefined) {
|
||||
if (!identifier) return '-';
|
||||
const event = eventThingModels.value.find(
|
||||
(item: ThingModelData) => item.identifier === identifier,
|
||||
);
|
||||
if (!event?.event?.type) return '-';
|
||||
return getEventTypeLabel(event.event.type) || '-';
|
||||
};
|
||||
}
|
||||
|
||||
/** 解析参数 */
|
||||
const parseParams = (params: string) => {
|
||||
function parseParams(params: string) {
|
||||
try {
|
||||
const parsed = JSON.parse(params);
|
||||
if (parsed.params) {
|
||||
@@ -100,7 +109,7 @@ const parseParams = (params: string) => {
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
@@ -111,58 +120,58 @@ onMounted(() => {
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<a-form
|
||||
<Form
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
layout="inline"
|
||||
@submit.prevent
|
||||
style="margin-bottom: 16px"
|
||||
>
|
||||
<a-form-item label="标识符" name="identifier">
|
||||
<a-select
|
||||
<Form.Item label="标识符" name="identifier">
|
||||
<Select
|
||||
v-model:value="queryParams.identifier"
|
||||
placeholder="请选择事件标识符"
|
||||
allow-clear
|
||||
style="width: 240px"
|
||||
>
|
||||
<a-select-option
|
||||
<Select.Option
|
||||
v-for="event in eventThingModels"
|
||||
:key="event.identifier"
|
||||
:value="event.identifier!"
|
||||
>
|
||||
{{ event.name }}({{ event.identifier }})
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="时间范围" name="times">
|
||||
<a-range-picker
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="时间范围" name="times">
|
||||
<RangePicker
|
||||
v-model:value="queryParams.times"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 360px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleQuery">
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" @click="handleQuery">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:search" />
|
||||
</template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button @click="resetQuery" style="margin-left: 8px">
|
||||
</Button>
|
||||
<Button @click="resetQuery" style="margin-left: 8px">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:refresh" />
|
||||
</template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<a-divider style="margin: 16px 0" />
|
||||
<Divider style="margin: 16px 0" />
|
||||
|
||||
<!-- 事件列表 -->
|
||||
<a-table v-loading="loading" :data-source="list" :pagination="false">
|
||||
<a-table-column
|
||||
<Table v-loading="loading" :data-source="list" :pagination="false">
|
||||
<Table.Column
|
||||
title="上报时间"
|
||||
align="center"
|
||||
data-index="reportTime"
|
||||
@@ -175,20 +184,20 @@ onMounted(() => {
|
||||
: '-'
|
||||
}}
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column
|
||||
</Table.Column>
|
||||
<Table.Column
|
||||
title="标识符"
|
||||
align="center"
|
||||
data-index="identifier"
|
||||
:width="160"
|
||||
>
|
||||
<template #default="{ record }">
|
||||
<a-tag color="blue" size="small">
|
||||
<Tag color="blue" size="small">
|
||||
{{ record.request?.identifier }}
|
||||
</a-tag>
|
||||
</Tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column
|
||||
</Table.Column>
|
||||
<Table.Column
|
||||
title="事件名称"
|
||||
align="center"
|
||||
data-index="eventName"
|
||||
@@ -197,8 +206,8 @@ onMounted(() => {
|
||||
<template #default="{ record }">
|
||||
{{ getEventName(record.request?.identifier) }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column
|
||||
</Table.Column>
|
||||
<Table.Column
|
||||
title="事件类型"
|
||||
align="center"
|
||||
data-index="eventType"
|
||||
@@ -207,13 +216,13 @@ onMounted(() => {
|
||||
<template #default="{ record }">
|
||||
{{ getEventType(record.request?.identifier) }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="输入参数" align="center" data-index="params">
|
||||
</Table.Column>
|
||||
<Table.Column title="输入参数" align="center" data-index="params">
|
||||
<template #default="{ record }">
|
||||
{{ parseParams(record.request.params) }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</Table.Column>
|
||||
</Table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- 设备属性管理 -->
|
||||
<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';
|
||||
|
||||
@@ -8,15 +8,27 @@ import { ContentWrap } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
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';
|
||||
|
||||
const props = defineProps<{ deviceId: number }>();
|
||||
|
||||
const loading = ref(true); // 列表的加载中
|
||||
const list = ref<IotDevicePropertyDetailRespVO[]>([]); // 显示的列表数据
|
||||
const filterList = ref<IotDevicePropertyDetailRespVO[]>([]); // 完整的数据列表
|
||||
const list = ref<IotDeviceApi.DevicePropertyDetail[]>([]); // 显示的列表数据
|
||||
const filterList = ref<IotDeviceApi.DevicePropertyDetail[]>([]); // 完整的数据列表
|
||||
const queryParams = reactive({
|
||||
keyword: '' as string,
|
||||
});
|
||||
@@ -25,7 +37,7 @@ let autoRefreshTimer: any = null; // 定时器
|
||||
const viewMode = ref<'card' | 'list'>('card'); // 视图模式状态
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
async function getList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
@@ -33,50 +45,46 @@ const getList = async () => {
|
||||
identifier: undefined as string | undefined,
|
||||
name: undefined as string | undefined,
|
||||
};
|
||||
filterList.value = await DeviceApi.getLatestDeviceProperties(params);
|
||||
filterList.value = await getLatestDeviceProperties(params);
|
||||
handleFilter();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 前端筛选数据 */
|
||||
const handleFilter = () => {
|
||||
function handleFilter() {
|
||||
if (queryParams.keyword.trim()) {
|
||||
const keyword = queryParams.keyword.toLowerCase();
|
||||
list.value = filterList.value.filter(
|
||||
(item: IotDevicePropertyDetailRespVO) =>
|
||||
(item: IotDeviceApi.DevicePropertyDetail) =>
|
||||
item.identifier?.toLowerCase().includes(keyword) ||
|
||||
item.name?.toLowerCase().includes(keyword),
|
||||
);
|
||||
} else {
|
||||
list.value = filterList.value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
function handleQuery() {
|
||||
handleFilter();
|
||||
};
|
||||
}
|
||||
|
||||
/** 历史操作 */
|
||||
const historyRef = ref();
|
||||
const openHistory = (
|
||||
deviceId: number,
|
||||
identifier: string,
|
||||
dataType: string,
|
||||
) => {
|
||||
function openHistory(deviceId: number, identifier: string, dataType: string) {
|
||||
historyRef.value.open(deviceId, identifier, dataType);
|
||||
};
|
||||
}
|
||||
|
||||
/** 格式化属性值和单位 */
|
||||
const formatValueWithUnit = (item: IotDevicePropertyDetailRespVO) => {
|
||||
function formatValueWithUnit(item: IotDeviceApi.DevicePropertyDetail) {
|
||||
if (item.value === null || item.value === undefined || item.value === '') {
|
||||
return '-';
|
||||
}
|
||||
const unitName = item.dataSpecs?.unitName;
|
||||
return unitName ? `${item.value} ${unitName}` : item.value;
|
||||
};
|
||||
}
|
||||
|
||||
/** 监听自动刷新 */
|
||||
watch(autoRefresh, (newValue) => {
|
||||
@@ -108,7 +116,7 @@ onMounted(() => {
|
||||
<!-- 搜索工作栏 -->
|
||||
<div class="flex items-center justify-between" style="margin-bottom: 16px">
|
||||
<div class="flex items-center" style="gap: 16px">
|
||||
<a-input
|
||||
<Input
|
||||
v-model:value="queryParams.keyword"
|
||||
placeholder="请输入属性名称、标识符"
|
||||
allow-clear
|
||||
@@ -117,32 +125,32 @@ onMounted(() => {
|
||||
/>
|
||||
<div class="flex items-center" style="gap: 8px">
|
||||
<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>
|
||||
<a-button-group>
|
||||
<a-button
|
||||
<Button.Group>
|
||||
<Button
|
||||
:type="viewMode === 'card' ? 'primary' : 'default'"
|
||||
@click="viewMode = 'card'"
|
||||
>
|
||||
<IconifyIcon icon="ep:grid" />
|
||||
</a-button>
|
||||
<a-button
|
||||
</Button>
|
||||
<Button
|
||||
:type="viewMode === 'list' ? 'primary' : 'default'"
|
||||
@click="viewMode = 'list'"
|
||||
>
|
||||
<IconifyIcon icon="ep:list" />
|
||||
</a-button>
|
||||
</a-button-group>
|
||||
</Button>
|
||||
</Button.Group>
|
||||
</div>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<a-divider style="margin: 16px 0" />
|
||||
<Divider style="margin: 16px 0" />
|
||||
|
||||
<!-- 卡片视图 -->
|
||||
<template v-if="viewMode === 'card'">
|
||||
<a-row :gutter="16" v-loading="loading">
|
||||
<a-col
|
||||
<Row :gutter="16" v-loading="loading">
|
||||
<Col
|
||||
v-for="item in list"
|
||||
:key="item.identifier"
|
||||
:xs="24"
|
||||
@@ -151,7 +159,7 @@ onMounted(() => {
|
||||
:lg="6"
|
||||
class="mb-4"
|
||||
>
|
||||
<a-card
|
||||
<Card
|
||||
class="relative h-full overflow-hidden transition-colors"
|
||||
:body-style="{ padding: '0' }"
|
||||
>
|
||||
@@ -171,15 +179,15 @@ onMounted(() => {
|
||||
<div class="font-600 flex-1 text-[16px]">{{ item.name }}</div>
|
||||
<!-- 标识符 -->
|
||||
<div class="mr-2 inline-flex items-center">
|
||||
<a-tag size="small" color="blue">
|
||||
<Tag size="small" color="blue">
|
||||
{{ item.identifier }}
|
||||
</a-tag>
|
||||
</Tag>
|
||||
</div>
|
||||
<!-- 数据类型标签 -->
|
||||
<div class="mr-2 inline-flex items-center">
|
||||
<a-tag size="small">
|
||||
<Tag size="small">
|
||||
{{ item.dataType }}
|
||||
</a-tag>
|
||||
</Tag>
|
||||
</div>
|
||||
<!-- 数据图标 - 可点击 -->
|
||||
<div
|
||||
@@ -211,26 +219,22 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</template>
|
||||
|
||||
<!-- 列表视图 -->
|
||||
<a-table v-else v-loading="loading" :data-source="list" :pagination="false">
|
||||
<a-table-column
|
||||
title="属性标识符"
|
||||
align="center"
|
||||
data-index="identifier"
|
||||
/>
|
||||
<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">
|
||||
<Table v-else v-loading="loading" :data-source="list" :pagination="false">
|
||||
<Table.Column title="属性标识符" align="center" data-index="identifier" />
|
||||
<Table.Column title="属性名称" align="center" data-index="name" />
|
||||
<Table.Column title="数据类型" align="center" data-index="dataType" />
|
||||
<Table.Column title="属性值" align="center" data-index="value">
|
||||
<template #default="{ record }">
|
||||
{{ formatValueWithUnit(record) }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column
|
||||
</Table.Column>
|
||||
<Table.Column
|
||||
title="更新时间"
|
||||
align="center"
|
||||
data-index="updateTime"
|
||||
@@ -239,20 +243,20 @@ onMounted(() => {
|
||||
<template #default="{ record }">
|
||||
{{ record.updateTime ? formatDate(record.updateTime) : '-' }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="操作" align="center">
|
||||
</Table.Column>
|
||||
<Table.Column title="操作" align="center">
|
||||
<template #default="{ record }">
|
||||
<a-button
|
||||
<Button
|
||||
type="link"
|
||||
@click="
|
||||
openHistory(props.deviceId, record.identifier, record.dataType)
|
||||
"
|
||||
>
|
||||
查看数据
|
||||
</a-button>
|
||||
</Button>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</Table.Column>
|
||||
</Table>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<DeviceDetailsThingModelPropertyHistory
|
||||
@@ -264,7 +268,7 @@ onMounted(() => {
|
||||
<style scoped>
|
||||
/* 移除 a-row 的额外边距 */
|
||||
:deep(.ant-row) {
|
||||
margin-left: -8px !important;
|
||||
margin-right: -8px !important;
|
||||
margin-left: -8px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,17 +4,27 @@ import type { Dayjs } from 'dayjs';
|
||||
|
||||
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 { IconifyIcon } from '@vben/icons';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
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 { DeviceApi } from '#/api/iot/device/device';
|
||||
import { getHistoryDevicePropertyList } from '#/api/iot/device/device';
|
||||
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
/** IoT 设备属性历史数据详情 */
|
||||
@@ -26,7 +36,7 @@ const dialogVisible = ref(false); // 弹窗的是否展示
|
||||
const loading = ref(false);
|
||||
const exporting = ref(false);
|
||||
const viewMode = ref<'chart' | 'list'>('chart'); // 视图模式状态
|
||||
const list = ref<IotDevicePropertyRespVO[]>([]); // 列表的数据
|
||||
const list = ref<IotDeviceApi.DevicePropertyDetail[]>([]); // 列表的数据
|
||||
const total = ref(0); // 总数据量
|
||||
const thingModelDataType = ref<string>(''); // 物模型数据类型
|
||||
const propertyIdentifier = ref<string>(''); // 属性标识符
|
||||
@@ -62,7 +72,7 @@ const maxValue = computed(() => {
|
||||
if (isComplexDataType.value || list.value.length === 0) return '-';
|
||||
const values = list.value
|
||||
.map((item) => Number(item.value))
|
||||
.filter((v) => !isNaN(v));
|
||||
.filter((v) => !Number.isNaN(v));
|
||||
return values.length > 0 ? Math.max(...values).toFixed(2) : '-';
|
||||
});
|
||||
|
||||
@@ -70,7 +80,7 @@ const minValue = computed(() => {
|
||||
if (isComplexDataType.value || list.value.length === 0) return '-';
|
||||
const values = list.value
|
||||
.map((item) => Number(item.value))
|
||||
.filter((v) => !isNaN(v));
|
||||
.filter((v) => !Number.isNaN(v));
|
||||
return values.length > 0 ? Math.min(...values).toFixed(2) : '-';
|
||||
});
|
||||
|
||||
@@ -78,7 +88,7 @@ const avgValue = computed(() => {
|
||||
if (isComplexDataType.value || list.value.length === 0) return '-';
|
||||
const values = list.value
|
||||
.map((item) => Number(item.value))
|
||||
.filter((v) => !isNaN(v));
|
||||
.filter((v) => !Number.isNaN(v));
|
||||
if (values.length === 0) return '-';
|
||||
const sum = values.reduce((acc, val) => acc + val, 0);
|
||||
return (sum / values.length).toFixed(2);
|
||||
@@ -120,11 +130,11 @@ const paginationConfig = computed(() => ({
|
||||
}));
|
||||
|
||||
/** 获得设备历史数据 */
|
||||
const getList = async () => {
|
||||
async function getList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await DeviceApi.getHistoryDevicePropertyList(queryParams);
|
||||
list.value = data?.list || [];
|
||||
const data = await getHistoryDevicePropertyList(queryParams);
|
||||
list.value = (data?.list as IotDeviceApi.DevicePropertyDetail[]) || [];
|
||||
total.value = list.value.length;
|
||||
|
||||
// 如果是图表模式且不是复杂数据类型,渲染图表
|
||||
@@ -143,10 +153,10 @@ const getList = async () => {
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 渲染图表 */
|
||||
const renderChart = () => {
|
||||
function renderChart() {
|
||||
if (!list.value || list.value.length === 0) return;
|
||||
|
||||
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;
|
||||
queryParams.deviceId = deviceId;
|
||||
queryParams.identifier = identifier;
|
||||
@@ -271,10 +281,10 @@ const open = async (deviceId: number, identifier: string, dataType: string) => {
|
||||
// 等待弹窗完全渲染后再获取数据
|
||||
await nextTick();
|
||||
await getList();
|
||||
};
|
||||
}
|
||||
|
||||
/** 时间变化处理 */
|
||||
const handleTimeChange = () => {
|
||||
function handleTimeChange() {
|
||||
if (!dateRange.value || dateRange.value.length !== 2) {
|
||||
return;
|
||||
}
|
||||
@@ -285,15 +295,15 @@ const handleTimeChange = () => {
|
||||
];
|
||||
|
||||
getList();
|
||||
};
|
||||
}
|
||||
|
||||
/** 刷新数据 */
|
||||
const handleRefresh = () => {
|
||||
function handleRefresh() {
|
||||
getList();
|
||||
};
|
||||
}
|
||||
|
||||
/** 导出数据 */
|
||||
const handleExport = async () => {
|
||||
async function handleExport() {
|
||||
if (list.value.length === 0) {
|
||||
message.warning('暂无数据可导出');
|
||||
return;
|
||||
@@ -338,22 +348,22 @@ const handleExport = async () => {
|
||||
} finally {
|
||||
exporting.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 关闭弹窗 */
|
||||
const handleClose = () => {
|
||||
function handleClose() {
|
||||
dialogVisible.value = false;
|
||||
list.value = [];
|
||||
total.value = 0;
|
||||
};
|
||||
}
|
||||
|
||||
/** 格式化复杂数据类型 */
|
||||
const formatComplexValue = (value: any) => {
|
||||
function formatComplexValue(value: any) {
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
}
|
||||
|
||||
/** 监听视图模式变化,重新渲染图表 */
|
||||
watch(viewMode, async (newMode) => {
|
||||
@@ -380,78 +390,78 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
<div class="property-history-container">
|
||||
<!-- 工具栏 -->
|
||||
<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"
|
||||
:show-time="{ format: 'HH:mm:ss' }"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
:placeholder="['开始时间', '结束时间']"
|
||||
class="!w-[400px]"
|
||||
@change="handleTimeChange"
|
||||
@press-enter="handleTimeChange"
|
||||
/>
|
||||
|
||||
<!-- 刷新按钮 -->
|
||||
<a-button @click="handleRefresh" :loading="loading">
|
||||
<Button @click="handleRefresh" :loading="loading">
|
||||
<template #icon>
|
||||
<Icon icon="ant-design:reload-outlined" />
|
||||
<IconifyIcon icon="ant-design:reload-outlined" />
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
</Button>
|
||||
|
||||
<!-- 导出按钮 -->
|
||||
<a-button
|
||||
<Button
|
||||
@click="handleExport"
|
||||
:loading="exporting"
|
||||
:disabled="list.length === 0"
|
||||
>
|
||||
<template #icon>
|
||||
<Icon icon="ant-design:export-outlined" />
|
||||
<IconifyIcon icon="ant-design:export-outlined" />
|
||||
</template>
|
||||
导出
|
||||
</a-button>
|
||||
</Button>
|
||||
|
||||
<!-- 视图切换 -->
|
||||
<a-button-group class="ml-auto">
|
||||
<a-button
|
||||
<Button.Group class="ml-auto">
|
||||
<Button
|
||||
:type="viewMode === 'chart' ? 'primary' : 'default'"
|
||||
@click="viewMode = 'chart'"
|
||||
:disabled="isComplexDataType"
|
||||
>
|
||||
<template #icon>
|
||||
<Icon icon="ant-design:line-chart-outlined" />
|
||||
<IconifyIcon icon="ant-design:line-chart-outlined" />
|
||||
</template>
|
||||
图表
|
||||
</a-button>
|
||||
<a-button
|
||||
</Button>
|
||||
<Button
|
||||
:type="viewMode === 'list' ? 'primary' : 'default'"
|
||||
@click="viewMode = 'list'"
|
||||
>
|
||||
<template #icon>
|
||||
<Icon icon="ant-design:table-outlined" />
|
||||
<IconifyIcon icon="ant-design:table-outlined" />
|
||||
</template>
|
||||
列表
|
||||
</a-button>
|
||||
</a-button-group>
|
||||
</a-space>
|
||||
</Button>
|
||||
</Button.Group>
|
||||
</Space>
|
||||
|
||||
<!-- 数据统计信息 -->
|
||||
<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 v-if="viewMode === 'chart' && !isComplexDataType">
|
||||
最大值: {{ maxValue }} | 最小值: {{ minValue }} | 平均值:
|
||||
{{ avgValue }}
|
||||
</span>
|
||||
</a-space>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据展示区域 -->
|
||||
<a-spin :spinning="loading" :delay="200">
|
||||
<Spin :spinning="loading" :delay="200">
|
||||
<!-- 图表模式 -->
|
||||
<div v-if="viewMode === 'chart'" class="chart-container">
|
||||
<a-empty
|
||||
<Empty
|
||||
v-if="list.length === 0"
|
||||
:image="Empty.PRESENTED_IMAGE_SIMPLE"
|
||||
description="暂无数据"
|
||||
@@ -462,7 +472,7 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
|
||||
<!-- 表格模式 -->
|
||||
<div v-else class="table-container">
|
||||
<a-table
|
||||
<Table
|
||||
:data-source="list"
|
||||
:columns="tableColumns"
|
||||
:pagination="paginationConfig"
|
||||
@@ -475,19 +485,19 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
{{ formatDate(new Date(record.updateTime)) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<a-tag v-if="isComplexDataType" color="processing">
|
||||
<Tag v-if="isComplexDataType" color="processing">
|
||||
{{ formatComplexValue(record.value) }}
|
||||
</a-tag>
|
||||
</Tag>
|
||||
<span v-else class="font-medium">{{ record.value }}</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</Table>
|
||||
</div>
|
||||
</a-spin>
|
||||
</Spin>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<a-button @click="handleClose">关闭</a-button>
|
||||
<Button @click="handleClose">关闭</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -8,9 +8,17 @@ import { ContentWrap } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
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 {
|
||||
getThingModelServiceCallTypeLabel,
|
||||
IotDeviceMessageMethodEnum,
|
||||
@@ -48,8 +56,8 @@ const getList = async () => {
|
||||
if (!props.deviceId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await DeviceApi.getDeviceMessagePairPage(queryParams);
|
||||
list.value = data.list;
|
||||
const data = await getDeviceMessagePairPage(queryParams);
|
||||
list.value = data.list || [];
|
||||
total.value = data.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@@ -112,58 +120,58 @@ onMounted(() => {
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<a-form
|
||||
<Form
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
layout="inline"
|
||||
@submit.prevent
|
||||
style="margin-bottom: 16px"
|
||||
>
|
||||
<a-form-item label="标识符" name="identifier">
|
||||
<a-select
|
||||
<Form.Item label="标识符" name="identifier">
|
||||
<Select
|
||||
v-model:value="queryParams.identifier"
|
||||
placeholder="请选择服务标识符"
|
||||
allow-clear
|
||||
style="width: 240px"
|
||||
>
|
||||
<a-select-option
|
||||
<Select.Option
|
||||
v-for="service in serviceThingModels"
|
||||
:key="service.identifier"
|
||||
:value="service.identifier!"
|
||||
>
|
||||
{{ service.name }}({{ service.identifier }})
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="时间范围" name="times">
|
||||
<a-range-picker
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="时间范围" name="times">
|
||||
<RangePicker
|
||||
v-model:value="queryParams.times"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 360px"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleQuery">
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" @click="handleQuery">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:search" />
|
||||
</template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button @click="resetQuery" style="margin-left: 8px">
|
||||
</Button>
|
||||
<Button @click="resetQuery" style="margin-left: 8px">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="ep:refresh" />
|
||||
</template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<a-divider style="margin: 16px 0" />
|
||||
<Divider style="margin: 16px 0" />
|
||||
|
||||
<!-- 服务调用列表 -->
|
||||
<a-table v-loading="loading" :data-source="list" :pagination="false">
|
||||
<a-table-column
|
||||
<Table v-loading="loading" :data-source="list" :pagination="false">
|
||||
<Table.Column
|
||||
title="调用时间"
|
||||
align="center"
|
||||
data-index="requestTime"
|
||||
@@ -176,8 +184,8 @@ onMounted(() => {
|
||||
: '-'
|
||||
}}
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column
|
||||
</Table.Column>
|
||||
<Table.Column
|
||||
title="响应时间"
|
||||
align="center"
|
||||
data-index="responseTime"
|
||||
@@ -188,20 +196,20 @@ onMounted(() => {
|
||||
record.reply?.reportTime ? formatDate(record.reply.reportTime) : '-'
|
||||
}}
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column
|
||||
</Table.Column>
|
||||
<Table.Column
|
||||
title="标识符"
|
||||
align="center"
|
||||
data-index="identifier"
|
||||
:width="160"
|
||||
>
|
||||
<template #default="{ record }">
|
||||
<a-tag color="blue" size="small">
|
||||
<Tag color="blue" size="small">
|
||||
{{ record.request?.identifier }}
|
||||
</a-tag>
|
||||
</Tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column
|
||||
</Table.Column>
|
||||
<Table.Column
|
||||
title="服务名称"
|
||||
align="center"
|
||||
data-index="serviceName"
|
||||
@@ -210,8 +218,8 @@ onMounted(() => {
|
||||
<template #default="{ record }">
|
||||
{{ getServiceName(record.request?.identifier) }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column
|
||||
</Table.Column>
|
||||
<Table.Column
|
||||
title="调用方式"
|
||||
align="center"
|
||||
data-index="callType"
|
||||
@@ -220,13 +228,13 @@ onMounted(() => {
|
||||
<template #default="{ record }">
|
||||
{{ getCallType(record.request?.identifier) }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="输入参数" align="center" data-index="inputParams">
|
||||
</Table.Column>
|
||||
<Table.Column title="输入参数" align="center" data-index="inputParams">
|
||||
<template #default="{ record }">
|
||||
{{ parseParams(record.request?.params) }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="输出参数" align="center" data-index="outputParams">
|
||||
</Table.Column>
|
||||
<Table.Column title="输出参数" align="center" data-index="outputParams">
|
||||
<template #default="{ record }">
|
||||
<span v-if="record.reply">
|
||||
{{
|
||||
@@ -235,8 +243,8 @@ onMounted(() => {
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</Table.Column>
|
||||
</Table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DeviceVO } from '#/api/iot/device/device';
|
||||
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 { onMounted, ref, unref } from 'vue';
|
||||
@@ -9,11 +9,11 @@ import { useRoute, useRouter } from 'vue-router';
|
||||
import { Page } from '@vben/common-ui';
|
||||
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 { DeviceTypeEnum, ProductApi } from '#/api/iot/product/product';
|
||||
import { ThingModelApi } from '#/api/iot/thingmodel';
|
||||
import { getDevice } from '#/api/iot/device/device';
|
||||
import { DeviceTypeEnum, getProduct } from '#/api/iot/product/product';
|
||||
import { getThingModelListByProductId } from '#/api/iot/thingmodel';
|
||||
|
||||
import DeviceDetailConfig from './DeviceDetailConfig.vue';
|
||||
import DeviceDetailsHeader from './DeviceDetailsHeader.vue';
|
||||
@@ -27,38 +27,38 @@ defineOptions({ name: 'IoTDeviceDetail' });
|
||||
const route = useRoute();
|
||||
const id = Number(route.params.id); // 将字符串转换为数字
|
||||
const loading = ref(true); // 加载中
|
||||
const product = ref<ProductVO>({} as ProductVO); // 产品详情
|
||||
const device = ref<DeviceVO>({} as DeviceVO); // 设备详情
|
||||
const product = ref<IotProductApi.Product>({} as IotProductApi.Product); // 产品详情
|
||||
const device = ref<IotDeviceApi.Device>({} as IotDeviceApi.Device); // 设备详情
|
||||
const activeTab = ref('info'); // 默认激活的标签页
|
||||
const thingModelList = ref<ThingModelData[]>([]); // 物模型列表数据
|
||||
|
||||
/** 获取设备详情 */
|
||||
const getDeviceData = async () => {
|
||||
async function getDeviceData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
device.value = await DeviceApi.getDevice(id);
|
||||
device.value = await getDevice(id);
|
||||
await getProductData(device.value.productId);
|
||||
await getThingModelList(device.value.productId);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 获取产品详情 */
|
||||
const getProductData = async (id: number) => {
|
||||
product.value = await ProductApi.getProduct(id);
|
||||
};
|
||||
async function getProductData(id: number) {
|
||||
product.value = await getProduct(id);
|
||||
}
|
||||
|
||||
/** 获取物模型列表 */
|
||||
const getThingModelList = async (productId: number) => {
|
||||
async function getThingModelList(productId: number) {
|
||||
try {
|
||||
const data = await ThingModelApi.getThingModelList(productId);
|
||||
const data = await getThingModelListByProductId(productId);
|
||||
thingModelList.value = data || [];
|
||||
} catch (error) {
|
||||
console.error('获取物模型列表失败:', error);
|
||||
thingModelList.value = [];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
const tabbarStore = useTabbarStore(); // 视图操作
|
||||
@@ -83,47 +83,47 @@ onMounted(async () => {
|
||||
@refresh="getDeviceData"
|
||||
/>
|
||||
|
||||
<a-tabs v-model:active-key="activeTab" class="device-detail-tabs mt-4">
|
||||
<a-tab-pane key="info" tab="设备信息">
|
||||
<Tabs v-model:active-key="activeTab" class="device-detail-tabs mt-4">
|
||||
<Tabs.Pane key="info" tab="设备信息">
|
||||
<DeviceDetailsInfo
|
||||
v-if="activeTab === 'info'"
|
||||
:product="product"
|
||||
:device="device"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="model" tab="物模型数据">
|
||||
</Tabs.Pane>
|
||||
<Tabs.Pane key="model" tab="物模型数据">
|
||||
<DeviceDetailsThingModel
|
||||
v-if="activeTab === 'model' && device.id"
|
||||
:device-id="device.id"
|
||||
:thing-model-list="thingModelList"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane
|
||||
</Tabs.Pane>
|
||||
<Tabs.Pane
|
||||
v-if="product.deviceType === DeviceTypeEnum.GATEWAY"
|
||||
key="sub-device"
|
||||
tab="子设备管理"
|
||||
/>
|
||||
<a-tab-pane key="log" tab="设备消息">
|
||||
<Tabs.Pane key="log" tab="设备消息">
|
||||
<DeviceDetailsMessage
|
||||
v-if="activeTab === 'log' && device.id"
|
||||
:device-id="device.id"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="simulator" tab="模拟设备">
|
||||
</Tabs.Pane>
|
||||
<Tabs.Pane key="simulator" tab="模拟设备">
|
||||
<DeviceDetailsSimulator
|
||||
v-if="activeTab === 'simulator'"
|
||||
:product="product"
|
||||
:device="device"
|
||||
:thing-model-list="thingModelList"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="config" tab="设备配置">
|
||||
</Tabs.Pane>
|
||||
<Tabs.Pane key="config" tab="设备配置">
|
||||
<DeviceDetailConfig
|
||||
v-if="activeTab === 'config'"
|
||||
:device="device"
|
||||
@success="getDeviceData"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</Tabs.Pane>
|
||||
</Tabs>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user