Browse Source

!622 【功能完善】IOT: 产品物模型
Merge pull request !622 from puhui999/feature/iot

芋道源码 8 tháng trước cách đây
mục cha
commit
c4ba7ed43e

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

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

+ 3 - 3
src/utils/dict.ts

@@ -1,8 +1,8 @@
 /**
  * 数据字典工具类
  */
-import { useDictStoreWithOut } from '@/store/modules/dict'
-import { ElementPlusInfoType } from '@/types/elementPlus'
+import {useDictStoreWithOut} from '@/store/modules/dict'
+import {ElementPlusInfoType} from '@/types/elementPlus'
 
 const dictStore = useDictStoreWithOut()
 
@@ -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_FUNCTION_TYPE = 'iot_product_function_type', // IOT 产品功能类型
+  IOT_PRODUCT_THINK_MODEL_TYPE = 'iot_product_think_model_type', // IOT 产品功能类型
   IOT_DATA_TYPE = 'iot_data_type', // IOT 数据类型
   IOT_UNIT_TYPE = 'iot_unit_type', // IOT 单位类型
   IOT_RW_TYPE = 'iot_rw_type', // IOT 读写类型

+ 0 - 111
src/views/iot/product/product/detail/ThingModel/ThingModelDataSpecs.vue

@@ -1,111 +0,0 @@
-<template>
-  <el-form-item label="数据类型" prop="dataType">
-    <el-select v-model="property.dataType" placeholder="请选择数据类型" @change="handleChange">
-      <el-option
-        v-for="option in dataTypeOptions"
-        :key="option.value"
-        :label="option.label"
-        :value="option.value"
-      />
-    </el-select>
-  </el-form-item>
-  <!-- 数值型配置 -->
-  <ThingModelNumberTypeDataSpecs
-    v-if="
-      [DataSpecsDataType.INT, DataSpecsDataType.DOUBLE, DataSpecsDataType.FLOAT].includes(
-        property.dataType || ''
-      )
-    "
-    v-model="property.dataSpecs"
-  />
-  <!-- 枚举型配置 -->
-  <ThingModelEnumTypeDataSpecs
-    v-if="property.dataType === DataSpecsDataType.ENUM"
-    v-model="property.dataSpecsList"
-  />
-  <!-- 布尔型配置 -->
-  <el-form-item v-if="property.dataType === DataSpecsDataType.BOOL" label="布尔值" prop="bool">
-    <template v-for="item 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-input
-          v-model="item.name"
-          :placeholder="`如:${item.value === 0 ? '关' : '开'}`"
-          class="w-255px!"
-        />
-      </div>
-    </template>
-  </el-form-item>
-  <!-- 文本型配置 -->
-  <el-form-item v-if="property.dataType === DataSpecsDataType.TEXT" label="数据长度" prop="text">
-    <el-input v-model="property.dataSpecs.length" class="w-255px!" placeholder="请输入文本字节长度">
-      <template #append>字节</template>
-    </el-input>
-  </el-form-item>
-  <!-- 时间型配置 -->
-  <el-form-item v-if="property.dataType === DataSpecsDataType.DATE" label="时间格式" prop="date">
-    <el-input class="w-255px!" disabled placeholder="String类型的UTC时间戳(毫秒)" />
-  </el-form-item>
-  <!-- 数组型配置-->
-  <ThingModelArrayTypeDataSpecs
-    v-if="property.dataType === DataSpecsDataType.ARRAY"
-    v-model="property.dataSpecs"
-  />
-  <!-- TODO puhui999: Struct 属性待完善 -->
-  <el-form-item label="读写类型" prop="accessMode">
-    <el-radio-group v-model="property.accessMode">
-      <el-radio label="rw">读写</el-radio>
-      <el-radio label="r">只读</el-radio>
-    </el-radio-group>
-  </el-form-item>
-  <el-form-item label="属性描述" prop="description">
-    <el-input v-model="property.description" placeholder="请输入属性描述" type="textarea" />
-  </el-form-item>
-</template>
-
-<script lang="ts" setup>
-import { useVModel } from '@vueuse/core'
-import { DataSpecsDataType, dataTypeOptions } from './config'
-import {
-  ThingModelArrayTypeDataSpecs,
-  ThingModelEnumTypeDataSpecs,
-  ThingModelNumberTypeDataSpecs
-} from './dataSpecs'
-import { ThingModelProperty } from '@/api/iot/thinkmodelfunction'
-
-/** IoT 物模型数据 */
-defineOptions({ name: 'ThingModelDataSpecs' })
-
-const props = defineProps<{ modelValue: any }>()
-const emits = defineEmits(['update:modelValue'])
-const property = useVModel(props, 'modelValue', emits) as Ref<ThingModelProperty>
-
-/** 属性值的数据类型切换时初始化相关数据 */
-const handleChange = (dataType: any) => {
-  property.value.dataSpecsList = []
-  property.value.dataSpecs = {}
-
-  property.value.dataSpecs.dataType = dataType
-  switch (dataType) {
-    case DataSpecsDataType.ENUM:
-      property.value.dataSpecsList.push({
-        dataType: DataSpecsDataType.ENUM,
-        name: '', // 枚举项的名称
-        value: undefined // 枚举值
-      })
-      break
-    case DataSpecsDataType.BOOL:
-      for (let i = 0; i < 2; i++) {
-        property.value.dataSpecsList.push({
-          dataType: DataSpecsDataType.BOOL,
-          name: '', // 布尔值的名称
-          value: i // 布尔值
-        })
-      }
-      break
-  }
-}
-</script>
-
-<style lang="scss" scoped></style>

