feat(iot):【网关设备:80%】动态注册的初步实现(已测试)

This commit is contained in:
YunaiV
2026-01-25 18:50:26 +08:00
parent c55465a6c0
commit 1ce562601f
17 changed files with 507 additions and 173 deletions

View File

@@ -21,7 +21,6 @@ export namespace IotDeviceApi {
offlineTime?: Date; // 最后离线时间 offlineTime?: Date; // 最后离线时间
activeTime?: Date; // 设备激活时间 activeTime?: Date; // 设备激活时间
deviceSecret?: string; // 设备密钥,用于设备认证 deviceSecret?: string; // 设备密钥,用于设备认证
authType?: string; // 认证类型(如一机一密、动态注册)
config?: string; // 设备配置 config?: string; // 设备配置
latitude?: number; // 设备位置的纬度 latitude?: number; // 设备位置的纬度
longitude?: number; // 设备位置的经度 longitude?: number; // 设备位置的经度
@@ -201,3 +200,35 @@ export function getDeviceMessagePairPage(params: PageParam) {
export function sendDeviceMessage(params: IotDeviceApi.DeviceMessageSendReq) { export function sendDeviceMessage(params: IotDeviceApi.DeviceMessageSendReq) {
return requestClient.post('/iot/device/message/send', params); return requestClient.post('/iot/device/message/send', params);
} }
/** 绑定子设备到网关设备 */
export function bindDeviceGateway(gatewayId: number, subIds: number[]) {
return requestClient.put<boolean>('/iot/device/bind-gateway', {
gatewayId,
subIds,
});
}
/** 解绑子设备与网关设备 */
export function unbindDeviceGateway(gatewayId: number, subIds: number[]) {
return requestClient.put<boolean>('/iot/device/unbind-gateway', {
gatewayId,
subIds,
});
}
/** 获取网关设备的子设备列表 */
export function getSubDeviceList(gatewayId: number) {
return requestClient.get<IotDeviceApi.Device[]>(
'/iot/device/sub-device-list',
{ params: { gatewayId } },
);
}
/** 获取未绑定的子设备分页 */
export function getUnboundSubDevicePage(params: PageParam) {
return requestClient.get<PageResult<IotDeviceApi.Device>>(
'/iot/device/unbound-sub-device-page',
{ params },
);
}

View File

@@ -8,6 +8,7 @@ export namespace IotProductApi {
id?: number; // 产品编号 id?: number; // 产品编号
name: string; // 产品名称 name: string; // 产品名称
productKey?: string; // 产品标识 productKey?: string; // 产品标识
productSecret?: string; // 产品密钥
protocolId?: number; // 协议编号 protocolId?: number; // 协议编号
protocolType?: number; // 接入协议类型 protocolType?: number; // 接入协议类型
categoryId?: number; // 产品所属品类标识符 categoryId?: number; // 产品所属品类标识符
@@ -21,6 +22,7 @@ export namespace IotProductApi {
codecType?: string; // 数据格式(编解码器类型) codecType?: string; // 数据格式(编解码器类型)
dataFormat?: number; // 数据格式 dataFormat?: number; // 数据格式
validateType?: number; // 认证方式 validateType?: number; // 认证方式
registerEnabled?: boolean; // 是否开启动态注册
deviceCount?: number; // 设备数量 deviceCount?: number; // 设备数量
createTime?: Date; // 创建时间 createTime?: Date; // 创建时间
} }
@@ -67,8 +69,13 @@ export function updateProductStatus(id: number, status: number) {
} }
/** 查询产品(精简)列表 */ /** 查询产品(精简)列表 */
export function getSimpleProductList() { export function getSimpleProductList(deviceType?: number) {
return requestClient.get<IotProductApi.Product[]>('/iot/product/simple-list'); return requestClient.get<IotProductApi.Product[]>(
'/iot/product/simple-list',
{
params: { deviceType },
},
);
} }
/** 根据 ProductKey 获取产品信息 */ /** 根据 ProductKey 获取产品信息 */

View File

@@ -1,11 +1,10 @@
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DeviceTypeEnum, DICT_TYPE } from '@vben/constants'; import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks'; import { getDictOptions } from '@vben/hooks';
import { z } from '#/adapter/form'; import { z } from '#/adapter/form';
import { getSimpleDeviceList } from '#/api/iot/device/device';
import { getSimpleDeviceGroupList } from '#/api/iot/device/group'; import { getSimpleDeviceGroupList } from '#/api/iot/device/group';
import { getSimpleProductList } from '#/api/iot/product/product'; import { getSimpleProductList } from '#/api/iot/product/product';
@@ -64,21 +63,6 @@ export function useBasicFormSchema(): VbenFormSchema[] {
'支持英文字母、数字、下划线_、中划线-)、点号(.)、半角冒号(:)和特殊字符@', '支持英文字母、数字、下划线_、中划线-)、点号(.)、半角冒号(:)和特殊字符@',
), ),
}, },
{
fieldName: 'gatewayId',
label: '网关设备',
component: 'ApiSelect',
componentProps: {
api: () => getSimpleDeviceList(DeviceTypeEnum.GATEWAY),
labelField: 'deviceName',
valueField: 'id',
placeholder: '子设备可选择父设备',
},
dependencies: {
triggerFields: ['deviceType'],
show: (values) => values.deviceType === DeviceTypeEnum.GATEWAY_SUB,
},
},
]; ];
} }

