ソースを参照

!626 【代码完善】IOT: ThingModel StructDataSpecs 组件
Merge pull request !626 from puhui999/feature/iot

芋道源码 7 ヶ月 前
コミット
eb5d350b09

+ 23 - 24
src/api/iot/thinkmodel/index.ts → src/api/iot/thingmodel/index.ts

@@ -3,7 +3,7 @@ import request from '@/config/axios'
 /**
  * IoT 产品物模型
  */
-export interface ThinkModelData {
+export interface ThingModelData {
   id?: number // 物模型功能编号
   identifier?: string // 功能标识
   name?: string // 功能名称
@@ -12,29 +12,29 @@ export interface ThinkModelData {
   productKey?: string // 产品标识
   dataType: string // 数据类型,与 dataSpecs 的 dataType 保持一致
   type: ProductFunctionTypeEnum // 功能类型
-  property: ThinkModelProperty // 属性
-  event?: ThinkModelEvent // 事件
-  service?: ThinkModelService // 服务
+  property: ThingModelProperty // 属性
+  event?: ThingModelEvent // 事件
+  service?: ThingModelService // 服务
 }
 
 /**
- * ThinkModelProperty 类型
+ * ThingModelProperty 类型
  */
-export interface ThinkModelProperty {
+export interface ThingModelProperty {
   [key: string]: any
 }
 
 /**
- * ThinkModelEvent 类型
+ * ThingModelEvent 类型
  */
-export interface ThinkModelEvent {
+export interface ThingModelEvent {
   [key: string]: any
 }
 
 /**
- * ThinkModelService 类型
+ * ThingModelService 类型
  */
-export interface ThinkModelService {
+export interface ThingModelService {
   [key: string]: any
 }
 
@@ -52,38 +52,37 @@ export enum ProductFunctionAccessModeEnum {
 }
 
 // IoT 产品物模型 API
-export const ThinkModelApi = {
+export const ThingModelApi = {
   // 查询产品物模型分页
-  // TODO @puhui999:product 前缀,是不是去掉哈。
-  getThinkModelPage: async (params: any) => {
-    return await request.get({ url: `/iot/product-think-model/page`, params })
+  getThingModelPage: async (params: any) => {
+    return await request.get({ url: `/iot/product-thing-model/page`, params })
   },
 
   // 获得产品物模型
-  getThinkModelListByProductId: async (params: any) => {
+  getThingModelListByProductId: async (params: any) => {
     return await request.get({
-      url: `/iot/product-think-model/list-by-product-id`,
+      url: `/iot/product-thing-model/list-by-product-id`,
       params
     })
   },
 
   // 查询产品物模型详情
-  getThinkModel: async (id: number) => {
-    return await request.get({ url: `/iot/product-think-model/get?id=` + id })
+  getThingModel: async (id: number) => {
+    return await request.get({ url: `/iot/product-thing-model/get?id=` + id })
   },
 
   // 新增产品物模型
-  createThinkModel: async (data: ThinkModelData) => {
-    return await request.post({ url: `/iot/product-think-model/create`, data })
+  createThingModel: async (data: ThingModelData) => {
+    return await request.post({ url: `/iot/product-thing-model/create`, data })
   },
 
   // 修改产品物模型
-  updateThinkModel: async (data: ThinkModelData) => {
-    return await request.put({ url: `/iot/product-think-model/update`, data })
+  updateThingModel: async (data: ThingModelData) => {
+    return await request.put({ url: `/iot/product-thing-model/update`, data })
   },
 
   // 删除产品物模型
-  deleteThinkModel: async (id: number) => {
-    return await request.delete({ url: `/iot/product-think-model/delete?id=` + id })
+  deleteThingModel: async (id: number) => {
+    return await request.delete({ url: `/iot/product-thing-model/delete?id=` + id })
   }
 }

+ 1 - 1
src/utils/dict.ts

@@ -236,7 +236,7 @@ export enum DICT_TYPE {
   IOT_DATA_FORMAT = 'iot_data_format', // IOT 数据格式
   IOT_PROTOCOL_TYPE = 'iot_protocol_type', // IOT 接入网关协议
   IOT_DEVICE_STATUS = 'iot_device_status', // IOT 设备状态
-  IOT_PRODUCT_THINK_MODEL_TYPE = 'iot_product_think_model_type', // IOT 产品功能类型
+  IOT_PRODUCT_THING_MODEL_TYPE = 'iot_product_thing_model_type', // IOT 产品功能类型
   IOT_DATA_TYPE = 'iot_data_type', // IOT 数据类型
   IOT_UNIT_TYPE = 'iot_unit_type', // IOT 单位类型
   IOT_RW_TYPE = 'iot_rw_type', // IOT 读写类型

+ 3 - 3
src/views/iot/product/product/detail/index.vue

@@ -8,8 +8,8 @@
       <el-tab-pane label="Topic 类列表" name="topic">
         <ProductTopic v-if="activeTab === 'topic'" :product="product" />
       </el-tab-pane>
-      <el-tab-pane label="功能定义" lazy name="thinkModel">
-        <IoTProductThinkModel ref="thinkModelRef" />
+      <el-tab-pane label="功能定义" lazy name="thingModel">
+        <IoTProductThingModel ref="thingModelRef" />
       </el-tab-pane>
       <el-tab-pane label="消息解析" name="message" />
       <el-tab-pane label="服务端订阅" name="subscription" />
@@ -22,7 +22,7 @@ import { DeviceApi } from '@/api/iot/device/device'
 import ProductDetailsHeader from './ProductDetailsHeader.vue'
 import ProductDetailsInfo from './ProductDetailsInfo.vue'
 import ProductTopic from './ProductTopic.vue'
-import IoTProductThinkModel from '@/views/iot/thinkmodel/index.vue'
+import IoTProductThingModel from '@/views/iot/thingmodel/index.vue'
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import { useRouter } from 'vue-router'
 import { IOT_PROVIDE_KEY } from '@/views/iot/utils/constants'

+ 4 - 2
src/views/iot/product/product/index.vue

@@ -97,7 +97,9 @@
                 </div>
                 <div class="mb-2.5 last:mb-0">
                   <span class="text-[#717c8e] mr-2.5">产品标识</span>
-                  <span class="text-[#0b1d30] whitespace-normal break-all">{{ item.productKey }}</span>
+                  <span class="text-[#0b1d30] whitespace-normal break-all">
+                    {{ item.productKey }}
+                  </span>
                 </div>
               </div>
               <div class="w-[100px] h-[100px]">
@@ -309,7 +311,7 @@ const openObjectModel = (item: ProductVO) => {
   push({
     name: 'IoTProductDetail',
     params: { id: item.id },
-    query: { tab: 'thinkModel' }
+    query: { tab: 'thingModel' }
   })
 }
 

+ 29 - 69
src/views/iot/thinkmodel/ThinkModelDataSpecs.vue → src/views/iot/thingmodel/ThingModelDataSpecs.vue

@@ -5,8 +5,9 @@
     prop="property.dataType"
   >
     <el-select v-model="property.dataType" placeholder="请选择数据类型" @change="handleChange">
+      <!-- ARRAY 和 STRUCT 类型数据相互嵌套时,最多支持递归嵌套2层(父和子) -->
       <el-option
-        v-for="option in dataTypeOptions"
+        v-for="option in getDataTypeOptions"
         :key="option.value"
         :label="option.label"
         :value="option.value"
@@ -14,7 +15,7 @@
     </el-select>
   </el-form-item>
   <!-- 数值型配置 -->
-  <ThinkModelNumberTypeDataSpecs
+  <ThingModelNumberDataSpecs
     v-if="
       [DataSpecsDataType.INT, DataSpecsDataType.DOUBLE, DataSpecsDataType.FLOAT].includes(
         property.dataType || ''
@@ -23,17 +24,12 @@
     v-model="property.dataSpecs"
   />
   <!-- 枚举型配置 -->
-  <ThinkModelEnumTypeDataSpecs
+  <ThingModelEnumDataSpecs
     v-if="property.dataType === DataSpecsDataType.ENUM"
     v-model="property.dataSpecsList"
   />
   <!-- 布尔型配置 -->
-  <el-form-item
-    v-if="property.dataType === DataSpecsDataType.BOOL"
-    :rules="[{ required: true, message: '请输入布尔值名称', trigger: 'blur' }]"
-    label="布尔值"
-    prop="property.dataSpecsList"
-  >
+  <el-form-item v-if="property.dataType === DataSpecsDataType.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>
@@ -58,10 +54,6 @@
   <!-- 文本型配置 -->
   <el-form-item
     v-if="property.dataType === DataSpecsDataType.TEXT"
-    :rules="[
-      { required: true, message: '请输入文本字节长度', trigger: 'blur' },
-      { validator: validateTextLength, trigger: 'blur' }
-    ]"
     label="数据长度"
     prop="property.dataSpecs.length"
   >
@@ -74,16 +66,16 @@
     <el-input class="w-255px!" disabled placeholder="String类型的UTC时间戳(毫秒)" />
   </el-form-item>
   <!-- 数组型配置-->
-  <ThinkModelArrayTypeDataSpecs
+  <ThingModelArrayDataSpecs
     v-if="property.dataType === DataSpecsDataType.ARRAY"
     v-model="property.dataSpecs"
   />
-  <!-- TODO puhui999: Struct 属性待完善 -->
-  <el-form-item
-    :rules="[{ required: true, message: '请选择读写类型', trigger: 'change' }]"
-    label="读写类型"
-    prop="property.accessMode"
-  >
+  <!-- Struct 型配置-->
+  <ThingModelStructDataSpecs
+    v-if="property.dataType === DataSpecsDataType.STRUCT"
+    v-model="property.dataSpecsList"
+  />
+  <el-form-item v-if="!isStructDataSpecs" label="读写类型" prop="property.accessMode">
     <el-radio-group v-model="property.accessMode">
       <el-radio label="rw">读写</el-radio>
       <el-radio label="r">只读</el-radio>
@@ -102,22 +94,29 @@
 
 <script lang="ts" setup>
 import { useVModel } from '@vueuse/core'
-import { DataSpecsDataType, dataTypeOptions } from './config'
+import { DataSpecsDataType, dataTypeOptions, validateBoolName } from './config'
 import {
-  ThinkModelArrayTypeDataSpecs,
-  ThinkModelEnumTypeDataSpecs,
-  ThinkModelNumberTypeDataSpecs
+  ThingModelArrayDataSpecs,
+  ThingModelEnumDataSpecs,
+  ThingModelNumberDataSpecs,
+  ThingModelStructDataSpecs
 } from './dataSpecs'
-import { ThinkModelProperty } from '@/api/iot/thinkmodel'
-import { isEmpty } from '@/utils/is'
+import { ThingModelProperty } from '@/api/iot/thingmodel'
 
 /** IoT 物模型数据 */
-defineOptions({ name: 'ThinkModelDataSpecs' })
+defineOptions({ name: 'ThingModelDataSpecs' })
 
-const props = defineProps<{ modelValue: any }>()
+const props = defineProps<{ modelValue: any; isStructDataSpecs?: boolean }>()
 const emits = defineEmits(['update:modelValue'])
-const property = useVModel(props, 'modelValue', emits) as Ref<ThinkModelProperty>
-
+const property = useVModel(props, 'modelValue', emits) as Ref<ThingModelProperty>
+const getDataTypeOptions = computed(() => {
+  return !props.isStructDataSpecs
+    ? dataTypeOptions
+    : dataTypeOptions.filter(
+        (item) =>
+          !([DataSpecsDataType.STRUCT, DataSpecsDataType.ARRAY] as any[]).includes(item.value)
+      )
+}) // 获得数据类型列表
 /** 属性值的数据类型切换时初始化相关数据 */
 const handleChange = (dataType: any) => {
   property.value.dataSpecsList = []
@@ -143,45 +142,6 @@ const handleChange = (dataType: any) => {
       break
   }
 }
-
-// TODO @puhui999:一些校验的规则,是不是写到 utils 里。
-/** 校验布尔值名称 */
-const validateBoolName = (_: 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 validateTextLength = (_: any, value: any, callback: any) => {
-  if (isEmpty(value)) {
-    callback(new Error('文本长度不能为空'))
-    return
-  }
-  if (isNaN(Number(value))) {
-    callback(new Error('文本长度必须是数字'))
-    return
-  }
-  callback()
-}
 </script>
 
 <style lang="scss" scoped>

+ 13 - 49
src/views/iot/thinkmodel/ThinkModelForm.vue → src/views/iot/thingmodel/ThingModelForm.vue

@@ -4,13 +4,13 @@
       ref="formRef"
       v-loading="formLoading"
       :model="formData"
-      :rules="formRules"
+      :rules="ThingModelFormRules"
       label-width="100px"
     >
       <el-form-item label="功能类型" prop="type">
         <el-radio-group v-model="formData.type">
           <el-radio-button
-            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_THINK_MODEL_TYPE)"
+            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_THING_MODEL_TYPE)"
             :key="dict.value"
             :value="dict.value"
           >
@@ -25,7 +25,7 @@
         <el-input v-model="formData.identifier" placeholder="请输入标识符" />
       </el-form-item>
       <!-- 属性配置 -->
-      <ThinkModelDataSpecs
+      <ThingModelDataSpecs
         v-if="formData.type === ProductFunctionTypeEnum.PROPERTY"
         v-model="formData.property"
       />
@@ -40,15 +40,15 @@
 
 <script lang="ts" setup>
 import { ProductVO } from '@/api/iot/product/product'
-import ThinkModelDataSpecs from './ThinkModelDataSpecs.vue'
-import { ProductFunctionTypeEnum, ThinkModelApi, ThinkModelData } from '@/api/iot/thinkmodel'
+import ThingModelDataSpecs from './ThingModelDataSpecs.vue'
+import { ProductFunctionTypeEnum, ThingModelApi, ThingModelData } from '@/api/iot/thingmodel'
 import { IOT_PROVIDE_KEY } from '@/views/iot/utils/constants'
-import { DataSpecsDataType } from './config'
+import { DataSpecsDataType, ThingModelFormRules } from './config'
 import { cloneDeep } from 'lodash-es'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 
 /** IoT 物模型数据表单 */
-defineOptions({ name: 'IoTProductThinkModelForm' })
+defineOptions({ name: 'IoTProductThingModelForm' })
 
 const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) // 注入产品信息
 
@@ -59,7 +59,7 @@ const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref<ThinkModelData>({
+const formData = ref<ThingModelData>({
   type: ProductFunctionTypeEnum.PROPERTY,
   dataType: DataSpecsDataType.INT,
   property: {
@@ -69,43 +69,7 @@ const formData = ref<ThinkModelData>({
     }
   }
 })
-const formRules = reactive({
-  name: [
-    { required: true, message: '功能名称不能为空', trigger: 'blur' },
-    {
-      pattern: /^[\u4e00-\u9fa5a-zA-Z0-9][\u4e00-\u9fa5a-zA-Z0-9\-_/\.]{0,29}$/,
-      message:
-        '支持中文、大小写字母、日文、数字、短划线、下划线、斜杠和小数点,必须以中文、英文或数字开头,不超过 30 个字符',
-      trigger: 'blur'
-    }
-  ],
-  type: [{ required: true, message: '功能类型不能为空', trigger: 'blur' }],
-  identifier: [
-    { required: true, message: '标识符不能为空', trigger: 'blur' },
-    {
-      pattern: /^[a-zA-Z0-9_]{1,50}$/,
-      message: '支持大小写字母、数字和下划线,不超过 50 个字符',
-      trigger: 'blur'
-    },
-    {
-      validator: (_: any, value: string, callback: any) => {
-        const reservedKeywords = ['set', 'get', 'post', 'property', 'event', 'time', 'value']
-        if (reservedKeywords.includes(value)) {
-          callback(
-            new Error(
-              'set, get, post, property, event, time, value 是系统保留字段,不能用于标识符定义'
-            )
-          )
-        } else if (/^\d+$/.test(value)) {
-          callback(new Error('标识符不能是纯数字'))
-        } else {
-          callback()
-        }
-      },
-      trigger: 'blur'
-    }
-  ]
-})
+
 const formRef = ref() // 表单 Ref
 
 /** 打开弹窗 */
@@ -117,7 +81,7 @@ const open = async (type: string, id?: number) => {
   if (id) {
     formLoading.value = true
     try {
-      formData.value = await ThinkModelApi.getThinkModel(id)
+      formData.value = await ThingModelApi.getThingModel(id)
     } finally {
       formLoading.value = false
     }
@@ -131,7 +95,7 @@ const submitForm = async () => {
   await formRef.value.validate()
   formLoading.value = true
   try {
-    const data = cloneDeep(formData.value) as ThinkModelData
+    const data = cloneDeep(formData.value) as ThingModelData
     // 信息补全
     data.productId = product!.value.id
     data.productKey = product!.value.productKey
@@ -140,10 +104,10 @@ const submitForm = async () => {
     data.property.identifier = data.identifier
     data.property.name = data.name
     if (formType.value === 'create') {
-      await ThinkModelApi.createThinkModel(data)
+      await ThingModelApi.createThingModel(data)
       message.success(t('common.createSuccess'))
     } else {
-      await ThinkModelApi.updateThinkModel(data)
+      await ThingModelApi.updateThingModel(data)
       message.success(t('common.updateSuccess'))
     }
   } finally {

+ 151 - 0
src/views/iot/thingmodel/config.ts

@@ -0,0 +1,151 @@
+import {isEmpty} from '@/utils/is'
+
+/** dataSpecs 数值型数据结构 */
+export interface DataSpecsNumberDataVO {
+  dataType: 'int' | 'float' | 'double' // 数据类型,取值为 INT、FLOAT 或 DOUBLE
+  max: string // 最大值,必须与 dataType 设置一致,且为 STRING 类型
+  min: string // 最小值,必须与 dataType 设置一致,且为 STRING 类型
+  step: string // 步长,必须与 dataType 设置一致,且为 STRING 类型
+  precise?: string // 精度,当 dataType 为 FLOAT 或 DOUBLE 时可选
+  defaultValue?: string // 默认值,可选
+  unit: string // 单位的符号
+  unitName: string // 单位的名称
+}
+
+/** dataSpecs 枚举型数据结构 */
+export interface DataSpecsEnumOrBoolDataVO {
+  dataType: 'enum' | 'bool'
+  defaultValue?: string // 默认值,可选
+  name: string // 枚举项的名称
+  value: number | undefined // 枚举值
+}
+
+/** 属性值的数据类型 */
+export const DataSpecsDataType = {
+  INT: 'int',
+  FLOAT: 'float',
+  DOUBLE: 'double',
+  ENUM: 'enum',
+  BOOL: 'bool',
+  TEXT: 'text',
+  DATE: 'date',
+  STRUCT: 'struct',
+  ARRAY: 'array'
+} as const
+
+/** 物体模型数据类型配置项 */
+export const dataTypeOptions = [
+  { value: DataSpecsDataType.INT, label: 'int32 (整数型)' },
+  { value: DataSpecsDataType.FLOAT, label: 'float (单精度浮点型)' },
+  { value: DataSpecsDataType.DOUBLE, label: 'double (双精度浮点型)' },
+  { value: DataSpecsDataType.ENUM, label: 'enum(枚举型)' },
+  { value: DataSpecsDataType.BOOL, label: 'bool (布尔型)' },
+  { value: DataSpecsDataType.TEXT, label: 'text (文本型)' },
+  { value: DataSpecsDataType.DATE, label: 'date (时间型)' },
+  { value: DataSpecsDataType.STRUCT, label: 'struct (结构体)' },
+  { value: DataSpecsDataType.ARRAY, label: 'array (数组)' }
+]
+
+/** 获得物体模型数据类型配置项名称 */
+export const getDataTypeOptionsLabel = (value: string) => {
+  return dataTypeOptions.find((option) => option.value === value)?.label
+}
+
+/** 公共校验规则 */
+export const ThingModelFormRules = {
+  name: [
+    { required: true, message: '功能名称不能为空', trigger: 'blur' },
+    {
+      pattern: /^[\u4e00-\u9fa5a-zA-Z0-9][\u4e00-\u9fa5a-zA-Z0-9\-_/\.]{0,29}$/,
+      message:
+        '支持中文、大小写字母、日文、数字、短划线、下划线、斜杠和小数点,必须以中文、英文或数字开头,不超过 30 个字符',
+      trigger: 'blur'
+    }
+  ],
+  type: [{ required: true, message: '功能类型不能为空', trigger: 'blur' }],
+  identifier: [
+    { required: true, message: '标识符不能为空', trigger: 'blur' },
+    {
+      pattern: /^[a-zA-Z0-9_]{1,50}$/,
+      message: '支持大小写字母、数字和下划线,不超过 50 个字符',
+      trigger: 'blur'
+    },
+    {
+      validator: (_: any, value: string, callback: any) => {
+        const reservedKeywords = ['set', 'get', 'post', 'property', 'event', 'time', 'value']
+        if (reservedKeywords.includes(value)) {
+          callback(
+            new Error(
+              'set, get, post, property, event, time, value 是系统保留字段,不能用于标识符定义'
+            )
+          )
+        } else if (/^\d+$/.test(value)) {
+          callback(new Error('标识符不能是纯数字'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'blur'
+    }
+  ],
+  'property.dataSpecs.childDataType': [{ required: true, message: '元素类型不能为空' }],
+  'property.dataSpecs.size': [
+    { required: true, message: '元素个数不能为空' },
+    {
+      validator: (_: any, value: any, callback: any) => {
+        if (isEmpty(value)) {
+          callback(new Error('元素个数不能为空'))
+          return
+        }
+        if (isNaN(Number(value))) {
+          callback(new Error('元素个数必须是数字'))
+          return
+        }
+        callback()
+      },
+      trigger: 'blur'
+    }
+  ],
+  'property.dataSpecs.length': [
+    { required: true, message: '请输入文本字节长度', trigger: 'blur' },
+    {
+      validator: (_: any, value: any, callback: any) => {
+        if (isEmpty(value)) {
+          callback(new Error('文本长度不能为空'))
+          return
+        }
+        if (isNaN(Number(value))) {
+          callback(new Error('文本长度必须是数字'))
+          return
+        }
+        callback()
+      },
+      trigger: 'blur'
+    }
+  ],
+  'property.accessMode': [{ required: true, message: '请选择读写类型', trigger: 'change' }]
+}
+/** 校验布尔值名称 */
+export const validateBoolName = (_: 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()
+}

+ 15 - 27
src/views/iot/thinkmodel/dataSpecs/ThinkModelArrayTypeDataSpecs.vue → src/views/iot/thingmodel/dataSpecs/ThingModelArrayDataSpecs.vue

@@ -1,10 +1,6 @@
 <template>
-  <el-form-item
-    :rules="[{ required: true, message: '元素类型不能为空' }]"
-    label="元素类型"
-    prop="property.dataSpecs.childDataType"
-  >
-    <el-radio-group v-model="dataSpecs.childDataType">
+  <el-form-item label="元素类型" prop="property.dataSpecs.childDataType">
+    <el-radio-group v-model="dataSpecs.childDataType" @change="handleChange">
       <template v-for="item in dataTypeOptions" :key="item.value">
         <el-radio
           v-if="
@@ -19,43 +15,35 @@
       </template>
     </el-radio-group>
   </el-form-item>
-  <el-form-item
-    :rules="[
-      { required: true, message: '元素个数不能为空' },
-      { validator: validateSize, trigger: 'blur' }
-    ]"
-    label="元素个数"
-    prop="property.dataSpecs.size"
-  >
+  <el-form-item label="元素个数" prop="property.dataSpecs.size">
     <el-input v-model="dataSpecs.size" placeholder="请输入数组中的元素个数" />
   </el-form-item>
+  <!-- Struct 型配置-->
+  <ThingModelStructDataSpecs
+    v-if="dataSpecs.childDataType === DataSpecsDataType.STRUCT"
+    v-model="dataSpecs.dataSpecsList"
+  />
 </template>
 
 <script lang="ts" setup>
 import { useVModel } from '@vueuse/core'
 import { DataSpecsDataType, dataTypeOptions } from '../config'
-import { isEmpty } from '@/utils/is'
-
-// TODO @puhui999:参数校验,是不是还是定义一个变量,统一管,好阅读点哈?
+import ThingModelStructDataSpecs from './ThingModelStructDataSpecs.vue'
 
 /** 数组型的 dataSpecs 配置组件 */
-defineOptions({ name: 'ThinkModelArrayTypeDataSpecs' })
+defineOptions({ name: 'ThingModelArrayDataSpecs' })
 
 const props = defineProps<{ modelValue: any }>()
 const emits = defineEmits(['update:modelValue'])
 const dataSpecs = useVModel(props, 'modelValue', emits) as Ref<any>
 
-/** 校验元素个数 */
-const validateSize = (_: any, value: any, callback: any) => {
-  if (isEmpty(value)) {
-    callback(new Error('元素个数不能为空'))
-    return
-  }
-  if (isNaN(Number(value))) {
-    callback(new Error('元素个数必须是数字'))
+/** 元素类型改变时间。当值为 struct 时,对 dataSpecs 中的 dataSpecsList 进行初始化 */
+const handleChange = (val: string) => {
+  if (val !== DataSpecsDataType.STRUCT) {
     return
   }
-  callback()
+
+  dataSpecs.value.dataSpecsList = []
 }
 </script>
 

+ 1 - 4
src/views/iot/thinkmodel/dataSpecs/ThinkModelEnumTypeDataSpecs.vue → src/views/iot/thingmodel/dataSpecs/ThingModelEnumDataSpecs.vue

@@ -2,7 +2,6 @@
   <el-form-item
     :rules="[{ required: true, validator: validateEnumList, trigger: 'change' }]"
     label="枚举项"
-    prop="property.dataSpecsList"
   >
     <div class="flex flex-col">
       <div class="flex items-center">
@@ -48,7 +47,7 @@ import { DataSpecsDataType, DataSpecsEnumOrBoolDataVO } from '../config'
 import { isEmpty } from '@/utils/is'
 
 /** 枚举型的 dataSpecs 配置组件 */
-defineOptions({ name: 'ThinkModelEnumTypeDataSpecs' })
+defineOptions({ name: 'ThingModelEnumDataSpecs' })
 
 const props = defineProps<{ modelValue: any }>()
 const emits = defineEmits(['update:modelValue'])
@@ -113,7 +112,6 @@ const validateEnumName = (_: any, value: string, callback: any) => {
     callback(new Error('枚举描述长度不能超过20个字符'))
     return
   }
-
   callback()
 }
 
@@ -147,7 +145,6 @@ const validateEnumList = (_: any, __: any, callback: any) => {
     callback(new Error('存在重复的枚举值'))
     return
   }
-
   callback()
 }
 </script>

+ 1 - 1
src/views/iot/thinkmodel/dataSpecs/ThinkModelNumberTypeDataSpecs.vue → src/views/iot/thingmodel/dataSpecs/ThingModelNumberDataSpecs.vue

@@ -62,7 +62,7 @@ import { UnifyUnitSpecsDTO } from '@/views/iot/utils/constants'
 import { DataSpecsNumberDataVO } from '../config'
 
 /** 数值型的 dataSpecs 配置组件 */
-defineOptions({ name: 'ThinkModelNumberTypeDataSpecs' })
+defineOptions({ name: 'ThingModelNumberDataSpecs' })
 
 const props = defineProps<{ modelValue: any }>()
 const emits = defineEmits(['update:modelValue'])

+ 161 - 0
src/views/iot/thingmodel/dataSpecs/ThingModelStructDataSpecs.vue

@@ -0,0 +1,161 @@
+<template>
+  <!-- struct 数据展示 -->
+  <el-form-item
+    :rules="[{ required: true, validator: validateList, trigger: 'change' }]"
+    label="JSON 对象"
+  >
+    <div
+      v-for="(item, index) in dataSpecsList"
+      :key="index"
+      class="w-1/1 struct-item flex justify-between px-10px mb-10px"
+    >
+      <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>
+      </div>
+    </div>
+    <el-button link type="primary" @click="openStructForm(null)">+新增参数</el-button>
+  </el-form-item>
+
+  <!-- struct 表单 -->
+  <Dialog v-model="dialogVisible" :title="dialogTitle" append-to-body>
+    <el-form
+      ref="structFormRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="ThingModelFormRules"
+      label-width="100px"
+    >
+      <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>
+      <!-- 属性配置 -->
+      <ThingModelDataSpecs v-model="formData.property" is-struct-data-specs />
+    </el-form>
+
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { useVModel } from '@vueuse/core'
+import ThingModelDataSpecs from '../ThingModelDataSpecs.vue'
+import { DataSpecsDataType, ThingModelFormRules } from '../config'
+import { isEmpty } from '@/utils/is'
+
+/** Struct 型的 dataSpecs 配置组件 */
+defineOptions({ name: 'ThingModelStructDataSpecs' })
+
+const props = defineProps<{ modelValue: any }>()
+const emits = defineEmits(['update:modelValue'])
+const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<any[]>
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('新增参数') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const structFormRef = ref() // 表单 ref
+const formData = ref<any>({
+  property: {
+    dataType: DataSpecsDataType.INT,
+    dataSpecs: {
+      dataType: DataSpecsDataType.INT
+    }
+  }
+})
+
+/** 打开 struct 表单 */
+const openStructForm = (val: any) => {
+  dialogVisible.value = true
+  resetForm()
+  if (isEmpty(val)) {
+    return
+  }
+  // 编辑时回显数据
+  formData.value = {
+    identifier: val.identifier,
+    name: val.name,
+    description: val.description,
+    property: {
+      dataType: val.childDataType,
+      dataSpecs: val.dataSpecs,
+      dataSpecsList: val.dataSpecsList
+    }
+  }
+}
+/** 删除 struct 项 */
+const deleteStructItem = (index: number) => {
+  dataSpecsList.value.splice(index, 1)
+}
+
+/** 添加参数 */
+const submitForm = async () => {
+  await structFormRef.value.validate()
+
+  try {
+    const data = unref(formData)
+    // 构建数据对象
+    const item = {
+      identifier: data.identifier,
+      name: data.name,
+      description: data.description,
+      dataType: DataSpecsDataType.STRUCT,
+      childDataType: data.property.dataType,
+      dataSpecs:
+        !!data.property.dataSpecs && Object.keys(data.property.dataSpecs).length > 1
+          ? data.property.dataSpecs
+          : undefined,
+      dataSpecsList: data.property.dataSpecsList
+    }
+
+    // 查找是否已有相同 identifier 的项
+    const existingIndex = dataSpecsList.value.findIndex(
+      (spec) => spec.identifier === data.identifier
+    )
+    if (existingIndex > -1) {
+      // 更新已有项
+      dataSpecsList.value[existingIndex] = item
+    } else {
+      // 添加新项
+      dataSpecsList.value.push(item)
+    }
+  } finally {
+    // 隐藏对话框
+    dialogVisible.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    property: {
+      dataType: DataSpecsDataType.INT,
+      dataSpecs: {
+        dataType: DataSpecsDataType.INT
+      }
+    }
+  }
+  structFormRef.value?.resetFields()
+}
+
+/** 校验 struct 不能为空 */
+const validateList = (_: any, __: any, callback: any) => {
+  if (isEmpty(dataSpecsList.value)) {
+    callback(new Error('struct 不能为空'))
+    return
+  }
+  callback()
+}
+</script>
+
+<style lang="scss" scoped>
+.struct-item {
+  background-color: #e4f2fd;
+}
+</style>

+ 11 - 0
src/views/iot/thingmodel/dataSpecs/index.ts

@@ -0,0 +1,11 @@
+import ThingModelEnumDataSpecs from './ThingModelEnumDataSpecs.vue'
+import ThingModelNumberDataSpecs from './ThingModelNumberDataSpecs.vue'
+import ThingModelArrayDataSpecs from './ThingModelArrayDataSpecs.vue'
+import ThingModelStructDataSpecs from './ThingModelStructDataSpecs.vue'
+
+export {
+  ThingModelEnumDataSpecs,
+  ThingModelNumberDataSpecs,
+  ThingModelArrayDataSpecs,
+  ThingModelStructDataSpecs
+}

+ 10 - 11
src/views/iot/thinkmodel/index.vue → src/views/iot/thingmodel/index.vue

@@ -1,4 +1,3 @@
-<!-- TODO 目录,应该是 thinkModel 哈。 -->
 <template>
   <ContentWrap>
     <!-- 搜索工作栏 -->
@@ -17,7 +16,7 @@
           placeholder="请选择功能类型"
         >
           <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_THINK_MODEL_TYPE)"
+            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_THING_MODEL_TYPE)"
             :key="dict.value"
             :label="dict.label"
             :value="dict.value"
@@ -50,7 +49,7 @@
       <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
         <el-table-column align="center" label="功能类型" prop="type">
           <template #default="scope">
-            <dict-tag :type="DICT_TYPE.IOT_PRODUCT_THINK_MODEL_TYPE" :value="scope.row.type" />
+            <dict-tag :type="DICT_TYPE.IOT_PRODUCT_THING_MODEL_TYPE" :value="scope.row.type" />
           </template>
         </el-table-column>
         <el-table-column align="center" label="功能名称" prop="name" />
@@ -97,23 +96,23 @@
     </el-tabs>
   </ContentWrap>
   <!-- 表单弹窗:添加/修改 -->
-  <ThinkModelForm ref="formRef" @success="getList" />
+  <ThingModelForm ref="formRef" @success="getList" />
 </template>
 <script lang="ts" setup>
-import { ThinkModelApi, ThinkModelData } from '@/api/iot/thinkmodel'
+import { ThingModelApi, ThingModelData } from '@/api/iot/thingmodel'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import ThinkModelForm from './ThinkModelForm.vue'
+import ThingModelForm from './ThingModelForm.vue'
 import { ProductVO } from '@/api/iot/product/product'
 import { IOT_PROVIDE_KEY } from '@/views/iot/utils/constants'
-import { getDataTypeOptionsLabel } from '@/views/iot/thinkmodel/config'
+import { getDataTypeOptionsLabel } from '@/views/iot/thingmodel/config'
 
-defineOptions({ name: 'IoTProductThinkModel' })
+defineOptions({ name: 'IoTProductThingModel' })
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 
 const loading = ref(true) // 列表的加载中
-const list = ref<ThinkModelData[]>([]) // 列表的数据
+const list = ref<ThingModelData[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
 const queryParams = reactive({
   pageNo: 1,
@@ -131,7 +130,7 @@ const getList = async () => {
   loading.value = true
   try {
     queryParams.productId = product?.value?.id || -1
-    const data = await ThinkModelApi.getThinkModelPage(queryParams)
+    const data = await ThingModelApi.getThingModelPage(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -163,7 +162,7 @@ const handleDelete = async (id: number) => {
     // 删除的二次确认
     await message.delConfirm()
     // 发起删除
-    await ThinkModelApi.deleteThinkModel(id)
+    await ThingModelApi.deleteThingModel(id)
     message.success(t('common.delSuccess'))
     // 刷新列表
     await getList()

+ 0 - 50
src/views/iot/thinkmodel/config.ts

@@ -1,50 +0,0 @@
-/** dataSpecs 数值型数据结构 */
-export interface DataSpecsNumberDataVO {
-  dataType: 'int' | 'float' | 'double' // 数据类型,取值为 INT、FLOAT 或 DOUBLE
-  max: string // 最大值,必须与 dataType 设置一致,且为 STRING 类型
-  min: string // 最小值,必须与 dataType 设置一致,且为 STRING 类型
-  step: string // 步长,必须与 dataType 设置一致,且为 STRING 类型
-  precise?: string // 精度,当 dataType 为 FLOAT 或 DOUBLE 时可选
-  defaultValue?: string // 默认值,可选
-  unit: string // 单位的符号
-  unitName: string // 单位的名称
-}
-
-/** dataSpecs 枚举型数据结构 */
-export interface DataSpecsEnumOrBoolDataVO {
-  dataType: 'enum' | 'bool'
-  defaultValue?: string // 默认值,可选
-  name: string // 枚举项的名称
-  value: number | undefined // 枚举值
-}
-
-/** 属性值的数据类型 */
-export const DataSpecsDataType = {
-  INT: 'int',
-  FLOAT: 'float',
-  DOUBLE: 'double',
-  ENUM: 'enum',
-  BOOL: 'bool',
-  TEXT: 'text',
-  DATE: 'date',
-  STRUCT: 'struct',
-  ARRAY: 'array'
-} as const
-
-/** 物体模型数据类型配置项 */
-export const dataTypeOptions = [
-  { value: DataSpecsDataType.INT, label: 'int32 (整数型)' },
-  { value: DataSpecsDataType.FLOAT, label: 'float (单精度浮点型)' },
-  { value: DataSpecsDataType.DOUBLE, label: 'double (双精度浮点型)' },
-  { value: DataSpecsDataType.ENUM, label: 'enum(枚举型)' },
-  { value: DataSpecsDataType.BOOL, label: 'bool (布尔型)' },
-  { value: DataSpecsDataType.TEXT, label: 'text (文本型)' },
-  { value: DataSpecsDataType.DATE, label: 'date (时间型)' },
-  { value: DataSpecsDataType.STRUCT, label: 'struct (结构体)' },
-  { value: DataSpecsDataType.ARRAY, label: 'array (数组)' }
-]
-
-/** 获得物体模型数据类型配置项名称 */
-export const getDataTypeOptionsLabel = (value: string) => {
-  return dataTypeOptions.find((option) => option.value === value)?.label
-}

+ 0 - 5
src/views/iot/thinkmodel/dataSpecs/index.ts

@@ -1,5 +0,0 @@
-import ThinkModelEnumTypeDataSpecs from './ThinkModelEnumTypeDataSpecs.vue'
-import ThinkModelNumberTypeDataSpecs from './ThinkModelNumberTypeDataSpecs.vue'
-import ThinkModelArrayTypeDataSpecs from './ThinkModelArrayTypeDataSpecs.vue'
-
-export { ThinkModelEnumTypeDataSpecs, ThinkModelNumberTypeDataSpecs, ThinkModelArrayTypeDataSpecs }