+ 0 - 57
src/views/iot/product/product/detail/ThingModel/dataSpecs/ThingModelEnumTypeDataSpecs.vue

@@ -1,57 +0,0 @@
-<template>
-  <el-form-item label="枚举项" prop="enum">
-    <div class="flex flex-col">
-      <div class="flex items-center">
-        <span class="flex-1"> 参数值 </span>
-        <span class="flex-1"> 参数描述 </span>
-      </div>
-      <div
-        v-for="(item, index) in dataSpecsList"
-        :key="index"
-        class="flex items-center justify-between mb-5px"
-      >
-        <el-input v-model="item.value" placeholder="请输入枚举值,如'0'" />
-        <span class="mx-2">~</span>
-        <el-input v-model="item.name" placeholder="对该枚举项的描述" />
-        <el-button link type="primary" class="ml-10px" @click="deleteEnum(index)">删除</el-button>
-      </div>
-      <el-button link type="primary" @click="addEnum">+添加枚举项</el-button>
-    </div>
-  </el-form-item>
-</template>
-
-<script lang="ts" setup>
-import { useVModel } from '@vueuse/core'
-import {
-  DataSpecsDataType,
-  DataSpecsEnumOrBoolDataVO
-} from '@/views/iot/product/product/detail/ThingModel/config'
-
-/** 枚举型的 dataSpecs 配置组件 */
-defineOptions({ name: 'ThingModelEnumTypeDataSpecs' })
-
-const props = defineProps<{ modelValue: any }>()
-const emits = defineEmits(['update:modelValue'])
-const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<DataSpecsEnumOrBoolDataVO[]>
-const message = useMessage()
-
-/** 添加枚举项 */
-const addEnum = () => {
-  dataSpecsList.value.push({
-    dataType: DataSpecsDataType.ENUM,
-    name: '', // 枚举项的名称
-    value: undefined // 枚举值
-  })
-}
-
-/** 删除枚举项 */
-const deleteEnum = (index: number) => {
-  if (dataSpecsList.value.length === 1) {
-    message.warning('至少需要一个枚举项')
-    return
-  }
-  dataSpecsList.value.splice(index, 1)
-}
-</script>
-
-<style lang="scss" scoped></style>

+ 0 - 50
src/views/iot/product/product/detail/ThingModel/dataSpecs/ThingModelNumberTypeDataSpecs.vue

