Pre Merge pull request !227 from huppygo/master
commit
38a9407164
|
|
@ -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[] {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
/** 创建告警配置 */
|
||||
|
|
|
|||
|
|
@ -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[] {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
// 获取告警级别颜色
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
|
|
@ -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') // 默认选中设备属性
|
||||
|
|
|
|||
|
|
@ -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) || '-'
|
||||
|
|
|
|||
|
|
@ -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 '-'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) || '-'
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
/** 新增/修改设备分组的表单 */
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
|
|
|||
|
|
@ -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, // 默认启用
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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[] {
|
||||
|
|
|
|||
|
|
@ -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[] {
|
||||
|
|
|
|||
|
|
@ -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 = {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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[] {
|
||||
|
|
|
|||
|
|
@ -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[] {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 // 只加载启用的配置
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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] || ''
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 确保响应式更新完成后再添加新条件
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
// 如果没有外部传入的值,才清空数据
|
||||
|
|
|
|||
|
|
@ -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]: '请输入浮点数',
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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' })
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化:确保至少有一个触发器 */
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, watch } from 'vue'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import {
|
||||
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
||||
|
|
|
|||
|
|
@ -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' })
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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[] {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<!-- dataType:array 数组类型 -->
|
||||
<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'
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
<!-- dataType:enum 数组类型 -->
|
||||
<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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,66 +1,43 @@
|
|||
<!-- dataType:number 数组类型 -->
|
||||
<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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!-- dataType:struct 数组类型 -->
|
||||
<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) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue