feat(iot):【网关设备:80%】动态注册的初步实现(已测试)
This commit is contained in:
@@ -21,7 +21,6 @@ export namespace IotDeviceApi {
|
||||
offlineTime?: Date; // 最后离线时间
|
||||
activeTime?: Date; // 设备激活时间
|
||||
deviceSecret?: string; // 设备密钥,用于设备认证
|
||||
authType?: string; // 认证类型(如一机一密、动态注册)
|
||||
config?: string; // 设备配置
|
||||
latitude?: number; // 设备位置的纬度
|
||||
longitude?: number; // 设备位置的经度
|
||||
@@ -201,3 +200,35 @@ export function getDeviceMessagePairPage(params: PageParam) {
|
||||
export function sendDeviceMessage(params: IotDeviceApi.DeviceMessageSendReq) {
|
||||
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 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export namespace IotProductApi {
|
||||
id?: number; // 产品编号
|
||||
name: string; // 产品名称
|
||||
productKey?: string; // 产品标识
|
||||
productSecret?: string; // 产品密钥
|
||||
protocolId?: number; // 协议编号
|
||||
protocolType?: number; // 接入协议类型
|
||||
categoryId?: number; // 产品所属品类标识符
|
||||
@@ -21,6 +22,7 @@ export namespace IotProductApi {
|
||||
codecType?: string; // 数据格式(编解码器类型)
|
||||
dataFormat?: number; // 数据格式
|
||||
validateType?: number; // 认证方式
|
||||
registerEnabled?: boolean; // 是否开启动态注册
|
||||
deviceCount?: number; // 设备数量
|
||||
createTime?: Date; // 创建时间
|
||||
}
|
||||
@@ -67,8 +69,13 @@ export function updateProductStatus(id: number, status: number) {
|
||||
}
|
||||
|
||||
/** 查询产品(精简)列表 */
|
||||
export function getSimpleProductList() {
|
||||
return requestClient.get<IotProductApi.Product[]>('/iot/product/simple-list');
|
||||
export function getSimpleProductList(deviceType?: number) {
|
||||
return requestClient.get<IotProductApi.Product[]>(
|
||||
'/iot/product/simple-list',
|
||||
{
|
||||
params: { deviceType },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 根据 ProductKey 获取产品信息 */
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
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 { z } from '#/adapter/form';
|
||||
import { getSimpleDeviceList } from '#/api/iot/device/device';
|
||||
import { getSimpleDeviceGroupList } from '#/api/iot/device/group';
|
||||
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,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@ import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||
|
||||
import { computed, ref, watchEffect } from 'vue';
|
||||
|
||||
import { IotDeviceMessageMethodEnum } from '@vben/constants';
|
||||
|
||||
import { Alert, Button, message, Textarea } from 'ant-design-vue';
|
||||
|
||||
import { sendDeviceMessage, updateDevice } from '#/api/iot/device/device';
|
||||
import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
defineOptions({ name: 'DeviceDetailConfig' });
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from 'vue';
|
||||
|
||||
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 { 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 { getDeviceMessagePage } from '#/api/iot/device/device';
|
||||
import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
const props = defineProps<{
|
||||
deviceId: number;
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { ContentWrap } from '@vben/common-ui';
|
||||
import { IotDeviceMessageMethodEnum } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
@@ -26,7 +27,6 @@ import {
|
||||
import { sendDeviceMessage } from '#/api/iot/device/device';
|
||||
import {
|
||||
DeviceStateEnum,
|
||||
IotDeviceMessageMethodEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PageParam } from '@vben/request';
|
||||
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { VbenFormSchema, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
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 { Page } from '@vben/common-ui';
|
||||
import { confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
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 { getDevicePage } from '#/api/iot/device/device';
|
||||
import {
|
||||
bindDeviceGateway,
|
||||
getSubDeviceList,
|
||||
getUnboundSubDevicePage,
|
||||
unbindDeviceGateway,
|
||||
} from '#/api/iot/device/device';
|
||||
import { getSimpleProductList } from '#/api/iot/product/product';
|
||||
|
||||
interface Props {
|
||||
@@ -24,10 +27,10 @@ interface Props {
|
||||
const props = defineProps<Props>();
|
||||
const router = useRouter();
|
||||
|
||||
const products = ref<IotProductApi.Product[]>([]); // 产品列表
|
||||
|
||||
/** 子设备列表表格列配置 */
|
||||
function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{
|
||||
field: 'deviceName',
|
||||
title: 'DeviceName',
|
||||
@@ -39,10 +42,9 @@ function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'productId',
|
||||
title: '所属产品',
|
||||
field: 'productName',
|
||||
title: '产品名称',
|
||||
minWidth: 120,
|
||||
slots: { default: 'product' },
|
||||
},
|
||||
{
|
||||
field: 'state',
|
||||
@@ -56,21 +58,125 @@ function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
{
|
||||
field: 'onlineTime',
|
||||
title: '最后上线时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
minWidth: 160,
|
||||
formatter: ({ cellValue }) => formatDateTime(cellValue),
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
title: '操作',
|
||||
width: 150,
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 搜索表单 schema */
|
||||
function useGridFormSchema(): VbenFormSchema[] {
|
||||
const [Grid, gridApi] = useVbenVxeGrid<IotDeviceApi.Device>({
|
||||
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 [
|
||||
{
|
||||
fieldName: 'productId',
|
||||
label: '产品',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: () => getSimpleProductList(DeviceTypeEnum.GATEWAY_SUB),
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
placeholder: '请选择产品',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'deviceName',
|
||||
label: 'DeviceName',
|
||||
@@ -80,116 +186,171 @@ function useGridFormSchema(): VbenFormSchema[] {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function useAddGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{ type: 'checkbox', width: 40 },
|
||||
{
|
||||
fieldName: 'status',
|
||||
label: '设备状态',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: getDictOptions(DICT_TYPE.IOT_DEVICE_STATE, 'number'),
|
||||
placeholder: '请选择设备状态',
|
||||
allowClear: true,
|
||||
field: 'deviceName',
|
||||
title: 'DeviceName',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'nickname',
|
||||
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: {
|
||||
schema: useGridFormSchema(),
|
||||
schema: useAddGridFormSchema(),
|
||||
submitOnChange: true,
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
columns: useAddGridColumns(),
|
||||
height: 400,
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getUnboundSubDevicePage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
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: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
gridEvents: {
|
||||
checkboxAll: handleAddSelectionChange,
|
||||
checkboxChange: handleAddSelectionChange,
|
||||
},
|
||||
});
|
||||
|
||||
/** 获取产品名称 */
|
||||
function getProductName(productId: number) {
|
||||
const product = products.value.find((p) => p.id === productId);
|
||||
return product?.name || '-';
|
||||
/** 处理添加弹窗表格选择变化 */
|
||||
function handleAddSelectionChange() {
|
||||
const records = addGridApi.grid?.getCheckboxRecords() || [];
|
||||
addSelectedRowKeys.value = records.map(
|
||||
(record: IotDeviceApi.Device) => record.id!,
|
||||
);
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function openDetail(id: number) {
|
||||
router.push({ name: 'IoTDeviceDetail', params: { id } });
|
||||
}
|
||||
|
||||
/** 监听设备ID变化 */
|
||||
watch(
|
||||
() => props.deviceId,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
gridApi.query();
|
||||
const [AddModal, addModalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
if (addSelectedRowKeys.value.length === 0) {
|
||||
message.warning('请先选择要添加的子设备');
|
||||
return;
|
||||
}
|
||||
addModalApi.lock();
|
||||
try {
|
||||
await bindDeviceGateway(props.deviceId, addSelectedRowKeys.value);
|
||||
message.success('绑定成功');
|
||||
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>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<!-- 子设备列表 -->
|
||||
<Grid>
|
||||
<template #product="{ row }">
|
||||
{{ getProductName(row.productId) }}
|
||||
<Grid table-title="子设备列表">
|
||||
<template #toolbar-tools>
|
||||
<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 #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '查看详情',
|
||||
label: '查看',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.VIEW,
|
||||
onClick: openDetail.bind(null, row.id!),
|
||||
onClick: () => openDeviceDetail(row.id!),
|
||||
},
|
||||
{
|
||||
label: '解绑',
|
||||
type: 'link',
|
||||
danger: true,
|
||||
onClick: () => handleUnbind(row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
<!-- 添加子设备弹窗 -->
|
||||
<AddModal title="添加子设备" class="w-3/5">
|
||||
<AddGrid />
|
||||
</AddModal>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||
import { computed, onMounted, reactive, watch } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { IotDeviceMessageMethodEnum } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
@@ -15,7 +16,6 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getDeviceMessagePairPage } from '#/api/iot/device/device';
|
||||
import {
|
||||
getEventTypeLabel,
|
||||
IotDeviceMessageMethodEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||
import { computed, onMounted, reactive, watch } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { IotDeviceMessageMethodEnum } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
@@ -15,7 +16,6 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getDeviceMessagePairPage } from '#/api/iot/device/device';
|
||||
import {
|
||||
getThingModelServiceCallTypeLabel,
|
||||
IotDeviceMessageMethodEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ProductSelect } from './select.vue';
|
||||
@@ -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>
|
||||
@@ -153,9 +153,20 @@ export function useBasicFormSchema(
|
||||
];
|
||||
}
|
||||
|
||||
/** 高级设置表单字段(图标、图片、产品描述) */
|
||||
/** 高级设置表单字段(图标、图片、产品描述、动态注册) */
|
||||
export function useAdvancedFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'registerEnabled',
|
||||
label: '动态注册',
|
||||
component: 'Switch',
|
||||
componentProps: {
|
||||
checkedChildren: '开',
|
||||
unCheckedChildren: '关',
|
||||
},
|
||||
defaultValue: false,
|
||||
help: '设备动态注册无需一一烧录设备证书(DeviceSecret),每台设备烧录相同的产品证书,即 ProductKey 和 ProductSecret ,云端鉴权通过后下发设备证书,您可以根据需要开启或关闭动态注册,保障安全性。',
|
||||
},
|
||||
{
|
||||
fieldName: 'icon',
|
||||
label: '产品图标',
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
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';
|
||||
|
||||
@@ -13,11 +15,28 @@ interface Props {
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
const showProductSecret = ref(false); // 是否显示产品密钥
|
||||
|
||||
/** 格式化日期 */
|
||||
function formatDate(date?: Date | string) {
|
||||
if (!date) return '-';
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -54,6 +73,23 @@ function formatDate(date?: Date | string) {
|
||||
>
|
||||
<DictTag :type="DICT_TYPE.IOT_NET_TYPE" :value="product.netType" />
|
||||
</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="产品描述">
|
||||
{{ product.description || '-' }}
|
||||
</Descriptions.Item>
|
||||
|
||||
@@ -68,6 +68,7 @@ async function getAdvancedFormValues() {
|
||||
}
|
||||
// 表单未挂载(折叠状态),从 formData 中获取
|
||||
return {
|
||||
registerEnabled: formData.value?.registerEnabled,
|
||||
icon: formData.value?.icon,
|
||||
picUrl: formData.value?.picUrl,
|
||||
description: formData.value?.description,
|
||||
@@ -120,6 +121,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
await formApi.setValues(formData.value);
|
||||
// 如果存在高级字段数据,自动展开 Collapse
|
||||
if (
|
||||
formData.value?.registerEnabled ||
|
||||
formData.value?.icon ||
|
||||
formData.value?.picUrl ||
|
||||
formData.value?.description
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { IotDeviceMessageMethodEnum } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
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 { getSimpleProductList } from '#/api/iot/product/product';
|
||||
import { getThingModelListByProductId } from '#/api/iot/thingmodel';
|
||||
import {
|
||||
IotDeviceMessageMethodEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
import { IoTThingModelTypeEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
const formData = ref<any[]>([]);
|
||||
const productList = ref<any[]>([]); // 产品列表
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// TODO @AI:感觉这块,放到 biz-iot-enum 里好点。
|
||||
|
||||
/** 检查值是否为空 */
|
||||
const isEmpty = (value: any): boolean => {
|
||||
return value === null || value === undefined || value === '';
|
||||
@@ -22,49 +24,6 @@ export const IoTThingModelTypeEnum = {
|
||||
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 产品物模型服务调用方式枚举
|
||||
export const IoTThingModelServiceCallTypeEnum = {
|
||||
ASYNC: {
|
||||
|
||||
Reference in New Issue
Block a user