@@ -1,50 +0,0 @@
-<template>
-  <el-form-item label="取值范围" prop="max">
-    <div class="flex items-center justify-between">
-      <el-input v-model="dataSpecs.min" placeholder="请输入最小值" />
-      <span class="mx-2">~</span>
-      <el-input v-model="dataSpecs.max" placeholder="请输入最大值" />
-    </div>
-  </el-form-item>
-  <el-form-item label="步长" prop="step">
-    <el-input v-model="dataSpecs.step" placeholder="请输入步长" />
-  </el-form-item>
-  <el-form-item label="单位" prop="unit">
-    <el-select
-      :model-value="dataSpecs.unit ? dataSpecs.unitName + '-' + dataSpecs.unit : ''"
-      filterable
-      placeholder="请选择单位"
-      style="width: 240px"
-      @change="unitChange"
-    >
-      <el-option
-        v-for="(item, index) in UnifyUnitSpecsDTO"
-        :key="index"
-        :label="item.Name + '-' + item.Symbol"
-        :value="item.Name + '-' + item.Symbol"
-      />
-    </el-select>
-  </el-form-item>
-</template>
-
-<script lang="ts" setup>
-import { useVModel } from '@vueuse/core'
-import { UnifyUnitSpecsDTO } from '@/views/iot/utils/constants'
-import { DataSpecsNumberDataVO } from '../config'
-
-/** 数值型的 dataSpecs 配置组件 */
-defineOptions({ name: 'ThingModelNumberTypeDataSpecs' })
-
-const props = defineProps<{ modelValue: any }>()
-const emits = defineEmits(['update:modelValue'])
-const dataSpecs = useVModel(props, 'modelValue', emits) as Ref<DataSpecsNumberDataVO>
-
-/** 单位发生变化时触发 */
-const unitChange = (UnitSpecs: string) => {
-  const [unitName, unit] = UnitSpecs.split('-')
-  dataSpecs.value.unitName = unitName
-  dataSpecs.value.unit = unit
-}
-</script>
-
-<style lang="scss" scoped></style>

+ 0 - 5
src/views/iot/product/product/detail/ThingModel/dataSpecs/index.ts

@@ -1,5 +0,0 @@
-import ThingModelEnumTypeDataSpecs from './ThingModelEnumTypeDataSpecs.vue'
-import ThingModelNumberTypeDataSpecs from './ThingModelNumberTypeDataSpecs.vue'
-import ThingModelArrayTypeDataSpecs from './ThingModelArrayTypeDataSpecs.vue'
-
-export { ThingModelEnumTypeDataSpecs, ThingModelNumberTypeDataSpecs, ThingModelArrayTypeDataSpecs }

+ 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="thingModel">
-        <IoTProductThingModel ref="thingModelRef" />
+      <el-tab-pane label="功能定义" lazy name="thinkModel">
+        <IoTProductThinkModel ref="thinkModelRef" />
       </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 IoTProductThingModel from './ThingModel/index.vue'
+import IoTProductThinkModel from '@/views/iot/thinkmodel/index.vue'
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import { useRouter } from 'vue-router'
 import { IOT_PROVIDE_KEY } from '@/views/iot/utils/constants'

+ 1 - 1
src/views/iot/product/product/index.vue

@@ -309,7 +309,7 @@ const openObjectModel = (item: ProductVO) => {
   push({
     name: 'IoTProductDetail',
     params: { id: item.id },
-    query: { tab: 'thingModel' }
+    query: { tab: 'thinkModel' }
   })
 }
 

+ 195 - 0
src/views/iot/thinkmodel/ThinkModelDataSpecs.vue

