修复iot大量页面问题 产品 设备 新增编辑等功能修复,物模型列表和相关功能修复

pull/227/head
Administrator 2025-10-12 18:57:29 +08:00
parent 7824506e86
commit dea99acd78
69 changed files with 998 additions and 863 deletions

View File

@ -1,12 +1,9 @@
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 { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { getSimpleRuleSceneList } from '#/api/iot/rule/scene'; import { getSimpleRuleSceneList } from '#/api/iot/rule/scene';
import { getSimpleUserList } from '#/api/system/user'; import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils'; import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
/** 新增/修改告警配置的表单 */ /** 新增/修改告警配置的表单 */
export function useFormSchema(): VbenFormSchema[] { export function useFormSchema(): VbenFormSchema[] {

View File

@ -9,6 +9,7 @@ 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 { deleteAlertConfig, getAlertConfigPage } from '#/api/iot/alert/config'; import { deleteAlertConfig, getAlertConfigPage } from '#/api/iot/alert/config';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { DICT_TYPE, getDictLabel } from '#/utils';
import { useGridColumns, useGridFormSchema } from './data'; import { useGridColumns, useGridFormSchema } from './data';
import AlertConfigForm from '../modules/AlertConfigForm.vue'; import AlertConfigForm from '../modules/AlertConfigForm.vue';
@ -27,14 +28,8 @@ function onRefresh() {
// //
const getLevelText = (level?: number) => { const getLevelText = (level?: number) => {
const levelMap: Record<number, string> = { if (!level) return '-';
1: '提示', return getDictLabel(DICT_TYPE.IOT_ALERT_LEVEL, level);
2: '一般',
3: '警告',
4: '严重',
5: '紧急',
};
return level ? levelMap[level] || `级别${level}` : '-';
}; };
// //
@ -51,14 +46,8 @@ const getLevelColor = (level?: number) => {
// //
const getReceiveTypeText = (type?: number) => { const getReceiveTypeText = (type?: number) => {
const typeMap: Record<number, string> = { if (!type) return '-';
1: '站内信', return getDictLabel(DICT_TYPE.IOT_ALERT_RECEIVE_TYPE, type);
2: '邮箱',
3: '短信',
4: '微信',
5: '钉钉',
};
return type ? typeMap[type] || `类型${type}` : '-';
}; };
/** 创建告警配置 */ /** 创建告警配置 */

View File

@ -1,13 +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 { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { getSimpleAlertConfigList } from '#/api/iot/alert/config'; import { getSimpleAlertConfigList } from '#/api/iot/alert/config';
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 { getRangePickerDefaultProps } from '#/utils'; import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
/** 列表的搜索表单 */ /** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] { export function useGridFormSchema(): VbenFormSchema[] {

View File

@ -12,6 +12,7 @@ import type { AlertRecord } from '#/api/iot/alert/record';
import { getAlertRecordPage, processAlertRecord } from '#/api/iot/alert/record'; import { getAlertRecordPage, processAlertRecord } from '#/api/iot/alert/record';
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 { DICT_TYPE, getDictLabel } from '#/utils';
import { useGridColumns, useGridFormSchema } from './data'; import { useGridColumns, useGridFormSchema } from './data';
@ -33,14 +34,8 @@ const loadData = async () => {
// //
const getLevelText = (level?: number) => { const getLevelText = (level?: number) => {
const levelMap: Record<number, string> = { if (!level) return '-';
1: '提示', return getDictLabel(DICT_TYPE.IOT_ALERT_LEVEL, level);
2: '一般',
3: '警告',
4: '严重',
5: '紧急',
};
return level ? levelMap[level] || `级别${level}` : '-';
}; };
// //

View File

@ -1,10 +1,8 @@
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 { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { z } from '#/adapter/form'; import { z } from '#/adapter/form';
import { DICT_TYPE, getDictOptions } from '#/utils';
import { getSimpleDeviceGroupList } from '#/api/iot/device/group'; import { getSimpleDeviceGroupList } from '#/api/iot/device/group';
import { DeviceTypeEnum, getSimpleProductList } from '#/api/iot/product/product'; import { DeviceTypeEnum, getSimpleProductList } from '#/api/iot/product/product';

View File

@ -6,11 +6,11 @@ import { useRoute, useRouter } from 'vue-router';
import { Button, Card, Input, message, Select, Space, Tag } from 'ant-design-vue'; import { Button, Card, Input, message, Select, Space, Tag } from 'ant-design-vue';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { downloadFileFromBlobPart } from '@vben/utils'; import { downloadFileFromBlobPart } from '@vben/utils';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { DICT_TYPE, getDictOptions } from '#/utils';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { import {
deleteDevice, deleteDevice,
@ -295,7 +295,7 @@ onMounted(async () => {
style="width: 200px" style="width: 200px"
> >
<Select.Option <Select.Option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DEVICE_STATUS)" v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DEVICE_STATE)"
:key="dict.value" :key="dict.value"
:value="dict.value" :value="dict.value"
> >

View File

@ -11,10 +11,10 @@ import {
Row, Row,
Tag, Tag,
} from 'ant-design-vue'; } from 'ant-design-vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictLabel } from '@vben/hooks';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { DICT_TYPE, getDictLabel } from '#/utils';
import { DeviceStateEnum, getDevicePage } from '#/api/iot/device/device'; import { DeviceStateEnum, getDevicePage } from '#/api/iot/device/device';
defineOptions({ name: 'DeviceCardView' }); defineOptions({ name: 'DeviceCardView' });

View File

@ -3,12 +3,16 @@ import { computed } from 'vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { useVbenForm, useVbenModal } from '@vben/common-ui'; import { useVbenForm, useVbenModal } from '@vben/common-ui';
import { useAppConfig } from '@vben/hooks';
import { importDeviceTemplate } from '#/api/iot/device/device';
import { downloadFileFromBlobPart } from '@vben/utils'; import { downloadFileFromBlobPart } from '@vben/utils';
import { importDeviceTemplate } from '#/api/iot/device/device';
import { useImportFormSchema } from '../data'; import { useImportFormSchema } from '../data';
// @ts-ignore - Vite environment variables
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
defineOptions({ name: 'IoTDeviceImportForm' }); defineOptions({ name: 'IoTDeviceImportForm' });
const emit = defineEmits(['success']); const emit = defineEmits(['success']);
@ -50,7 +54,7 @@ const [Modal, modalApi] = useVbenModal({
// 使 fetch // 使 fetch
const accessToken = localStorage.getItem('accessToken') || ''; const accessToken = localStorage.getItem('accessToken') || '';
const response = await fetch( const response = await fetch(
`${import.meta.env.VITE_GLOB_API_URL}/iot/device/import?updateSupport=${values.updateSupport}`, `${apiURL}/iot/device/import?updateSupport=${values.updateSupport}`,
{ {
method: 'POST', method: 'POST',
headers: { headers: {

View File

@ -168,8 +168,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue' import { ref, reactive, onMounted, computed } from 'vue'
import { DICT_TYPE } from '@vben/constants' import { DICT_TYPE, getDictOptions } from '#/utils'
import { getDictOptions } from '@vben/hooks'
import { formatDate } from '@vben/utils' import { formatDate } from '@vben/utils'
import type { IotDeviceApi } from '#/api/iot/device/device' import type { IotDeviceApi } from '#/api/iot/device/device'
import { getDevicePage } from '#/api/iot/device/device' import { getDevicePage } from '#/api/iot/device/device'

View File

@ -30,14 +30,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watchEffect } from 'vue' import { ref, watchEffect } from 'vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { DeviceApi } from '#/api/iot/device/device' import { DeviceApi, type IotDeviceApi } from '#/api/iot/device/device'
import type { DeviceVO } from '#/api/iot/device/device'
import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants' import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants'
defineOptions({ name: 'DeviceDetailConfig' }) defineOptions({ name: 'DeviceDetailConfig' })
const props = defineProps<{ const props = defineProps<{
device: DeviceVO device: IotDeviceApi.Device
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@ -118,7 +117,7 @@ const updateDeviceConfig = async () => {
await DeviceApi.updateDevice({ await DeviceApi.updateDevice({
id: props.device.id, id: props.device.id,
config: JSON.stringify(config.value) config: JSON.stringify(config.value)
} as DeviceVO) } as IotDeviceApi.Device)
message.success({ content: '更新成功!' }) message.success({ content: '更新成功!' })
// success // success
emit('success') emit('success')

View File

@ -43,11 +43,11 @@ import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import DeviceForm from '../DeviceForm.vue' import DeviceForm from '../DeviceForm.vue'
import type { ProductVO } from '#/api/iot/product/product' import type { ProductVO } from '#/api/iot/product/product'
import type { DeviceVO } from '#/api/iot/device/device' import type { IotDeviceApi } from '#/api/iot/device/device'
interface Props { interface Props {
product: ProductVO product: ProductVO
device: DeviceVO device: IotDeviceApi.Device
loading?: boolean loading?: boolean
} }

View File

@ -128,21 +128,19 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { DICT_TYPE } from '@vben/constants'
import type { ProductVO } from '#/api/iot/product/product' import type { ProductVO } from '#/api/iot/product/product'
import { formatDate } from '@vben/utils' import { formatDate } from '@vben/utils'
import type { DeviceVO } from '#/api/iot/device/device' import { DeviceApi, type IotDeviceApi } from '#/api/iot/device/device'
import { DeviceApi } from '#/api/iot/device/device' import { DICT_TYPE } from '#/utils'
import type { IotDeviceAuthInfoVO } from '#/api/iot/device/device'
// //
const { product, device } = defineProps<{ product: ProductVO; device: DeviceVO }>() // Props const { product, device } = defineProps<{ product: ProductVO; device: IotDeviceApi.Device }>() // Props
const emit = defineEmits(['refresh']) // Emits const emit = defineEmits(['refresh']) // Emits
const authDialogVisible = ref(false) // const authDialogVisible = ref(false) //
const authPasswordVisible = ref(false) // const authPasswordVisible = ref(false) //
const authInfo = ref<IotDeviceAuthInfoVO>({} as IotDeviceAuthInfoVO) // const authInfo = ref<IotDeviceApi.DeviceAuthInfo>({} as IotDeviceApi.DeviceAuthInfo) //
/** 控制地图显示的标志 */ /** 控制地图显示的标志 */
const showMap = computed(() => { const showMap = computed(() => {

View File

@ -79,10 +79,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, watch, onMounted, onBeforeUnmount } from 'vue' import { ref, reactive, computed, watch, onMounted, onBeforeUnmount } from 'vue'
import { DICT_TYPE } from '@vben/constants'
import { DeviceApi } from '#/api/iot/device/device' import { DeviceApi } from '#/api/iot/device/device'
import { formatDate } from '@vben/utils' import { formatDate } from '@vben/utils'
import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants' import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants'
import { DICT_TYPE } from '#/utils'
const props = defineProps<{ const props = defineProps<{
deviceId: number deviceId: number

View File

@ -42,7 +42,12 @@
<!-- 事件上报 --> <!-- 事件上报 -->
<a-tab-pane :key="IotDeviceMessageMethodEnum.EVENT_POST.method" tab="事件上报"> <a-tab-pane :key="IotDeviceMessageMethodEnum.EVENT_POST.method" tab="事件上报">
<ContentWrap> <ContentWrap>
<a-table :dataSource="eventList" :columns="eventColumns" :pagination="false"> <a-table
:dataSource="eventList"
:columns="eventColumns"
:pagination="false"
:scroll="{ x: 800 }"
>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'dataType'"> <template v-if="column.key === 'dataType'">
{{ record.event?.dataType ?? '-' }} {{ record.event?.dataType ?? '-' }}
@ -121,7 +126,12 @@
<!-- 服务调用 --> <!-- 服务调用 -->
<a-tab-pane :key="IotDeviceMessageMethodEnum.SERVICE_INVOKE.method" tab="设备服务调用"> <a-tab-pane :key="IotDeviceMessageMethodEnum.SERVICE_INVOKE.method" tab="设备服务调用">
<ContentWrap> <ContentWrap>
<a-table :dataSource="serviceList" :columns="serviceColumns" :pagination="false"> <a-table
:dataSource="serviceList"
:columns="serviceColumns"
:pagination="false"
:scroll="{ x: 800 }"
>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'dataDefinition'"> <template v-if="column.key === 'dataDefinition'">
<DataDefinition :data="record" /> <DataDefinition :data="record" />
@ -168,16 +178,15 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import type { ProductVO } from '#/api/iot/product/product' import type { ProductVO } from '#/api/iot/product/product'
import type { ThingModelData } from '#/api/iot/thingmodel' import type { ThingModelApi } from '#/api/iot/thingmodel'
import { DeviceApi, DeviceStateEnum } from '#/api/iot/device/device' import { DeviceApi, DeviceStateEnum, type IotDeviceApi } from '#/api/iot/device/device'
import type { DeviceVO } from '#/api/iot/device/device'
import DeviceDetailsMessage from './DeviceDetailsMessage.vue' import DeviceDetailsMessage from './DeviceDetailsMessage.vue'
import { IotDeviceMessageMethodEnum, IoTThingModelTypeEnum } from '#/views/iot/utils/constants' import { IotDeviceMessageMethodEnum, IoTThingModelTypeEnum } from '#/views/iot/utils/constants'
const props = defineProps<{ const props = defineProps<{
product: ProductVO product: ProductVO
device: DeviceVO device: IotDeviceApi.Device
thingModelList: ThingModelData[] thingModelList: ThingModelApi.ThingModel[]
}>() }>()
// //
@ -369,7 +378,7 @@ const handlePropertyPost = async () => {
} }
// //
const handleEventPost = async (row: ThingModelData) => { const handleEventPost = async (row: ThingModelApi.ThingModel) => {
try { try {
const valueStr = formData.value[row.identifier!] const valueStr = formData.value[row.identifier!]
let params: any = {} let params: any = {}
@ -451,7 +460,7 @@ const handlePropertySet = async () => {
} }
// //
const handleServiceInvoke = async (row: ThingModelData) => { const handleServiceInvoke = async (row: ThingModelApi.ThingModel) => {
try { try {
const valueStr = formData.value[row.identifier!] const valueStr = formData.value[row.identifier!]
let params: any = {} let params: any = {}

View File

@ -22,15 +22,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { ContentWrap } from '@vben/common-ui' import type { ThingModelApi } from '#/api/iot/thingmodel'
import type { ThingModelData } from '#/api/iot/thingmodel' import ContentWrap from '#/components/content-wrap/content-wrap.vue'
import DeviceDetailsThingModelProperty from './DeviceDetailsThingModelProperty.vue' import DeviceDetailsThingModelProperty from './DeviceDetailsThingModelProperty.vue'
import DeviceDetailsThingModelEvent from './DeviceDetailsThingModelEvent.vue' import DeviceDetailsThingModelEvent from './DeviceDetailsThingModelEvent.vue'
import DeviceDetailsThingModelService from './DeviceDetailsThingModelService.vue' import DeviceDetailsThingModelService from './DeviceDetailsThingModelService.vue'
const props = defineProps<{ const props = defineProps<{
deviceId: number deviceId: number
thingModelList: ThingModelData[] thingModelList: ThingModelApi.ThingModel[]
}>() }>()
const activeTab = ref('property') // const activeTab = ref('property') //

View File

@ -93,10 +93,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue' import { ref, reactive, computed, onMounted } from 'vue'
import { Pagination } from 'ant-design-vue' import { Pagination } from 'ant-design-vue'
import { ContentWrap } from '@vben/common-ui'
import { IconifyIcon } from '@vben/icons' import { IconifyIcon } from '@vben/icons'
import ContentWrap from '#/components/content-wrap/content-wrap.vue'
import { DeviceApi } from '#/api/iot/device/device' import { DeviceApi } from '#/api/iot/device/device'
import type { ThingModelData } from '#/api/iot/thingmodel' import type { ThingModelApi } from '#/api/iot/thingmodel'
import { formatDate } from '@vben/utils' import { formatDate } from '@vben/utils'
import { import {
getEventTypeLabel, getEventTypeLabel,
@ -106,7 +106,7 @@ import {
const props = defineProps<{ const props = defineProps<{
deviceId: number deviceId: number
thingModelList: ThingModelData[] thingModelList: ThingModelApi.ThingModel[]
}>() }>()
const loading = ref(false) // const loading = ref(false) //
@ -125,7 +125,7 @@ const queryFormRef = ref() // 搜索的表单
/** 事件类型的物模型数据 */ /** 事件类型的物模型数据 */
const eventThingModels = computed(() => { const eventThingModels = computed(() => {
return props.thingModelList.filter( return props.thingModelList.filter(
(item: ThingModelData) => String(item.type) === String(IoTThingModelTypeEnum.EVENT) (item: ThingModelApi.ThingModel) => String(item.type) === String(IoTThingModelTypeEnum.EVENT)
) )
}) })
@ -160,7 +160,7 @@ const resetQuery = () => {
const getEventName = (identifier: string | undefined) => { const getEventName = (identifier: string | undefined) => {
if (!identifier) return '-' if (!identifier) return '-'
const event = eventThingModels.value.find( const event = eventThingModels.value.find(
(item: ThingModelData) => item.identifier === identifier (item: ThingModelApi.ThingModel) => item.identifier === identifier
) )
return event?.name || identifier return event?.name || identifier
} }
@ -169,7 +169,7 @@ const getEventName = (identifier: string | undefined) => {
const getEventType = (identifier: string | undefined) => { const getEventType = (identifier: string | undefined) => {
if (!identifier) return '-' if (!identifier) return '-'
const event = eventThingModels.value.find( const event = eventThingModels.value.find(
(item: ThingModelData) => item.identifier === identifier (item: ThingModelApi.ThingModel) => item.identifier === identifier
) )
if (!event?.event?.type) return '-' if (!event?.event?.type) return '-'
return getEventTypeLabel(event.event.type) || '-' return getEventTypeLabel(event.event.type) || '-'

View File

@ -148,17 +148,17 @@
</style> </style>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, watch, onMounted, onBeforeUnmount } from 'vue' import { ref, reactive, watch, onMounted, onBeforeUnmount } from 'vue'
import { ContentWrap } from '@vben/common-ui'
import { IconifyIcon } from '@vben/icons' import { IconifyIcon } from '@vben/icons'
import { DeviceApi, type IotDevicePropertyDetailRespVO } from '#/api/iot/device/device' import ContentWrap from '#/components/content-wrap/content-wrap.vue'
import { DeviceApi, type IotDeviceApi } from '#/api/iot/device/device'
import { formatDate } from '@vben/utils' import { formatDate } from '@vben/utils'
import DeviceDetailsThingModelPropertyHistory from './DeviceDetailsThingModelPropertyHistory.vue' import DeviceDetailsThingModelPropertyHistory from './DeviceDetailsThingModelPropertyHistory.vue'
const props = defineProps<{ deviceId: number }>() const props = defineProps<{ deviceId: number }>()
const loading = ref(true) // const loading = ref(true) //
const list = ref<IotDevicePropertyDetailRespVO[]>([]) // const list = ref<IotDeviceApi.DevicePropertyDetail[]>([]) //
const filterList = ref<IotDevicePropertyDetailRespVO[]>([]) // const filterList = ref<IotDeviceApi.DevicePropertyDetail[]>([]) //
const queryParams = reactive({ const queryParams = reactive({
keyword: '' as string keyword: '' as string
}) })
@ -189,7 +189,7 @@ const handleFilter = () => {
} else { } else {
const keyword = queryParams.keyword.toLowerCase() const keyword = queryParams.keyword.toLowerCase()
list.value = filterList.value.filter( list.value = filterList.value.filter(
(item: IotDevicePropertyDetailRespVO) => (item: IotDeviceApi.DevicePropertyDetail) =>
item.identifier?.toLowerCase().includes(keyword) || item.identifier?.toLowerCase().includes(keyword) ||
item.name?.toLowerCase().includes(keyword) item.name?.toLowerCase().includes(keyword)
) )
@ -208,7 +208,7 @@ const openHistory = (deviceId: number, identifier: string, dataType: string) =>
} }
/** 格式化属性值和单位 */ /** 格式化属性值和单位 */
const formatValueWithUnit = (item: IotDevicePropertyDetailRespVO) => { const formatValueWithUnit = (item: IotDeviceApi.DevicePropertyDetail) => {
if (item.value === null || item.value === undefined || item.value === '') { if (item.value === null || item.value === undefined || item.value === '') {
return '-' return '-'
} }

View File

@ -108,10 +108,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue' import { ref, reactive, computed, onMounted } from 'vue'
import { Pagination } from 'ant-design-vue' import { Pagination } from 'ant-design-vue'
import { ContentWrap } from '@vben/common-ui'
import { IconifyIcon } from '@vben/icons' import { IconifyIcon } from '@vben/icons'
import ContentWrap from '#/components/content-wrap/content-wrap.vue'
import { DeviceApi } from '#/api/iot/device/device' import { DeviceApi } from '#/api/iot/device/device'
import type { ThingModelData } from '#/api/iot/thingmodel' import type { ThingModelApi } from '#/api/iot/thingmodel'
import { formatDate } from '@vben/utils' import { formatDate } from '@vben/utils'
import { import {
getThingModelServiceCallTypeLabel, getThingModelServiceCallTypeLabel,
@ -121,7 +121,7 @@ import {
const props = defineProps<{ const props = defineProps<{
deviceId: number deviceId: number
thingModelList: ThingModelData[] thingModelList: ThingModelApi.ThingModel[]
}>() }>()
const loading = ref(false) // const loading = ref(false) //
@ -140,7 +140,7 @@ const queryFormRef = ref() // 搜索的表单
/** 服务类型的物模型数据 */ /** 服务类型的物模型数据 */
const serviceThingModels = computed(() => { const serviceThingModels = computed(() => {
return props.thingModelList.filter( return props.thingModelList.filter(
(item: ThingModelData) => String(item.type) === String(IoTThingModelTypeEnum.SERVICE) (item: ThingModelApi.ThingModel) => String(item.type) === String(IoTThingModelTypeEnum.SERVICE)
) )
}) })
@ -175,7 +175,7 @@ const resetQuery = () => {
const getServiceName = (identifier: string | undefined) => { const getServiceName = (identifier: string | undefined) => {
if (!identifier) return '-' if (!identifier) return '-'
const service = serviceThingModels.value.find( const service = serviceThingModels.value.find(
(item: ThingModelData) => item.identifier === identifier (item: ThingModelApi.ThingModel) => item.identifier === identifier
) )
return service?.name || identifier return service?.name || identifier
} }
@ -184,7 +184,7 @@ const getServiceName = (identifier: string | undefined) => {
const getCallType = (identifier: string | undefined) => { const getCallType = (identifier: string | undefined) => {
if (!identifier) return '-' if (!identifier) return '-'
const service = serviceThingModels.value.find( const service = serviceThingModels.value.find(
(item: ThingModelData) => item.identifier === identifier (item: ThingModelApi.ThingModel) => item.identifier === identifier
) )
if (!service?.service?.callType) return '-' if (!service?.service?.callType) return '-'
return getThingModelServiceCallTypeLabel(service.service.callType) || '-' return getThingModelServiceCallTypeLabel(service.service.callType) || '-'

View File

@ -47,11 +47,11 @@ import { message } from 'ant-design-vue'
import { useTabbarStore } from '@vben/stores' import { useTabbarStore } from '@vben/stores'
import { Page } from '@vben/common-ui' import { Page } from '@vben/common-ui'
import { DeviceApi } from '#/api/iot/device/device' import { DeviceApi } from '#/api/iot/device/device'
import type { DeviceVO } from '#/api/iot/device/device' import type { IotDeviceApi } from '#/api/iot/device/device'
import { DeviceTypeEnum, ProductApi } from '#/api/iot/product/product' import { DeviceTypeEnum, ProductApi } from '#/api/iot/product/product'
import type { ProductVO } from '#/api/iot/product/product' import type { ProductVO } from '#/api/iot/product/product'
import { ThingModelApi } from '#/api/iot/thingmodel' import { ThingModelApi } from '#/api/iot/thingmodel'
import type { ThingModelData } from '#/api/iot/thingmodel' import type { ThingModelApi as ThingModelApiTypes } from '#/api/iot/thingmodel'
import DeviceDetailsHeader from './DeviceDetailsHeader.vue' import DeviceDetailsHeader from './DeviceDetailsHeader.vue'
import DeviceDetailsInfo from './DeviceDetailsInfo.vue' import DeviceDetailsInfo from './DeviceDetailsInfo.vue'
import DeviceDetailsThingModel from './DeviceDetailsThingModel.vue' import DeviceDetailsThingModel from './DeviceDetailsThingModel.vue'
@ -65,9 +65,9 @@ const route = useRoute()
const id = Number(route.params.id) // const id = Number(route.params.id) //
const loading = ref(true) // const loading = ref(true) //
const product = ref<ProductVO>({} as ProductVO) // const product = ref<ProductVO>({} as ProductVO) //
const device = ref<DeviceVO>({} as DeviceVO) // const device = ref<IotDeviceApi.Device>({} as IotDeviceApi.Device) //
const activeTab = ref('info') // const activeTab = ref('info') //
const thingModelList = ref<ThingModelData[]>([]) // const thingModelList = ref<ThingModelApiTypes.ThingModel[]>([]) //
/** 获取设备详情 */ /** 获取设备详情 */
const getDeviceData = async () => { const getDeviceData = async () => {

View File

@ -1,9 +1,8 @@
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 { DICT_TYPE } from '@vben/constants';
import { z } from '#/adapter/form'; import { z } from '#/adapter/form';
import { DICT_TYPE } from '#/utils';
import { getSimpleDeviceGroupList } from '#/api/iot/device/group'; import { getSimpleDeviceGroupList } from '#/api/iot/device/group';
/** 新增/修改设备分组的表单 */ /** 新增/修改设备分组的表单 */

View File

@ -5,7 +5,7 @@ import { message, Form, Input, Select, Spin } from 'ant-design-vue';
import type { OtaTask } from '#/api/iot/ota/task'; import type { OtaTask } from '#/api/iot/ota/task';
import * as IoTOtaTaskApi from '#/api/iot/ota/task'; import * as IoTOtaTaskApi from '#/api/iot/ota/task';
import { IoTOtaTaskDeviceScopeEnum } from '#/views/iot/utils/constants'; import { IoTOtaTaskDeviceScopeEnum } from '#/views/iot/utils/constants';
import type { DeviceVO } from '#/api/iot/device/device'; import type { IotDeviceApi } from '#/api/iot/device/device';
import * as DeviceApi from '#/api/iot/device/device'; import * as DeviceApi from '#/api/iot/device/device';
/** IoT OTA 升级任务表单 */ /** IoT OTA 升级任务表单 */
@ -32,11 +32,11 @@ const formRules = {
deviceScope: [{ required: true, message: '请选择升级范围', trigger: 'change' as const, type: 'number' as const }], deviceScope: [{ required: true, message: '请选择升级范围', trigger: 'change' as const, type: 'number' as const }],
deviceIds: [{ required: true, message: '请至少选择一个设备', trigger: 'change' as const, type: 'array' as const }], deviceIds: [{ required: true, message: '请至少选择一个设备', trigger: 'change' as const, type: 'array' as const }],
}; };
const devices = ref<DeviceVO[]>([]); const devices = ref<IotDeviceApi.Device[]>([]);
/** 设备选项 */ /** 设备选项 */
const deviceOptions = computed(() => { const deviceOptions = computed(() => {
return devices.value.map((device) => ({ return devices.value.map((device: IotDeviceApi.Device) => ({
label: device.nickname label: device.nickname
? `${device.deviceName} (${device.nickname})` ? `${device.deviceName} (${device.nickname})`
: device.deviceName, : device.deviceName,

View File

@ -2,12 +2,12 @@ import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { IotProductCategoryApi } from '#/api/iot/product/category'; import type { IotProductCategoryApi } from '#/api/iot/product/category';
import { DICT_TYPE } from '@vben/constants';
import { handleTree } from '@vben/utils'; import { handleTree } from '@vben/utils';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { z } from '#/adapter/form'; import { z } from '#/adapter/form';
import { DICT_TYPE } from '#/utils';
import { import {
deleteProductCategory, deleteProductCategory,
getProductCategoryPage, getProductCategoryPage,

View File

@ -1,15 +1,14 @@
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 { ref } from 'vue'; import { h, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { downloadFileFromBlobPart } from '@vben/utils'; import { downloadFileFromBlobPart } from '@vben/utils';
import { message } from 'ant-design-vue'; import { Button, message } from 'ant-design-vue';
import { z } from '#/adapter/form'; import { z } from '#/adapter/form';
import { DICT_TYPE, getDictOptions } from '#/utils';
import { getSimpleProductCategoryList } from '#/api/iot/product/category'; import { getSimpleProductCategoryList } from '#/api/iot/product/category';
import { import {
deleteProduct, deleteProduct,
@ -18,7 +17,7 @@ import {
} from '#/api/iot/product/product'; } from '#/api/iot/product/product';
/** 新增/修改产品的表单 */ /** 新增/修改产品的表单 */
export function useFormSchema(): VbenFormSchema[] { export function useFormSchema(formApi?: any): VbenFormSchema[] {
return [ return [
{ {
component: 'Input', component: 'Input',
@ -28,6 +27,55 @@ export function useFormSchema(): VbenFormSchema[] {
show: () => false, show: () => false,
}, },
}, },
// 创建时的 ProductKey 字段(带生成按钮)
{
fieldName: 'productKey',
label: 'ProductKey',
component: 'Input',
componentProps: {
placeholder: '请输入 ProductKey',
},
dependencies: {
triggerFields: ['id'],
if(values) {
// 仅在创建时显示(没有 id
return !values.id;
},
},
rules: z
.string()
.min(1, 'ProductKey 不能为空')
.max(32, 'ProductKey 长度不能超过 32 个字符'),
suffix: () => {
return h(Button, {
type: 'default',
onClick: () => {
formApi?.setFieldValue('productKey', generateProductKey());
},
}, { default: () => '重新生成' });
},
},
// 编辑时的 ProductKey 字段(禁用,无按钮)
{
fieldName: 'productKey',
label: 'ProductKey',
component: 'Input',
componentProps: {
placeholder: '请输入 ProductKey',
disabled: true,
},
dependencies: {
triggerFields: ['id'],
if(values) {
// 仅在编辑时显示(有 id
return !!values.id;
},
},
rules: z
.string()
.min(1, 'ProductKey 不能为空')
.max(32, 'ProductKey 长度不能超过 32 个字符'),
},
{ {
fieldName: 'name', fieldName: 'name',
label: '产品名称', label: '产品名称',
@ -74,43 +122,22 @@ export function useFormSchema(): VbenFormSchema[] {
rules: 'required', rules: 'required',
}, },
{ {
fieldName: 'protocolType', fieldName: 'codecType',
label: '接入协议', label: '数据格式',
component: 'Select', component: 'Select',
componentProps: { componentProps: {
options: getDictOptions(DICT_TYPE.IOT_PROTOCOL_TYPE, 'number'), options: getDictOptions(DICT_TYPE.IOT_CODEC_TYPE, 'string'),
placeholder: '请选择接入协议', placeholder: '请选择数据格式',
}, },
rules: 'required', rules: 'required',
}, },
{ {
fieldName: 'dataFormat', fieldName: 'locationType',
label: '数据格式', label: '定位类型',
component: 'RadioGroup', component: 'Select',
componentProps: { componentProps: {
options: getDictOptions(DICT_TYPE.IOT_DATA_FORMAT, 'number'), options: getDictOptions(DICT_TYPE.IOT_LOCATION_TYPE, 'number'),
buttonStyle: 'solid', placeholder: '请选择定位类型',
optionType: 'button',
},
rules: 'required',
},
{
fieldName: 'description',
label: '产品描述',
component: 'Textarea',
componentProps: {
placeholder: '请输入产品描述',
rows: 3,
},
},
{
fieldName: 'validateType',
label: '认证方式',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.IOT_VALIDATE_TYPE, 'number'),
buttonStyle: 'solid',
optionType: 'button',
}, },
rules: 'required', rules: 'required',
}, },
@ -125,6 +152,30 @@ export function useFormSchema(): VbenFormSchema[] {
}, },
rules: 'required', rules: 'required',
}, },
{
fieldName: 'icon',
label: '产品图标',
component: 'IconPicker',
componentProps: {
placeholder: '请选择产品图标',
prefix: 'carbon',
autoFetchApi: false,
},
},
{
fieldName: 'picUrl',
label: '产品图片',
component: 'ImageUpload',
},
{
fieldName: 'description',
label: '产品描述',
component: 'Textarea',
componentProps: {
placeholder: '请输入产品描述',
rows: 3,
},
}
]; ];
} }
@ -265,3 +316,13 @@ export function useImagePreview() {
handlePreviewImage, handlePreviewImage,
}; };
} }
/** 生成 ProductKey包含大小写字母和数字 */
export function generateProductKey(): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < 16; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}

View File

@ -12,10 +12,10 @@ import {
Tag, Tag,
Tooltip, Tooltip,
} from 'ant-design-vue'; } from 'ant-design-vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictLabel } from '@vben/hooks';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { DICT_TYPE, getDictLabel } from '#/utils';
import { getProductPage } from '#/api/iot/product/product'; import { getProductPage } from '#/api/iot/product/product';
defineOptions({ name: 'ProductCardView' }); defineOptions({ name: 'ProductCardView' });

View File

@ -7,7 +7,7 @@ import { useVbenForm, useVbenModal } from '@vben/common-ui';
import { createProduct, getProduct, updateProduct, type IotProductApi } from '#/api/iot/product/product'; import { createProduct, getProduct, updateProduct, type IotProductApi } from '#/api/iot/product/product';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { useFormSchema } from '../data'; import { generateProductKey, useFormSchema } from '../data';
defineOptions({ name: 'IoTProductForm' }); defineOptions({ name: 'IoTProductForm' });
@ -25,10 +25,15 @@ const [Form, formApi] = useVbenForm({
}, },
wrapperClass: 'grid-cols-2', wrapperClass: 'grid-cols-2',
layout: 'horizontal', layout: 'horizontal',
schema: useFormSchema(), schema: [],
showDefaultActions: false, showDefaultActions: false,
}); });
// formApi schema
formApi.setState({
schema: useFormSchema(formApi),
});
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
async onConfirm() { async onConfirm() {
const { valid } = await formApi.validate(); const { valid } = await formApi.validate();
@ -58,8 +63,11 @@ const [Modal, modalApi] = useVbenModal({
if (!data || !data.id) { if (!data || !data.id) {
// //
await formApi.setValues({ await formApi.setValues({
productKey: generateProductKey(), // ProductKey
deviceType: 0, // deviceType: 0, //
dataFormat: 1, // JSON dataFormat: 1, // JSON
codecType: 'Alink', // Alink
locationType: 0, //
validateType: 1, // validateType: 1, //
status: 0, // status: 0, //
}); });

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { DICT_TYPE } from '@vben/constants'; import { DICT_TYPE } from '#/utils';
import { DictTag } from '#/components/dict-tag'; import { DictTag } from '#/components/dict-tag';
import type { IotProductApi } from '#/api/iot/product/product'; import type { IotProductApi } from '#/api/iot/product/product';

View File

@ -1,11 +1,8 @@
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 { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { getSimpleProductList } from '#/api/iot/product/product'; import { getSimpleProductList } from '#/api/iot/product/product';
import { getRangePickerDefaultProps } from '#/utils'; import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
/** 列表的搜索表单 */ /** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] { export function useGridFormSchema(): VbenFormSchema[] {

View File

@ -1,10 +1,7 @@
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 { DICT_TYPE } from '@vben/constants'; import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
import { getDictOptions } from '@vben/hooks';
import { getRangePickerDefaultProps } from '#/utils';
/** 列表的搜索表单 */ /** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] { export function useGridFormSchema(): VbenFormSchema[] {

View File

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue'; import { computed, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
@ -10,7 +10,7 @@ import {
createDataSink, createDataSink,
getDataSink, getDataSink,
updateDataSink, updateDataSink,
} from '#/api/iot/rule/data'; } from '#/api/iot/rule/data/sink';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { useSinkFormSchema } from './data'; import { useSinkFormSchema } from './data';
@ -105,8 +105,8 @@ const [Modal, modalApi] = useVbenModal({
// //
watch( watch(
() => formApi.form.type, () => formApi.form.values?.type,
(newType) => { (newType: any) => {
if (formData.value && newType !== formData.value.type) { if (formData.value && newType !== formData.value.type) {
formData.value.config = {}; formData.value.config = {};
} }

View File

@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, watch, onMounted } from 'vue';
import { Input, Select, FormItem } from 'ant-design-vue'; import { Input, Select, FormItem } from 'ant-design-vue';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
@ -11,7 +12,7 @@ const props = defineProps<{
modelValue: any; modelValue: any;
}>(); }>();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit) as Ref<any>; const config = useVModel(props, 'modelValue', emit);
// noinspection HttpUrlsUsage // noinspection HttpUrlsUsage
/** URL处理 */ /** URL处理 */
@ -23,22 +24,25 @@ const fullUrl = computed(() => {
/** 监听 URL 变化 */ /** 监听 URL 变化 */
watch([urlPrefix, urlPath], () => { watch([urlPrefix, urlPath], () => {
config.value.url = fullUrl.value; if (config.value) {
config.value.url = fullUrl.value;
}
}); });
/** 组件初始化 */ /** 组件初始化 */
onMounted(() => { onMounted(() => {
if (!isEmpty(config.value)) { if (!isEmpty(config.value)) {
// URL // URL
if (config.value.url) { const configVal = config.value as any;
if (config.value.url.startsWith('https://')) { if (configVal && configVal.url) {
if (configVal.url.startsWith('https://')) {
urlPrefix.value = 'https://'; urlPrefix.value = 'https://';
urlPath.value = config.value.url.substring(8); urlPath.value = configVal.url.substring(8);
} else if (config.value.url.startsWith('http://')) { } else if (configVal.url.startsWith('http://')) {
urlPrefix.value = 'http://'; urlPrefix.value = 'http://';
urlPath.value = config.value.url.substring(7); urlPath.value = configVal.url.substring(7);
} else { } else {
urlPath.value = config.value.url; urlPath.value = configVal.url;
} }
} }
return; return;

View File

@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue';
import { Input, Switch, FormItem } from 'ant-design-vue'; import { Input, Switch, FormItem } from 'ant-design-vue';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
@ -9,7 +10,7 @@ const props = defineProps<{
modelValue: any; modelValue: any;
}>(); }>();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit) as Ref<any>; const config = useVModel(props, 'modelValue', emit);
/** 组件初始化 */ /** 组件初始化 */
onMounted(() => { onMounted(() => {

View File

@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue';
import { Input, FormItem } from 'ant-design-vue'; import { Input, FormItem } from 'ant-design-vue';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
@ -9,7 +10,7 @@ const props = defineProps<{
modelValue: any; modelValue: any;
}>(); }>();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit) as Ref<any>; const config = useVModel(props, 'modelValue', emit);
/** 组件初始化 */ /** 组件初始化 */
onMounted(() => { onMounted(() => {

View File

@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue';
import { Input, InputNumber, FormItem } from 'ant-design-vue'; import { Input, InputNumber, FormItem } from 'ant-design-vue';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
@ -9,7 +10,7 @@ const props = defineProps<{
modelValue: any; modelValue: any;
}>(); }>();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit) as Ref<any>; const config = useVModel(props, 'modelValue', emit);
/** 组件初始化 */ /** 组件初始化 */
onMounted(() => { onMounted(() => {

View File

@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue';
import { Input, InputNumber, FormItem } from 'ant-design-vue'; import { Input, InputNumber, FormItem } from 'ant-design-vue';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
@ -9,7 +10,7 @@ const props = defineProps<{
modelValue: any; modelValue: any;
}>(); }>();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit) as Ref<any>; const config = useVModel(props, 'modelValue', emit);
/** 组件初始化 */ /** 组件初始化 */
onMounted(() => { onMounted(() => {

View File

@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue';
import { Input, FormItem } from 'ant-design-vue'; import { Input, FormItem } from 'ant-design-vue';
import { useVModel } from '@vueuse/core'; import { useVModel } from '@vueuse/core';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
@ -9,7 +10,7 @@ const props = defineProps<{
modelValue: any; modelValue: any;
}>(); }>();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const config = useVModel(props, 'modelValue', emit) as Ref<any>; const config = useVModel(props, 'modelValue', emit);
/** 组件初始化 */ /** 组件初始化 */
onMounted(() => { onMounted(() => {

View File

@ -18,6 +18,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch } from 'vue'
import { Delete, Plus } from '@element-plus/icons-vue' import { Delete, Plus } from '@element-plus/icons-vue'
import { isEmpty } from '@vben/utils' import { isEmpty } from '@vben/utils'
@ -50,7 +51,7 @@ const removeItem = (index: number) => {
/** 更新 modelValue */ /** 更新 modelValue */
const updateModelValue = () => { const updateModelValue = () => {
const result: Record<string, string> = {} const result: Record<string, string> = {}
items.value.forEach((item) => { items.value.forEach((item: KeyValueItem) => {
if (item.key) { if (item.key) {
result[item.key] = item.value result[item.key] = item.value
} }
@ -62,7 +63,7 @@ const updateModelValue = () => {
watch(items, updateModelValue, { deep: true }) watch(items, updateModelValue, { deep: true })
watch( watch(
() => props.modelValue, () => props.modelValue,
(val) => { (val: Record<string, string>) => {
// //
if (isEmpty(val) || !isEmpty(items.value)) { if (isEmpty(val) || !isEmpty(items.value)) {
return return

View File

@ -1,10 +1,7 @@
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 { DICT_TYPE } from '@vben/constants'; import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
import { getDictOptions } from '@vben/hooks';
import { getRangePickerDefaultProps } from '#/utils';
/** 列表的搜索表单 */ /** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] { export function useGridFormSchema(): VbenFormSchema[] {

View File

@ -1,11 +1,8 @@
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 { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { z } from '#/adapter/form'; import { z } from '#/adapter/form';
import { getRangePickerDefaultProps } from '#/utils'; import { CommonStatusEnum, DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
/** 新增/修改的表单 */ /** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] { export function useFormSchema(): VbenFormSchema[] {

View File

@ -12,9 +12,9 @@
<!-- 基础信息配置 --> <!-- 基础信息配置 -->
<BasicInfoSection v-model="formData" :rules="formRules" /> <BasicInfoSection v-model="formData" :rules="formRules" />
<!-- 触发器配置 --> <!-- 触发器配置 -->
<TriggerSection v-model:triggers="formData.triggers" /> <TriggerSection v-model:triggers="formData.triggers!" />
<!-- 执行器配置 --> <!-- 执行器配置 -->
<ActionSection v-model:actions="formData.actions" /> <ActionSection v-model:actions="formData.actions!" />
</el-form> </el-form>
<template #footer> <template #footer>
<div class="drawer-footer"> <div class="drawer-footer">
@ -32,19 +32,23 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, watch, nextTick } from 'vue'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import BasicInfoSection from './sections/BasicInfoSection.vue' import BasicInfoSection from './sections/BasicInfoSection.vue'
import TriggerSection from './sections/TriggerSection.vue' import TriggerSection from './sections/TriggerSection.vue'
import ActionSection from './sections/ActionSection.vue' import ActionSection from './sections/ActionSection.vue'
import { IotSceneRule } from '#/api/iot/rule/scene' import type { IotSceneRule } from '#/api/iot/rule/scene'
import { RuleSceneApi } from '#/api/iot/rule/scene' import {
createSceneRule,
updateSceneRule
} from '#/api/iot/rule/scene'
import { import {
IotRuleSceneTriggerTypeEnum, IotRuleSceneTriggerTypeEnum,
IotRuleSceneActionTypeEnum, IotRuleSceneActionTypeEnum,
isDeviceTrigger isDeviceTrigger
} from '#/views/iot/utils/constants' } from '#/views/iot/utils/constants'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { CommonStatusEnum } from '@/utils/constants' import { CommonStatusEnum } from '#/utils/constants'
/** IoT 场景联动规则表单 - 主表单组件 */ /** IoT 场景联动规则表单 - 主表单组件 */
defineOptions({ name: 'RuleSceneForm' }) defineOptions({ name: 'RuleSceneForm' })
@ -76,7 +80,7 @@ const createDefaultFormData = (): IotSceneRule => {
status: CommonStatusEnum.ENABLE, // status: CommonStatusEnum.ENABLE, //
triggers: [ triggers: [
{ {
type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST, type: String(IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST),
productId: undefined, productId: undefined,
deviceId: undefined, deviceId: undefined,
identifier: undefined, identifier: undefined,
@ -115,7 +119,7 @@ const validateTriggers = (_rule: any, value: any, callback: any) => {
} }
// //
if (isDeviceTrigger(trigger.type)) { if (isDeviceTrigger(Number(trigger.type))) {
if (!trigger.productId) { if (!trigger.productId) {
callback(new Error(`触发器 ${i + 1}: 产品不能为空`)) callback(new Error(`触发器 ${i + 1}: 产品不能为空`))
return return
@ -139,7 +143,7 @@ const validateTriggers = (_rule: any, value: any, callback: any) => {
} }
// //
if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) { if (Number(trigger.type) === IotRuleSceneTriggerTypeEnum.TIMER) {
if (!trigger.cronExpression) { if (!trigger.cronExpression) {
callback(new Error(`触发器 ${i + 1}: CRON表达式不能为空`)) callback(new Error(`触发器 ${i + 1}: CRON表达式不能为空`))
return return
@ -172,9 +176,10 @@ const validateActions = (_rule: any, value: any, callback: any) => {
} }
// //
const actionTypeNum = Number(action.type)
if ( if (
action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET || actionTypeNum === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET ||
action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE actionTypeNum === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
) { ) {
if (!action.productId) { if (!action.productId) {
callback(new Error(`执行器 ${i + 1}: 产品不能为空`)) callback(new Error(`执行器 ${i + 1}: 产品不能为空`))
@ -186,7 +191,7 @@ const validateActions = (_rule: any, value: any, callback: any) => {
} }
// //
if (action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE) { if (actionTypeNum === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE) {
if (!action.identifier) { if (!action.identifier) {
callback(new Error(`执行器 ${i + 1}: 服务不能为空`)) callback(new Error(`执行器 ${i + 1}: 服务不能为空`))
return return
@ -201,8 +206,8 @@ const validateActions = (_rule: any, value: any, callback: any) => {
// //
if ( if (
action.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER || actionTypeNum === IotRuleSceneActionTypeEnum.ALERT_TRIGGER ||
action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER actionTypeNum === IotRuleSceneActionTypeEnum.ALERT_RECOVER
) { ) {
if (!action.alertConfigId) { if (!action.alertConfigId) {
callback(new Error(`执行器 ${i + 1}: 告警配置不能为空`)) callback(new Error(`执行器 ${i + 1}: 告警配置不能为空`))
@ -251,11 +256,11 @@ const handleSubmit = async () => {
try { try {
if (isEdit.value) { if (isEdit.value) {
// //
await RuleSceneApi.updateRuleScene(formData.value) await updateSceneRule(formData.value)
ElMessage.success('更新成功') ElMessage.success('更新成功')
} else { } else {
// //
await RuleSceneApi.createRuleScene(formData.value) await createSceneRule(formData.value)
ElMessage.success('创建成功') ElMessage.success('创建成功')
} }
@ -287,7 +292,7 @@ const initFormData = () => {
? props.ruleScene.triggers ? props.ruleScene.triggers
: [ : [
{ {
type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST, type: String(IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST),
productId: undefined, productId: undefined,
deviceId: undefined, deviceId: undefined,
identifier: undefined, identifier: undefined,

View File

@ -30,8 +30,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { AlertConfigApi } from '#/api/iot/alert/config' import { getAlertConfigPage } from '#/api/iot/alert/config'
/** 告警配置组件 */ /** 告警配置组件 */
defineOptions({ name: 'AlertConfig' }) defineOptions({ name: 'AlertConfig' })
@ -63,7 +64,7 @@ const handleChange = (value?: number) => {
const loadAlertConfigs = async () => { const loadAlertConfigs = async () => {
loading.value = true loading.value = true
try { try {
const data = await AlertConfigApi.getAlertConfigPage({ const data = await getAlertConfigPage({
pageNo: 1, pageNo: 1,
pageSize: 100, pageSize: 100,
enabled: true // enabled: true //

View File

@ -7,7 +7,7 @@
<el-form-item label="条件类型" required> <el-form-item label="条件类型" required>
<el-select <el-select
:model-value="condition.type" :model-value="condition.type"
@update:model-value="(value) => updateConditionField('type', value)" @update:model-value="(value: string) => updateConditionField('type', value)"
@change="handleConditionTypeChange" @change="handleConditionTypeChange"
placeholder="请选择条件类型" placeholder="请选择条件类型"
class="w-full" class="w-full"
@ -29,7 +29,7 @@
<el-form-item label="产品" required> <el-form-item label="产品" required>
<ProductSelector <ProductSelector
:model-value="condition.productId" :model-value="condition.productId"
@update:model-value="(value) => updateConditionField('productId', value)" @update:model-value="(value: number | undefined) => updateConditionField('productId', value)"
@change="handleProductChange" @change="handleProductChange"
/> />
</el-form-item> </el-form-item>
@ -38,7 +38,7 @@
<el-form-item label="设备" required> <el-form-item label="设备" required>
<DeviceSelector <DeviceSelector
:model-value="condition.deviceId" :model-value="condition.deviceId"
@update:model-value="(value) => updateConditionField('deviceId', value)" @update:model-value="(value: number | undefined) => updateConditionField('deviceId', value)"
:product-id="condition.productId" :product-id="condition.productId"
@change="handleDeviceChange" @change="handleDeviceChange"
/> />
@ -48,7 +48,7 @@
<!-- 设备状态条件配置 --> <!-- 设备状态条件配置 -->
<div <div
v-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS" v-if="condition.type === String(IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS)"
class="flex flex-col gap-16px" class="flex flex-col gap-16px"
> >
<!-- 状态和操作符选择 --> <!-- 状态和操作符选择 -->
@ -58,7 +58,7 @@
<el-form-item label="操作符" required> <el-form-item label="操作符" required>
<el-select <el-select
:model-value="condition.operator" :model-value="condition.operator"
@update:model-value="(value) => updateConditionField('operator', value)" @update:model-value="(value: string | undefined) => updateConditionField('operator', value)"
placeholder="请选择操作符" placeholder="请选择操作符"
class="w-full" class="w-full"
> >
@ -76,8 +76,8 @@
<el-col :span="12"> <el-col :span="12">
<el-form-item label="设备状态" required> <el-form-item label="设备状态" required>
<el-select <el-select
:model-value="condition.param" :model-value="condition.value"
@update:model-value="(value) => updateConditionField('param', value)" @update:model-value="(value: any) => updateConditionField('value', value)"
placeholder="请选择设备状态" placeholder="请选择设备状态"
class="w-full" class="w-full"
> >
@ -95,7 +95,7 @@
<!-- 设备属性条件配置 --> <!-- 设备属性条件配置 -->
<div <div
v-else-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY" v-else-if="condition.type === String(IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY)"
class="space-y-16px" class="space-y-16px"
> >
<!-- 属性配置 --> <!-- 属性配置 -->
@ -105,7 +105,7 @@
<el-form-item label="监控项" required> <el-form-item label="监控项" required>
<PropertySelector <PropertySelector
:model-value="condition.identifier" :model-value="condition.identifier"
@update:model-value="(value) => updateConditionField('identifier', value)" @update:model-value="(value: string | undefined) => updateConditionField('identifier', value)"
:trigger-type="triggerType" :trigger-type="triggerType"
:product-id="condition.productId" :product-id="condition.productId"
:device-id="condition.deviceId" :device-id="condition.deviceId"
@ -119,7 +119,7 @@
<el-form-item label="操作符" required> <el-form-item label="操作符" required>
<OperatorSelector <OperatorSelector
:model-value="condition.operator" :model-value="condition.operator"
@update:model-value="(value) => updateConditionField('operator', value)" @update:model-value="(value: string | undefined) => updateConditionField('operator', value)"
:property-type="propertyType" :property-type="propertyType"
@change="handleOperatorChange" @change="handleOperatorChange"
/> />
@ -130,8 +130,8 @@
<el-col :span="12"> <el-col :span="12">
<el-form-item label="比较值" required> <el-form-item label="比较值" required>
<ValueInput <ValueInput
:model-value="condition.param" :model-value="condition.value"
@update:model-value="(value) => updateConditionField('param', value)" @update:model-value="(value: any) => updateConditionField('value', value)"
:property-type="propertyType" :property-type="propertyType"
:operator="condition.operator" :operator="condition.operator"
:property-config="propertyConfig" :property-config="propertyConfig"
@ -143,7 +143,7 @@
<!-- 当前时间条件配置 --> <!-- 当前时间条件配置 -->
<CurrentTimeConditionConfig <CurrentTimeConditionConfig
v-else-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME" v-else-if="condition.type === String(IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME)"
:model-value="condition" :model-value="condition"
@update:model-value="updateCondition" @update:model-value="updateCondition"
/> />
@ -151,6 +151,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import CurrentTimeConditionConfig from './CurrentTimeConditionConfig.vue' import CurrentTimeConditionConfig from './CurrentTimeConditionConfig.vue'
import ProductSelector from '../selectors/ProductSelector.vue' import ProductSelector from '../selectors/ProductSelector.vue'
@ -208,8 +209,8 @@ const propertyType = ref<string>('string') // 属性类型
const propertyConfig = ref<any>(null) // const propertyConfig = ref<any>(null) //
const isDeviceCondition = computed(() => { const isDeviceCondition = computed(() => {
return ( return (
condition.value.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS || condition.value.type === String(IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS) ||
condition.value.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY condition.value.type === String(IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY)
) )
}) // }) //
@ -236,10 +237,10 @@ const updateCondition = (newCondition: TriggerCondition) => {
* 处理条件类型变化事件 * 处理条件类型变化事件
* @param type 条件类型 * @param type 条件类型
*/ */
const handleConditionTypeChange = (type: number) => { const handleConditionTypeChange = (type: string) => {
// //
const isCurrentTime = type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME const isCurrentTime = type === String(IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME)
const isDeviceStatus = type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS const isDeviceStatus = type === String(IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS)
// //
if (isCurrentTime || isDeviceStatus) { if (isCurrentTime || isDeviceStatus) {
@ -258,18 +259,18 @@ const handleConditionTypeChange = (type: number) => {
: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value : IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
// //
condition.value.param = '' condition.value.value = ''
} }
/** 处理产品变化事件 */ /** 处理产品变化事件 */
const handleProductChange = (_: number) => { const handleProductChange = (_?: number) => {
// //
condition.value.deviceId = undefined condition.value.deviceId = undefined
condition.value.identifier = '' condition.value.identifier = ''
} }
/** 处理设备变化事件 */ /** 处理设备变化事件 */
const handleDeviceChange = (_: number) => { const handleDeviceChange = (_?: number) => {
// //
condition.value.identifier = '' condition.value.identifier = ''
} }
@ -284,13 +285,13 @@ const handlePropertyChange = (propertyInfo: { type: string; config: any }) => {
// //
condition.value.operator = IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value condition.value.operator = IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
condition.value.param = '' condition.value.value = ''
} }
/** 处理操作符变化事件 */ /** 处理操作符变化事件 */
const handleOperatorChange = () => { const handleOperatorChange = () => {
// //
condition.value.param = '' condition.value.value = ''
} }
</script> </script>

View File

@ -7,7 +7,7 @@
<el-form-item label="时间条件" required> <el-form-item label="时间条件" required>
<el-select <el-select
:model-value="condition.operator" :model-value="condition.operator"
@update:model-value="(value) => updateConditionField('operator', value)" @update:model-value="(value: string) => updateConditionField('operator', value)"
placeholder="请选择时间条件" placeholder="请选择时间条件"
class="w-full" class="w-full"
> >
@ -86,6 +86,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, watch } from 'vue'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { IotRuleSceneTriggerTimeOperatorEnum } from '#/views/iot/utils/constants' import { IotRuleSceneTriggerTimeOperatorEnum } from '#/views/iot/utils/constants'
import type { TriggerCondition } from '#/api/iot/rule/scene' import type { TriggerCondition } from '#/api/iot/rule/scene'
@ -168,17 +169,17 @@ const needsSecondTimeInput = computed(() => {
return condition.value.operator === IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value return condition.value.operator === IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value
}) })
// param // value TriggerCondition 使 value
const timeValue = computed(() => { const timeValue = computed(() => {
if (!condition.value.param) return '' if (!condition.value.value) return ''
const params = condition.value.param.split(',') const params = String(condition.value.value).split(',')
return params[0] || '' return params[0] || ''
}) })
// param // value
const timeValue2 = computed(() => { const timeValue2 = computed(() => {
if (!condition.value.param) return '' if (!condition.value.value) return ''
const params = condition.value.param.split(',') const params = String(condition.value.value).split(',')
return params[1] || '' return params[1] || ''
}) })
@ -187,8 +188,8 @@ const timeValue2 = computed(() => {
* @param field 字段名 * @param field 字段名
* @param value 字段值 * @param value 字段值
*/ */
const updateConditionField = (field: any, value: any) => { const updateConditionField = (field: keyof TriggerCondition, value: any) => {
condition.value[field] = value ;(condition.value as any)[field] = value
} }
/** /**
@ -196,14 +197,14 @@ const updateConditionField = (field: any, value: any) => {
* @param value 时间值 * @param value 时间值
*/ */
const handleTimeValueChange = (value: string) => { const handleTimeValueChange = (value: string) => {
const currentParams = condition.value.param ? condition.value.param.split(',') : [] const currentParams = condition.value.value ? String(condition.value.value).split(',') : []
currentParams[0] = value || '' currentParams[0] = value || ''
// //
if (needsSecondTimeInput.value) { if (needsSecondTimeInput.value) {
condition.value.param = currentParams.slice(0, 2).join(',') condition.value.value = currentParams.slice(0, 2).join(',')
} else { } else {
condition.value.param = currentParams[0] condition.value.value = currentParams[0]
} }
} }
@ -212,22 +213,22 @@ const handleTimeValueChange = (value: string) => {
* @param value 时间值 * @param value 时间值
*/ */
const handleTimeValue2Change = (value: string) => { const handleTimeValue2Change = (value: string) => {
const currentParams = condition.value.param ? condition.value.param.split(',') : [''] const currentParams = condition.value.value ? String(condition.value.value).split(',') : ['']
currentParams[1] = value || '' currentParams[1] = value || ''
condition.value.param = currentParams.slice(0, 2).join(',') condition.value.value = currentParams.slice(0, 2).join(',')
} }
/** 监听操作符变化,清理不相关的时间值 */ /** 监听操作符变化,清理不相关的时间值 */
watch( watch(
() => condition.value.operator, () => condition.value.operator,
(newOperator) => { (newOperator: string | undefined) => {
if (newOperator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) { if (newOperator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
// //
condition.value.param = '' condition.value.value = ''
} else if (!needsSecondTimeInput.value) { } else if (!needsSecondTimeInput.value) {
// //
const currentParams = condition.value.param ? condition.value.param.split(',') : [] const currentParams = condition.value.value ? String(condition.value.value).split(',') : []
condition.value.param = currentParams[0] || '' condition.value.value = currentParams[0] || ''
} }
} }
) )

View File

@ -76,6 +76,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import ProductSelector from '../selectors/ProductSelector.vue' import ProductSelector from '../selectors/ProductSelector.vue'
import DeviceSelector from '../selectors/DeviceSelector.vue' import DeviceSelector from '../selectors/DeviceSelector.vue'
@ -87,7 +88,7 @@ import {
IoTThingModelAccessModeEnum, IoTThingModelAccessModeEnum,
IoTDataSpecsDataTypeEnum IoTDataSpecsDataTypeEnum
} from '#/views/iot/utils/constants' } from '#/views/iot/utils/constants'
import { ThingModelApi } from '#/api/iot/thingmodel' import { exportThingModelTSL } from '#/api/iot/thingmodel'
/** 设备控制配置组件 */ /** 设备控制配置组件 */
defineOptions({ name: 'DeviceControlConfig' }) defineOptions({ name: 'DeviceControlConfig' })
@ -108,30 +109,30 @@ const selectedService = ref<ThingModelService | null>(null) // 选中的服务
const serviceList = ref<ThingModelService[]>([]) // const serviceList = ref<ThingModelService[]>([]) //
const loadingServices = ref(false) // const loadingServices = ref(false) //
// // Action 使 value
const paramsValue = computed({ const paramsValue = computed({
get: () => { get: () => {
// params JSON // value JSON
if (action.value.params && typeof action.value.params === 'object') { if (action.value.value && typeof action.value.value === 'object') {
return JSON.stringify(action.value.params, null, 2) return JSON.stringify(action.value.value, null, 2)
} }
// params // value
return action.value.params || '' return action.value.value || ''
}, },
set: (value: string) => { set: (value: string) => {
// JSON // JSON
action.value.params = value.trim() || '' action.value.value = value.trim() || ''
} }
}) })
// //
const isPropertySetAction = computed(() => { const isPropertySetAction = computed(() => {
return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET return action.value.type === String(IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET)
}) })
// //
const isServiceInvokeAction = computed(() => { const isServiceInvokeAction = computed(() => {
return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE return action.value.type === String(IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE)
}) })
/** /**
@ -143,7 +144,7 @@ const handleProductChange = (productId?: number) => {
if (action.value.productId !== productId) { if (action.value.productId !== productId) {
action.value.deviceId = undefined action.value.deviceId = undefined
action.value.identifier = undefined // action.value.identifier = undefined //
action.value.params = '' // action.value.value = '' //
selectedService.value = null // selectedService.value = null //
serviceList.value = [] // serviceList.value = [] //
} }
@ -165,7 +166,7 @@ const handleProductChange = (productId?: number) => {
const handleDeviceChange = (deviceId?: number) => { const handleDeviceChange = (deviceId?: number) => {
// //
if (action.value.deviceId !== deviceId) { if (action.value.deviceId !== deviceId) {
action.value.params = '' // action.value.value = '' //
} }
} }
@ -175,20 +176,20 @@ const handleDeviceChange = (deviceId?: number) => {
*/ */
const handleServiceChange = (serviceIdentifier?: string) => { const handleServiceChange = (serviceIdentifier?: string) => {
// //
const service = serviceList.value.find((s) => s.identifier === serviceIdentifier) || null const service = serviceList.value.find((s: ThingModelService) => s.identifier === serviceIdentifier) || null
selectedService.value = service selectedService.value = service
// //
action.value.params = '' action.value.value = ''
// //
if (service && service.inputParams && service.inputParams.length > 0) { if (service && service.inputData && service.inputData.length > 0) {
const defaultParams = {} const defaultParams: Record<string, any> = {}
service.inputParams.forEach((param) => { service.inputData.forEach((param: any) => {
defaultParams[param.identifier] = getDefaultValueForParam(param) defaultParams[param.identifier] = getDefaultValueForParam(param)
}) })
// JSON // JSON
action.value.params = JSON.stringify(defaultParams, null, 2) action.value.value = JSON.stringify(defaultParams, null, 2)
} }
} }
@ -201,7 +202,7 @@ const getThingModelTSL = async (productId: number) => {
if (!productId) return null if (!productId) return null
try { try {
return await ThingModelApi.getThingModelTSLByProductId(productId) return await exportThingModelTSL(productId)
} catch (error) { } catch (error) {
console.error('获取物模型TSL数据失败:', error) console.error('获取物模型TSL数据失败:', error)
return null return null
@ -280,7 +281,7 @@ const loadServiceFromTSL = async (productId: number, serviceIdentifier: string)
await loadServiceList(productId) await loadServiceList(productId)
// //
const service = serviceList.value.find((s: any) => s.identifier === serviceIdentifier) const service = serviceList.value.find((s: ThingModelService) => s.identifier === serviceIdentifier)
if (service) { if (service) {
selectedService.value = service selectedService.value = service
} }
@ -345,7 +346,7 @@ onMounted(() => {
/** 监听关键字段的变化,避免深度监听导致的性能问题 */ /** 监听关键字段的变化,避免深度监听导致的性能问题 */
watch( watch(
() => [action.value.productId, action.value.type, action.value.identifier], () => [action.value.productId, action.value.type, action.value.identifier] as const,
async ([newProductId, , newIdentifier], [oldProductId, , oldIdentifier]) => { async ([newProductId, , newIdentifier], [oldProductId, , oldIdentifier]) => {
// //
if (!isInitialized.value) return if (!isInitialized.value) return
@ -366,7 +367,7 @@ watch(
isServiceInvokeAction.value && isServiceInvokeAction.value &&
newIdentifier newIdentifier
) { ) {
const service = serviceList.value.find((s: any) => s.identifier === newIdentifier) const service = serviceList.value.find((s: ThingModelService) => s.identifier === newIdentifier)
if (service) { if (service) {
selectedService.value = service selectedService.value = service
} }

View File

@ -28,7 +28,7 @@
<MainConditionInnerConfig <MainConditionInnerConfig
:model-value="trigger" :model-value="trigger"
@update:model-value="updateCondition" @update:model-value="updateCondition"
:trigger-type="trigger.type" :trigger-type="Number(trigger.type)"
@trigger-type-change="handleTriggerTypeChange" @trigger-type-change="handleTriggerTypeChange"
/> />
</div> </div>
@ -54,7 +54,7 @@
</div> </div>
<el-tag size="small" type="success">"主条件"为且关系</el-tag> <el-tag size="small" type="success">"主条件"为且关系</el-tag>
<el-tag size="small" type="info"> <el-tag size="small" type="info">
{{ trigger.conditionGroups?.length || 0 }} 个子条件组 {{ (trigger.conditionGroups?.length || 0) }} 个子条件组
</el-tag> </el-tag>
</div> </div>
<div class="flex items-center gap-8px"> <div class="flex items-center gap-8px">
@ -103,7 +103,7 @@
<span>子条件组 {{ subGroupIndex + 1 }}</span> <span>子条件组 {{ subGroupIndex + 1 }}</span>
</div> </div>
<el-tag size="small" type="warning" class="font-500">组内条件为"且"关系</el-tag> <el-tag size="small" type="warning" class="font-500">组内条件为"且"关系</el-tag>
<el-tag size="small" type="info"> {{ subGroup?.length || 0 }}个条件 </el-tag> <el-tag size="small" type="info"> {{ subGroup?.conditions?.length || 0 }}个条件 </el-tag>
</div> </div>
<el-button <el-button
type="danger" type="danger"
@ -118,9 +118,9 @@
</div> </div>
<SubConditionGroupConfig <SubConditionGroupConfig
:model-value="subGroup" :model-value="subGroup.conditions || []"
@update:model-value="(value) => updateSubGroup(subGroupIndex, value)" @update:model-value="(value) => updateSubGroup(subGroupIndex, value)"
:trigger-type="trigger.type" :trigger-type="Number(trigger.type)"
:max-conditions="maxConditionsPerGroup" :max-conditions="maxConditionsPerGroup"
/> />
</div> </div>
@ -164,11 +164,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { nextTick } from 'vue'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import MainConditionInnerConfig from './MainConditionInnerConfig.vue' import MainConditionInnerConfig from './MainConditionInnerConfig.vue'
import SubConditionGroupConfig from './SubConditionGroupConfig.vue' import SubConditionGroupConfig from './SubConditionGroupConfig.vue'
import type { Trigger } from '#/api/iot/rule/scene' import type { Trigger, TriggerCondition } from '#/api/iot/rule/scene'
/** 设备触发配置组件 */ /** 设备触发配置组件 */
defineOptions({ name: 'DeviceTriggerConfig' }) defineOptions({ name: 'DeviceTriggerConfig' })
@ -201,7 +202,7 @@ const updateCondition = (condition: Trigger) => {
* @param type 触发器类型 * @param type 触发器类型
*/ */
const handleTriggerTypeChange = (type: number) => { const handleTriggerTypeChange = (type: number) => {
trigger.value.type = type trigger.value.type = String(type)
emit('trigger-type-change', type) emit('trigger-type-change', type)
} }
@ -219,7 +220,7 @@ const addSubGroup = async () => {
// 使 nextTick // 使 nextTick
await nextTick() await nextTick()
if (trigger.value.conditionGroups) { if (trigger.value.conditionGroups) {
trigger.value.conditionGroups.push([]) trigger.value.conditionGroups.push({ conditions: [] })
} }
} }
@ -236,11 +237,11 @@ const removeSubGroup = (index: number) => {
/** /**
* 更新子条件组 * 更新子条件组
* @param index 子条件组索引 * @param index 子条件组索引
* @param subGroup 子条件组数据 * @param conditions 子条件组数据
*/ */
const updateSubGroup = (index: number, subGroup: any) => { const updateSubGroup = (index: number, conditions: TriggerCondition[]) => {
if (trigger.value.conditionGroups) { if (trigger.value.conditionGroups && trigger.value.conditionGroups[index]) {
trigger.value.conditionGroups[index] = subGroup trigger.value.conditionGroups[index].conditions = conditions
} }
} }

View File

@ -25,7 +25,7 @@
<el-form-item label="产品" required> <el-form-item label="产品" required>
<ProductSelector <ProductSelector
:model-value="condition.productId" :model-value="condition.productId"
@update:model-value="(value) => updateConditionField('productId', value)" @update:model-value="(value: number | undefined) => updateConditionField('productId', value)"
@change="handleProductChange" @change="handleProductChange"
/> />
</el-form-item> </el-form-item>
@ -34,7 +34,7 @@
<el-form-item label="设备" required> <el-form-item label="设备" required>
<DeviceSelector <DeviceSelector
:model-value="condition.deviceId" :model-value="condition.deviceId"
@update:model-value="(value) => updateConditionField('deviceId', value)" @update:model-value="(value: number | undefined) => updateConditionField('deviceId', value)"
:product-id="condition.productId" :product-id="condition.productId"
@change="handleDeviceChange" @change="handleDeviceChange"
/> />
@ -49,7 +49,7 @@
<el-form-item label="监控项" required> <el-form-item label="监控项" required>
<PropertySelector <PropertySelector
:model-value="condition.identifier" :model-value="condition.identifier"
@update:model-value="(value) => updateConditionField('identifier', value)" @update:model-value="(value: string | undefined) => updateConditionField('identifier', value)"
:trigger-type="triggerType" :trigger-type="triggerType"
:product-id="condition.productId" :product-id="condition.productId"
:device-id="condition.deviceId" :device-id="condition.deviceId"
@ -63,7 +63,7 @@
<el-form-item label="操作符" required> <el-form-item label="操作符" required>
<OperatorSelector <OperatorSelector
:model-value="condition.operator" :model-value="condition.operator"
@update:model-value="(value) => updateConditionField('operator', value)" @update:model-value="(value: string | undefined) => updateConditionField('operator', value)"
:property-type="propertyType" :property-type="propertyType"
/> />
</el-form-item> </el-form-item>
@ -92,7 +92,7 @@
<ValueInput <ValueInput
v-else v-else
:model-value="condition.value" :model-value="condition.value"
@update:model-value="(value) => updateConditionField('value', value)" @update:model-value="(value: any) => updateConditionField('value', value)"
:property-type="propertyType" :property-type="propertyType"
:operator="condition.operator" :operator="condition.operator"
:property-config="propertyConfig" :property-config="propertyConfig"
@ -110,7 +110,7 @@
<el-form-item label="产品" required> <el-form-item label="产品" required>
<ProductSelector <ProductSelector
:model-value="condition.productId" :model-value="condition.productId"
@update:model-value="(value) => updateConditionField('productId', value)" @update:model-value="(value: number | undefined) => updateConditionField('productId', value)"
@change="handleProductChange" @change="handleProductChange"
/> />
</el-form-item> </el-form-item>
@ -119,7 +119,7 @@
<el-form-item label="设备" required> <el-form-item label="设备" required>
<DeviceSelector <DeviceSelector
:model-value="condition.deviceId" :model-value="condition.deviceId"
@update:model-value="(value) => updateConditionField('deviceId', value)" @update:model-value="(value: number | undefined) => updateConditionField('deviceId', value)"
:product-id="condition.productId" :product-id="condition.productId"
@change="handleDeviceChange" @change="handleDeviceChange"
/> />
@ -131,7 +131,7 @@
<el-form-item label="操作符" required> <el-form-item label="操作符" required>
<el-select <el-select
:model-value="condition.operator" :model-value="condition.operator"
@update:model-value="(value) => updateConditionField('operator', value)" @update:model-value="(value: string | undefined) => updateConditionField('operator', value)"
placeholder="请选择操作符" placeholder="请选择操作符"
class="w-full" class="w-full"
> >
@ -146,7 +146,7 @@
<el-form-item label="参数" required> <el-form-item label="参数" required>
<el-select <el-select
:model-value="condition.value" :model-value="condition.value"
@update:model-value="(value) => updateConditionField('value', value)" @update:model-value="(value: string | undefined) => updateConditionField('value', value)"
placeholder="请选择操作符" placeholder="请选择操作符"
class="w-full" class="w-full"
> >
@ -175,6 +175,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue'
import { useVModel } from '@vueuse/core'
import ProductSelector from '../selectors/ProductSelector.vue' import ProductSelector from '../selectors/ProductSelector.vue'
import DeviceSelector from '../selectors/DeviceSelector.vue' import DeviceSelector from '../selectors/DeviceSelector.vue'
import PropertySelector from '../selectors/PropertySelector.vue' import PropertySelector from '../selectors/PropertySelector.vue'
@ -190,7 +192,6 @@ import {
IotRuleSceneTriggerConditionParameterOperatorEnum, IotRuleSceneTriggerConditionParameterOperatorEnum,
IoTDeviceStatusEnum IoTDeviceStatusEnum
} from '#/views/iot/utils/constants' } from '#/views/iot/utils/constants'
import { useVModel } from '@vueuse/core'
/** 主条件内部配置组件 */ /** 主条件内部配置组件 */
defineOptions({ name: 'MainConditionInnerConfig' }) defineOptions({ name: 'MainConditionInnerConfig' })
@ -294,8 +295,8 @@ const eventConfig = computed(() => {
* @param field 字段名 * @param field 字段名
* @param value 字段值 * @param value 字段值
*/ */
const updateConditionField = (field: any, value: any) => { const updateConditionField = (field: keyof Trigger, value: any) => {
condition.value[field] = value ;(condition.value as any)[field] = value
} }
/** /**

View File

@ -79,7 +79,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { nextTick } from 'vue' import { computed, nextTick } from 'vue'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import ConditionConfig from './ConditionConfig.vue' import ConditionConfig from './ConditionConfig.vue'
import type { TriggerCondition } from '#/api/iot/rule/scene' import type { TriggerCondition } from '#/api/iot/rule/scene'
@ -118,12 +118,12 @@ const addCondition = async () => {
} }
const newCondition: TriggerCondition = { const newCondition: TriggerCondition = {
type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, // type: String(IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY), //
productId: undefined, productId: undefined,
deviceId: undefined, deviceId: undefined,
identifier: '', identifier: '',
operator: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value, // 使 operator: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value, // 使
param: '' value: ''
} }
// 使 nextTick // 使 nextTick

View File

@ -139,6 +139,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, nextTick, onMounted } from 'vue'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { InfoFilled } from '@element-plus/icons-vue' import { InfoFilled } from '@element-plus/icons-vue'
import { import {
@ -294,22 +295,6 @@ const emptyMessage = computed(() => {
} }
}) })
//
const noConfigMessage = computed(() => {
switch (props.type) {
case JsonParamsInputTypeEnum.SERVICE:
return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.SERVICE
case JsonParamsInputTypeEnum.EVENT:
return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.EVENT
case JsonParamsInputTypeEnum.PROPERTY:
return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.PROPERTY
case JsonParamsInputTypeEnum.CUSTOM:
return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.CUSTOM
default:
return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.DEFAULT
}
})
/** /**
* 处理参数变化事件 * 处理参数变化事件
*/ */
@ -371,7 +356,7 @@ const clearParams = () => {
*/ */
const getParamTypeName = (dataType: string) => { const getParamTypeName = (dataType: string) => {
// 使 constants.ts getDataTypeName // 使 constants.ts getDataTypeName
const typeMap = { const typeMap: Record<string, string> = {
[IoTDataSpecsDataTypeEnum.INT]: '整数', [IoTDataSpecsDataTypeEnum.INT]: '整数',
[IoTDataSpecsDataTypeEnum.FLOAT]: '浮点数', [IoTDataSpecsDataTypeEnum.FLOAT]: '浮点数',
[IoTDataSpecsDataTypeEnum.DOUBLE]: '双精度', [IoTDataSpecsDataTypeEnum.DOUBLE]: '双精度',
@ -391,7 +376,7 @@ const getParamTypeName = (dataType: string) => {
* @returns 标签样式 * @returns 标签样式
*/ */
const getParamTypeTag = (dataType: string) => { const getParamTypeTag = (dataType: string) => {
const tagMap = { const tagMap: Record<string, string> = {
[IoTDataSpecsDataTypeEnum.INT]: 'primary', [IoTDataSpecsDataTypeEnum.INT]: 'primary',
[IoTDataSpecsDataTypeEnum.FLOAT]: 'success', [IoTDataSpecsDataTypeEnum.FLOAT]: 'success',
[IoTDataSpecsDataTypeEnum.DOUBLE]: 'success', [IoTDataSpecsDataTypeEnum.DOUBLE]: 'success',
@ -412,7 +397,7 @@ const getParamTypeTag = (dataType: string) => {
*/ */
const getExampleValue = (param: any) => { const getExampleValue = (param: any) => {
const exampleConfig = const exampleConfig =
JSON_PARAMS_EXAMPLE_VALUES[param.dataType] || JSON_PARAMS_EXAMPLE_VALUES.DEFAULT (JSON_PARAMS_EXAMPLE_VALUES as any)[param.dataType] || JSON_PARAMS_EXAMPLE_VALUES.DEFAULT
return exampleConfig.display return exampleConfig.display
} }
@ -425,10 +410,10 @@ const generateExampleJson = () => {
return '{}' return '{}'
} }
const example = {} const example: Record<string, any> = {}
paramsList.value.forEach((param) => { paramsList.value.forEach((param: any) => {
const exampleConfig = const exampleConfig =
JSON_PARAMS_EXAMPLE_VALUES[param.dataType] || JSON_PARAMS_EXAMPLE_VALUES.DEFAULT (JSON_PARAMS_EXAMPLE_VALUES as any)[param.dataType] || JSON_PARAMS_EXAMPLE_VALUES.DEFAULT
example[param.identifier] = exampleConfig.value example[param.identifier] = exampleConfig.value
}) })
@ -461,7 +446,7 @@ const handleDataDisplay = (value: string) => {
// //
watch( watch(
() => localValue.value, () => localValue.value,
async (newValue, oldValue) => { async (newValue: string | undefined, oldValue: string | undefined) => {
// //
if (newValue === oldValue) return if (newValue === oldValue) return
@ -483,7 +468,7 @@ onMounted(async () => {
// //
watch( watch(
() => props.config, () => props.config,
(newConfig, oldConfig) => { (newConfig: JsonParamsConfig | undefined, oldConfig: JsonParamsConfig | undefined) => {
// //
if (JSON.stringify(newConfig) !== JSON.stringify(oldConfig)) { if (JSON.stringify(newConfig) !== JSON.stringify(oldConfig)) {
// //

View File

@ -122,6 +122,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { import {
IoTDataSpecsDataTypeEnum, IoTDataSpecsDataTypeEnum,
@ -202,7 +203,7 @@ const getInputType = () => {
/** 获取占位符文本 */ /** 获取占位符文本 */
const getPlaceholder = () => { const getPlaceholder = () => {
const typeMap = { const typeMap: Record<string, string> = {
[IoTDataSpecsDataTypeEnum.TEXT]: '请输入字符串', [IoTDataSpecsDataTypeEnum.TEXT]: '请输入字符串',
[IoTDataSpecsDataTypeEnum.INT]: '请输入整数', [IoTDataSpecsDataTypeEnum.INT]: '请输入整数',
[IoTDataSpecsDataTypeEnum.FLOAT]: '请输入浮点数', [IoTDataSpecsDataTypeEnum.FLOAT]: '请输入浮点数',

View File

@ -49,7 +49,7 @@
<span>执行器 {{ index + 1 }}</span> <span>执行器 {{ index + 1 }}</span>
</div> </div>
<el-tag :type="getActionTypeTag(action.type)" size="small" class="font-500"> <el-tag :type="getActionTypeTag(action.type)" size="small" class="font-500">
{{ getActionTypeLabel(action.type) }} {{ getActionTypeLabel(Number(action.type)) }}
</el-tag> </el-tag>
</div> </div>
<div class="flex items-center gap-8px"> <div class="flex items-center gap-8px">
@ -74,8 +74,8 @@
<el-form-item label="执行类型" required> <el-form-item label="执行类型" required>
<el-select <el-select
:model-value="action.type" :model-value="action.type"
@update:model-value="(value) => updateActionType(index, value)" @update:model-value="(value: any) => updateActionType(index, value)"
@change="(value) => onActionTypeChange(action, value)" @change="(value: any) => onActionTypeChange(action, value)"
placeholder="请选择执行类型" placeholder="请选择执行类型"
class="w-full" class="w-full"
> >
@ -91,21 +91,21 @@
<!-- 设备控制配置 --> <!-- 设备控制配置 -->
<DeviceControlConfig <DeviceControlConfig
v-if="isDeviceAction(action.type)" v-if="isDeviceAction(Number(action.type))"
:model-value="action" :model-value="action"
@update:model-value="(value) => updateAction(index, value)" @update:model-value="(value: any) => updateAction(index, value)"
/> />
<!-- 告警配置 - 只有恢复告警时才显示 --> <!-- 告警配置 - 只有恢复告警时才显示 -->
<AlertConfig <AlertConfig
v-if="action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER" v-if="Number(action.type) === IotRuleSceneActionTypeEnum.ALERT_RECOVER"
:model-value="action.alertConfigId" :model-value="action.alertConfigId"
@update:model-value="(value) => updateActionAlertConfig(index, value)" @update:model-value="(value: any) => updateActionAlertConfig(index, value)"
/> />
<!-- 触发告警提示 - 触发告警时显示 --> <!-- 触发告警提示 - 触发告警时显示 -->
<div <div
v-if="action.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER" v-if="Number(action.type) === IotRuleSceneActionTypeEnum.ALERT_TRIGGER"
class="border border-[var(--el-border-color-light)] rounded-6px p-16px bg-[var(--el-fill-color-blank)]" class="border border-[var(--el-border-color-light)] rounded-6px p-16px bg-[var(--el-fill-color-blank)]"
> >
<div class="flex items-center gap-8px mb-8px"> <div class="flex items-center gap-8px mb-8px">
@ -136,13 +136,18 @@
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import DeviceControlConfig from '../configs/DeviceControlConfig.vue' import DeviceControlConfig from '../configs/DeviceControlConfig.vue'
import AlertConfig from '../configs/AlertConfig.vue' import AlertConfig from '../configs/AlertConfig.vue'
import type { Action } from '#/api/iot/rule/scene' import type { Action as BaseAction } from '#/api/iot/rule/scene'
import { import {
getActionTypeLabel, getActionTypeLabel,
getActionTypeOptions, getActionTypeOptions,
IotRuleSceneActionTypeEnum IotRuleSceneActionTypeEnum
} from '#/views/iot/utils/constants' } from '#/views/iot/utils/constants'
// Action params
interface Action extends BaseAction {
params?: any
}
/** 执行器配置组件 */ /** 执行器配置组件 */
defineOptions({ name: 'ActionSection' }) defineOptions({ name: 'ActionSection' })
@ -157,14 +162,15 @@ const emit = defineEmits<{
const actions = useVModel(props, 'actions', emit) const actions = useVModel(props, 'actions', emit)
/** 获取执行器标签类型(用于 el-tag 的 type 属性) */ /** 获取执行器标签类型(用于 el-tag 的 type 属性) */
const getActionTypeTag = (type: number): 'primary' | 'success' | 'info' | 'warning' | 'danger' => { const getActionTypeTag = (type?: string): 'primary' | 'success' | 'info' | 'warning' | 'danger' => {
const actionTypeTags = { const typeNum = Number(type)
const actionTypeTags: Record<number, 'primary' | 'success' | 'danger' | 'warning'> = {
[IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET]: 'primary', [IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET]: 'primary',
[IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE]: 'success', [IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE]: 'success',
[IotRuleSceneActionTypeEnum.ALERT_TRIGGER]: 'danger', [IotRuleSceneActionTypeEnum.ALERT_TRIGGER]: 'danger',
[IotRuleSceneActionTypeEnum.ALERT_RECOVER]: 'warning' [IotRuleSceneActionTypeEnum.ALERT_RECOVER]: 'warning'
} as const }
return actionTypeTags[type] || 'info' return actionTypeTags[typeNum] || 'info'
} }
/** 判断是否为设备执行器类型 */ /** 判断是否为设备执行器类型 */
@ -191,7 +197,7 @@ const isAlertAction = (type: number): boolean => {
*/ */
const createDefaultActionData = (): Action => { const createDefaultActionData = (): Action => {
return { return {
type: IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET, // type: String(IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET), //
productId: undefined, productId: undefined,
deviceId: undefined, deviceId: undefined,
identifier: undefined, // 使 identifier: undefined, // 使
@ -222,8 +228,8 @@ const removeAction = (index: number) => {
* @param type 执行器类型 * @param type 执行器类型
*/ */
const updateActionType = (index: number, type: number) => { const updateActionType = (index: number, type: number) => {
actions.value[index].type = type actions.value[index]!.type = String(type)
onActionTypeChange(actions.value[index], type) onActionTypeChange(actions.value[index]!, type)
} }
/** /**
@ -241,7 +247,7 @@ const updateAction = (index: number, action: Action) => {
* @param alertConfigId 告警配置ID * @param alertConfigId 告警配置ID
*/ */
const updateActionAlertConfig = (index: number, alertConfigId?: number) => { const updateActionAlertConfig = (index: number, alertConfigId?: number) => {
actions.value[index].alertConfigId = alertConfigId actions.value[index]!.alertConfigId = alertConfigId
} }
/** /**
@ -258,7 +264,7 @@ const onActionTypeChange = (action: Action, type: number) => {
action.params = '' action.params = ''
} }
// identifier // identifier
if (action.identifier && type !== action.type) { if (action.identifier && type !== Number(action.type)) {
action.identifier = undefined action.identifier = undefined
} }
} else if (isAlertAction(type)) { } else if (isAlertAction(type)) {

View File

@ -30,7 +30,7 @@
<el-form-item label="场景状态" prop="status" required> <el-form-item label="场景状态" prop="status" required>
<el-radio-group v-model="formData.status"> <el-radio-group v-model="formData.status">
<el-radio <el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value" :key="dict.value"
:label="dict.value" :label="dict.value"
> >
@ -57,8 +57,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { DICT_TYPE, getIntDictOptions } from '@vben/constants'
import type { IotSceneRule } from '#/api/iot/rule/scene' import type { IotSceneRule } from '#/api/iot/rule/scene'
import { DICT_TYPE, getDictOptions } from '#/utils'
/** 基础信息配置组件 */ /** 基础信息配置组件 */
defineOptions({ name: 'BasicInfoSection' }) defineOptions({ name: 'BasicInfoSection' })

View File

@ -36,7 +36,7 @@
<span>触发器 {{ index + 1 }}</span> <span>触发器 {{ index + 1 }}</span>
</div> </div>
<el-tag size="small" :type="getTriggerTagType(triggerItem.type)" class="font-500"> <el-tag size="small" :type="getTriggerTagType(triggerItem.type)" class="font-500">
{{ getTriggerTypeLabel(triggerItem.type) }} {{ getTriggerTypeLabel(Number(triggerItem.type)) }}
</el-tag> </el-tag>
</div> </div>
<div class="flex items-center gap-8px"> <div class="flex items-center gap-8px">
@ -58,16 +58,16 @@
<div class="p-16px space-y-16px"> <div class="p-16px space-y-16px">
<!-- 设备触发配置 --> <!-- 设备触发配置 -->
<DeviceTriggerConfig <DeviceTriggerConfig
v-if="isDeviceTrigger(triggerItem.type)" v-if="isDeviceTrigger(Number(triggerItem.type))"
:model-value="triggerItem" :model-value="triggerItem"
:index="index" :index="index"
@update:model-value="(value) => updateTriggerDeviceConfig(index, value)" @update:model-value="(value: any) => updateTriggerDeviceConfig(index, value)"
@trigger-type-change="(type) => updateTriggerType(index, type)" @trigger-type-change="(type: any) => updateTriggerType(index, type)"
/> />
<!-- 定时触发配置 --> <!-- 定时触发配置 -->
<div <div
v-else-if="triggerItem.type === IotRuleSceneTriggerTypeEnum.TIMER" v-else-if="Number(triggerItem.type) === IotRuleSceneTriggerTypeEnum.TIMER"
class="flex flex-col gap-16px" class="flex flex-col gap-16px"
> >
<div <div
@ -86,7 +86,7 @@
<el-form-item label="CRON表达式" required> <el-form-item label="CRON表达式" required>
<Crontab <Crontab
:model-value="triggerItem.cronExpression || '0 0 12 * * ?'" :model-value="triggerItem.cronExpression || '0 0 12 * * ?'"
@update:model-value="(value) => updateTriggerCronConfig(index, value)" @update:model-value="(value: any) => updateTriggerCronConfig(index, value)"
/> />
</el-form-item> </el-form-item>
</div> </div>
@ -113,9 +113,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from 'vue'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import DeviceTriggerConfig from '../configs/DeviceTriggerConfig.vue' import DeviceTriggerConfig from '../configs/DeviceTriggerConfig.vue'
import { Crontab } from '@/components/Crontab' import Crontab from '#/components/cron-tab/cron-tab.vue'
import type { Trigger } from '#/api/iot/rule/scene' import type { Trigger } from '#/api/iot/rule/scene'
import { import {
getTriggerTypeLabel, getTriggerTypeLabel,
@ -137,17 +138,18 @@ const emit = defineEmits<{
const triggers = useVModel(props, 'triggers', emit) const triggers = useVModel(props, 'triggers', emit)
/** 获取触发器标签类型(用于 el-tag 的 type 属性) */ /** 获取触发器标签类型(用于 el-tag 的 type 属性) */
const getTriggerTagType = (type: number): 'primary' | 'success' | 'info' | 'warning' | 'danger' => { const getTriggerTagType = (type?: string): 'primary' | 'success' | 'info' | 'warning' | 'danger' => {
if (type === IotRuleSceneTriggerTypeEnum.TIMER) { const typeNum = Number(type)
if (typeNum === IotRuleSceneTriggerTypeEnum.TIMER) {
return 'warning' return 'warning'
} }
return isDeviceTrigger(type) ? 'success' : 'info' return isDeviceTrigger(typeNum) ? 'success' : 'info'
} }
/** 添加触发器 */ /** 添加触发器 */
const addTrigger = () => { const addTrigger = () => {
const newTrigger: Trigger = { const newTrigger: Trigger = {
type: IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE, type: String(IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE),
productId: undefined, productId: undefined,
deviceId: undefined, deviceId: undefined,
identifier: undefined, identifier: undefined,
@ -175,7 +177,7 @@ const removeTrigger = (index: number) => {
* @param type 触发器类型 * @param type 触发器类型
*/ */
const updateTriggerType = (index: number, type: number) => { const updateTriggerType = (index: number, type: number) => {
triggers.value[index].type = type triggers.value[index]!.type = String(type)
onTriggerTypeChange(index, type) onTriggerTypeChange(index, type)
} }
@ -194,7 +196,7 @@ const updateTriggerDeviceConfig = (index: number, newTrigger: Trigger) => {
* @param cronExpression CRON 表达式 * @param cronExpression CRON 表达式
*/ */
const updateTriggerCronConfig = (index: number, cronExpression?: string) => { const updateTriggerCronConfig = (index: number, cronExpression?: string) => {
triggers.value[index].cronExpression = cronExpression triggers.value[index]!.cronExpression = cronExpression
} }
/** /**
@ -204,13 +206,15 @@ const updateTriggerCronConfig = (index: number, cronExpression?: string) => {
*/ */
const onTriggerTypeChange = (index: number, _: number) => { const onTriggerTypeChange = (index: number, _: number) => {
const triggerItem = triggers.value[index] const triggerItem = triggers.value[index]
triggerItem.productId = undefined if (triggerItem) {
triggerItem.deviceId = undefined triggerItem.productId = undefined
triggerItem.identifier = undefined triggerItem.deviceId = undefined
triggerItem.operator = undefined triggerItem.identifier = undefined
triggerItem.value = undefined triggerItem.operator = undefined
triggerItem.cronExpression = undefined triggerItem.value = undefined
triggerItem.conditionGroups = [] triggerItem.cronExpression = undefined
triggerItem.conditionGroups = []
}
} }
/** 初始化:确保至少有一个触发器 */ /** 初始化:确保至少有一个触发器 */

View File

@ -32,9 +32,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue'
import { DeviceApi } from '#/api/iot/device/device' import { DeviceApi } from '#/api/iot/device/device'
import { DEVICE_SELECTOR_OPTIONS } from '#/views/iot/utils/constants' import { DEVICE_SELECTOR_OPTIONS } from '#/views/iot/utils/constants'
import { DICT_TYPE } from '@vben/constants' import { DICT_TYPE } from '#/utils'
/** 设备选择器组件 */ /** 设备选择器组件 */
defineOptions({ name: 'DeviceSelector' }) defineOptions({ name: 'DeviceSelector' })
@ -86,7 +87,7 @@ const getDeviceList = async () => {
// //
watch( watch(
() => props.productId, () => props.productId,
(newProductId) => { (newProductId: number | undefined) => {
if (newProductId) { if (newProductId) {
getDeviceList() getDeviceList()
} else { } else {

View File

@ -34,6 +34,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, watch } from 'vue'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { import {
IotRuleSceneTriggerConditionParameterOperatorEnum, IotRuleSceneTriggerConditionParameterOperatorEnum,

View File

@ -31,8 +31,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ProductApi } from '#/api/iot/product/product' import { ProductApi } from '#/api/iot/product/product'
import { DICT_TYPE } from '@vben/constants' import { DICT_TYPE } from '#/utils'
/** 产品选择器组件 */ /** 产品选择器组件 */
defineOptions({ name: 'ProductSelector' }) defineOptions({ name: 'ProductSelector' })

View File

@ -153,6 +153,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { InfoFilled } from '@element-plus/icons-vue' import { InfoFilled } from '@element-plus/icons-vue'
import { import {
@ -166,13 +167,11 @@ import {
THING_MODEL_GROUP_LABELS THING_MODEL_GROUP_LABELS
} from '#/views/iot/utils/constants' } from '#/views/iot/utils/constants'
import type { import type {
IotThingModelTSLResp,
ThingModelEvent, ThingModelEvent,
ThingModelParam,
ThingModelProperty, ThingModelProperty,
ThingModelService ThingModelService
} from '#/api/iot/thingmodel' } from '#/api/iot/thingmodel'
import { ThingModelApi } from '#/api/iot/thingmodel' import { exportThingModelTSL } from '#/api/iot/thingmodel'
/** 属性选择器组件 */ /** 属性选择器组件 */
defineOptions({ name: 'PropertySelector' }) defineOptions({ name: 'PropertySelector' })
@ -190,8 +189,8 @@ interface PropertySelectorItem {
range?: string range?: string
eventType?: string eventType?: string
callType?: string callType?: string
inputParams?: ThingModelParam[] inputParams?: any[]
outputParams?: ThingModelParam[] outputParams?: any[]
property?: ThingModelProperty property?: ThingModelProperty
event?: ThingModelEvent event?: ThingModelEvent
service?: ThingModelService service?: ThingModelService
@ -213,30 +212,30 @@ const localValue = useVModel(props, 'modelValue', emit)
const loading = ref(false) // const loading = ref(false) //
const propertyList = ref<PropertySelectorItem[]>([]) // const propertyList = ref<PropertySelectorItem[]>([]) //
const thingModelTSL = ref<IotThingModelTSLResp | null>(null) // TSL const thingModelTSL = ref<any | null>(null) // TSL
// //
const propertyGroups = computed(() => { const propertyGroups = computed(() => {
const groups: { label: string; options: any[] }[] = [] const groups: { label: string; options: PropertySelectorItem[] }[] = []
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST) { if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST) {
groups.push({ groups.push({
label: THING_MODEL_GROUP_LABELS.PROPERTY, label: THING_MODEL_GROUP_LABELS.PROPERTY,
options: propertyList.value.filter((p) => p.type === IoTThingModelTypeEnum.PROPERTY) options: propertyList.value.filter((p: PropertySelectorItem) => p.type === IoTThingModelTypeEnum.PROPERTY)
}) })
} }
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST) { if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST) {
groups.push({ groups.push({
label: THING_MODEL_GROUP_LABELS.EVENT, label: THING_MODEL_GROUP_LABELS.EVENT,
options: propertyList.value.filter((p) => p.type === IoTThingModelTypeEnum.EVENT) options: propertyList.value.filter((p: PropertySelectorItem) => p.type === IoTThingModelTypeEnum.EVENT)
}) })
} }
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE) { if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE) {
groups.push({ groups.push({
label: THING_MODEL_GROUP_LABELS.SERVICE, label: THING_MODEL_GROUP_LABELS.SERVICE,
options: propertyList.value.filter((p) => p.type === IoTThingModelTypeEnum.SERVICE) options: propertyList.value.filter((p: PropertySelectorItem) => p.type === IoTThingModelTypeEnum.SERVICE)
}) })
} }
@ -245,7 +244,7 @@ const propertyGroups = computed(() => {
// //
const selectedProperty = computed(() => { const selectedProperty = computed(() => {
return propertyList.value.find((p) => p.identifier === localValue.value) return propertyList.value.find((p: PropertySelectorItem) => p.identifier === localValue.value)
}) })
/** /**
@ -253,7 +252,7 @@ const selectedProperty = computed(() => {
* @param value 选中的属性标识符 * @param value 选中的属性标识符
*/ */
const handleChange = (value: string) => { const handleChange = (value: string) => {
const property = propertyList.value.find((p) => p.identifier === value) const property = propertyList.value.find((p: PropertySelectorItem) => p.identifier === value)
if (property) { if (property) {
emit('change', { emit('change', {
type: property.dataType, type: property.dataType,
@ -274,7 +273,7 @@ const getThingModelTSL = async () => {
loading.value = true loading.value = true
try { try {
const tslData = await ThingModelApi.getThingModelTSLByProductId(props.productId) const tslData = await exportThingModelTSL(props.productId)
if (tslData) { if (tslData) {
thingModelTSL.value = tslData thingModelTSL.value = tslData
@ -302,7 +301,7 @@ const parseThingModelData = () => {
} }
// //
if (tsl.properties && Array.isArray(tsl.properties)) { if (tsl.properties && Array.isArray(tsl.properties)) {
tsl.properties.forEach((prop) => { tsl.properties.forEach((prop: any) => {
properties.push({ properties.push({
identifier: prop.identifier, identifier: prop.identifier,
name: prop.name, name: prop.name,
@ -320,7 +319,7 @@ const parseThingModelData = () => {
// //
if (tsl.events && Array.isArray(tsl.events)) { if (tsl.events && Array.isArray(tsl.events)) {
tsl.events.forEach((event) => { tsl.events.forEach((event: any) => {
properties.push({ properties.push({
identifier: event.identifier, identifier: event.identifier,
name: event.name, name: event.name,
@ -337,7 +336,7 @@ const parseThingModelData = () => {
// //
if (tsl.services && Array.isArray(tsl.services)) { if (tsl.services && Array.isArray(tsl.services)) {
tsl.services.forEach((service) => { tsl.services.forEach((service: any) => {
properties.push({ properties.push({
identifier: service.identifier, identifier: service.identifier,
name: service.name, name: service.name,

View File

@ -1,8 +1,7 @@
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 { DICT_TYPE } from '@vben/constants'; import { DICT_TYPE, getDictOptions } from '#/utils';
import { getDictOptions } from '@vben/hooks';
/** 列表的搜索表单 */ /** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] { export function useGridFormSchema(): VbenFormSchema[] {

View File

@ -1,14 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VxeGridInstance } from '#/adapter/vxe-table'; import { onMounted, provide, ref } from 'vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteThingModel, getThingModelPage } from '#/api/iot/thingmodel'; import { deleteThingModel, getThingModelPage } from '#/api/iot/thingmodel';
import { getProduct } from '#/api/iot/product/product';
import type { IotProductApi } from '#/api/iot/product/product';
import { useGridColumns, useGridFormSchema } from './data'; import { useGridColumns, useGridFormSchema } from './data';
import { getDataTypeOptionsLabel } from '../utils/constants'; import { getDataTypeOptionsLabel, IOT_PROVIDE_KEY } from '../utils/constants';
import ThingModelForm from './modules/ThingModelForm.vue';
import ThingModelTSL from './modules/ThingModelTSL.vue';
defineOptions({ name: 'IoTThingModel' }); defineOptions({ name: 'IoTThingModel' });
@ -16,17 +20,27 @@ const props = defineProps<{
productId: number; productId: number;
}>(); }>();
//
const product = ref<IotProductApi.Product>({} as IotProductApi.Product);
//
provide(IOT_PROVIDE_KEY.PRODUCT, product);
//
const thingModelFormRef = ref();
const thingModelTSLRef = ref();
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: { gridOptions: {
columns: useGridColumns(), columns: useGridColumns(),
proxyConfig: { proxyConfig: {
ajax: { ajax: {
query: async ({ page, form }) => { query: async ({ page }: any, formValues: any) => {
return await getThingModelPage({ return await getThingModelPage({
pageNo: page.currentPage, pageNo: page.currentPage,
pageSize: page.pageSize, pageSize: page.pageSize,
productId: props.productId, productId: props.productId,
...form, ...formValues,
}); });
}, },
}, },
@ -37,20 +51,14 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
}); });
const xGrid = shallowRef<VxeGridInstance>();
const formRef = ref();
const tslRef = ref();
// //
const handleCreate = () => { const handleCreate = () => {
// TODO: thingModelFormRef.value?.open('create');
console.log('新增功能');
}; };
// //
const handleEdit = (row: any) => { const handleEdit = (row: any) => {
// TODO: thingModelFormRef.value?.open('update', row.id);
console.log('编辑功能:', row);
}; };
// //
@ -66,30 +74,54 @@ const handleDelete = async (row: any) => {
// TSL // TSL
const handleOpenTSL = () => { const handleOpenTSL = () => {
// TODO: TSL thingModelTSLRef.value?.open();
console.log('打开 TSL');
}; };
// //
const getDataTypeLabel = (row: any) => { const getDataTypeLabel = (row: any) => {
return getDataTypeOptionsLabel(row.property?.dataType) || '-'; return getDataTypeOptionsLabel(row.property?.dataType) || '-';
}; };
//
const handleRefresh = () => {
gridApi.reload();
};
//
const getProductData = async () => {
try {
product.value = await getProduct(props.productId);
} catch (error) {
console.error('获取产品信息失败:', error);
}
};
//
onMounted(async () => {
await getProductData();
});
</script> </script>
<template> <template>
<Page <Page>
description="管理产品的物模型定义,包括属性、服务和事件"
title="物模型管理"
>
<Grid ref="xGrid"> <Grid ref="xGrid">
<template #toolbar-tools> <template #toolbar-tools>
<VbenButton @click="handleCreate"> <TableAction
<Icon icon="ant-design:plus-outlined" class="mr-1" /> :actions="[
添加功能 {
</VbenButton> label: '添加功能',
<VbenButton type="success" @click="handleOpenTSL"> type: 'primary',
TSL icon: ACTION_ICON.ADD,
</VbenButton> onClick: handleCreate,
},
{
label: 'TSL',
type: 'default',
color: 'success',
onClick: handleOpenTSL,
},
]"
/>
</template> </template>
<!-- 数据类型列 --> <!-- 数据类型列 -->
@ -98,20 +130,40 @@ const getDataTypeLabel = (row: any) => {
</template> </template>
<!-- 数据定义列 --> <!-- 数据定义列 -->
<template #dataDefinition="{ row }"> <template #dataDefinition>
<!-- TODO: 实现数据定义组件 --> <!-- TODO: 实现数据定义组件 -->
<span class="text-gray-400">-</span> <span class="text-gray-400">-</span>
</template> </template>
<!-- 操作列 --> <!-- 操作列 -->
<template #actions="{ row }"> <template #actions="{ row }">
<VbenButton size="small" type="primary" @click="handleEdit(row)"> <TableAction
编辑 :actions="[
</VbenButton> {
<VbenButton size="small" type="danger" @click="handleDelete(row)"> label: '编辑',
删除 type: 'link',
</VbenButton> icon: ACTION_ICON.EDIT,
onClick: handleEdit.bind(null, row),
},
{
label: '删除',
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
popConfirm: {
title: '确认删除该功能吗?',
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template> </template>
</Grid> </Grid>
<!-- 物模型表单 -->
<ThingModelForm ref="thingModelFormRef" @success="handleRefresh" />
<!-- TSL 弹窗 -->
<ThingModelTSL ref="thingModelTSLRef" />
</Page> </Page>
</template> </template>

View File

@ -1,32 +1,29 @@
<!-- 产品的物模型表单event --> <!-- 产品的物模型表单event -->
<template> <template>
<el-form-item <a-form-item label="事件类型">
:rules="[{ required: true, message: '请选择事件类型', trigger: 'change' }]" <a-radio-group v-model:value="thingModelEvent.type">
label="事件类型" <a-radio
prop="event.type"
>
<el-radio-group v-model="thingModelEvent.type">
<el-radio
v-for="eventType in Object.values(IoTThingModelEventTypeEnum)" v-for="eventType in Object.values(IoTThingModelEventTypeEnum)"
:key="eventType.value" :key="eventType.value"
:value="eventType.value" :value="eventType.value"
> >
{{ eventType.label }} {{ eventType.label }}
</el-radio> </a-radio>
</el-radio-group> </a-radio-group>
</el-form-item> </a-form-item>
<el-form-item label="输出参数"> <a-form-item label="输出参数">
<ThingModelInputOutputParam <ThingModelInputOutputParam
v-model="thingModelEvent.outputParams" v-model="thingModelEvent.outputData!"
:direction="IoTThingModelParamDirectionEnum.OUTPUT" :direction="IoTThingModelParamDirectionEnum.OUTPUT"
/> />
</el-form-item> </a-form-item>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { watch, type Ref } from 'vue'
import ThingModelInputOutputParam from './ThingModelInputOutputParam.vue' import ThingModelInputOutputParam from './ThingModelInputOutputParam.vue'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { ThingModelEvent } from '#/api/iot/thingmodel' import type { ThingModelEvent } from '#/api/iot/thingmodel'
import { isEmpty } from '@vben/utils' import { isEmpty } from '@vben/utils'
import { import {
IoTThingModelEventTypeEnum, IoTThingModelEventTypeEnum,
@ -43,15 +40,18 @@ const thingModelEvent = useVModel(props, 'modelValue', emits) as Ref<ThingModelE
// INFO // INFO
watch( watch(
() => thingModelEvent.value.type, () => thingModelEvent.value.type,
(val: string) => (val: string | undefined) => {
isEmpty(val) && (thingModelEvent.value.type = IoTThingModelEventTypeEnum.INFO.value), if (isEmpty(val)) {
thingModelEvent.value.type = IoTThingModelEventTypeEnum.INFO.value
}
},
{ immediate: true } { immediate: true }
) )
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.el-form-item) { :deep(.a-form-item) {
.el-form-item { .a-form-item {
margin-bottom: 0; margin-bottom: 0;
} }
} }

View File

@ -1,29 +1,32 @@
<!-- 产品的物模型表单 --> <!-- 产品的物模型表单 -->
<template> <template>
<Dialog v-model="dialogVisible" :title="dialogTitle"> <a-modal
v-model:open="dialogVisible"
:title="dialogTitle"
:confirm-loading="formLoading"
@ok="submitForm"
>
<a-form <a-form
ref="formRef" ref="formRef"
:loading="formLoading"
:model="formData" :model="formData"
:rules="ThingModelFormRules"
:label-col="{ span: 6 }" :label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }" :wrapper-col="{ span: 18 }"
> >
<a-form-item label="功能类型" name="type"> <a-form-item label="功能类型">
<a-radio-group v-model:value="formData.type"> <a-radio-group v-model:value="formData.type">
<a-radio-button <a-radio-button
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_THING_MODEL_TYPE)" v-for="dict in getDictOptions(DICT_TYPE.IOT_THING_MODEL_TYPE)"
:key="dict.value" :key="dict.value"
:value="dict.value" :value="Number(dict.value)"
> >
{{ dict.label }} {{ dict.label }}
</a-radio-button> </a-radio-button>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<a-form-item label="功能名称" name="name"> <a-form-item label="功能名称">
<a-input v-model:value="formData.name" placeholder="请输入功能名称" /> <a-input v-model:value="formData.name" placeholder="请输入功能名称" />
</a-form-item> </a-form-item>
<a-form-item label="标识符" name="identifier"> <a-form-item label="标识符">
<a-input v-model:value="formData.identifier" placeholder="请输入标识符" /> <a-input v-model:value="formData.identifier" placeholder="请输入标识符" />
</a-form-item> </a-form-item>
<!-- 属性配置 --> <!-- 属性配置 -->
@ -41,48 +44,45 @@
v-if="formData.type === IoTThingModelTypeEnum.EVENT" v-if="formData.type === IoTThingModelTypeEnum.EVENT"
v-model="formData.event" v-model="formData.event"
/> />
<a-form-item label="描述" name="description"> <a-form-item label="描述">
<a-textarea <a-textarea
v-model:value="formData.description" v-model:value="formData.desc"
:maxlength="200" :maxlength="200"
:rows="3" :rows="3"
placeholder="请输入属性描述" placeholder="请输入属性描述"
/> />
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-modal>
<template #footer>
<a-button :disabled="formLoading" type="primary" @click="submitForm"> </a-button>
<a-button @click="dialogVisible = false"> </a-button>
</template>
</Dialog>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, inject, type Ref } from 'vue' import { ref, inject, type Ref } from 'vue'
import { useI18n } from 'vue-i18n' import { $t } from '@vben/locales'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import type { ProductVO } from '#/api/iot/product/product' import type { ProductVO } from '#/api/iot/product/product'
import ThingModelProperty from './ThingModelProperty.vue' import ThingModelProperty from './ThingModelProperty.vue'
import ThingModelService from './ThingModelService.vue' import ThingModelService from './ThingModelService.vue'
import ThingModelEvent from './ThingModelEvent.vue' import ThingModelEvent from './ThingModelEvent.vue'
import { ThingModelApi, ThingModelFormRules } from '#/api/iot/thingmodel' import { ThingModelApi } from '#/api/iot/thingmodel'
import type { ThingModelData } from '#/api/iot/thingmodel' import type { ThingModelData } from '#/api/iot/thingmodel'
import { import {
IOT_PROVIDE_KEY, IOT_PROVIDE_KEY,
IoTDataSpecsDataTypeEnum, IoTDataSpecsDataTypeEnum,
IoTThingModelTypeEnum IoTThingModelTypeEnum
} from '#/views/iot/utils/constants' } from '#/views/iot/utils/constants'
import { cloneDeep } from 'lodash-es' import { DICT_TYPE, getDictOptions } from '#/utils'
import { DICT_TYPE, getIntDictOptions } from '@vben/constants'
//
const cloneDeep = <T>(obj: T): T => {
return JSON.parse(JSON.stringify(obj))
}
/** IoT 物模型数据表单 */ /** IoT 物模型数据表单 */
defineOptions({ name: 'IoTThingModelForm' }) defineOptions({ name: 'IoTThingModelForm' })
const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) // const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) //
const { t } = useI18n() //
const dialogVisible = ref(false) // const dialogVisible = ref(false) //
const dialogTitle = ref('') // const dialogTitle = ref('') //
const formLoading = ref(false) // 12 const formLoading = ref(false) // 12
@ -96,22 +96,56 @@ const formData = ref<ThingModelData>({
dataType: IoTDataSpecsDataTypeEnum.INT dataType: IoTDataSpecsDataTypeEnum.INT
} }
}, },
service: {}, service: {
event: {} inputData: [],
outputData: []
},
event: {
outputData: []
}
} as ThingModelData) } as ThingModelData)
const formRef = ref() // Ref const formRef = ref() // Ref
/** 重置表单 */
const resetForm = () => {
formData.value = {
type: IoTThingModelTypeEnum.PROPERTY,
dataType: IoTDataSpecsDataTypeEnum.INT,
property: {
dataType: IoTDataSpecsDataTypeEnum.INT,
dataSpecs: {
dataType: IoTDataSpecsDataTypeEnum.INT
}
},
service: {
inputData: [],
outputData: []
},
event: {
outputData: []
}
} as ThingModelData
formRef.value?.resetFields()
}
/** 打开弹窗 */ /** 打开弹窗 */
const open = async (type: string, id?: number) => { const open = async (type: string, id?: number) => {
dialogVisible.value = true dialogVisible.value = true
dialogTitle.value = t('action.' + type) dialogTitle.value = type === 'create'
? $t('ui.actionTitle.create', ['物模型功能'])
: $t('ui.actionTitle.edit', ['物模型功能'])
formType.value = type formType.value = type
resetForm() resetForm()
if (id) { if (id) {
formLoading.value = true formLoading.value = true
try { try {
formData.value = await ThingModelApi.getThingModel(id) const result = await ThingModelApi.getThingModel(id)
// Convert ThingModel to ThingModelData
formData.value = {
...result,
type: Number(result.type)
} as ThingModelData
// //
if (!formData.value.property || Object.keys(formData.value.property).length === 0) { if (!formData.value.property || Object.keys(formData.value.property).length === 0) {
formData.value.dataType = IoTDataSpecsDataTypeEnum.INT formData.value.dataType = IoTDataSpecsDataTypeEnum.INT
@ -124,11 +158,29 @@ const open = async (type: string, id?: number) => {
} }
// //
if (!formData.value.service || Object.keys(formData.value.service).length === 0) { if (!formData.value.service || Object.keys(formData.value.service).length === 0) {
formData.value.service = {} formData.value.service = {
inputData: [],
outputData: []
}
} else {
//
if (!formData.value.service.inputData) {
formData.value.service.inputData = []
}
if (!formData.value.service.outputData) {
formData.value.service.outputData = []
}
} }
// //
if (!formData.value.event || Object.keys(formData.value.event).length === 0) { if (!formData.value.event || Object.keys(formData.value.event).length === 0) {
formData.value.event = {} formData.value.event = {
outputData: []
}
} else {
//
if (!formData.value.event.outputData) {
formData.value.event.outputData = []
}
} }
} finally { } finally {
formLoading.value = false formLoading.value = false
@ -140,7 +192,83 @@ defineExpose({ open, close: () => (dialogVisible.value = false) })
/** 提交表单 */ /** 提交表单 */
const emit = defineEmits(['success']) const emit = defineEmits(['success'])
const submitForm = async () => { const submitForm = async () => {
await formRef.value.validate() //
//
if (!formData.value.type) {
message.error('请选择功能类型')
return
}
if (!formData.value.name) {
message.error('请输入功能名称')
return
}
if (!formData.value.identifier) {
message.error('请输入标识符')
return
}
//
if (formData.value.type === IoTThingModelTypeEnum.PROPERTY) {
//
if (!formData.value.property?.dataType) {
message.error('请选择数据类型')
return
}
//
if ([IoTDataSpecsDataTypeEnum.INT, IoTDataSpecsDataTypeEnum.DOUBLE, IoTDataSpecsDataTypeEnum.FLOAT].includes(formData.value.property.dataType as any)) {
const specs = formData.value.property.dataSpecs
if (!specs || specs.min === undefined || specs.min === '') {
message.error('最小值不能为空')
return
}
if (!specs || specs.max === undefined || specs.max === '') {
message.error('最大值不能为空')
return
}
if (!specs || specs.step === undefined || specs.step === '') {
message.error('步长不能为空')
return
}
if (!specs || !specs.unit) {
message.error('请选择单位')
return
}
}
//
if (formData.value.property.dataType === IoTDataSpecsDataTypeEnum.ENUM) {
if (!formData.value.property.dataSpecsList || formData.value.property.dataSpecsList.length === 0) {
message.error('请配置枚举项')
return
}
for (const item of formData.value.property.dataSpecsList) {
if (!item.name) {
message.error('枚举描述不能为空')
return
}
if (item.value === undefined || item.value === '') {
message.error('枚举值不能为空')
return
}
}
}
//
if (formData.value.property.dataType === IoTDataSpecsDataTypeEnum.BOOL) {
if (!formData.value.property.dataSpecsList || formData.value.property.dataSpecsList.length !== 2) {
message.error('请配置布尔值')
return
}
for (const item of formData.value.property.dataSpecsList) {
if (!item.name) {
message.error('布尔值描述不能为空')
return
}
}
}
}
formLoading.value = true formLoading.value = true
try { try {
const data = cloneDeep(formData.value) as ThingModelData const data = cloneDeep(formData.value) as ThingModelData
@ -148,12 +276,16 @@ const submitForm = async () => {
data.productId = product!.value.id data.productId = product!.value.id
data.productKey = product!.value.productKey data.productKey = product!.value.productKey
fillExtraAttributes(data) fillExtraAttributes(data)
//
// console.log(':', data)
if (formType.value === 'create') { if (formType.value === 'create') {
await ThingModelApi.createThingModel(data) await ThingModelApi.createThingModel(data)
message.success({ content: t('common.createSuccess') }) message.success('创建成功')
} else { } else {
await ThingModelApi.updateThingModel(data) await ThingModelApi.updateThingModel(data)
message.success({ content: t('common.updateSuccess') }) message.success('更新成功')
} }
// //
dialogVisible.value = false dialogVisible.value = false
@ -180,6 +312,13 @@ const fillExtraAttributes = (data: any) => {
data.dataType = data.service.dataType data.dataType = data.service.dataType
data.service.identifier = data.identifier data.service.identifier = data.identifier
data.service.name = data.name data.service.name = data.name
//
if (!data.service.inputData || data.service.inputData.length === 0) {
delete data.service.inputData
}
if (!data.service.outputData || data.service.outputData.length === 0) {
delete data.service.outputData
}
delete data.property delete data.property
delete data.event delete data.event
} }
@ -189,6 +328,10 @@ const fillExtraAttributes = (data: any) => {
data.dataType = data.event.dataType data.dataType = data.event.dataType
data.event.identifier = data.identifier data.event.identifier = data.identifier
data.event.name = data.name data.event.name = data.name
//
if (!data.event.outputData || data.event.outputData.length === 0) {
delete data.event.outputData
}
delete data.property delete data.property
delete data.service delete data.service
} }
@ -203,21 +346,4 @@ const removeDataSpecs = (val: any) => {
delete val.dataSpecsList delete val.dataSpecsList
} }
} }
/** 重置表单 */
const resetForm = () => {
formData.value = {
type: IoTThingModelTypeEnum.PROPERTY,
dataType: IoTDataSpecsDataTypeEnum.INT,
property: {
dataType: IoTDataSpecsDataTypeEnum.INT,
dataSpecs: {
dataType: IoTDataSpecsDataTypeEnum.INT
}
},
service: {},
event: {}
} as ThingModelData
formRef.value?.resetFields()
}
</script> </script>

View File

@ -7,51 +7,52 @@
> >
<span>参数名称{{ item.name }}</span> <span>参数名称{{ item.name }}</span>
<div class="btn"> <div class="btn">
<el-button link type="primary" @click="openParamForm(item)"></el-button> <a-button type="link" @click="openParamForm(item)"></a-button>
<el-divider direction="vertical" /> <a-divider type="vertical" />
<el-button link type="danger" @click="deleteParamItem(index)"></el-button> <a-button type="link" danger @click="deleteParamItem(index)"></a-button>
</div> </div>
</div> </div>
<el-button link type="primary" @click="openParamForm(null)">+</el-button> <a-button type="link" @click="openParamForm(null)">+</a-button>
<!-- param 表单 --> <!-- param 表单 -->
<Dialog v-model="dialogVisible" title="新增参数" append-to-body> <a-modal
<el-form v-model:open="dialogVisible"
title="新增参数"
:confirm-loading="formLoading"
@ok="submitForm"
@cancel="dialogVisible = false"
>
<a-form
ref="paramFormRef" ref="paramFormRef"
v-loading="formLoading"
:model="formData" :model="formData"
:rules="ThingModelFormRules" :label-col="{ span: 6 }"
label-width="100px" :wrapper-col="{ span: 18 }"
> >
<el-form-item label="参数名称" prop="name"> <a-form-item label="参数名称">
<el-input v-model="formData.name" placeholder="请输入功能名称" /> <a-input v-model:value="formData.name" placeholder="请输入功能名称" />
</el-form-item> </a-form-item>
<el-form-item label="标识符" prop="identifier"> <a-form-item label="标识符">
<el-input v-model="formData.identifier" placeholder="请输入标识符" /> <a-input v-model:value="formData.identifier" placeholder="请输入标识符" />
</el-form-item> </a-form-item>
<!-- 属性配置 --> <!-- 属性配置 -->
<ThingModelProperty v-model="formData.property" is-params /> <ThingModelProperty v-model="formData.property" is-params />
</el-form> </a-form>
<template #footer> </a-modal>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, unref } from 'vue'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import ThingModelProperty from './ThingModelProperty.vue' import ThingModelProperty from './ThingModelProperty.vue'
import { isEmpty } from '@vben/utils' import { isEmpty } from '@vben/utils'
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants' import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants'
import { ThingModelFormRules } from '#/api/iot/thingmodel'
/** 输入输出参数配置组件 */ /** 输入输出参数配置组件 */
defineOptions({ name: 'ThingModelInputOutputParam' }) defineOptions({ name: 'ThingModelInputOutputParam' })
const props = defineProps<{ modelValue: any; direction: string }>() const props = defineProps<{ modelValue: any[] | undefined; direction: string }>()
const emits = defineEmits(['update:modelValue']) const emits = defineEmits(['update:modelValue'])
const thingModelParams = useVModel(props, 'modelValue', emits) as Ref<any[]> const thingModelParams = useVModel(props, 'modelValue', emits)
const dialogVisible = ref(false) // const dialogVisible = ref(false) //
const formLoading = ref(false) // 12 const formLoading = ref(false) // 12
const paramFormRef = ref() // ref const paramFormRef = ref() // ref
@ -69,25 +70,26 @@ const formData = ref<any>({
const openParamForm = (val: any) => { const openParamForm = (val: any) => {
dialogVisible.value = true dialogVisible.value = true
resetForm() resetForm()
if (isEmpty(val)) { if (isEmpty(val) || !val) {
return return
} }
// //
const paramData = val as any
formData.value = { formData.value = {
identifier: val.identifier, identifier: paramData.identifier,
name: val.name, name: paramData.name,
description: val.description, description: paramData.description,
property: { property: {
dataType: val.dataType, dataType: paramData.dataType,
dataSpecs: val.dataSpecs, dataSpecs: paramData.dataSpecs,
dataSpecsList: val.dataSpecsList dataSpecsList: paramData.dataSpecsList
} }
} }
} }
/** 删除 param 项 */ /** 删除 param 项 */
const deleteParamItem = (index: number) => { const deleteParamItem = (index: number) => {
thingModelParams.value.splice(index, 1) thingModelParams.value?.splice(index, 1)
} }
/** 添加参数 */ /** 添加参数 */
@ -96,8 +98,13 @@ const submitForm = async () => {
if (isEmpty(thingModelParams.value)) { if (isEmpty(thingModelParams.value)) {
thingModelParams.value = [] thingModelParams.value = []
} }
// // - 使
await paramFormRef.value.validate() if (!formData.value.name) {
return
}
if (!formData.value.identifier) {
return
}
try { try {
// //
const data = unref(formData) const data = unref(formData)
@ -116,14 +123,20 @@ const submitForm = async () => {
} }
// identifier // identifier
if (!thingModelParams.value) {
thingModelParams.value = []
}
const existingIndex = thingModelParams.value.findIndex( const existingIndex = thingModelParams.value.findIndex(
(spec) => spec.identifier === data.identifier (spec: any) => spec.identifier === data.identifier
) )
if (existingIndex > -1) { if (existingIndex > -1) {
thingModelParams.value[existingIndex] = item thingModelParams.value[existingIndex] = item
} else { } else {
thingModelParams.value.push(item) thingModelParams.value.push(item)
} }
//
console.log('添加参数后的列表:', thingModelParams.value)
} finally { } finally {
dialogVisible.value = false dialogVisible.value = false
} }

View File

@ -1,20 +1,17 @@
<!-- 产品的物模型表单property --> <!-- 产品的物模型表单property -->
<template> <template>
<el-form-item <a-form-item label="数据类型">
:rules="[{ required: true, message: '请选择数据类型', trigger: 'change' }]" <a-select v-model:value="property.dataType" placeholder="请选择数据类型" @change="handleChange">
label="数据类型"
prop="property.dataType"
>
<el-select v-model="property.dataType" placeholder="请选择数据类型" @change="handleChange">
<!-- ARRAY STRUCT 类型数据相互嵌套时最多支持递归嵌套 2 父和子 --> <!-- ARRAY STRUCT 类型数据相互嵌套时最多支持递归嵌套 2 父和子 -->
<el-option <a-select-option
v-for="option in getDataTypeOptions2" v-for="option in getDataTypeOptions2"
:key="option.value" :key="option.value"
:label="`${option.value}(${option.label})`"
:value="option.value" :value="option.value"
/> >
</el-select> {{ `${option.value}(${option.label})` }}
</el-form-item> </a-select-option>
</a-select>
</a-form-item>
<!-- 数值型配置 --> <!-- 数值型配置 -->
<ThingModelNumberDataSpecs <ThingModelNumberDataSpecs
v-if=" v-if="
@ -22,56 +19,47 @@
IoTDataSpecsDataTypeEnum.INT, IoTDataSpecsDataTypeEnum.INT,
IoTDataSpecsDataTypeEnum.DOUBLE, IoTDataSpecsDataTypeEnum.DOUBLE,
IoTDataSpecsDataTypeEnum.FLOAT IoTDataSpecsDataTypeEnum.FLOAT
].includes(property.dataType || '') ].includes(property.dataType as any)
" "
v-model="property.dataSpecs" v-model="property.dataSpecs"
/> />
<!-- 枚举型配置 --> <!-- 枚举型配置 -->
<ThingModelEnumDataSpecs <ThingModelEnumDataSpecs
v-if="property.dataType === IoTDataSpecsDataTypeEnum.ENUM" v-if="property.dataType === IoTDataSpecsDataTypeEnum.ENUM"
v-model="property.dataSpecsList" v-model="property.dataSpecsList!"
/> />
<!-- 布尔型配置 --> <!-- 布尔型配置 -->
<el-form-item v-if="property.dataType === IoTDataSpecsDataTypeEnum.BOOL" label="布尔值"> <a-form-item v-if="property.dataType === IoTDataSpecsDataTypeEnum.BOOL" label="布尔值">
<template v-for="(item, index) in property.dataSpecsList" :key="item.value"> <template v-for="(item, index) in property.dataSpecsList" :key="item.value">
<div class="flex items-center justify-start w-1/1 mb-5px"> <div class="flex items-center justify-start w-1/1 mb-5px">
<span>{{ item.value }}</span> <span>{{ item.value }}</span>
<span class="mx-2">-</span> <span class="mx-2">-</span>
<el-form-item <div class="flex-1">
:prop="`property.dataSpecsList[${index}].name`" <a-input
:rules="[ v-model:value="item.name"
{ required: true, message: '枚举描述不能为空' },
{ validator: validateBoolName, trigger: 'blur' }
]"
class="flex-1 mb-0"
>
<el-input
v-model="item.name"
:placeholder="`如:${item.value === 0 ? '关' : '开'}`" :placeholder="`如:${item.value === 0 ? '关' : '开'}`"
class="w-255px!" class="w-255px!"
/> />
</el-form-item> </div>
</div> </div>
</template> </template>
</el-form-item> </a-form-item>
<!-- 文本型配置 --> <!-- 文本型配置 -->
<el-form-item <a-form-item
v-if="property.dataType === IoTDataSpecsDataTypeEnum.TEXT" v-if="property.dataType === IoTDataSpecsDataTypeEnum.TEXT"
label="数据长度" label="数据长度"
prop="property.dataSpecs.length"
> >
<el-input v-model="property.dataSpecs.length" class="w-255px!" placeholder="请输入文本字节长度"> <a-input v-model:value="property.dataSpecs.length" class="w-255px!" placeholder="请输入文本字节长度">
<template #append>字节</template> <template #addonAfter>字节</template>
</el-input> </a-input>
</el-form-item> </a-form-item>
<!-- 时间型配置 --> <!-- 时间型配置 -->
<el-form-item <a-form-item
v-if="property.dataType === IoTDataSpecsDataTypeEnum.DATE" v-if="property.dataType === IoTDataSpecsDataTypeEnum.DATE"
label="时间格式" label="时间格式"
prop="date"
> >
<el-input class="w-255px!" disabled placeholder="String 类型的 UTC 时间戳(毫秒)" /> <a-input class="w-255px!" disabled placeholder="String 类型的 UTC 时间戳(毫秒)" />
</el-form-item> </a-form-item>
<!-- 数组型配置--> <!-- 数组型配置-->
<ThingModelArrayDataSpecs <ThingModelArrayDataSpecs
v-if="property.dataType === IoTDataSpecsDataTypeEnum.ARRAY" v-if="property.dataType === IoTDataSpecsDataTypeEnum.ARRAY"
@ -82,28 +70,30 @@
v-if="property.dataType === IoTDataSpecsDataTypeEnum.STRUCT" v-if="property.dataType === IoTDataSpecsDataTypeEnum.STRUCT"
v-model="property.dataSpecsList" v-model="property.dataSpecsList"
/> />
<el-form-item v-if="!isStructDataSpecs && !isParams" label="读写类型" prop="property.accessMode"> <a-form-item v-if="!isStructDataSpecs && !isParams" label="读写类型">
<el-radio-group v-model="property.accessMode"> <a-radio-group v-model:value="property.accessMode">
<el-radio <a-radio
v-for="accessMode in Object.values(IoTThingModelAccessModeEnum)" v-for="accessMode in Object.values(IoTThingModelAccessModeEnum)"
:key="accessMode.value" :key="accessMode.value"
:label="accessMode.value" :value="accessMode.value"
> >
{{ accessMode.label }} {{ accessMode.label }}
</el-radio> </a-radio>
</el-radio-group> </a-radio-group>
</el-form-item> </a-form-item>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { computed, watch, type Ref } from 'vue'
import { import {
ThingModelArrayDataSpecs, ThingModelArrayDataSpecs,
ThingModelEnumDataSpecs, ThingModelEnumDataSpecs,
ThingModelNumberDataSpecs, ThingModelNumberDataSpecs,
ThingModelStructDataSpecs ThingModelStructDataSpecs
} from './dataSpecs' } from './dataSpecs'
import { ThingModelProperty, validateBoolName } from '#/api/iot/thingmodel' import type { ThingModelProperty } from '#/api/iot/thingmodel'
import { validateBoolName } from '#/api/iot/thingmodel'
import { isEmpty } from '@vben/utils' import { isEmpty } from '@vben/utils'
import { import {
getDataTypeOptions, getDataTypeOptions,
@ -158,19 +148,21 @@ const handleChange = (dataType: any) => {
/** 默认选中读写 */ /** 默认选中读写 */
watch( watch(
() => property.value.accessMode, () => property.value.accessMode,
(val: string) => { (val: string | undefined) => {
if (props.isStructDataSpecs || props.isParams) { if (props.isStructDataSpecs || props.isParams) {
return return
} }
isEmpty(val) && (property.value.accessMode = IoTThingModelAccessModeEnum.READ_WRITE.value) if (isEmpty(val)) {
property.value.accessMode = IoTThingModelAccessModeEnum.READ_WRITE.value
}
}, },
{ immediate: true } { immediate: true }
) )
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.el-form-item) { :deep(.a-form-item) {
.el-form-item { .a-form-item {
margin-bottom: 0; margin-bottom: 0;
} }
} }

View File

@ -1,38 +1,35 @@
<!-- 产品的物模型表单service --> <!-- 产品的物模型表单service -->
<template> <template>
<el-form-item <a-form-item label="调用方式">
:rules="[{ required: true, message: '请选择调用方式', trigger: 'change' }]" <a-radio-group v-model:value="service.callType">
label="调用方式" <a-radio
prop="service.callType"
>
<el-radio-group v-model="service.callType">
<el-radio
v-for="callType in Object.values(IoTThingModelServiceCallTypeEnum)" v-for="callType in Object.values(IoTThingModelServiceCallTypeEnum)"
:key="callType.value" :key="callType.value"
:value="callType.value" :value="callType.value"
> >
{{ callType.label }} {{ callType.label }}
</el-radio> </a-radio>
</el-radio-group> </a-radio-group>
</el-form-item> </a-form-item>
<el-form-item label="输入参数"> <a-form-item label="输入参数">
<ThingModelInputOutputParam <ThingModelInputOutputParam
v-model="service.inputParams" v-model="service.inputData!"
:direction="IoTThingModelParamDirectionEnum.INPUT" :direction="IoTThingModelParamDirectionEnum.INPUT"
/> />
</el-form-item> </a-form-item>
<el-form-item label="输出参数"> <a-form-item label="输出参数">
<ThingModelInputOutputParam <ThingModelInputOutputParam
v-model="service.outputParams" v-model="service.outputData!"
:direction="IoTThingModelParamDirectionEnum.OUTPUT" :direction="IoTThingModelParamDirectionEnum.OUTPUT"
/> />
</el-form-item> </a-form-item>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { watch, type Ref } from 'vue'
import ThingModelInputOutputParam from './ThingModelInputOutputParam.vue' import ThingModelInputOutputParam from './ThingModelInputOutputParam.vue'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { ThingModelService } from '#/api/iot/thingmodel' import type { ThingModelService } from '#/api/iot/thingmodel'
import { isEmpty } from '@vben/utils' import { isEmpty } from '@vben/utils'
import { import {
IoTThingModelParamDirectionEnum, IoTThingModelParamDirectionEnum,
@ -49,15 +46,18 @@ const service = useVModel(props, 'modelValue', emits) as Ref<ThingModelService>
/** 默认选中ASYNC 异步 */ /** 默认选中ASYNC 异步 */
watch( watch(
() => service.value.callType, () => service.value.callType,
(val: string) => (val: string | undefined) => {
isEmpty(val) && (service.value.callType = IoTThingModelServiceCallTypeEnum.ASYNC.value), if (isEmpty(val)) {
service.value.callType = IoTThingModelServiceCallTypeEnum.ASYNC.value
}
},
{ immediate: true } { immediate: true }
) )
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.el-form-item) { :deep(.a-form-item) {
.el-form-item { .a-form-item {
margin-bottom: 0; margin-bottom: 0;
} }
} }

View File

@ -1,25 +1,35 @@
<template> <template>
<Dialog v-model="dialogVisible" :title="dialogTitle"> <a-modal
<JsonEditor v-model:open="dialogVisible"
v-model="thingModelTSL" :title="dialogTitle"
:mode="viewMode === 'editor' ? 'code' : 'view'" :footer="null"
height="600px" width="800px"
>
<div class="mb-4">
<a-radio-group v-model:value="viewMode" size="small">
<a-radio-button value="view">代码视图</a-radio-button>
<a-radio-button value="editor">编辑器视图</a-radio-button>
</a-radio-group>
</div>
<!-- 代码视图 - 只读展示 -->
<div v-if="viewMode === 'view'" class="json-viewer-container">
<pre class="json-code"><code>{{ formattedTSL }}</code></pre>
</div>
<!-- 编辑器视图 - 可编辑 -->
<a-textarea
v-else
v-model:value="tslString"
:rows="20"
placeholder="请输入 JSON 格式的物模型 TSL"
class="json-editor"
/> />
<template #footer> </a-modal>
<el-radio-group v-model="viewMode" size="small">
<el-radio-button label="code">代码视图</el-radio-button>
<el-radio-button label="editor">编辑器视图</el-radio-button>
</el-radio-group>
</template>
</Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import hljs from 'highlight.js' // import { ref, inject, computed, watch, type Ref } from 'vue'
import 'highlight.js/styles/github.css' // import { getThingModelTSL } from '#/api/iot/thingmodel'
import json from 'highlight.js/lib/languages/json' import type { ProductVO } from '#/api/iot/product/product'
import { ThingModelApi } from '#/api/iot/thingmodel'
import { ProductVO } from '#/api/iot/product/product'
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants' import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants'
defineOptions({ name: 'ThingModelTSL' }) defineOptions({ name: 'ThingModelTSL' })
@ -27,24 +37,69 @@ defineOptions({ name: 'ThingModelTSL' })
const dialogVisible = ref(false) // const dialogVisible = ref(false) //
const dialogTitle = ref('物模型 TSL') // const dialogTitle = ref('物模型 TSL') //
const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) // const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) //
const viewMode = ref('code') // code-editor- const viewMode = ref('view') // view-editor-
/** 打开弹窗 */ /** 打开弹窗 */
const open = () => { const open = async () => {
dialogVisible.value = true dialogVisible.value = true
await getTsl()
} }
defineExpose({ open }) defineExpose({ open })
/** 获取 TSL */ /** 获取 TSL */
const thingModelTSL = ref({}) const thingModelTSL = ref({})
const tslString = ref('') //
const getTsl = async () => { const getTsl = async () => {
thingModelTSL.value = await ThingModelApi.getThingModelTSLByProductId(product?.value?.id || 0) thingModelTSL.value = await getThingModelTSL(product?.value?.id || 0)
// JSON
tslString.value = JSON.stringify(thingModelTSL.value, null, 2)
} }
/** 初始化 **/ /** 格式化的 TSL 用于只读展示 */
onMounted(async () => { const formattedTSL = computed(() => {
// try {
hljs.registerLanguage('json', json) if (typeof thingModelTSL.value === 'string') {
await getTsl() return JSON.stringify(JSON.parse(thingModelTSL.value), null, 2)
}
return JSON.stringify(thingModelTSL.value, null, 2)
} catch {
return JSON.stringify(thingModelTSL.value, null, 2)
}
})
/** 监听编辑器内容变化,实时更新数据 */
watch(tslString, (newValue) => {
try {
thingModelTSL.value = JSON.parse(newValue)
} catch {
// JSON
}
}) })
</script> </script>
<style scoped>
.json-viewer-container {
background-color: #f5f5f5;
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 12px;
max-height: 600px;
overflow-y: auto;
}
.json-code {
margin: 0;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-size: 13px;
line-height: 1.5;
color: #333;
white-space: pre-wrap;
word-wrap: break-word;
}
.json-editor {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-size: 13px;
}
</style>

View File

@ -1,6 +1,6 @@
<template> <template>
<!-- 属性 --> <!-- 属性 -->
<template v-if="data.type === IoTThingModelTypeEnum.PROPERTY"> <template v-if="data.type === IoTThingModelTypeEnum.PROPERTY && data.property">
<!-- 非列表型数值 --> <!-- 非列表型数值 -->
<div <div
v-if=" v-if="
@ -8,14 +8,14 @@
IoTDataSpecsDataTypeEnum.INT, IoTDataSpecsDataTypeEnum.INT,
IoTDataSpecsDataTypeEnum.DOUBLE, IoTDataSpecsDataTypeEnum.DOUBLE,
IoTDataSpecsDataTypeEnum.FLOAT IoTDataSpecsDataTypeEnum.FLOAT
].includes(data.property.dataType) ].includes(data.property.dataType as any)
" "
> >
取值范围{{ `${data.property.dataSpecs.min}~${data.property.dataSpecs.max}` }} 取值范围{{ `${data.property.dataSpecs?.min}~${data.property.dataSpecs?.max}` }}
</div> </div>
<!-- 非列表型文本 --> <!-- 非列表型文本 -->
<div v-if="IoTDataSpecsDataTypeEnum.TEXT === data.property.dataType"> <div v-if="IoTDataSpecsDataTypeEnum.TEXT === data.property.dataType">
数据长度{{ data.property.dataSpecs.length }} 数据长度{{ data.property.dataSpecs?.length }}
</div> </div>
<!-- 列表型: 数组结构时间特殊 --> <!-- 列表型: 数组结构时间特殊 -->
<div <div
@ -24,7 +24,7 @@
IoTDataSpecsDataTypeEnum.ARRAY, IoTDataSpecsDataTypeEnum.ARRAY,
IoTDataSpecsDataTypeEnum.STRUCT, IoTDataSpecsDataTypeEnum.STRUCT,
IoTDataSpecsDataTypeEnum.DATE IoTDataSpecsDataTypeEnum.DATE
].includes(data.property.dataType) ].includes(data.property.dataType as any)
" "
> >
- -
@ -33,7 +33,7 @@
<div <div
v-if=" v-if="
[IoTDataSpecsDataTypeEnum.BOOL, IoTDataSpecsDataTypeEnum.ENUM].includes( [IoTDataSpecsDataTypeEnum.BOOL, IoTDataSpecsDataTypeEnum.ENUM].includes(
data.property.dataType data.property.dataType as any
) )
" "
> >
@ -46,17 +46,17 @@
</div> </div>
</template> </template>
<!-- 服务 --> <!-- 服务 -->
<div v-if="data.type === IoTThingModelTypeEnum.SERVICE"> <div v-if="data.type === IoTThingModelTypeEnum.SERVICE && data.service?.callType">
调用方式{{ getThingModelServiceCallTypeLabel(data.service!.callType) }} 调用方式{{ getThingModelServiceCallTypeLabel(data.service.callType) }}
</div> </div>
<!-- 事件 --> <!-- 事件 -->
<div v-if="data.type === IoTThingModelTypeEnum.EVENT"> <div v-if="data.type === IoTThingModelTypeEnum.EVENT && data.event?.type">
事件类型{{ getEventTypeLabel(data.event!.type) }} 事件类型{{ getEventTypeLabel(data.event.type) }}
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ThingModelData } from '#/api/iot/thingmodel' import type { ThingModelData } from '#/api/iot/thingmodel'
import { import {
getEventTypeLabel, getEventTypeLabel,
getThingModelServiceCallTypeLabel, getThingModelServiceCallTypeLabel,

View File

@ -1,9 +1,9 @@
<!-- dataTypearray 数组类型 --> <!-- dataTypearray 数组类型 -->
<template> <template>
<el-form-item label="元素类型" prop="property.dataSpecs.childDataType"> <a-form-item label="元素类型" name="property.dataSpecs.childDataType">
<el-radio-group v-model="dataSpecs.childDataType" @change="handleChange"> <a-radio-group v-model:value="dataSpecs.childDataType" @change="handleChange">
<template v-for="item in getDataTypeOptions()" :key="item.value"> <template v-for="item in getDataTypeOptions()" :key="item.value">
<el-radio <a-radio
v-if=" v-if="
!( !(
[ [
@ -17,13 +17,13 @@
class="w-1/3" class="w-1/3"
> >
{{ `${item.value}(${item.label})` }} {{ `${item.value}(${item.label})` }}
</el-radio> </a-radio>
</template> </template>
</el-radio-group> </a-radio-group>
</el-form-item> </a-form-item>
<el-form-item label="元素个数" prop="property.dataSpecs.size"> <a-form-item label="元素个数" name="property.dataSpecs.size">
<el-input v-model="dataSpecs.size" placeholder="请输入数组中的元素个数" /> <a-input v-model:value="dataSpecs.size" placeholder="请输入数组中的元素个数" />
</el-form-item> </a-form-item>
<!-- Struct 型配置--> <!-- Struct 型配置-->
<ThingModelStructDataSpecs <ThingModelStructDataSpecs
v-if="dataSpecs.childDataType === IoTDataSpecsDataTypeEnum.STRUCT" v-if="dataSpecs.childDataType === IoTDataSpecsDataTypeEnum.STRUCT"
@ -32,6 +32,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { Ref } from 'vue'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import ThingModelStructDataSpecs from './ThingModelStructDataSpecs.vue' import ThingModelStructDataSpecs from './ThingModelStructDataSpecs.vue'
import { getDataTypeOptions, IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants' import { getDataTypeOptions, IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants'

View File

@ -1,9 +1,6 @@
<!-- dataTypeenum 数组类型 --> <!-- dataTypeenum 数组类型 -->
<template> <template>
<el-form-item <a-form-item label="枚举项">
:rules="[{ required: true, validator: validateEnumList, trigger: 'change' }]"
label="枚举项"
>
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex items-center"> <div class="flex items-center">
<span class="flex-1"> 参数值 </span> <span class="flex-1"> 参数值 </span>
@ -14,46 +11,33 @@
:key="index" :key="index"
class="flex items-center justify-between mb-5px" class="flex items-center justify-between mb-5px"
> >
<el-form-item <div class="flex-1">
:prop="`property.dataSpecsList[${index}].value`" <a-input v-model:value="item.value" placeholder="请输入枚举值,如'0'" />
:rules="[ </div>
{ required: true, message: '枚举值不能为空' },
{ validator: validateEnumValue, trigger: 'blur' }
]"
class="flex-1 mb-0"
>
<el-input v-model="item.value" placeholder="请输入枚举值,如'0'" />
</el-form-item>
<span class="mx-2">~</span> <span class="mx-2">~</span>
<el-form-item <div class="flex-1">
:prop="`property.dataSpecsList[${index}].name`" <a-input v-model:value="item.name" placeholder="对该枚举项的描述" />
:rules="[ </div>
{ required: true, message: '枚举描述不能为空' }, <a-button class="ml-10px" type="link" @click="deleteEnum(index)"></a-button>
{ validator: validateEnumName, trigger: 'blur' }
]"
class="flex-1 mb-0"
>
<el-input v-model="item.name" placeholder="对该枚举项的描述" />
</el-form-item>
<el-button class="ml-10px" link type="primary" @click="deleteEnum(index)"></el-button>
</div> </div>
<el-button link type="primary" @click="addEnum">+</el-button> <a-button type="link" @click="addEnum">+</a-button>
</div> </div>
</el-form-item> </a-form-item>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { Ref } from 'vue'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { isEmpty } from '@vben/utils' import { message } from 'ant-design-vue'
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants' import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants'
import { DataSpecsEnumOrBoolData } from '#/api/iot/thingmodel' import type { DataSpecsEnumOrBoolData } from '#/api/iot/thingmodel'
/** 枚举型的 dataSpecs 配置组件 */ /** 枚举型的 dataSpecs 配置组件 */
defineOptions({ name: 'ThingModelEnumDataSpecs' }) defineOptions({ name: 'ThingModelEnumDataSpecs' })
const props = defineProps<{ modelValue: any }>() const props = defineProps<{ modelValue: DataSpecsEnumOrBoolData[] }>()
const emits = defineEmits(['update:modelValue']) const emits = defineEmits(['update:modelValue'])
const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<DataSpecsEnumOrBoolData[]> const dataSpecsList: Ref<DataSpecsEnumOrBoolData[]> = useVModel(props, 'modelValue', emits)
/** 添加枚举项 */ /** 添加枚举项 */
@ -62,7 +46,7 @@ const addEnum = () => {
dataType: IoTDataSpecsDataTypeEnum.ENUM, dataType: IoTDataSpecsDataTypeEnum.ENUM,
name: '', // name: '', //
value: undefined // value: undefined //
}) } as any)
} }
/** 删除枚举项 */ /** 删除枚举项 */
@ -73,87 +57,11 @@ const deleteEnum = (index: number) => {
} }
dataSpecsList.value.splice(index, 1) dataSpecsList.value.splice(index, 1)
} }
/** 校验枚举值 */
const validateEnumValue = (_: any, value: any, callback: any) => {
if (isEmpty(value)) {
callback(new Error('枚举值不能为空'))
return
}
if (isNaN(Number(value))) {
callback(new Error('枚举值必须是数字'))
return
}
//
const values = dataSpecsList.value.map((item) => item.value)
if (values.filter((v) => v === value).length > 1) {
callback(new Error('枚举值不能重复'))
return
}
callback()
}
/** 校验枚举描述 */
const validateEnumName = (_: any, value: string, callback: any) => {
if (isEmpty(value)) {
callback(new Error('枚举描述不能为空'))
return
}
//
if (!/^[\u4e00-\u9fa5a-zA-Z0-9]/.test(value)) {
callback(new Error('枚举描述必须以中文、英文字母或数字开头'))
return
}
//
if (!/^[\u4e00-\u9fa5a-zA-Z0-9][a-zA-Z0-9\u4e00-\u9fa5_-]*$/.test(value)) {
callback(new Error('枚举描述只能包含中文、英文字母、数字、下划线和短划线'))
return
}
//
if (value.length > 20) {
callback(new Error('枚举描述长度不能超过20个字符'))
return
}
callback()
}
/** 校验整个枚举列表 */
const validateEnumList = (_: any, __: any, callback: any) => {
if (isEmpty(dataSpecsList.value)) {
callback(new Error('请至少添加一个枚举项'))
return
}
//
const hasEmptyValue = dataSpecsList.value.some(
(item) => isEmpty(item.value) || isEmpty(item.name)
)
if (hasEmptyValue) {
callback(new Error('存在未填写的枚举值或描述'))
return
}
//
const hasInvalidNumber = dataSpecsList.value.some((item) => isNaN(Number(item.value)))
if (hasInvalidNumber) {
callback(new Error('存在非数字的枚举值'))
return
}
//
const values = dataSpecsList.value.map((item) => item.value)
const uniqueValues = new Set(values)
if (values.length !== uniqueValues.size) {
callback(new Error('存在重复的枚举值'))
return
}
callback()
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.el-form-item) { :deep(.a-form-item) {
.el-form-item { .a-form-item {
margin-bottom: 0; margin-bottom: 0;
} }
} }

View File

@ -1,66 +1,43 @@
<!-- dataTypenumber 数组类型 --> <!-- dataTypenumber 数组类型 -->
<template> <template>
<el-form-item label="取值范围"> <a-form-item label="取值范围">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<el-form-item <div class="flex-1">
:rules="[ <a-input v-model:value="dataSpecs.min" placeholder="请输入最小值" />
{ required: true, message: '最小值不能为空' }, </div>
{ validator: validateMin, trigger: 'blur' }
]"
class="mb-0"
prop="property.dataSpecs.min"
>
<el-input v-model="dataSpecs.min" placeholder="请输入最小值" />
</el-form-item>
<span class="mx-2">~</span> <span class="mx-2">~</span>
<el-form-item <div class="flex-1">
:rules="[ <a-input v-model:value="dataSpecs.max" placeholder="请输入最大值" />
{ required: true, message: '最大值不能为空' }, </div>
{ validator: validateMax, trigger: 'blur' }
]"
class="mb-0"
prop="property.dataSpecs.max"
>
<el-input v-model="dataSpecs.max" placeholder="请输入最大值" />
</el-form-item>
</div> </div>
</el-form-item> </a-form-item>
<el-form-item <a-form-item label="步长">
:rules="[ <a-input v-model:value="dataSpecs.step" placeholder="请输入步长" />
{ required: true, message: '步长不能为空' }, </a-form-item>
{ validator: validateStep, trigger: 'blur' } <a-form-item label="单位">
]" <a-select
label="步长"
prop="property.dataSpecs.step"
>
<el-input v-model="dataSpecs.step" placeholder="请输入步长" />
</el-form-item>
<el-form-item
:rules="[{ required: true, message: '请选择单位' }]"
label="单位"
prop="property.dataSpecs.unit"
>
<el-select
:model-value="dataSpecs.unit ? dataSpecs.unitName + '-' + dataSpecs.unit : ''" :model-value="dataSpecs.unit ? dataSpecs.unitName + '-' + dataSpecs.unit : ''"
filterable show-search
placeholder="请选择单位" placeholder="请选择单位"
class="w-1/1" class="w-1/1"
@change="unitChange" @change="unitChange"
> >
<el-option <a-select-option
v-for="(item, index) in getStrDictOptions(DICT_TYPE.IOT_THING_MODEL_UNIT)" v-for="(item, index) in getDictOptions(DICT_TYPE.IOT_THING_MODEL_UNIT)"
:key="index" :key="index"
:label="item.label + '-' + item.value"
:value="item.label + '-' + item.value" :value="item.label + '-' + item.value"
/> >
</el-select> {{ item.label + '-' + item.value }}
</el-form-item> </a-select-option>
</a-select>
</a-form-item>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { Ref } from 'vue'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { DICT_TYPE, getStrDictOptions } from '@vben/constants' import type { DataSpecsNumberData } from '#/api/iot/thingmodel'
import { DataSpecsNumberData } from '#/api/iot/thingmodel' import { DICT_TYPE, getDictOptions } from '#/utils'
/** 数值型的 dataSpecs 配置组件 */ /** 数值型的 dataSpecs 配置组件 */
defineOptions({ name: 'ThingModelNumberDataSpecs' }) defineOptions({ name: 'ThingModelNumberDataSpecs' })
@ -75,64 +52,11 @@ const unitChange = (UnitSpecs: string) => {
dataSpecs.value.unitName = unitName dataSpecs.value.unitName = unitName
dataSpecs.value.unit = unit dataSpecs.value.unit = unit
} }
/** 校验最小值 */
const validateMin = (_: any, __: any, callback: any) => {
const min = Number(dataSpecs.value.min)
const max = Number(dataSpecs.value.max)
if (isNaN(min)) {
callback(new Error('请输入有效的数值'))
return
}
if (max !== undefined && !isNaN(max) && min >= max) {
callback(new Error('最小值必须小于最大值'))
return
}
callback()
}
/** 校验最大值 */
const validateMax = (_: any, __: any, callback: any) => {
const min = Number(dataSpecs.value.min)
const max = Number(dataSpecs.value.max)
if (isNaN(max)) {
callback(new Error('请输入有效的数值'))
return
}
if (min !== undefined && !isNaN(min) && max <= min) {
callback(new Error('最大值必须大于最小值'))
return
}
callback()
}
/** 校验步长 */
const validateStep = (_: any, __: any, callback: any) => {
const step = Number(dataSpecs.value.step)
if (isNaN(step)) {
callback(new Error('请输入有效的数值'))
return
}
if (step <= 0) {
callback(new Error('步长必须大于0'))
return
}
const min = Number(dataSpecs.value.min)
const max = Number(dataSpecs.value.max)
if (!isNaN(min) && !isNaN(max) && step > max - min) {
callback(new Error('步长不能大于最大值和最小值的差值'))
return
}
callback()
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.el-form-item) { :deep(.a-form-item) {
.el-form-item { .a-form-item {
margin-bottom: 0; margin-bottom: 0;
} }
} }

View File

@ -1,7 +1,7 @@
<!-- dataTypestruct 数组类型 --> <!-- dataTypestruct 数组类型 -->
<template> <template>
<!-- struct 数据展示 --> <!-- struct 数据展示 -->
<el-form-item <a-form-item
:rules="[{ required: true, validator: validateList, trigger: 'change' }]" :rules="[{ required: true, validator: validateList, trigger: 'change' }]"
label="JSON 对象" label="JSON 对象"
> >
@ -12,52 +12,53 @@
> >
<span>参数名称{{ item.name }}</span> <span>参数名称{{ item.name }}</span>
<div class="btn"> <div class="btn">
<el-button link type="primary" @click="openStructForm(item)"></el-button> <a-button type="link" @click="openStructForm(item)"></a-button>
<el-divider direction="vertical" /> <a-divider type="vertical" />
<el-button link type="danger" @click="deleteStructItem(index)"></el-button> <a-button type="link" danger @click="deleteStructItem(index)"></a-button>
</div> </div>
</div> </div>
<el-button link type="primary" @click="openStructForm(null)">+</el-button> <a-button type="link" @click="openStructForm(null)">+</a-button>
</el-form-item> </a-form-item>
<!-- struct 表单 --> <!-- struct 表单 -->
<Dialog v-model="dialogVisible" :title="dialogTitle" append-to-body> <Dialog v-model="dialogVisible" :title="dialogTitle" append-to-body>
<el-form <a-form
ref="structFormRef" ref="structFormRef"
v-loading="formLoading"
:model="formData" :model="formData"
:rules="ThingModelFormRules" :label-col="{ span: 6 }"
label-width="100px" :wrapper-col="{ span: 18 }"
> >
<el-form-item label="参数名称" prop="name"> <a-spin :spinning="formLoading">
<el-input v-model="formData.name" placeholder="请输入功能名称" /> <a-form-item label="参数名称" name="name">
</el-form-item> <a-input v-model:value="formData.name" placeholder="请输入功能名称" />
<el-form-item label="标识符" prop="identifier"> </a-form-item>
<el-input v-model="formData.identifier" placeholder="请输入标识符" /> <a-form-item label="标识符" name="identifier">
</el-form-item> <a-input v-model:value="formData.identifier" placeholder="请输入标识符" />
<!-- 属性配置 --> </a-form-item>
<ThingModelProperty v-model="formData.property" is-struct-data-specs /> <!-- 属性配置 -->
</el-form> <ThingModelProperty v-model="formData.property" is-struct-data-specs />
</a-spin>
</a-form>
<template #footer> <template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button> <a-button :disabled="formLoading" type="primary" @click="submitForm"> </a-button>
<el-button @click="dialogVisible = false"> </el-button> <a-button @click="dialogVisible = false"> </a-button>
</template> </template>
</Dialog> </Dialog>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, unref, onMounted, nextTick } from 'vue'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import ThingModelProperty from '../ThingModelProperty.vue' import ThingModelProperty from '../ThingModelProperty.vue'
import { isEmpty } from '@vben/utils' import { isEmpty } from '@vben/utils'
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants' import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants'
import { ThingModelFormRules } from '#/api/iot/thingmodel'
/** Struct 型的 dataSpecs 配置组件 */ /** Struct 型的 dataSpecs 配置组件 */
defineOptions({ name: 'ThingModelStructDataSpecs' }) defineOptions({ name: 'ThingModelStructDataSpecs' })
const props = defineProps<{ modelValue: any }>() const props = defineProps<{ modelValue: any[] | undefined }>()
const emits = defineEmits(['update:modelValue']) const emits = defineEmits(['update:modelValue'])
const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<any[]> const dataSpecsList = useVModel(props, 'modelValue', emits)
const dialogVisible = ref(false) // const dialogVisible = ref(false) //
const dialogTitle = ref('新增参数') // const dialogTitle = ref('新增参数') //
const formLoading = ref(false) // 12 const formLoading = ref(false) // 12
@ -75,25 +76,26 @@ const formData = ref<any>({
const openStructForm = (val: any) => { const openStructForm = (val: any) => {
dialogVisible.value = true dialogVisible.value = true
resetForm() resetForm()
if (isEmpty(val)) { if (isEmpty(val) || !val) {
return return
} }
// //
const structData = val as any
formData.value = { formData.value = {
identifier: val.identifier, identifier: structData.identifier,
name: val.name, name: structData.name,
description: val.description, description: structData.description,
property: { property: {
dataType: val.childDataType, dataType: structData.childDataType,
dataSpecs: val.dataSpecs, dataSpecs: structData.dataSpecs,
dataSpecsList: val.dataSpecsList dataSpecsList: structData.dataSpecsList
} }
} }
} }
/** 删除 struct 项 */ /** 删除 struct 项 */
const deleteStructItem = (index: number) => { const deleteStructItem = (index: number) => {
dataSpecsList.value.splice(index, 1) dataSpecsList.value?.splice(index, 1)
} }
/** 添加参数 */ /** 添加参数 */
@ -117,8 +119,11 @@ const submitForm = async () => {
} }
// identifier // identifier
if (!dataSpecsList.value) {
dataSpecsList.value = []
}
const existingIndex = dataSpecsList.value.findIndex( const existingIndex = dataSpecsList.value.findIndex(
(spec) => spec.identifier === data.identifier (spec: any) => spec.identifier === data.identifier
) )
if (existingIndex > -1) { if (existingIndex > -1) {
dataSpecsList.value[existingIndex] = item dataSpecsList.value[existingIndex] = item

View File

@ -156,7 +156,7 @@ export const getDataTypeOptionsLabel = (value: string) => {
/** 获取数据类型显示名称(用于属性选择器) */ /** 获取数据类型显示名称(用于属性选择器) */
export const getDataTypeName = (dataType: string): string => { export const getDataTypeName = (dataType: string): string => {
const typeMap = { const typeMap: Record<string, string> = {
[IoTDataSpecsDataTypeEnum.INT]: '整数', [IoTDataSpecsDataTypeEnum.INT]: '整数',
[IoTDataSpecsDataTypeEnum.FLOAT]: '浮点数', [IoTDataSpecsDataTypeEnum.FLOAT]: '浮点数',
[IoTDataSpecsDataTypeEnum.DOUBLE]: '双精度', [IoTDataSpecsDataTypeEnum.DOUBLE]: '双精度',
@ -174,7 +174,7 @@ export const getDataTypeName = (dataType: string): string => {
export const getDataTypeTagType = ( export const getDataTypeTagType = (
dataType: string dataType: string
): 'primary' | 'success' | 'info' | 'warning' | 'danger' => { ): 'primary' | 'success' | 'info' | 'warning' | 'danger' => {
const tagMap = { const tagMap: Record<string, 'primary' | 'success' | 'info' | 'warning' | 'danger'> = {
[IoTDataSpecsDataTypeEnum.INT]: 'primary', [IoTDataSpecsDataTypeEnum.INT]: 'primary',
[IoTDataSpecsDataTypeEnum.FLOAT]: 'success', [IoTDataSpecsDataTypeEnum.FLOAT]: 'success',
[IoTDataSpecsDataTypeEnum.DOUBLE]: 'success', [IoTDataSpecsDataTypeEnum.DOUBLE]: 'success',
@ -184,7 +184,7 @@ export const getDataTypeTagType = (
[IoTDataSpecsDataTypeEnum.DATE]: 'primary', [IoTDataSpecsDataTypeEnum.DATE]: 'primary',
[IoTDataSpecsDataTypeEnum.STRUCT]: 'info', [IoTDataSpecsDataTypeEnum.STRUCT]: 'info',
[IoTDataSpecsDataTypeEnum.ARRAY]: 'warning' [IoTDataSpecsDataTypeEnum.ARRAY]: 'warning'
} as const }
return tagMap[dataType] || 'info' return tagMap[dataType] || 'info'
} }