Ver Fonte

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

芋道源码 há 8 meses atrás
pai
commit
773b8dead2

+ 49 - 29
src/api/iot/thinkmodelfunction/index.ts

@@ -1,17 +1,41 @@
 import request from '@/config/axios'
 
-// IoT 产品物模型 VO
-export interface ThinkModelFunctionVO {
-  id: number // 物模型功能编号
-  identifier: string // 功能标识
-  name: string // 功能名称
-  description: string // 功能描述
-  productId: number // 产品编号
-  productKey: string // 产品标识
-  type: number // 功能类型
-  property: string // 属性
-  event: string // 事件
-  service: string // 服务
+/**
+ * IoT 产品物模型
+ */
+export interface ThingModelData {
+  id?: number // 物模型功能编号
+  identifier?: string // 功能标识
+  name?: string // 功能名称
+  description?: string // 功能描述
+  productId?: number // 产品编号
+  productKey?: string // 产品标识
+  dataType: string // 数据类型,与 dataSpecs 的 dataType 保持一致
+  type: ProductFunctionTypeEnum // 功能类型
+  property: ThingModelProperty // 属性
+  event?: ThingModelEvent // 事件
+  service?: ThingModelService // 服务
+}
+
+/**
+ * ThingModelProperty 类型
+ */
+export interface ThingModelProperty {
+  [key: string]: any
+}
+
+/**
+ * ThingModelEvent 类型
+ */
+export interface ThingModelEvent {
+  [key: string]: any
+}
+
+/**
+ * ThingModelService 类型
+ */
+export interface ThingModelService {
+  [key: string]: any
 }
 
 // IOT 产品功能(物模型)类型枚举类
@@ -30,39 +54,35 @@ export enum ProductFunctionAccessModeEnum {
 // IoT 产品物模型 API
 export const ThinkModelFunctionApi = {
   // 查询产品物模型分页
-  getThinkModelFunctionPage: async (params: any) => {
-    return await request.get({ url: `/iot/think-model-function/page`, params })
+  getProductThingModelPage: async (params: any) => {
+    return await request.get({ url: `/iot/product-thing-model/page`, params })
   },
+
   // 获得产品物模型
-  getThinkModelFunctionListByProductId: async (params: any) => {
+  getProductThingModelListByProductId: async (params: any) => {
     return await request.get({
-      url: `/iot/think-model-function/list-by-product-id`,
+      url: `/iot/product-thing-model/list-by-product-id`,
       params
     })
   },
 
   // 查询产品物模型详情
-  getThinkModelFunction: async (id: number) => {
-    return await request.get({ url: `/iot/think-model-function/get?id=` + id })
+  getProductThingModel: async (id: number) => {
+    return await request.get({ url: `/iot/product-thing-model/get?id=` + id })
   },
 
   // 新增产品物模型
-  createThinkModelFunction: async (data: ThinkModelFunctionVO) => {
-    return await request.post({ url: `/iot/think-model-function/create`, data })
+  createProductThingModel: async (data: ThingModelData) => {
+    return await request.post({ url: `/iot/product-thing-model/create`, data })
   },
 
   // 修改产品物模型
-  updateThinkModelFunction: async (data: ThinkModelFunctionVO) => {
-    return await request.put({ url: `/iot/think-model-function/update`, data })
+  updateProductThingModel: async (data: ThingModelData) => {
+    return await request.put({ url: `/iot/product-thing-model/update`, data })
   },
 
   // 删除产品物模型
-  deleteThinkModelFunction: async (id: number) => {
-    return await request.delete({ url: `/iot/think-model-function/delete?id=` + id })
-  },
-
-  // 导出产品物模型 Excel
-  exportThinkModelFunction: async (params) => {
-    return await request.download({ url: `/iot/think-model-function/export-excel`, params })
+  deleteProductThingModel: async (id: number) => {
+    return await request.delete({ url: `/iot/product-thing-model/delete?id=` + id })
   }
 }

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

@@ -0,0 +1,110 @@
+<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'
+
+/** 物模型数据 */
+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>

+ 171 - 0
src/views/iot/product/product/detail/ThingModel/ThingModelForm.vue

@@ -0,0 +1,171 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+    >
+      <el-form-item label="功能类型" prop="type">
+        <el-radio-group v-model="formData.type">
+          <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-group>
+      </el-form-item>
+      <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-if="formData.type === ProductFunctionTypeEnum.PROPERTY"
+        v-model="formData.property"
+      />
+    </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 { ProductVO } from '@/api/iot/product/product'
+import ThingModelDataSpecs from './ThingModelDataSpecs.vue'
+import {
+  ProductFunctionTypeEnum,
+  ThingModelData,
+  ThinkModelFunctionApi
+} from '@/api/iot/thinkmodelfunction'
+import { IOT_PROVIDE_KEY } from '@/views/iot/utils/constants'
+import { DataSpecsDataType } from './config'
+import { cloneDeep } from 'lodash-es'
+
+defineOptions({ name: 'IoTProductThingModelForm' })
+
+const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) // 注入产品信息
+
+const { t } = useI18n()
+const message = useMessage()
+
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const formLoading = ref(false)
+const formType = ref('')
+const formData = ref<ThingModelData>({
+  type: ProductFunctionTypeEnum.PROPERTY,
+  dataType: DataSpecsDataType.INT,
+  property: {
+    dataType: DataSpecsDataType.INT,
+    dataSpecs: {
+      dataType: DataSpecsDataType.INT
+    }
+  }
+})
+// TODO puhui999: 表单校验待完善
+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: (rule, value, callback) => {
+        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.dataType.type': [{ required: true, message: '数据类型不能为空', trigger: 'blur' }],
+  'property.accessMode': [{ required: true, message: '读写类型不能为空', trigger: 'blur' }]
+})
+const formRef = ref()
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await ThinkModelFunctionApi.getProductThingModel(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open, close: () => (dialogVisible.value = false) })
+
+/** 提交表单 */
+const emit = defineEmits(['success'])
+const submitForm = async () => {
+  await formRef.value.validate()
+  formLoading.value = true
+  try {
+    const data = cloneDeep(formData.value) as ThingModelData
+    // 信息补全
+    data.productId = product!.value.id
+    data.productKey = product!.value.productKey
+    data.description = data.property.description
+    data.dataType = data.property.dataType
+    data.property.identifier = data.identifier
+    data.property.name = data.name
+    if (formType.value === 'create') {
+      await ThinkModelFunctionApi.createProductThingModel(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await ThinkModelFunctionApi.updateProductThingModel(data)
+      message.success(t('common.updateSuccess'))
+    }
+  } finally {
+    dialogVisible.value = false // 确保关闭弹框
+    emit('success')
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    type: ProductFunctionTypeEnum.PROPERTY,
+    dataType: DataSpecsDataType.INT,
+    property: {
+      dataType: DataSpecsDataType.INT,
+      dataSpecs: {
+        dataType: DataSpecsDataType.INT
+      }
+    }
+  }
+  formRef.value?.resetFields()
+}
+</script>

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

@@ -0,0 +1,50 @@
+/** 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
+}

+ 34 - 0
src/views/iot/product/product/detail/ThingModel/dataSpecs/ThingModelArrayTypeDataSpecs.vue

@@ -0,0 +1,34 @@
+<template>
+  <el-form-item label="元素类型" prop="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)
+          "
+        >
+          {{ item.label }}
+        </el-radio>
+      </template>
+    </el-radio-group>
+  </el-form-item>
+  <el-form-item label="元素个数" prop="size">
+    <el-input v-model="dataSpecs.size" placeholder="请输入数组中的元素个数" />
+  </el-form-item>
+</template>
+
+<script lang="ts" setup>
+import { useVModel } from '@vueuse/core'
+import { DataSpecsDataType, dataTypeOptions } from '../config'
+
+/** 数组型的 dataSpecs 配置组件 */
+defineOptions({ name: 'ThingModelArrayTypeDataSpecs' })
+const props = defineProps<{ modelValue: any }>()
+const emits = defineEmits(['update:modelValue'])
+const dataSpecs = useVModel(props, 'modelValue', emits) as Ref<any>
+</script>
+
+<style lang="scss" scoped></style>

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

@@ -0,0 +1,56 @@
+<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>

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

@@ -0,0 +1,49 @@
+<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>

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

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

+ 50 - 30
src/views/iot/product/product/detail/ThinkModelFunction.vue → src/views/iot/product/product/detail/ThingModel/index.vue

@@ -2,18 +2,18 @@
   <ContentWrap>
     <!-- 搜索工作栏 -->
     <el-form
-      class="-mb-15px"
-      :model="queryParams"
       ref="queryFormRef"
       :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
       label-width="68px"
     >
       <el-form-item label="功能类型" prop="name">
         <el-select
           v-model="queryParams.type"
-          placeholder="请选择功能类型"
-          clearable
           class="!w-240px"
+          clearable
+          placeholder="请选择功能类型"
         >
           <el-option
             v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_FUNCTION_TYPE)"
@@ -24,46 +24,62 @@
         </el-select>
       </el-form-item>
       <el-form-item>
-        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button @click="handleQuery">
+          <Icon class="mr-5px" icon="ep:search" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon class="mr-5px" icon="ep:refresh" />
+          重置
+        </el-button>
         <el-button
-          type="primary"
+          v-hasPermi="[`iot:product-thing-model:create`]"
           plain
+          type="primary"
           @click="openForm('create')"
-          v-hasPermi="['iot:think-model-function:create']"
         >
-          <Icon icon="ep:plus" class="mr-5px" /> 添加功能
+          <Icon class="mr-5px" icon="ep:plus" />
+          添加功能
         </el-button>
       </el-form-item>
     </el-form>
   </ContentWrap>
   <ContentWrap>
     <el-tabs>
-      <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-        <el-table-column label="功能类型" align="center" prop="type">
+      <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" />
           </template>
         </el-table-column>
-        <el-table-column label="功能名称" align="center" prop="name" />
-        <el-table-column label="标识符" align="center" prop="identifier" />
-        <el-table-column label="数据类型" align="center" prop="identifier" />
-        <el-table-column label="数据定义" align="center" prop="identifier" />
-        <el-table-column label="操作" align="center">
+        <el-table-column align="center" label="功能名称" prop="name" />
+        <el-table-column align="center" label="标识符" prop="identifier" />
+        <el-table-column align="center" label="数据类型" prop="identifier">
+          <template #default="{ row }">
+            {{ dataTypeOptionsLabel(row.property.dataType) ?? '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column align="center" label="数据定义" prop="identifier">
+          <template #default="{ row }">
+            <!-- TODO puhui999: 数据定义展示待完善 -->
+            {{ row.property.dataSpecs ?? row.property.dataSpecsList }}
+          </template>
+        </el-table-column>
+        <el-table-column align="center" label="操作">
           <template #default="scope">
             <el-button
+              v-hasPermi="[`iot:product-thing-model:update`]"
               link
               type="primary"
               @click="openForm('update', scope.row.id)"
-              v-hasPermi="[`iot:think-model-function:update`]"
             >
               编辑
             </el-button>
             <el-button
+              v-hasPermi="['iot:product-thing-model:delete']"
               link
               type="danger"
               @click="handleDelete(scope.row.id)"
-              v-hasPermi="['iot:think-model-function:delete']"
             >
               删除
             </el-button>
@@ -72,29 +88,31 @@
       </el-table>
       <!-- 分页 -->
       <Pagination
-        :total="total"
-        v-model:page="queryParams.pageNo"
         v-model:limit="queryParams.pageSize"
+        v-model:page="queryParams.pageNo"
+        :total="total"
         @pagination="getList"
       />
     </el-tabs>
   </ContentWrap>
   <!-- 表单弹窗:添加/修改 -->
-  <ThinkModelFunctionForm ref="formRef" :product="product" @success="getList" />
+  <ThingModelForm ref="formRef" @success="getList" />
 </template>
-<script setup lang="ts">
-import { ProductVO } from '@/api/iot/product/product'
-import { ThinkModelFunctionApi, ThinkModelFunctionVO } from '@/api/iot/thinkmodelfunction'
+<script lang="ts" setup>
+import { ThingModelData, ThinkModelFunctionApi } from '@/api/iot/thinkmodelfunction'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import ThinkModelFunctionForm from './ThinkModelFunctionForm.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/product/product/detail/ThingModel/config'
 
-const props = defineProps<{ product: ProductVO }>()
+defineOptions({ name: 'IoTProductThingModel' })
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 
 const loading = ref(true) // 列表的加载中
-const list = ref<ThinkModelFunctionVO[]>([]) // 列表的数据
+const list = ref<ThingModelData[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
 const queryParams = reactive({
   pageNo: 1,
@@ -104,13 +122,15 @@ const queryParams = reactive({
 })
 
 const queryFormRef = ref() // 搜索的表单
+const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) // 注入产品信息
+const dataTypeOptionsLabel = computed(() => (value: string) => getDataTypeOptionsLabel(value)) // 解析数据类型
 
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
-    queryParams.productId = props.product.id
-    const data = await ThinkModelFunctionApi.getThinkModelFunctionPage(queryParams)
+    queryParams.productId = product?.value?.id || -1
+    const data = await ThinkModelFunctionApi.getProductThingModelPage(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -142,7 +162,7 @@ const handleDelete = async (id: number) => {
     // 删除的二次确认
     await message.delConfirm()
     // 发起删除
-    await ThinkModelFunctionApi.deleteThinkModelFunction(id)
+    await ThinkModelFunctionApi.deleteProductThingModel(id)
     message.success(t('common.delSuccess'))
     // 刷新列表
     await getList()

+ 0 - 232
src/views/iot/product/product/detail/ThinkModelFunctionForm.vue

@@ -1,232 +0,0 @@
-<template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
-    <el-form
-      ref="formRef"
-      :model="formData"
-      :rules="formRules"
-      label-width="100px"
-      v-loading="formLoading"
-    >
-      <el-form-item label="功能类型" prop="type">
-        <el-radio-group v-model="formData.type">
-          <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-group>
-      </el-form-item>
-      <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="请输入标识符"
-          :disabled="formType === 'update'"
-        />
-      </el-form-item>
-      <el-form-item label="数据类型" prop="type">
-        <el-select
-          v-model="formData.property.dataType.type"
-          placeholder="请选择数据类型"
-          :disabled="formType === 'update'"
-        >
-          <el-option key="int" label="int32 (整数型)" value="int" />
-          <el-option key="float" label="float (单精度浮点型)" value="float" />
-          <el-option key="double" label="double (双精度浮点型)" value="double" />
-          <!--          <el-option key="text" label="text (文本型)" value="text" />-->
-          <!--          <el-option key="date" label="date (日期型)" value="date" />-->
-          <!--          <el-option key="bool" label="bool (布尔型)" value="bool" />-->
-          <!--          <el-option key="enum" label="enum (枚举型)" value="enum" />-->
-          <!--          <el-option key="struct" label="struct (结构体)" value="struct" />-->
-          <!--          <el-option key="array" label="array (数组)" value="array" />-->
-        </el-select>
-      </el-form-item>
-      <el-form-item label="取值范围" prop="max">
-        <el-input v-model="formData.property.dataType.specs.min" placeholder="请输入最小值" />
-        <span class="mx-2">~</span>
-        <el-input v-model="formData.property.dataType.specs.max" placeholder="请输入最大值" />
-      </el-form-item>
-      <el-form-item label="步长" prop="step">
-        <el-input v-model="formData.property.dataType.specs.step" placeholder="请输入步长" />
-      </el-form-item>
-      <el-form-item label="单位" prop="unit">
-        <el-input v-model="formData.property.dataType.specs.unit" placeholder="请输入单位" />
-      </el-form-item>
-      <el-form-item label="读写类型" prop="accessMode">
-        <el-radio-group v-model="formData.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="property.description">
-        <el-input
-          type="textarea"
-          v-model="formData.property.description"
-          placeholder="请输入属性描述"
-        />
-      </el-form-item>
-    </el-form>
-
-    <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-
-<script setup lang="ts">
-import { ProductVO } from '@/api/iot/product/product'
-import {
-  ProductFunctionAccessModeEnum,
-  ProductFunctionTypeEnum,
-  ThinkModelFunctionApi,
-  ThinkModelFunctionVO
-} from '@/api/iot/thinkmodelfunction'
-
-const props = defineProps<{ product: ProductVO }>()
-
-defineOptions({ name: 'ThinkModelFunctionForm' })
-
-const { t } = useI18n()
-const message = useMessage()
-
-const dialogVisible = ref(false)
-const dialogTitle = ref('')
-const formLoading = ref(false)
-const formType = ref('')
-const formData = ref({
-  id: undefined,
-  productId: undefined,
-  productKey: undefined,
-  identifier: undefined,
-  name: undefined,
-  description: undefined,
-  type: ProductFunctionTypeEnum.PROPERTY,
-  property: {
-    identifier: undefined,
-    name: undefined,
-    accessMode: ProductFunctionAccessModeEnum.READ_WRITE,
-    required: true,
-    dataType: {
-      type: undefined,
-      specs: {
-        min: undefined,
-        max: undefined,
-        step: undefined,
-        unit: undefined
-      }
-    },
-    description: undefined // 添加这一行
-  }
-})
-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: (rule, value, callback) => {
-        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.dataType.type': [{ required: true, message: '数据类型不能为空', trigger: 'blur' }],
-  'property.accessMode': [{ required: true, message: '读写类型不能为空', trigger: 'blur' }]
-})
-const formRef = ref()
-
-/** 打开弹窗 */
-const open = async (type: string, id?: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  if (id) {
-    formLoading.value = true
-    try {
-      formData.value = await ThinkModelFunctionApi.getThinkModelFunction(id)
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-defineExpose({ open, close: () => (dialogVisible.value = false) })
-
-/** 提交表单 */
-const emit = defineEmits(['success'])
-const submitForm = async () => {
-  await formRef.value.validate()
-  formLoading.value = true
-  try {
-    const data = formData.value as unknown as ThinkModelFunctionVO
-    data.productId = props.product.id
-    data.productKey = props.product.productKey
-    if (formType.value === 'create') {
-      await ThinkModelFunctionApi.createThinkModelFunction(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await ThinkModelFunctionApi.updateThinkModelFunction(data)
-      message.success(t('common.updateSuccess'))
-    }
-  } finally {
-    dialogVisible.value = false // 确保关闭弹框
-    emit('success')
-    formLoading.value = false
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    productId: undefined,
-    productKey: undefined,
-    identifier: undefined,
-    name: undefined,
-    description: undefined,
-    type: ProductFunctionTypeEnum.PROPERTY,
-    property: {
-      identifier: undefined,
-      name: undefined,
-      accessMode: ProductFunctionAccessModeEnum.READ_WRITE,
-      required: true,
-      dataType: {
-        type: undefined,
-        specs: {
-          min: undefined,
-          max: undefined,
-          step: undefined,
-          unit: undefined
-        }
-      },
-      description: undefined // 确保重置 description 字段
-    }
-  }
-  formRef.value?.resetFields()
-}
-</script>

+ 6 - 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="功能定义" name="function">
-        <ThinkModelFunction v-if="activeTab === 'function'" :product="product" />
+      <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,9 +22,10 @@ import { DeviceApi } from '@/api/iot/device/device'
 import ProductDetailsHeader from './ProductDetailsHeader.vue'
 import ProductDetailsInfo from './ProductDetailsInfo.vue'
 import ProductTopic from './ProductTopic.vue'
-import ThinkModelFunction from './ThinkModelFunction.vue'
+import IoTProductThingModel from './ThingModel/index.vue'
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import { useRouter } from 'vue-router'
+import { IOT_PROVIDE_KEY } from '@/views/iot/utils/constants'
 
 defineOptions({ name: 'IoTProductDetail' })
 
@@ -38,6 +39,8 @@ const loading = ref(true) // 加载中
 const product = ref<ProductVO>({} as ProductVO) // 详情
 const activeTab = ref('info') // 默认为 info 标签页
 
+provide(IOT_PROVIDE_KEY.PRODUCT, product) // 提供产品信息给产品信息详情页的所有子组件
+
 /** 获取详情 */
 const getProductData = async (id: number) => {
   loading.value = true

+ 55 - 47
src/views/iot/product/product/index.vue

@@ -2,49 +2,57 @@
   <ContentWrap>
     <!-- 搜索工作栏 -->
     <el-form
-      class="-mb-15px"
-      :model="queryParams"
       ref="queryFormRef"
       :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
       label-width="68px"
     >
       <el-form-item label="产品名称" prop="name">
         <el-input
           v-model="queryParams.name"
-          placeholder="请输入产品名称"
+          class="!w-240px"
           clearable
+          placeholder="请输入产品名称"
           @keyup.enter="handleQuery"
-          class="!w-240px"
         />
       </el-form-item>
       <el-form-item label="ProductKey" prop="productKey">
         <el-input
           v-model="queryParams.productKey"
-          placeholder="请输入产品标识"
+          class="!w-240px"
           clearable
+          placeholder="请输入产品标识"
           @keyup.enter="handleQuery"
-          class="!w-240px"
         />
       </el-form-item>
       <el-form-item>
-        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button @click="handleQuery">
+          <Icon class="mr-5px" icon="ep:search" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon class="mr-5px" icon="ep:refresh" />
+          重置
+        </el-button>
         <el-button
-          type="primary"
+          v-hasPermi="['iot:product:create']"
           plain
+          type="primary"
           @click="openForm('create')"
-          v-hasPermi="['iot:product:create']"
         >
-          <Icon icon="ep:plus" class="mr-5px" /> 新增
+          <Icon class="mr-5px" icon="ep:plus" />
+          新增
         </el-button>
         <el-button
-          type="success"
+          v-hasPermi="['iot:product:export']"
+          :loading="exportLoading"
           plain
+          type="success"
           @click="handleExport"
-          :loading="exportLoading"
-          v-hasPermi="['iot:product:export']"
         >
-          <Icon icon="ep:download" class="mr-5px" /> 导出
+          <Icon class="mr-5px" icon="ep:download" />
+          导出
         </el-button>
       </el-form-item>
       <!-- 视图切换按钮 -->
@@ -64,8 +72,8 @@
   <!-- 卡片视图 -->
   <ContentWrap>
     <el-row v-if="viewMode === 'card'" :gutter="16">
-      <el-col v-for="item in list" :key="item.id" :xs="24" :sm="12" :md="12" :lg="6" class="mb-4">
-        <el-card class="h-full transition-colors" :body-style="{ padding: '0' }">
+      <el-col v-for="item in list" :key="item.id" :lg="6" :md="12" :sm="12" :xs="24" class="mb-4">
+        <el-card :body-style="{ padding: '0' }" class="h-full transition-colors">
           <!-- 内容区域 -->
           <div class="p-4">
             <!-- 标题区域 -->
@@ -103,41 +111,41 @@
             <!-- 按钮组 -->
             <div class="flex items-center px-0">
               <el-button
+                v-hasPermi="['iot:product:update']"
                 class="flex-1 !px-2 !h-[32px] text-[13px]"
-                type="primary"
                 plain
+                type="primary"
                 @click="openForm('update', item.id)"
-                v-hasPermi="['iot:product:update']"
               >
-                <Icon icon="ep:edit-pen" class="mr-1" />
+                <Icon class="mr-1" icon="ep:edit-pen" />
                 编辑
               </el-button>
               <el-button
                 class="flex-1 !px-2 !h-[32px] !ml-[10px] text-[13px]"
-                type="warning"
                 plain
+                type="warning"
                 @click="openDetail(item.id)"
               >
-                <Icon icon="ep:view" class="mr-1" />
+                <Icon class="mr-1" icon="ep:view" />
                 详情
               </el-button>
               <el-button
                 class="flex-1 !px-2 !h-[32px] !ml-[10px] text-[13px]"
-                type="success"
                 plain
+                type="success"
                 @click="openObjectModel(item)"
               >
-                <Icon icon="ep:scale-to-original" class="mr-1" />
+                <Icon class="mr-1" icon="ep:scale-to-original" />
                 物模型
               </el-button>
               <div class="mx-[10px] h-[20px] w-[1px] bg-[#dcdfe6]"></div>
               <el-button
+                v-hasPermi="['iot:product:delete']"
+                :disabled="item.status === 1"
                 class="!px-2 !h-[32px] text-[13px]"
-                type="danger"
                 plain
+                type="danger"
                 @click="handleDelete(item.id)"
-                v-hasPermi="['iot:product:delete']"
-                :disabled="item.status === 1"
               >
                 <Icon icon="ep:delete" />
               </el-button>
@@ -148,68 +156,68 @@
     </el-row>
 
     <!-- 列表视图 -->
-    <el-table v-else v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="ID" align="center" prop="id" />
-      <el-table-column label="ProductKey" align="center" prop="productKey" />
-      <el-table-column label="品类" align="center" prop="categoryName" />
-      <el-table-column label="设备类型" align="center" prop="deviceType">
+    <el-table v-else v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+      <el-table-column align="center" label="ID" prop="id" />
+      <el-table-column align="center" label="ProductKey" prop="productKey" />
+      <el-table-column align="center" label="品类" prop="categoryName" />
+      <el-table-column align="center" label="设备类型" prop="deviceType">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="scope.row.deviceType" />
         </template>
       </el-table-column>
-      <el-table-column label="产品图标" align="center" prop="icon">
+      <el-table-column align="center" label="产品图标" prop="icon">
         <template #default="scope">
           <el-image
             v-if="scope.row.icon"
+            :preview-src-list="[scope.row.icon]"
             :src="scope.row.icon"
             class="w-40px h-40px"
-            :preview-src-list="[scope.row.icon]"
           />
           <span v-else>-</span>
         </template>
       </el-table-column>
-      <el-table-column label="产品图片" align="center" prop="picture">
+      <el-table-column align="center" label="产品图片" prop="picture">
         <template #default="scope">
           <el-image
             v-if="scope.row.picUrl"
+            :preview-src-list="[scope.row.picture]"
             :src="scope.row.picUrl"
             class="w-40px h-40px"
-            :preview-src-list="[scope.row.picture]"
           />
           <span v-else>-</span>
         </template>
       </el-table-column>
       <el-table-column
-        label="创建时间"
+        :formatter="dateFormatter"
         align="center"
+        label="创建时间"
         prop="createTime"
-        :formatter="dateFormatter"
         width="180px"
       />
-      <el-table-column label="操作" align="center">
+      <el-table-column align="center" label="操作">
         <template #default="scope">
           <el-button
+            v-hasPermi="['iot:product:query']"
             link
             type="primary"
             @click="openDetail(scope.row.id)"
-            v-hasPermi="['iot:product:query']"
           >
             查看
           </el-button>
           <el-button
+            v-hasPermi="['iot:product:update']"
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
-            v-hasPermi="['iot:product:update']"
           >
             编辑
           </el-button>
           <el-button
+            v-hasPermi="['iot:product:delete']"
+            :disabled="scope.row.status === 1"
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['iot:product:delete']"
-            :disabled="scope.row.status === 1"
           >
             删除
           </el-button>
@@ -219,9 +227,9 @@
 
     <!-- 分页 -->
     <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
       v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
       @pagination="getList"
     />
   </ContentWrap>
@@ -230,7 +238,7 @@
   <ProductForm ref="formRef" @success="getList" />
 </template>
 
-<script setup lang="ts">
+<script lang="ts" setup>
 import { dateFormatter } from '@/utils/formatTime'
 import { ProductApi, ProductVO } from '@/api/iot/product/product'
 import ProductForm from './ProductForm.vue'
@@ -301,7 +309,7 @@ const openObjectModel = (item: ProductVO) => {
   push({
     name: 'IoTProductDetail',
     params: { id: item.id },
-    query: { tab: 'function' }
+    query: { tab: 'thingModel' }
   })
 }
 

+ 535 - 0
src/views/iot/utils/constants.ts

@@ -0,0 +1,535 @@
+/** iot 依赖注入 KEY */
+export const IOT_PROVIDE_KEY = {
+  PRODUCT: 'IOT_PRODUCT'
+}
+// TODO puhui999: 物模型数字数据类型单位类型,后面改成字典获取
+export const UnifyUnitSpecsDTO = [
+  {
+    Symbol: 'L/min',
+    Name: '升每分钟'
+  },
+  {
+    Symbol: 'mg/kg',
+    Name: '毫克每千克'
+  },
+  {
+    Symbol: 'NTU',
+    Name: '浊度'
+  },
+  {
+    Symbol: 'pH',
+    Name: 'PH值'
+  },
+  {
+    Symbol: 'dS/m',
+    Name: '土壤EC值'
+  },
+  {
+    Symbol: 'W/㎡',
+    Name: '太阳总辐射'
+  },
+  {
+    Symbol: 'mm/hour',
+    Name: '降雨量'
+  },
+  {
+    Symbol: 'var',
+    Name: '乏'
+  },
+  {
+    Symbol: 'cP',
+    Name: '厘泊'
+  },
+  {
+    Symbol: 'aw',
+    Name: '饱和度'
+  },
+  {
+    Symbol: 'pcs',
+    Name: '个'
+  },
+  {
+    Symbol: 'cst',
+    Name: '厘斯'
+  },
+  {
+    Symbol: 'bar',
+    Name: '巴'
+  },
+  {
+    Symbol: 'ppt',
+    Name: '纳克每升'
+  },
+  {
+    Symbol: 'ppb',
+    Name: '微克每升'
+  },
+  {
+    Symbol: 'uS/cm',
+    Name: '微西每厘米'
+  },
+  {
+    Symbol: 'N/C',
+    Name: '牛顿每库仑'
+  },
+  {
+    Symbol: 'V/m',
+    Name: '伏特每米'
+  },
+  {
+    Symbol: 'ml/min',
+    Name: '滴速'
+  },
+  {
+    Symbol: 'mmHg',
+    Name: '毫米汞柱'
+  },
+  {
+    Symbol: 'mmol/L',
+    Name: '血糖'
+  },
+  {
+    Symbol: 'mm/s',
+    Name: '毫米每秒'
+  },
+  {
+    Symbol: 'turn/m',
+    Name: '转每分钟'
+  },
+  {
+    Symbol: 'count',
+    Name: '次'
+  },
+  {
+    Symbol: 'gear',
+    Name: '档'
+  },
+  {
+    Symbol: 'stepCount',
+    Name: '步'
+  },
+  {
+    Symbol: 'Nm3/h',
+    Name: '标准立方米每小时'
+  },
+  {
+    Symbol: 'kV',
+    Name: '千伏'
+  },
+  {
+    Symbol: 'kVA',
+    Name: '千伏安'
+  },
+  {
+    Symbol: 'kVar',
+    Name: '千乏'
+  },
+  {
+    Symbol: 'uw/cm2',
+    Name: '微瓦每平方厘米'
+  },
+  {
+    Symbol: '只',
+    Name: '只'
+  },
+  {
+    Symbol: '%RH',
+    Name: '相对湿度'
+  },
+  {
+    Symbol: 'm³/s',
+    Name: '立方米每秒'
+  },
+  {
+    Symbol: 'kg/s',
+    Name: '公斤每秒'
+  },
+  {
+    Symbol: 'r/min',
+    Name: '转每分钟'
+  },
+  {
+    Symbol: 't/h',
+    Name: '吨每小时'
+  },
+  {
+    Symbol: 'KCL/h',
+    Name: '千卡每小时'
+  },
+  {
+    Symbol: 'L/s',
+    Name: '升每秒'
+  },
+  {
+    Symbol: 'Mpa',
+    Name: '兆帕'
+  },
+  {
+    Symbol: 'm³/h',
+    Name: '立方米每小时'
+  },
+  {
+    Symbol: 'kvarh',
+    Name: '千乏时'
+  },
+  {
+    Symbol: 'μg/L',
+    Name: '微克每升'
+  },
+  {
+    Symbol: 'kcal',
+    Name: '千卡路里'
+  },
+  {
+    Symbol: 'GB',
+    Name: '吉字节'
+  },
+  {
+    Symbol: 'MB',
+    Name: '兆字节'
+  },
+  {
+    Symbol: 'KB',
+    Name: '千字节'
+  },
+  {
+    Symbol: 'B',
+    Name: '字节'
+  },
+  {
+    Symbol: 'μg/(d㎡·d)',
+    Name: '微克每平方分米每天'
+  },
+  {
+    Symbol: '',
+    Name: '无'
+  },
+  {
+    Symbol: 'ppm',
+    Name: '百万分率'
+  },
+  {
+    Symbol: 'pixel',
+    Name: '像素'
+  },
+  {
+    Symbol: 'Lux',
+    Name: '照度'
+  },
+  {
+    Symbol: 'grav',
+    Name: '重力加速度'
+  },
+  {
+    Symbol: 'dB',
+    Name: '分贝'
+  },
+  {
+    Symbol: '%',
+    Name: '百分比'
+  },
+  {
+    Symbol: 'lm',
+    Name: '流明'
+  },
+  {
+    Symbol: 'bit',
+    Name: '比特'
+  },
+  {
+    Symbol: 'g/mL',
+    Name: '克每毫升'
+  },
+  {
+    Symbol: 'g/L',
+    Name: '克每升'
+  },
+  {
+    Symbol: 'mg/L',
+    Name: '毫克每升'
+  },
+  {
+    Symbol: 'μg/m³',
+    Name: '微克每立方米'
+  },
+  {
+    Symbol: 'mg/m³',
+    Name: '毫克每立方米'
+  },
+  {
+    Symbol: 'g/m³',
+    Name: '克每立方米'
+  },
+  {
+    Symbol: 'kg/m³',
+    Name: '千克每立方米'
+  },
+  {
+    Symbol: 'nF',
+    Name: '纳法'
+  },
+  {
+    Symbol: 'pF',
+    Name: '皮法'
+  },
+  {
+    Symbol: 'μF',
+    Name: '微法'
+  },
+  {
+    Symbol: 'F',
+    Name: '法拉'
+  },
+  {
+    Symbol: 'Ω',
+    Name: '欧姆'
+  },
+  {
+    Symbol: 'μA',
+    Name: '微安'
+  },
+  {
+    Symbol: 'mA',
+    Name: '毫安'
+  },
+  {
+    Symbol: 'kA',
+    Name: '千安'
+  },
+  {
+    Symbol: 'A',
+    Name: '安培'
+  },
+  {
+    Symbol: 'mV',
+    Name: '毫伏'
+  },
+  {
+    Symbol: 'V',
+    Name: '伏特'
+  },
+  {
+    Symbol: 'ms',
+    Name: '毫秒'
+  },
+  {
+    Symbol: 's',
+    Name: '秒'
+  },
+  {
+    Symbol: 'min',
+    Name: '分钟'
+  },
+  {
+    Symbol: 'h',
+    Name: '小时'
+  },
+  {
+    Symbol: 'day',
+    Name: '日'
+  },
+  {
+    Symbol: 'week',
+    Name: '周'
+  },
+  {
+    Symbol: 'month',
+    Name: '月'
+  },
+  {
+    Symbol: 'year',
+    Name: '年'
+  },
+  {
+    Symbol: 'kn',
+    Name: '节'
+  },
+  {
+    Symbol: 'km/h',
+    Name: '千米每小时'
+  },
+  {
+    Symbol: 'm/s',
+    Name: '米每秒'
+  },
+  {
+    Symbol: '″',
+    Name: '秒'
+  },
+  {
+    Symbol: '′',
+    Name: '分'
+  },
+  {
+    Symbol: '°',
+    Name: '度'
+  },
+  {
+    Symbol: 'rad',
+    Name: '弧度'
+  },
+  {
+    Symbol: 'Hz',
+    Name: '赫兹'
+  },
+  {
+    Symbol: 'μW',
+    Name: '微瓦'
+  },
+  {
+    Symbol: 'mW',
+    Name: '毫瓦'
+  },
+  {
+    Symbol: 'kW',
+    Name: '千瓦特'
+  },
+  {
+    Symbol: 'W',
+    Name: '瓦特'
+  },
+  {
+    Symbol: 'cal',
+    Name: '卡路里'
+  },
+  {
+    Symbol: 'kW·h',
+    Name: '千瓦时'
+  },
+  {
+    Symbol: 'Wh',
+    Name: '瓦时'
+  },
+  {
+    Symbol: 'eV',
+    Name: '电子伏'
+  },
+  {
+    Symbol: 'kJ',
+    Name: '千焦'
+  },
+  {
+    Symbol: 'J',
+    Name: '焦耳'
+  },
+  {
+    Symbol: '℉',
+    Name: '华氏度'
+  },
+  {
+    Symbol: 'K',
+    Name: '开尔文'
+  },
+  {
+    Symbol: 't',
+    Name: '吨'
+  },
+  {
+    Symbol: '°C',
+    Name: '摄氏度'
+  },
+  {
+    Symbol: 'mPa',
+    Name: '毫帕'
+  },
+  {
+    Symbol: 'hPa',
+    Name: '百帕'
+  },
+  {
+    Symbol: 'kPa',
+    Name: '千帕'
+  },
+  {
+    Symbol: 'Pa',
+    Name: '帕斯卡'
+  },
+  {
+    Symbol: 'mg',
+    Name: '毫克'
+  },
+  {
+    Symbol: 'g',
+    Name: '克'
+  },
+  {
+    Symbol: 'kg',
+    Name: '千克'
+  },
+  {
+    Symbol: 'N',
+    Name: '牛'
+  },
+  {
+    Symbol: 'mL',
+    Name: '毫升'
+  },
+  {
+    Symbol: 'L',
+    Name: '升'
+  },
+  {
+    Symbol: 'mm³',
+    Name: '立方毫米'
+  },
+  {
+    Symbol: 'cm³',
+    Name: '立方厘米'
+  },
+  {
+    Symbol: 'km³',
+    Name: '立方千米'
+  },
+  {
+    Symbol: 'm³',
+    Name: '立方米'
+  },
+  {
+    Symbol: 'h㎡',
+    Name: '公顷'
+  },
+  {
+    Symbol: 'c㎡',
+    Name: '平方厘米'
+  },
+  {
+    Symbol: 'm㎡',
+    Name: '平方毫米'
+  },
+  {
+    Symbol: 'k㎡',
+    Name: '平方千米'
+  },
+  {
+    Symbol: '㎡',
+    Name: '平方米'
+  },
+  {
+    Symbol: 'nm',
+    Name: '纳米'
+  },
+  {
+    Symbol: 'μm',
+    Name: '微米'
+  },
+  {
+    Symbol: 'mm',
+    Name: '毫米'
+  },
+  {
+    Symbol: 'cm',
+    Name: '厘米'
+  },
+  {
+    Symbol: 'dm',
+    Name: '分米'
+  },
+  {
+    Symbol: 'km',
+    Name: '千米'
+  },
+  {
+    Symbol: 'm',
+    Name: '米'
+  }
+]