@@ -0,0 +1,195 @@
+<template>
+  <el-form-item
+    :rules="[{ required: true, message: '请选择数据类型', trigger: 'change' }]"
+    label="数据类型"
+    prop="property.dataType"
+  >
+    <el-select v-model="property.dataType" placeholder="请选择数据类型" @change="handleChange">
+      <el-option
+        v-for="option in dataTypeOptions"
+        :key="option.value"
+        :label="option.label"
+        :value="option.value"
+      />
+    </el-select>
+  </el-form-item>
+  <!-- 数值型配置 -->
+  <ThinkModelNumberTypeDataSpecs
+    v-if="
+      [DataSpecsDataType.INT, DataSpecsDataType.DOUBLE, DataSpecsDataType.FLOAT].includes(
+        property.dataType || ''
+      )
+    "
+    v-model="property.dataSpecs"
+  />
+  <!-- 枚举型配置 -->
+  <ThinkModelEnumTypeDataSpecs
+    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"
+  >
+    <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"
+            :placeholder="`如:${item.value === 0 ? '关' : '开'}`"
+            class="w-255px!"
+          />
+        </el-form-item>
+      </div>
+    </template>
+  </el-form-item>
+  <!-- 文本型配置 -->
+  <el-form-item
+    v-if="property.dataType === DataSpecsDataType.TEXT"
+    :rules="[
+      { required: true, message: '请输入文本字节长度', trigger: 'blur' },
+      { validator: validateTextLength, trigger: 'blur' }
+    ]"
+    label="数据长度"
+    prop="property.dataSpecs.length"
+  >
+    <el-input v-model="property.dataSpecs.length" class="w-255px!" placeholder="请输入文本字节长度">
+      <template #append>字节</template>
+    </el-input>
+  </el-form-item>
+  <!-- 时间型配置 -->
+  <el-form-item v-if="property.dataType === DataSpecsDataType.DATE" label="时间格式" prop="date">
+    <el-input class="w-255px!" disabled placeholder="String类型的UTC时间戳(毫秒)" />
+  </el-form-item>
+  <!-- 数组型配置-->
+  <ThinkModelArrayTypeDataSpecs
+    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"
+  >
+    <el-radio-group v-model="property.accessMode">
+      <el-radio label="rw">读写</el-radio>
+      <el-radio label="r">只读</el-radio>
+    </el-radio-group>
+  </el-form-item>
+  <el-form-item label="属性描述" prop="description">
+    <el-input
+      v-model="property.description"
+      :maxlength="200"
+      :rows="3"
+      placeholder="请输入属性描述"
+      type="textarea"
+    />
+  </el-form-item>
+</template>
+
+<script lang="ts" setup>
+import { useVModel } from '@vueuse/core'
+import { DataSpecsDataType, dataTypeOptions } from './config'
+import {
+  ThinkModelArrayTypeDataSpecs,
+  ThinkModelEnumTypeDataSpecs,
+  ThinkModelNumberTypeDataSpecs
+} from './dataSpecs'
+import { ThinkModelProperty } from '@/api/iot/thinkmodel'
+import { isEmpty } from '@/utils/is'
+
+/** IoT 物模型数据 */
+defineOptions({ name: 'ThinkModelDataSpecs' })
+
+const props = defineProps<{ modelValue: any }>()
+const emits = defineEmits(['update:modelValue'])
+const property = useVModel(props, 'modelValue', emits) as Ref<ThinkModelProperty>
+
+/** 属性值的数据类型切换时初始化相关数据 */
+const handleChange = (dataType: any) => {
+  property.value.dataSpecsList = []
+  property.value.dataSpecs = {}
+
+  property.value.dataSpecs.dataType = dataType
+  switch (dataType) {
+    case DataSpecsDataType.ENUM:
+      property.value.dataSpecsList.push({
+        dataType: DataSpecsDataType.ENUM,
+        name: '', // 枚举项的名称
+        value: undefined // 枚举值
+      })
+      break
+    case DataSpecsDataType.BOOL:
+      for (let i = 0; i < 2; i++) {
+        property.value.dataSpecsList.push({
+          dataType: DataSpecsDataType.BOOL,
+          name: '', // 布尔值的名称
+          value: i // 布尔值
+        })
+      }
+      break
+  }
+}
+
+/** 校验布尔值名称 */
+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>
+:deep(.el-form-item) {
+  .el-form-item {
+    margin-bottom: 0;
+  }
+}
+</style>

+ 27 - 31
src/views/iot/product/product/detail/ThingModel/ThingModelForm.vue → src/views/iot/thinkmodel/ThinkModelForm.vue

@@ -9,10 +9,13 @@
     >
       <el-form-item label="功能类型" prop="type">
         <el-radio-group v-model="formData.type">
-          <!-- TODO @puhui999:从字典拿 -->
-          <el-radio-button :value="1"> 属性</el-radio-button>
-          <el-radio-button :value="2"> 服务</el-radio-button>
-          <el-radio-button :value="3"> 事件</el-radio-button>
+          <el-radio-button
+            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_THINK_MODEL_TYPE)"
+            :key="dict.value"
+            :value="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio-button>
         </el-radio-group>
       </el-form-item>
       <el-form-item label="功能名称" prop="name">
@@ -22,7 +25,7 @@
         <el-input v-model="formData.identifier" placeholder="请输入标识符" />
       </el-form-item>
       <!-- 属性配置 -->
