Pre Merge pull request !227 from huppygo/master

pull/227/MERGE
huppygo 2025-10-12 10:59:57 +00:00 committed by Gitee
commit 38a9407164
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
69 changed files with 998 additions and 863 deletions

View File

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

View File

@ -1,13 +1,10 @@
import type { VbenFormSchema } from '#/adapter/form';
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 { getSimpleDeviceList } from '#/api/iot/device/device';
import { getSimpleProductList } from '#/api/iot/product/product';
import { getRangePickerDefaultProps } from '#/utils';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
/** 列表的搜索表单 */
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 { getSimpleDeviceList } from '#/api/iot/device/device';
import { getSimpleProductList } from '#/api/iot/product/product';
import { DICT_TYPE, getDictLabel } from '#/utils';
import { useGridColumns, useGridFormSchema } from './data';
@ -33,14 +34,8 @@ const loadData = async () => {
//
const getLevelText = (level?: number) => {
const levelMap: Record<number, string> = {
1: '提示',
2: '一般',
3: '警告',
4: '严重',
5: '紧急',
};
return level ? levelMap[level] || `级别${level}` : '-';
if (!level) return '-';
return getDictLabel(DICT_TYPE.IOT_ALERT_LEVEL, level);
};
//

View File

@ -1,10 +1,8 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { z } from '#/adapter/form';
import { DICT_TYPE, getDictOptions } from '#/utils';
import { getSimpleDeviceGroupList } from '#/api/iot/device/group';
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 { Page, useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { downloadFileFromBlobPart } from '@vben/utils';
import { IconifyIcon } from '@vben/icons';
import { DICT_TYPE, getDictOptions } from '#/utils';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteDevice,
@ -295,7 +295,7 @@ onMounted(async () => {
style="width: 200px"
>
<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"
:value="dict.value"
>

View File

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

View File

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

View File

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

View File

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

View File

@ -43,11 +43,11 @@ import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import DeviceForm from '../DeviceForm.vue'
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 {
product: ProductVO
device: DeviceVO
device: IotDeviceApi.Device
loading?: boolean
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -148,17 +148,17 @@
</style>
<script setup lang="ts">
import { ref, reactive, watch, onMounted, onBeforeUnmount } from 'vue'
import { ContentWrap } from '@vben/common-ui'
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 DeviceDetailsThingModelPropertyHistory from './DeviceDetailsThingModelPropertyHistory.vue'
const props = defineProps<{ deviceId: number }>()
const loading = ref(true) //
const list = ref<IotDevicePropertyDetailRespVO[]>([]) //
const filterList = ref<IotDevicePropertyDetailRespVO[]>([]) //
const list = ref<IotDeviceApi.DevicePropertyDetail[]>([]) //
const filterList = ref<IotDeviceApi.DevicePropertyDetail[]>([]) //
const queryParams = reactive({
keyword: '' as string
})
@ -189,7 +189,7 @@ const handleFilter = () => {
} else {
const keyword = queryParams.keyword.toLowerCase()
list.value = filterList.value.filter(
(item: IotDevicePropertyDetailRespVO) =>
(item: IotDeviceApi.DevicePropertyDetail) =>
item.identifier?.toLowerCase().includes(keyword) ||
item.name?.toLowerCase().includes(keyword)
)
@ -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 === '') {
return '-'
}

View File

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

View File

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

View File

@ -1,9 +1,8 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE } from '@vben/constants';
import { z } from '#/adapter/form';
import { DICT_TYPE } from '#/utils';
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 * as IoTOtaTaskApi from '#/api/iot/ota/task';
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';
/** IoT OTA 升级任务表单 */
@ -32,11 +32,11 @@ const formRules = {
deviceScope: [{ required: true, message: '请选择升级范围', trigger: 'change' as const, type: 'number' 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(() => {
return devices.value.map((device) => ({
return devices.value.map((device: IotDeviceApi.Device) => ({
label: device.nickname
? `${device.deviceName} (${device.nickname})`
: device.deviceName,

View File

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

View File

@ -1,15 +1,14 @@
import type { VbenFormSchema } from '#/adapter/form';
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 { message } from 'ant-design-vue';
import { Button, message } from 'ant-design-vue';
import { z } from '#/adapter/form';
import { DICT_TYPE, getDictOptions } from '#/utils';
import { getSimpleProductCategoryList } from '#/api/iot/product/category';
import {
deleteProduct,
@ -18,7 +17,7 @@ import {
} from '#/api/iot/product/product';
/** 新增/修改产品的表单 */
export function useFormSchema(): VbenFormSchema[] {
export function useFormSchema(formApi?: any): VbenFormSchema[] {
return [
{
component: 'Input',
@ -28,6 +27,55 @@ export function useFormSchema(): VbenFormSchema[] {
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',
label: '产品名称',
@ -74,43 +122,22 @@ export function useFormSchema(): VbenFormSchema[] {
rules: 'required',
},
{
fieldName: 'protocolType',
label: '接入协议',
fieldName: 'codecType',
label: '数据格式',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.IOT_PROTOCOL_TYPE, 'number'),
placeholder: '请选择接入协议',
options: getDictOptions(DICT_TYPE.IOT_CODEC_TYPE, 'string'),
placeholder: '请选择数据格式',
},
rules: 'required',
},
{
fieldName: 'dataFormat',
label: '数据格式',
component: 'RadioGroup',
fieldName: 'locationType',
label: '定位类型',
component: 'Select',
componentProps: {
options: getDictOptions(DICT_TYPE.IOT_DATA_FORMAT, 'number'),
buttonStyle: 'solid',
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',
options: getDictOptions(DICT_TYPE.IOT_LOCATION_TYPE, 'number'),
placeholder: '请选择定位类型',
},
rules: 'required',
},
@ -125,6 +152,30 @@ export function useFormSchema(): VbenFormSchema[] {
},
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,
};
}
/** 生成 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,
Tooltip,
} from 'ant-design-vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictLabel } from '@vben/hooks';
import { IconifyIcon } from '@vben/icons';
import { DICT_TYPE, getDictLabel } from '#/utils';
import { getProductPage } from '#/api/iot/product/product';
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 { $t } from '#/locales';
import { useFormSchema } from '../data';
import { generateProductKey, useFormSchema } from '../data';
defineOptions({ name: 'IoTProductForm' });
@ -25,10 +25,15 @@ const [Form, formApi] = useVbenForm({
},
wrapperClass: 'grid-cols-2',
layout: 'horizontal',
schema: useFormSchema(),
schema: [],
showDefaultActions: false,
});
// formApi schema
formApi.setState({
schema: useFormSchema(formApi),
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
@ -58,8 +63,11 @@ const [Modal, modalApi] = useVbenModal({
if (!data || !data.id) {
//
await formApi.setValues({
productKey: generateProductKey(), // ProductKey
deviceType: 0, //
dataFormat: 1, // JSON
codecType: 'Alink', // Alink
locationType: 0, //
validateType: 1, //
status: 0, //
});

View File

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

View File

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

View File

@ -1,10 +1,7 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { getRangePickerDefaultProps } from '#/utils';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,7 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { getRangePickerDefaultProps } from '#/utils';
import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils';
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -139,6 +139,7 @@
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick, onMounted } from 'vue'
import { useVModel } from '@vueuse/core'
import { InfoFilled } from '@element-plus/icons-vue'
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) => {
// 使 constants.ts getDataTypeName
const typeMap = {
const typeMap: Record<string, string> = {
[IoTDataSpecsDataTypeEnum.INT]: '整数',
[IoTDataSpecsDataTypeEnum.FLOAT]: '浮点数',
[IoTDataSpecsDataTypeEnum.DOUBLE]: '双精度',
@ -391,7 +376,7 @@ const getParamTypeName = (dataType: string) => {
* @returns 标签样式
*/
const getParamTypeTag = (dataType: string) => {
const tagMap = {
const tagMap: Record<string, string> = {
[IoTDataSpecsDataTypeEnum.INT]: 'primary',
[IoTDataSpecsDataTypeEnum.FLOAT]: 'success',
[IoTDataSpecsDataTypeEnum.DOUBLE]: 'success',
@ -412,7 +397,7 @@ const getParamTypeTag = (dataType: string) => {
*/
const getExampleValue = (param: any) => {
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
}
@ -425,10 +410,10 @@ const generateExampleJson = () => {
return '{}'
}
const example = {}
paramsList.value.forEach((param) => {
const example: Record<string, any> = {}
paramsList.value.forEach((param: any) => {
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
})
@ -461,7 +446,7 @@ const handleDataDisplay = (value: string) => {
//
watch(
() => localValue.value,
async (newValue, oldValue) => {
async (newValue: string | undefined, oldValue: string | undefined) => {
//
if (newValue === oldValue) return
@ -483,7 +468,7 @@ onMounted(async () => {
//
watch(
() => props.config,
(newConfig, oldConfig) => {
(newConfig: JsonParamsConfig | undefined, oldConfig: JsonParamsConfig | undefined) => {
//
if (JSON.stringify(newConfig) !== JSON.stringify(oldConfig)) {
//

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,7 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { DICT_TYPE, getDictOptions } from '#/utils';
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {

View File

@ -1,14 +1,18 @@
<script setup lang="ts">
import type { VxeGridInstance } from '#/adapter/vxe-table';
import { onMounted, provide, ref } from 'vue';
import { message } from 'ant-design-vue';
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 { getProduct } from '#/api/iot/product/product';
import type { IotProductApi } from '#/api/iot/product/product';
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' });
@ -16,17 +20,27 @@ const props = defineProps<{
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({
gridOptions: {
columns: useGridColumns(),
proxyConfig: {
ajax: {
query: async ({ page, form }) => {
query: async ({ page }: any, formValues: any) => {
return await getThingModelPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
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 = () => {
// TODO:
console.log('新增功能');
thingModelFormRef.value?.open('create');
};
//
const handleEdit = (row: any) => {
// TODO:
console.log('编辑功能:', row);
thingModelFormRef.value?.open('update', row.id);
};
//
@ -66,30 +74,54 @@ const handleDelete = async (row: any) => {
// TSL
const handleOpenTSL = () => {
// TODO: TSL
console.log('打开 TSL');
thingModelTSLRef.value?.open();
};
//
const getDataTypeLabel = (row: any) => {
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>
<template>
<Page
description="管理产品的物模型定义,包括属性、服务和事件"
title="物模型管理"
>
<Page>
<Grid ref="xGrid">
<template #toolbar-tools>
<VbenButton @click="handleCreate">
<Icon icon="ant-design:plus-outlined" class="mr-1" />
添加功能
</VbenButton>
<VbenButton type="success" @click="handleOpenTSL">
TSL
</VbenButton>
<TableAction
:actions="[
{
label: '添加功能',
type: 'primary',
icon: ACTION_ICON.ADD,
onClick: handleCreate,
},
{
label: 'TSL',
type: 'default',
color: 'success',
onClick: handleOpenTSL,
},
]"
/>
</template>
<!-- 数据类型列 -->
@ -98,20 +130,40 @@ const getDataTypeLabel = (row: any) => {
</template>
<!-- 数据定义列 -->
<template #dataDefinition="{ row }">
<template #dataDefinition>
<!-- TODO: 实现数据定义组件 -->
<span class="text-gray-400">-</span>
</template>
<!-- 操作列 -->
<template #actions="{ row }">
<VbenButton size="small" type="primary" @click="handleEdit(row)">
编辑
</VbenButton>
<VbenButton size="small" type="danger" @click="handleDelete(row)">
删除
</VbenButton>
<TableAction
:actions="[
{
label: '编辑',
type: 'link',
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>
</Grid>
<!-- 物模型表单 -->
<ThingModelForm ref="thingModelFormRef" @success="handleRefresh" />
<!-- TSL 弹窗 -->
<ThingModelTSL ref="thingModelTSLRef" />
</Page>
</template>

View File

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

View File

@ -1,29 +1,32 @@
<!-- 产品的物模型表单 -->
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<a-modal
v-model:open="dialogVisible"
:title="dialogTitle"
:confirm-loading="formLoading"
@ok="submitForm"
>
<a-form
ref="formRef"
:loading="formLoading"
:model="formData"
:rules="ThingModelFormRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<a-form-item label="功能类型" name="type">
<a-form-item label="功能类型">
<a-radio-group v-model:value="formData.type">
<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"
:value="dict.value"
:value="Number(dict.value)"
>
{{ dict.label }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item label="功能名称" name="name">
<a-form-item label="功能名称">
<a-input v-model:value="formData.name" placeholder="请输入功能名称" />
</a-form-item>
<a-form-item label="标识符" name="identifier">
<a-form-item label="标识符">
<a-input v-model:value="formData.identifier" placeholder="请输入标识符" />
</a-form-item>
<!-- 属性配置 -->
@ -41,48 +44,45 @@
v-if="formData.type === IoTThingModelTypeEnum.EVENT"
v-model="formData.event"
/>
<a-form-item label="描述" name="description">
<a-form-item label="描述">
<a-textarea
v-model:value="formData.description"
v-model:value="formData.desc"
:maxlength="200"
:rows="3"
placeholder="请输入属性描述"
/>
</a-form-item>
</a-form>
<template #footer>
<a-button :disabled="formLoading" type="primary" @click="submitForm"> </a-button>
<a-button @click="dialogVisible = false"> </a-button>
</template>
</Dialog>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, inject, type Ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { $t } from '@vben/locales'
import { message } from 'ant-design-vue'
import type { ProductVO } from '#/api/iot/product/product'
import ThingModelProperty from './ThingModelProperty.vue'
import ThingModelService from './ThingModelService.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 {
IOT_PROVIDE_KEY,
IoTDataSpecsDataTypeEnum,
IoTThingModelTypeEnum
} from '#/views/iot/utils/constants'
import { cloneDeep } from 'lodash-es'
import { DICT_TYPE, getIntDictOptions } from '@vben/constants'
import { DICT_TYPE, getDictOptions } from '#/utils'
//
const cloneDeep = <T>(obj: T): T => {
return JSON.parse(JSON.stringify(obj))
}
/** IoT 物模型数据表单 */
defineOptions({ name: 'IoTThingModelForm' })
const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) //
const { t } = useI18n() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
@ -96,22 +96,56 @@ const formData = ref<ThingModelData>({
dataType: IoTDataSpecsDataTypeEnum.INT
}
},
service: {},
event: {}
service: {
inputData: [],
outputData: []
},
event: {
outputData: []
}
} as ThingModelData)
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) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
dialogTitle.value = type === 'create'
? $t('ui.actionTitle.create', ['物模型功能'])
: $t('ui.actionTitle.edit', ['物模型功能'])
formType.value = type
resetForm()
if (id) {
formLoading.value = true
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) {
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) {
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) {
formData.value.event = {}
formData.value.event = {
outputData: []
}
} else {
//
if (!formData.value.event.outputData) {
formData.value.event.outputData = []
}
}
} finally {
formLoading.value = false
@ -140,7 +192,83 @@ defineExpose({ open, close: () => (dialogVisible.value = false) })
/** 提交表单 */
const emit = defineEmits(['success'])
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
try {
const data = cloneDeep(formData.value) as ThingModelData
@ -148,12 +276,16 @@ const submitForm = async () => {
data.productId = product!.value.id
data.productKey = product!.value.productKey
fillExtraAttributes(data)
//
// console.log(':', data)
if (formType.value === 'create') {
await ThingModelApi.createThingModel(data)
message.success({ content: t('common.createSuccess') })
message.success('创建成功')
} else {
await ThingModelApi.updateThingModel(data)
message.success({ content: t('common.updateSuccess') })
message.success('更新成功')
}
//
dialogVisible.value = false
@ -180,6 +312,13 @@ const fillExtraAttributes = (data: any) => {
data.dataType = data.service.dataType
data.service.identifier = data.identifier
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.event
}
@ -189,6 +328,10 @@ const fillExtraAttributes = (data: any) => {
data.dataType = data.event.dataType
data.event.identifier = data.identifier
data.event.name = data.name
//
if (!data.event.outputData || data.event.outputData.length === 0) {
delete data.event.outputData
}
delete data.property
delete data.service
}
@ -203,21 +346,4 @@ const removeDataSpecs = (val: any) => {
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>

View File

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

View File

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

View File

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

View File

@ -1,25 +1,35 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<JsonEditor
v-model="thingModelTSL"
:mode="viewMode === 'editor' ? 'code' : 'view'"
height="600px"
<a-modal
v-model:open="dialogVisible"
:title="dialogTitle"
:footer="null"
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>
<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>
</a-modal>
</template>
<script setup lang="ts">
import hljs from 'highlight.js' //
import 'highlight.js/styles/github.css' //
import json from 'highlight.js/lib/languages/json'
import { ThingModelApi } from '#/api/iot/thingmodel'
import { ProductVO } from '#/api/iot/product/product'
import { ref, inject, computed, watch, type Ref } from 'vue'
import { getThingModelTSL } from '#/api/iot/thingmodel'
import type { ProductVO } from '#/api/iot/product/product'
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants'
defineOptions({ name: 'ThingModelTSL' })
@ -27,24 +37,69 @@ defineOptions({ name: 'ThingModelTSL' })
const dialogVisible = ref(false) //
const dialogTitle = ref('物模型 TSL') //
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
await getTsl()
}
defineExpose({ open })
/** 获取 TSL */
const thingModelTSL = ref({})
const tslString = ref('') //
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)
}
/** 初始化 **/
onMounted(async () => {
//
hljs.registerLanguage('json', json)
await getTsl()
/** 格式化的 TSL 用于只读展示 */
const formattedTSL = computed(() => {
try {
if (typeof thingModelTSL.value === 'string') {
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>
<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 v-if="data.type === IoTThingModelTypeEnum.PROPERTY">
<template v-if="data.type === IoTThingModelTypeEnum.PROPERTY && data.property">
<!-- 非列表型数值 -->
<div
v-if="
@ -8,14 +8,14 @@
IoTDataSpecsDataTypeEnum.INT,
IoTDataSpecsDataTypeEnum.DOUBLE,
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 v-if="IoTDataSpecsDataTypeEnum.TEXT === data.property.dataType">
数据长度{{ data.property.dataSpecs.length }}
数据长度{{ data.property.dataSpecs?.length }}
</div>
<!-- 列表型: 数组结构时间特殊 -->
<div
@ -24,7 +24,7 @@
IoTDataSpecsDataTypeEnum.ARRAY,
IoTDataSpecsDataTypeEnum.STRUCT,
IoTDataSpecsDataTypeEnum.DATE
].includes(data.property.dataType)
].includes(data.property.dataType as any)
"
>
-
@ -33,7 +33,7 @@
<div
v-if="
[IoTDataSpecsDataTypeEnum.BOOL, IoTDataSpecsDataTypeEnum.ENUM].includes(
data.property.dataType
data.property.dataType as any
)
"
>
@ -46,17 +46,17 @@
</div>
</template>
<!-- 服务 -->
<div v-if="data.type === IoTThingModelTypeEnum.SERVICE">
调用方式{{ getThingModelServiceCallTypeLabel(data.service!.callType) }}
<div v-if="data.type === IoTThingModelTypeEnum.SERVICE && data.service?.callType">
调用方式{{ getThingModelServiceCallTypeLabel(data.service.callType) }}
</div>
<!-- 事件 -->
<div v-if="data.type === IoTThingModelTypeEnum.EVENT">
事件类型{{ getEventTypeLabel(data.event!.type) }}
<div v-if="data.type === IoTThingModelTypeEnum.EVENT && data.event?.type">
事件类型{{ getEventTypeLabel(data.event.type) }}
</div>
</template>
<script lang="ts" setup>
import { ThingModelData } from '#/api/iot/thingmodel'
import type { ThingModelData } from '#/api/iot/thingmodel'
import {
getEventTypeLabel,
getThingModelServiceCallTypeLabel,

View File

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

View File

@ -1,9 +1,6 @@
<!-- dataTypeenum 数组类型 -->
<template>
<el-form-item
:rules="[{ required: true, validator: validateEnumList, trigger: 'change' }]"
label="枚举项"
>
<a-form-item label="枚举项">
<div class="flex flex-col">
<div class="flex items-center">
<span class="flex-1"> 参数值 </span>
@ -14,46 +11,33 @@
:key="index"
class="flex items-center justify-between mb-5px"
>
<el-form-item
:prop="`property.dataSpecsList[${index}].value`"
:rules="[
{ required: true, message: '枚举值不能为空' },
{ validator: validateEnumValue, trigger: 'blur' }
]"
class="flex-1 mb-0"
>
<el-input v-model="item.value" placeholder="请输入枚举值,如'0'" />
</el-form-item>
<div class="flex-1">
<a-input v-model:value="item.value" placeholder="请输入枚举值,如'0'" />
</div>
<span class="mx-2">~</span>
<el-form-item
:prop="`property.dataSpecsList[${index}].name`"
:rules="[
{ required: true, message: '枚举描述不能为空' },
{ 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 class="flex-1">
<a-input v-model:value="item.name" placeholder="对该枚举项的描述" />
</div>
<a-button class="ml-10px" type="link" @click="deleteEnum(index)"></a-button>
</div>
<el-button link type="primary" @click="addEnum">+</el-button>
<a-button type="link" @click="addEnum">+</a-button>
</div>
</el-form-item>
</a-form-item>
</template>
<script lang="ts" setup>
import type { Ref } from 'vue'
import { useVModel } from '@vueuse/core'
import { isEmpty } from '@vben/utils'
import { message } from 'ant-design-vue'
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants'
import { DataSpecsEnumOrBoolData } from '#/api/iot/thingmodel'
import type { DataSpecsEnumOrBoolData } from '#/api/iot/thingmodel'
/** 枚举型的 dataSpecs 配置组件 */
defineOptions({ name: 'ThingModelEnumDataSpecs' })
const props = defineProps<{ modelValue: any }>()
const props = defineProps<{ modelValue: DataSpecsEnumOrBoolData[] }>()
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,
name: '', //
value: undefined //
})
} as any)
}
/** 删除枚举项 */
@ -73,87 +57,11 @@ const deleteEnum = (index: number) => {
}
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>
<style lang="scss" scoped>
:deep(.el-form-item) {
.el-form-item {
:deep(.a-form-item) {
.a-form-item {
margin-bottom: 0;
}
}

View File

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

View File

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

View File

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