View File

@@ -4,10 +4,11 @@ import type { IotDeviceApi } from '#/api/iot/device/device';
import { computed, ref, watchEffect } from 'vue'; import { computed, ref, watchEffect } from 'vue';
import { IotDeviceMessageMethodEnum } from '@vben/constants';
import { Alert, Button, message, Textarea } from 'ant-design-vue'; import { Alert, Button, message, Textarea } from 'ant-design-vue';
import { sendDeviceMessage, updateDevice } from '#/api/iot/device/device'; import { sendDeviceMessage, updateDevice } from '#/api/iot/device/device';
import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants';
defineOptions({ name: 'DeviceDetailConfig' }); defineOptions({ name: 'DeviceDetailConfig' });

View File

@@ -11,7 +11,7 @@ import {
} from 'vue'; } from 'vue';
import { Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants'; import { DICT_TYPE, IotDeviceMessageMethodEnum } from '@vben/constants';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { formatDateTime } from '@vben/utils'; import { formatDateTime } from '@vben/utils';
@@ -19,7 +19,6 @@ import { Button, Select, Space, Switch, Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getDeviceMessagePage } from '#/api/iot/device/device'; import { getDeviceMessagePage } from '#/api/iot/device/device';
import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants';
const props = defineProps<{ const props = defineProps<{
deviceId: number; deviceId: number;

View File

@@ -9,6 +9,7 @@ import type { ThingModelData } from '#/api/iot/thingmodel';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui'; import { ContentWrap } from '@vben/common-ui';
import { IotDeviceMessageMethodEnum } from '@vben/constants';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { import {
@@ -26,7 +27,6 @@ import {
import { sendDeviceMessage } from '#/api/iot/device/device'; import { sendDeviceMessage } from '#/api/iot/device/device';
import { import {
DeviceStateEnum, DeviceStateEnum,
IotDeviceMessageMethodEnum,
IoTThingModelTypeEnum, IoTThingModelTypeEnum,
} from '#/views/iot/utils/constants'; } from '#/views/iot/utils/constants';

View File

@@ -1,20 +1,23 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { PageParam } from '@vben/request'; import type { VbenFormSchema, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { IotDeviceApi } from '#/api/iot/device/device'; import type { IotDeviceApi } from '#/api/iot/device/device';
import type { IotProductApi } from '#/api/iot/product/product';
import { onMounted, ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { Page } from '@vben/common-ui'; import { confirm, Page, useVbenModal } from '@vben/common-ui';
import { DeviceTypeEnum, DICT_TYPE } from '@vben/constants'; import { DeviceTypeEnum, DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks'; import { formatDateTime, isEmpty } from '@vben/utils';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getDevicePage } from '#/api/iot/device/device'; import {
bindDeviceGateway,
getSubDeviceList,
getUnboundSubDevicePage,
unbindDeviceGateway,
} from '#/api/iot/device/device';
import { getSimpleProductList } from '#/api/iot/product/product'; import { getSimpleProductList } from '#/api/iot/product/product';
interface Props { interface Props {
@@ -24,10 +27,10 @@ interface Props {
const props = defineProps<Props>(); const props = defineProps<Props>();
const router = useRouter(); const router = useRouter();
const products = ref<IotProductApi.Product[]>([]); // 产品列表 /** 子设备列表表格列配置 */
function useGridColumns(): VxeTableGridOptions['columns'] { function useGridColumns(): VxeTableGridOptions['columns'] {
return [ return [
{ type: 'checkbox', width: 40 },
{ {
field: 'deviceName', field: 'deviceName',
title: 'DeviceName', title: 'DeviceName',
@@ -39,10 +42,9 @@ function useGridColumns(): VxeTableGridOptions['columns'] {
minWidth: 120, minWidth: 120,
}, },
{ {
field: 'productId', field: 'productName',
title: '所属产品', title: '产品名称',
minWidth: 120, minWidth: 120,
slots: { default: 'product' },
}, },
{ {
field: 'state', field: 'state',
@@ -56,21 +58,125 @@ function useGridColumns(): VxeTableGridOptions['columns'] {
{ {
field: 'onlineTime', field: 'onlineTime',
title: '最后上线时间', title: '最后上线时间',
minWidth: 180, minWidth: 160,
formatter: 'formatDateTime', formatter: ({ cellValue }) => formatDateTime(cellValue),
}, },
{ {
field: 'actions',
title: '操作', title: '操作',
width: 150, width: 120,
fixed: 'right', fixed: 'right',
slots: { default: 'actions' }, slots: { default: 'actions' },
}, },
]; ];
} }
/** 搜索表单 schema */ const [Grid, gridApi] = useVbenVxeGrid<IotDeviceApi.Device>({
function useGridFormSchema(): VbenFormSchema[] { gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async () => {
if (!props.deviceId) {
return [];
}
return await getSubDeviceList(props.deviceId);
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
pagerConfig: {
enabled: false,
},
},
gridEvents: {
checkboxAll: handleRowCheckboxChange,
checkboxChange: handleRowCheckboxChange,
},
});
/** 获取子设备列表 */
function getList() {
gridApi.query();
}
/** 打开设备详情 */
function openDeviceDetail(id: number) {
router.push({ name: 'IoTDeviceDetail', params: { id } });
}
/** 多选框选中数据 */
const checkedIds = ref<number[]>([]);
function handleRowCheckboxChange({
records,
}: {
records: IotDeviceApi.Device[];
}) {
checkedIds.value = records.map((item) => item.id!);
}
/** 解绑单个设备 */
async function handleUnbind(row: IotDeviceApi.Device) {
await confirm({ content: `确定要解绑子设备【${row.deviceName}】吗?` });
const hideLoading = message.loading({
content: `正在解绑【${row.deviceName}】...`,
duration: 0,
});
try {
await unbindDeviceGateway(props.deviceId, [row.id!]);
message.success('解绑成功');
getList();
} finally {
hideLoading();
}
}
/** 批量解绑 */
async function handleUnbindBatch() {
await confirm({
content: `确定要解绑选中的 ${checkedIds.value.length} 个子设备吗?`,
});
const hideLoading = message.loading({
content: '正在批量解绑...',
duration: 0,
});
try {
await unbindDeviceGateway(props.deviceId, checkedIds.value);
checkedIds.value = [];
message.success('批量解绑成功');
getList();
} finally {
hideLoading();
}
}
// ===================== 添加子设备弹窗 =====================
const addSelectedRowKeys = ref<number[]>([]);
/** 添加弹窗搜索表单 schema */
function useAddGridFormSchema(): VbenFormSchema[] {
return [ return [
{
fieldName: 'productId',
label: '产品',
component: 'ApiSelect',
componentProps: {
api: () => getSimpleProductList(DeviceTypeEnum.GATEWAY_SUB),
labelField: 'name',
valueField: 'id',
placeholder: '请选择产品',
allowClear: true,
},
},
{ {
fieldName: 'deviceName', fieldName: 'deviceName',
label: 'DeviceName', label: 'DeviceName',
@@ -80,116 +186,171 @@ function useGridFormSchema(): VbenFormSchema[] {
allowClear: true, allowClear: true,
}, },
}, },
];
}
function useAddGridColumns(): VxeTableGridOptions['columns'] {
return [
{ type: 'checkbox', width: 40 },
{ {
fieldName: 'status', field: 'deviceName',
label: '设备状态', title: 'DeviceName',
component: 'Select', minWidth: 150,
componentProps: { },
options: getDictOptions(DICT_TYPE.IOT_DEVICE_STATE, 'number'), {
placeholder: '请选择设备状态', field: 'nickname',
allowClear: true, title: '备注名称',
minWidth: 120,
},
{
field: 'productName',
title: '产品名称',
minWidth: 120,
},
{
field: 'state',
title: '设备状态',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.IOT_DEVICE_STATE },
}, },
}, },
]; ];
} }
const [Grid, gridApi] = useVbenVxeGrid<IotDeviceApi.Device>({ const [AddGrid, addGridApi] = useVbenVxeGrid<IotDeviceApi.Device>({
formOptions: { formOptions: {
schema: useGridFormSchema(), schema: useAddGridFormSchema(),
submitOnChange: true,
}, },
gridOptions: { gridOptions: {
columns: useGridColumns(), columns: useAddGridColumns(),
height: 'auto', height: 400,
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getUnboundSubDevicePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: { rowConfig: {
keyField: 'id', keyField: 'id',
isHover: true, isHover: true,
}, },
proxyConfig: {
ajax: {
query: async (
{
page,
}: {
page: { currentPage: number; pageSize: number };
},
formValues?: { deviceName?: string; status?: number },
) => {
if (!props.deviceId) {
return { list: [], total: 0 };
}
return await getDevicePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
gatewayId: props.deviceId,
deviceType: DeviceTypeEnum.GATEWAY_SUB,
deviceName: formValues?.deviceName || undefined,
status: formValues?.status,
} as PageParam);
},
},
},
toolbarConfig: { toolbarConfig: {
refresh: true, refresh: true,
search: true, search: true,
}, },
pagerConfig: { },
enabled: true, gridEvents: {
}, checkboxAll: handleAddSelectionChange,
checkboxChange: handleAddSelectionChange,
}, },
}); });
/** 获取产品名称 */ /** 处理添加弹窗表格选择变化 */
function getProductName(productId: number) { function handleAddSelectionChange() {
const product = products.value.find((p) => p.id === productId); const records = addGridApi.grid?.getCheckboxRecords() || [];
return product?.name || '-'; addSelectedRowKeys.value = records.map(
(record: IotDeviceApi.Device) => record.id!,
);
} }
/** 查看详情 */ const [AddModal, addModalApi] = useVbenModal({
function openDetail(id: number) { async onConfirm() {
router.push({ name: 'IoTDeviceDetail', params: { id } }); if (addSelectedRowKeys.value.length === 0) {
} message.warning('请先选择要添加的子设备');
return;
/** 监听设备ID变化 */ }
watch( addModalApi.lock();
() => props.deviceId, try {
(newValue) => { await bindDeviceGateway(props.deviceId, addSelectedRowKeys.value);
if (newValue) { message.success('绑定成功');
gridApi.query(); await addModalApi.close();
addSelectedRowKeys.value = [];
getList();
} finally {
addModalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (isOpen) {
addSelectedRowKeys.value = [];
await addGridApi.formApi?.resetForm();
await addGridApi.query();
} }
}, },
);
/** 初始化 */
onMounted(async () => {
// 获取产品列表
products.value = await getSimpleProductList();
// 如果设备ID存在则查询列表
if (props.deviceId) {
gridApi.query();
}
}); });
/** 打开添加子设备弹窗 */
function openAddModal() {
addModalApi.open();
}
/** 监听 deviceId 变化 */
watch(
() => props.deviceId,
(newVal) => {
if (newVal) {
getList();
}
},
{ immediate: true },
);
</script> </script>
<template> <template>
<Page auto-content-height> <Page auto-content-height>
<!-- 子设备列表 --> <!-- 子设备列表 -->
<Grid> <Grid table-title="子设备列表">
<template #product="{ row }"> <template #toolbar-tools>
{{ getProductName(row.productId) }} <TableAction
:actions="[
{
label: '添加子设备',
type: 'primary',
icon: ACTION_ICON.ADD,
onClick: openAddModal,
},
{
label: '批量解绑',
type: 'primary',
danger: true,
icon: ACTION_ICON.DELETE,
disabled: isEmpty(checkedIds),
onClick: handleUnbindBatch,
},
]"
/>
</template> </template>
<template #actions="{ row }"> <template #actions="{ row }">
<TableAction <TableAction
:actions="[ :actions="[
{ {
label: '查看详情', label: '查看',
type: 'link', type: 'link',
icon: ACTION_ICON.VIEW, onClick: () => openDeviceDetail(row.id!),
onClick: openDetail.bind(null, row.id!), },
{
label: '解绑',
type: 'link',
danger: true,
onClick: () => handleUnbind(row),
}, },
]" ]"
/> />
</template> </template>
</Grid> </Grid>
<!-- 添加子设备弹窗 -->
<AddModal title="添加子设备" class="w-3/5">
<AddGrid />
</AddModal>
</Page> </Page>
</template> </template>

View File

@@ -6,6 +6,7 @@ import type { ThingModelData } from '#/api/iot/thingmodel';
import { computed, onMounted, reactive, watch } from 'vue'; import { computed, onMounted, reactive, watch } from 'vue';
import { Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
import { IotDeviceMessageMethodEnum } from '@vben/constants';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { formatDateTime } from '@vben/utils'; import { formatDateTime } from '@vben/utils';
@@ -15,7 +16,6 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getDeviceMessagePairPage } from '#/api/iot/device/device'; import { getDeviceMessagePairPage } from '#/api/iot/device/device';
import { import {
getEventTypeLabel, getEventTypeLabel,
IotDeviceMessageMethodEnum,
IoTThingModelTypeEnum, IoTThingModelTypeEnum,
} from '#/views/iot/utils/constants'; } from '#/views/iot/utils/constants';

View File

@@ -6,6 +6,7 @@ import type { ThingModelData } from '#/api/iot/thingmodel';
import { computed, onMounted, reactive, watch } from 'vue'; import { computed, onMounted, reactive, watch } from 'vue';
import { Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
import { IotDeviceMessageMethodEnum } from '@vben/constants';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { formatDateTime } from '@vben/utils'; import { formatDateTime } from '@vben/utils';
@@ -15,7 +16,6 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getDeviceMessagePairPage } from '#/api/iot/device/device'; import { getDeviceMessagePairPage } from '#/api/iot/device/device';
import { import {
getThingModelServiceCallTypeLabel, getThingModelServiceCallTypeLabel,
IotDeviceMessageMethodEnum,
IoTThingModelTypeEnum, IoTThingModelTypeEnum,
} from '#/views/iot/utils/constants'; } from '#/views/iot/utils/constants';

View File

@@ -0,0 +1 @@
export { default as ProductSelect } from './select.vue';

View File

@@ -0,0 +1,57 @@
<script lang="ts" setup>
import type { IotProductApi } from '#/api/iot/product/product';
import { onMounted, ref } from 'vue';
import { Select } from 'ant-design-vue';
import { getSimpleProductList } from '#/api/iot/product/product';
/** 产品下拉选择器组件 */
defineOptions({ name: 'ProductSelect' });
const props = defineProps<{
deviceType?: number; // 设备类型过滤
modelValue?: number;
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value?: number): void;
(e: 'change', value?: number): void;
}>();
const loading = ref(false);
const productList = ref<IotProductApi.Product[]>([]);
/** 处理选择变化 */
function handleChange(value?: number) {
emit('update:modelValue', value);
emit('change', value);
}
/** 获取产品列表 */
async function getProductList() {
try {
loading.value = true;
productList.value = (await getSimpleProductList(props.deviceType)) || [];
} finally {
loading.value = false;
}
}
onMounted(() => {
getProductList();
});
</script>
<template>
<Select
:value="modelValue"
:options="productList.map((p) => ({ label: p.name, value: p.id }))"
:loading="loading"
placeholder="请选择产品"
allow-clear
class="w-full"
@change="handleChange"
/>
</template>

View File

@@ -153,9 +153,20 @@ export function useBasicFormSchema(
]; ];
} }
/** 高级设置表单字段(图标、图片、产品描述) */ /** 高级设置表单字段(图标、图片、产品描述、动态注册 */
export function useAdvancedFormSchema(): VbenFormSchema[] { export function useAdvancedFormSchema(): VbenFormSchema[] {
return [ return [
{
fieldName: 'registerEnabled',
label: '动态注册',
component: 'Switch',
componentProps: {
checkedChildren: '开',
unCheckedChildren: '关',
},
defaultValue: false,
help: '设备动态注册无需一一烧录设备证书DeviceSecret每台设备烧录相同的产品证书即 ProductKey 和 ProductSecret ,云端鉴权通过后下发设备证书,您可以根据需要开启或关闭动态注册,保障安全性。',
},
{ {
fieldName: 'icon', fieldName: 'icon',
label: '产品图标', label: '产品图标',

View File

@@ -1,9 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { IotProductApi } from '#/api/iot/product/product'; import type { IotProductApi } from '#/api/iot/product/product';
import { ref } from 'vue';
import { DeviceTypeEnum, DICT_TYPE } from '@vben/constants'; import { DeviceTypeEnum, DICT_TYPE } from '@vben/constants';
import { Card, Descriptions } from 'ant-design-vue'; import { Button, Card, Descriptions, message } from 'ant-design-vue';
import { DictTag } from '#/components/dict-tag'; import { DictTag } from '#/components/dict-tag';
@@ -13,11 +15,28 @@ interface Props {
defineProps<Props>(); defineProps<Props>();
const showProductSecret = ref(false); // 是否显示产品密钥
/** 格式化日期 */ /** 格式化日期 */
function formatDate(date?: Date | string) { function formatDate(date?: Date | string) {
if (!date) return '-'; if (!date) return '-';
return new Date(date).toLocaleString('zh-CN'); return new Date(date).toLocaleString('zh-CN');
} }
/** 切换产品密钥显示状态 */
function toggleProductSecretVisible() {
showProductSecret.value = !showProductSecret.value;
}
/** 复制到剪贴板 */
async function copyToClipboard(text: string) {
try {
await navigator.clipboard.writeText(text);
message.success('复制成功');
} catch {
message.error('复制失败');
}
}
</script> </script>
<template> <template>
@@ -54,6 +73,23 @@ function formatDate(date?: Date | string) {
> >
<DictTag :type="DICT_TYPE.IOT_NET_TYPE" :value="product.netType" /> <DictTag :type="DICT_TYPE.IOT_NET_TYPE" :value="product.netType" />
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item v-if="product.productSecret" label="ProductSecret">
<span v-if="showProductSecret">{{ product.productSecret }}</span>
<span v-else>********</span>
<Button class="ml-2" size="small" @click="toggleProductSecretVisible">
{{ showProductSecret ? '隐藏' : '显示' }}
</Button>
<Button
class="ml-2"
size="small"
@click="copyToClipboard(product.productSecret || '')"
>
复制
</Button>
</Descriptions.Item>
<Descriptions.Item label="动态注册">
{{ product.registerEnabled ? '已开启' : '未开启' }}
</Descriptions.Item>
<Descriptions.Item :span="3" label="产品描述"> <Descriptions.Item :span="3" label="产品描述">
{{ product.description || '-' }} {{ product.description || '-' }}
</Descriptions.Item> </Descriptions.Item>

View File

@@ -68,6 +68,7 @@ async function getAdvancedFormValues() {
} }
// 表单未挂载(折叠状态),从 formData 中获取 // 表单未挂载(折叠状态),从 formData 中获取
return { return {
registerEnabled: formData.value?.registerEnabled,
icon: formData.value?.icon, icon: formData.value?.icon,
picUrl: formData.value?.picUrl, picUrl: formData.value?.picUrl,
description: formData.value?.description, description: formData.value?.description,
@@ -120,6 +121,7 @@ const [Modal, modalApi] = useVbenModal({
await formApi.setValues(formData.value); await formApi.setValues(formData.value);
// 如果存在高级字段数据,自动展开 Collapse // 如果存在高级字段数据,自动展开 Collapse
if ( if (
formData.value?.registerEnabled ||
formData.value?.icon || formData.value?.icon ||
formData.value?.picUrl || formData.value?.picUrl ||
formData.value?.description formData.value?.description

View File

@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'; import { computed, onMounted, reactive, ref } from 'vue';
import { IotDeviceMessageMethodEnum } from '@vben/constants';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { Button, Form, Select, Table } from 'ant-design-vue'; import { Button, Form, Select, Table } from 'ant-design-vue';
@@ -8,10 +9,7 @@ import { Button, Form, Select, Table } from 'ant-design-vue';
import { getSimpleDeviceList } from '#/api/iot/device/device'; import { getSimpleDeviceList } from '#/api/iot/device/device';
import { getSimpleProductList } from '#/api/iot/product/product'; import { getSimpleProductList } from '#/api/iot/product/product';
import { getThingModelListByProductId } from '#/api/iot/thingmodel'; import { getThingModelListByProductId } from '#/api/iot/thingmodel';
import { import { IoTThingModelTypeEnum } from '#/views/iot/utils/constants';
IotDeviceMessageMethodEnum,
IoTThingModelTypeEnum,
} from '#/views/iot/utils/constants';
const formData = ref<any[]>([]); const formData = ref<any[]>([]);
const productList = ref<any[]>([]); // 产品列表 const productList = ref<any[]>([]); // 产品列表

View File

@@ -1,3 +1,5 @@
// TODO @AI感觉这块放到 biz-iot-enum 里好点。
/** 检查值是否为空 */ /** 检查值是否为空 */
const isEmpty = (value: any): boolean => { const isEmpty = (value: any): boolean => {
return value === null || value === undefined || value === ''; return value === null || value === undefined || value === '';
@@ -22,49 +24,6 @@ export const IoTThingModelTypeEnum = {
EVENT: 3, // 事件 EVENT: 3, // 事件
}; };
/** IoT 设备消息的方法枚举 */
export const IotDeviceMessageMethodEnum = {
// ========== 设备状态 ==========
STATE_UPDATE: {
method: 'thing.state.update',
name: '设备状态变更',
upstream: true,
},
// ========== 设备属性 ==========
PROPERTY_POST: {
method: 'thing.property.post',
name: '属性上报',
upstream: true,
},
PROPERTY_SET: {
method: 'thing.property.set',
name: '属性设置',
upstream: false,
},
// ========== 设备事件 ==========
EVENT_POST: {
method: 'thing.event.post',
name: '事件上报',
upstream: true,
},
// ========== 服务调用 ==========
SERVICE_INVOKE: {
method: 'thing.service.invoke',
name: '服务调用',
upstream: false,
},
// ========== 设备配置 ==========
CONFIG_PUSH: {
method: 'thing.config.push',
name: '配置推送',
upstream: false,
},
};
// IoT 产品物模型服务调用方式枚举 // IoT 产品物模型服务调用方式枚举
export const IoTThingModelServiceCallTypeEnum = { export const IoTThingModelServiceCallTypeEnum = {
ASYNC: { ASYNC: {

View File

@@ -26,18 +26,105 @@ export const ProductStatusEnum = {
PUBLISHED: 1, // 已发布 PUBLISHED: 1, // 已发布
} as const; } as const;
/**
* 产品定位类型枚举
*/
export const LocationTypeEnum = {
IP: 1, // IP 定位
MANUAL: 3, // 手动定位
MODULE: 2, // 设备定位
} as const;
/** /**
* 数据格式(编解码器类型)枚举 * 数据格式(编解码器类型)枚举
*/ */
export const CodecTypeEnum = { export const CodecTypeEnum = {
ALINK: 'Alink', // 阿里云 Alink 协议 ALINK: 'Alink', // 阿里云 Alink 协议
} as const; } as const;
/**
* IoT 设备消息的方法枚举
*/
export const IotDeviceMessageMethodEnum = {
// ========== 设备状态 ==========
STATE_UPDATE: {
method: 'thing.state.update',
name: '设备状态更新',
upstream: true,
},
// ========== 拓扑管理 ==========
TOPO_ADD: {
method: 'thing.topo.add',
name: '添加拓扑关系',
upstream: true,
},
TOPO_DELETE: {
method: 'thing.topo.delete',
name: '删除拓扑关系',
upstream: true,
},
TOPO_GET: {
method: 'thing.topo.get',
name: '获取拓扑关系',
upstream: true,
},
TOPO_CHANGE: {
method: 'thing.topo.change',
name: '拓扑关系变更通知',
upstream: false,
},
// ========== 设备注册 ==========
DEVICE_REGISTER: {
method: 'thing.auth.register',
name: '设备动态注册',
upstream: true,
},
SUB_DEVICE_REGISTER: {
method: 'thing.auth.register.sub',
name: '子设备动态注册',
upstream: true,
},
// ========== 设备属性 ==========
PROPERTY_POST: {
method: 'thing.property.post',
name: '属性上报',
upstream: true,
},
PROPERTY_SET: {
method: 'thing.property.set',
name: '属性设置',
upstream: false,
},
PROPERTY_PACK_POST: {
method: 'thing.event.property.pack.post',
name: '批量上报(属性 + 事件 + 子设备)',
upstream: true,
},
// ========== 设备事件 ==========
EVENT_POST: {
method: 'thing.event.post',
name: '事件上报',
upstream: true,
},
// ========== 服务调用 ==========
SERVICE_INVOKE: {
method: 'thing.service.invoke',
name: '服务调用',
upstream: false,
},
// ========== 设备配置 ==========
CONFIG_PUSH: {
method: 'thing.config.push',
name: '配置推送',
upstream: false,
},
// ========== OTA 固件 ==========
OTA_UPGRADE: {
method: 'thing.ota.upgrade',
name: 'OTA 固件信息推送',
upstream: false,
},
OTA_PROGRESS: {
method: 'thing.ota.progress',
name: 'OTA 升级进度上报',
upstream: true,
},
} as const;