-      <ThingModelDataSpecs
+      <ThinkModelDataSpecs
         v-if="formData.type === ProductFunctionTypeEnum.PROPERTY"
         v-model="formData.property"
       />
@@ -37,30 +40,26 @@
 
 <script lang="ts" setup>
 import { ProductVO } from '@/api/iot/product/product'
-import ThingModelDataSpecs from './ThingModelDataSpecs.vue'
-import {
-  ProductFunctionTypeEnum,
-  ThingModelData,
-  ThinkModelFunctionApi
-} from '@/api/iot/thinkmodelfunction'
+import ThinkModelDataSpecs from './ThinkModelDataSpecs.vue'
+import { ProductFunctionTypeEnum, ThinkModelApi, ThinkModelData } from '@/api/iot/thinkmodel'
 import { IOT_PROVIDE_KEY } from '@/views/iot/utils/constants'
 import { DataSpecsDataType } from './config'
 import { cloneDeep } from 'lodash-es'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 
-// TODO @puhui999:这里注释下哈
-defineOptions({ name: 'IoTProductThingModelForm' })
+/** IoT 物模型数据表单 */
+defineOptions({ name: 'IoTProductThinkModelForm' })
 
 const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) // 注入产品信息
 
-// TODO @puhui999:变量必要的注释哈。 = = 虽然有点啰嗦,但是写下,保持统一;
-const { t } = useI18n()
-const message = useMessage()
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
 
-const dialogVisible = ref(false)
-const dialogTitle = ref('')
-const formLoading = ref(false)
-const formType = ref('')
-const formData = ref<ThingModelData>({
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref<ThinkModelData>({
   type: ProductFunctionTypeEnum.PROPERTY,
   dataType: DataSpecsDataType.INT,
   property: {
@@ -70,7 +69,6 @@ const formData = ref<ThingModelData>({
     }
   }
 })
-// TODO puhui999: 表单校验待完善
 const formRules = reactive({
   name: [
     { required: true, message: '功能名称不能为空', trigger: 'blur' },
@@ -90,7 +88,7 @@ const formRules = reactive({
       trigger: 'blur'
     },
     {
-      validator: (rule, value, callback) => {
+      validator: (_: any, value: string, callback: any) => {
         const reservedKeywords = ['set', 'get', 'post', 'property', 'event', 'time', 'value']
         if (reservedKeywords.includes(value)) {
           callback(
@@ -106,11 +104,9 @@ const formRules = reactive({
       },
       trigger: 'blur'
     }
-  ],
-  'property.dataType.type': [{ required: true, message: '数据类型不能为空', trigger: 'blur' }],
-  'property.accessMode': [{ required: true, message: '读写类型不能为空', trigger: 'blur' }]
+  ]
 })
-const formRef = ref()
+const formRef = ref() // 表单 Ref
 
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
@@ -121,7 +117,7 @@ const open = async (type: string, id?: number) => {
   if (id) {
     formLoading.value = true
     try {
-      formData.value = await ThinkModelFunctionApi.getProductThingModel(id)
+      formData.value = await ThinkModelApi.getThinkModel(id)
     } finally {
       formLoading.value = false
     }
@@ -135,7 +131,7 @@ const submitForm = async () => {
   await formRef.value.validate()
   formLoading.value = true
   try {
-    const data = cloneDeep(formData.value) as ThingModelData
+    const data = cloneDeep(formData.value) as ThinkModelData
     // 信息补全
     data.productId = product!.value.id
     data.productKey = product!.value.productKey
@@ -144,10 +140,10 @@ const submitForm = async () => {
     data.property.identifier = data.identifier
     data.property.name = data.name
     if (formType.value === 'create') {
-      await ThinkModelFunctionApi.createProductThingModel(data)
+      await ThinkModelApi.createThinkModel(data)
       message.success(t('common.createSuccess'))
     } else {
-      await ThinkModelFunctionApi.updateProductThingModel(data)
+      await ThinkModelApi.updateThinkModel(data)
       message.success(t('common.updateSuccess'))
     }
   } finally {

+ 0 - 0
src/views/iot/product/product/detail/ThingModel/config.ts → src/views/iot/thinkmodel/config.ts


+ 29 - 4
src/views/iot/product/product/detail/ThingModel/dataSpecs/ThingModelArrayTypeDataSpecs.vue → src/views/iot/thinkmodel/dataSpecs/ThinkModelArrayTypeDataSpecs.vue

@@ -1,21 +1,32 @@
 <template>
-  <el-form-item label="元素类型" prop="childDataType">
+  <el-form-item
+    :rules="[{ required: true, message: '元素类型不能为空' }]"
+    label="元素类型"
+    prop="property.dataSpecs.childDataType"
+  >
     <el-radio-group v-model="dataSpecs.childDataType">
       <template v-for="item in dataTypeOptions" :key="item.value">
         <el-radio
-          :value="item.value"
           v-if="
             !(
               [DataSpecsDataType.ENUM, DataSpecsDataType.ARRAY, DataSpecsDataType.DATE] as any[]
             ).includes(item.value)
           "
+          :value="item.value"
         >
           {{ item.label }}
         </el-radio>
       </template>
     </el-radio-group>
   </el-form-item>
-  <el-form-item label="元素个数" prop="size">
+  <el-form-item
+    :rules="[
+      { required: true, message: '元素个数不能为空' },
+      { validator: validateSize, trigger: 'blur' }
+    ]"
+    label="元素个数"
+    prop="property.dataSpecs.size"
+  >
     <el-input v-model="dataSpecs.size" placeholder="请输入数组中的元素个数" />
   </el-form-item>
 </template>
@@ -23,13 +34,27 @@
 <script lang="ts" setup>
 import { useVModel } from '@vueuse/core'
 import { DataSpecsDataType, dataTypeOptions } from '../config'
+import { isEmpty } from '@/utils/is'
 
 /** 数组型的 dataSpecs 配置组件 */
-defineOptions({ name: 'ThingModelArrayTypeDataSpecs' })
+defineOptions({ name: 'ThinkModelArrayTypeDataSpecs' })
 
 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('元素个数必须是数字'))
+    return
+  }
+  callback()
+}
 </script>
 
 <style lang="scss" scoped></style>

+ 164 - 0
src/views/iot/thinkmodel/dataSpecs/ThinkModelEnumTypeDataSpecs.vue

@@ -0,0 +1,164 @@
+<template>
+  <el-form-item
+    :rules="[{ required: true, validator: validateEnumList, trigger: 'change' }]"
+    label="枚举项"
+    prop="property.dataSpecsList"
+  >
+    <div class="flex flex-col">
+      <div class="flex items-center">
+        <span class="flex-1"> 参数值 </span>
+        <span class="flex-1"> 参数描述 </span>
+      </div>
+      <div
+        v-for="(item, index) in dataSpecsList"
+        :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>
+        <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>
+      <el-button link type="primary" @click="addEnum">+添加枚举项</el-button>
+    </div>
+  </el-form-item>
+</template>
+
+<script lang="ts" setup>
+import { useVModel } from '@vueuse/core'
+import { DataSpecsDataType, DataSpecsEnumOrBoolDataVO } from '../config'
+import { isEmpty } from '@/utils/is'
+
+/** 枚举型的 dataSpecs 配置组件 */
+defineOptions({ name: 'ThinkModelEnumTypeDataSpecs' })
+
+const props = defineProps<{ modelValue: any }>()
+const emits = defineEmits(['update:modelValue'])
+const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<DataSpecsEnumOrBoolDataVO[]>
+const message = useMessage()
+
+/** 添加枚举项 */
+const addEnum = () => {
+  dataSpecsList.value.push({
+    dataType: DataSpecsDataType.ENUM,
+    name: '', // 枚举项的名称
+    value: undefined // 枚举值
+  })
+}
+
+/** 删除枚举项 */
+const deleteEnum = (index: number) => {
+  if (dataSpecsList.value.length === 1) {
+    message.warning('至少需要一个枚举项')
+    return
+  }
+  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 {
+    margin-bottom: 0;
+  }
+}
+</style>

+ 145 - 0
src/views/iot/thinkmodel/dataSpecs/ThinkModelNumberTypeDataSpecs.vue

@@ -0,0 +1,145 @@
+<template>
+  <el-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>
+      <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>
+  </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
+      :model-value="dataSpecs.unit ? dataSpecs.unitName + '-' + dataSpecs.unit : ''"
+      filterable
+      placeholder="请选择单位"
+      style="width: 240px"
+      @change="unitChange"
+    >
+      <el-option
+        v-for="(item, index) in UnifyUnitSpecsDTO"
+        :key="index"
+        :label="item.Name + '-' + item.Symbol"
+        :value="item.Name + '-' + item.Symbol"
+      />
+    </el-select>
+  </el-form-item>
+</template>
+
+<script lang="ts" setup>
+import { useVModel } from '@vueuse/core'
+import { UnifyUnitSpecsDTO } from '@/views/iot/utils/constants'
+import { DataSpecsNumberDataVO } from '../config'
+
+/** 数值型的 dataSpecs 配置组件 */
+defineOptions({ name: 'ThinkModelNumberTypeDataSpecs' })
+
+const props = defineProps<{ modelValue: any }>()
+const emits = defineEmits(['update:modelValue'])
+const dataSpecs = useVModel(props, 'modelValue', emits) as Ref<DataSpecsNumberDataVO>
+
+/** 单位发生变化时触发 */
+const unitChange = (UnitSpecs: string) => {
+  const [unitName, unit] = UnitSpecs.split('-')
+  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)
+  const min = Number(dataSpecs.value.min)
+  const max = Number(dataSpecs.value.max)
+
+  if (isNaN(step)) {
+    callback(new Error('请输入有效的数值'))
+    return
+  }
+
+  if (step <= 0) {
+    callback(new Error('步长必须大于0'))
+    return
+  }
+
+  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 {
+    margin-bottom: 0;
+  }
+}
+</style>

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

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

+ 10 - 10
src/views/iot/product/product/detail/ThingModel/index.vue → src/views/iot/thinkmodel/index.vue

@@ -17,7 +17,7 @@
           placeholder="请选择功能类型"
         >
           <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_FUNCTION_TYPE)"
+            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_THINK_MODEL_TYPE)"
             :key="dict.value"
             :label="dict.label"
             :value="dict.value"
@@ -50,7 +50,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_FUNCTION_TYPE" :value="scope.row.type" />
+            <dict-tag :type="DICT_TYPE.IOT_PRODUCT_THINK_MODEL_TYPE" :value="scope.row.type" />
           </template>
         </el-table-column>
         <el-table-column align="center" label="功能名称" prop="name" />
@@ -97,23 +97,23 @@
     </el-tabs>
   </ContentWrap>
   <!-- 表单弹窗:添加/修改 -->
-  <ThingModelForm ref="formRef" @success="getList" />
+  <ThinkModelForm ref="formRef" @success="getList" />
 </template>
 <script lang="ts" setup>
-import { ThingModelData, ThinkModelFunctionApi } from '@/api/iot/thinkmodelfunction'
+import { ThinkModelApi, ThinkModelData } from '@/api/iot/thinkmodel'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import ThingModelForm from './ThingModelForm.vue'
+import ThinkModelForm from './ThinkModelForm.vue'
 import { ProductVO } from '@/api/iot/product/product'
 import { IOT_PROVIDE_KEY } from '@/views/iot/utils/constants'
-import { getDataTypeOptionsLabel } from '@/views/iot/product/product/detail/ThingModel/config'
+import { getDataTypeOptionsLabel } from '@/views/iot/thinkmodel/config'
 
-defineOptions({ name: 'IoTProductThingModel' })
+defineOptions({ name: 'IoTProductThinkModel' })
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 
 const loading = ref(true) // 列表的加载中
-const list = ref<ThingModelData[]>([]) // 列表的数据
+const list = ref<ThinkModelData[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
 const queryParams = reactive({
   pageNo: 1,
@@ -131,7 +131,7 @@ const getList = async () => {
   loading.value = true
   try {
     queryParams.productId = product?.value?.id || -1
-    const data = await ThinkModelFunctionApi.getProductThingModelPage(queryParams)
+    const data = await ThinkModelApi.getThinkModelPage(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -163,7 +163,7 @@ const handleDelete = async (id: number) => {
     // 删除的二次确认
     await message.delConfirm()
     // 发起删除
-    await ThinkModelFunctionApi.deleteProductThingModel(id)
+    await ThinkModelApi.deleteThinkModel(id)
     message.success(t('common.delSuccess'))
     // 刷新列表
     await getList()