!626 【代码完善】IOT: ThingModel StructDataSpecs 组件

Merge pull request !626 from puhui999/feature/iot
pull/628/MERGE
芋道源码 2024-12-24 01:26:26 +00:00 committed by Gitee
commit eb5d350b09
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
15 changed files with 423 additions and 246 deletions

View File

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

View File

@ -236,7 +236,7 @@ export enum DICT_TYPE {
IOT_DATA_FORMAT = 'iot_data_format', // IOT 数据格式 IOT_DATA_FORMAT = 'iot_data_format', // IOT 数据格式
IOT_PROTOCOL_TYPE = 'iot_protocol_type', // IOT 接入网关协议 IOT_PROTOCOL_TYPE = 'iot_protocol_type', // IOT 接入网关协议
IOT_DEVICE_STATUS = 'iot_device_status', // 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_DATA_TYPE = 'iot_data_type', // IOT 数据类型
IOT_UNIT_TYPE = 'iot_unit_type', // IOT 单位类型 IOT_UNIT_TYPE = 'iot_unit_type', // IOT 单位类型
IOT_RW_TYPE = 'iot_rw_type', // IOT 读写类型 IOT_RW_TYPE = 'iot_rw_type', // IOT 读写类型

View File

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

View File

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

View File

@ -5,8 +5,9 @@
prop="property.dataType" prop="property.dataType"
> >
<el-select v-model="property.dataType" placeholder="请选择数据类型" @change="handleChange"> <el-select v-model="property.dataType" placeholder="请选择数据类型" @change="handleChange">
<!-- ARRAY STRUCT 类型数据相互嵌套时最多支持递归嵌套2层父和子 -->
<el-option <el-option
v-for="option in dataTypeOptions" v-for="option in getDataTypeOptions"
:key="option.value" :key="option.value"
:label="option.label" :label="option.label"
:value="option.value" :value="option.value"
@ -14,7 +15,7 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<!-- 数值型配置 --> <!-- 数值型配置 -->
<ThinkModelNumberTypeDataSpecs <ThingModelNumberDataSpecs
v-if=" v-if="
[DataSpecsDataType.INT, DataSpecsDataType.DOUBLE, DataSpecsDataType.FLOAT].includes( [DataSpecsDataType.INT, DataSpecsDataType.DOUBLE, DataSpecsDataType.FLOAT].includes(
property.dataType || '' property.dataType || ''
@ -23,17 +24,12 @@
v-model="property.dataSpecs" v-model="property.dataSpecs"
/> />
<!-- 枚举型配置 --> <!-- 枚举型配置 -->
<ThinkModelEnumTypeDataSpecs <ThingModelEnumDataSpecs
v-if="property.dataType === DataSpecsDataType.ENUM" v-if="property.dataType === DataSpecsDataType.ENUM"
v-model="property.dataSpecsList" v-model="property.dataSpecsList"
/> />
<!-- 布尔型配置 --> <!-- 布尔型配置 -->
<el-form-item <el-form-item v-if="property.dataType === DataSpecsDataType.BOOL" label="布尔值">
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"> <template v-for="(item, index) in property.dataSpecsList" :key="item.value">
<div class="flex items-center justify-start w-1/1 mb-5px"> <div class="flex items-center justify-start w-1/1 mb-5px">
<span>{{ item.value }}</span> <span>{{ item.value }}</span>
@ -58,10 +54,6 @@
<!-- 文本型配置 --> <!-- 文本型配置 -->
<el-form-item <el-form-item
v-if="property.dataType === DataSpecsDataType.TEXT" v-if="property.dataType === DataSpecsDataType.TEXT"
:rules="[
{ required: true, message: '请输入文本字节长度', trigger: 'blur' },
{ validator: validateTextLength, trigger: 'blur' }
]"
label="数据长度" label="数据长度"
prop="property.dataSpecs.length" prop="property.dataSpecs.length"
> >
@ -74,16 +66,16 @@
<el-input class="w-255px!" disabled placeholder="String类型的UTC时间戳毫秒" /> <el-input class="w-255px!" disabled placeholder="String类型的UTC时间戳毫秒" />
</el-form-item> </el-form-item>
<!-- 数组型配置--> <!-- 数组型配置-->
<ThinkModelArrayTypeDataSpecs <ThingModelArrayDataSpecs
v-if="property.dataType === DataSpecsDataType.ARRAY" v-if="property.dataType === DataSpecsDataType.ARRAY"
v-model="property.dataSpecs" v-model="property.dataSpecs"
/> />
<!-- TODO puhui999: Struct 属性待完善 --> <!-- Struct 型配置-->
<el-form-item <ThingModelStructDataSpecs
:rules="[{ required: true, message: '请选择读写类型', trigger: 'change' }]" v-if="property.dataType === DataSpecsDataType.STRUCT"
label="读写类型" v-model="property.dataSpecsList"
prop="property.accessMode" />
> <el-form-item v-if="!isStructDataSpecs" label="读写类型" prop="property.accessMode">
<el-radio-group v-model="property.accessMode"> <el-radio-group v-model="property.accessMode">
<el-radio label="rw">读写</el-radio> <el-radio label="rw">读写</el-radio>
<el-radio label="r">只读</el-radio> <el-radio label="r">只读</el-radio>
@ -102,22 +94,29 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { DataSpecsDataType, dataTypeOptions } from './config' import { DataSpecsDataType, dataTypeOptions, validateBoolName } from './config'
import { import {
ThinkModelArrayTypeDataSpecs, ThingModelArrayDataSpecs,
ThinkModelEnumTypeDataSpecs, ThingModelEnumDataSpecs,
ThinkModelNumberTypeDataSpecs ThingModelNumberDataSpecs,
ThingModelStructDataSpecs
} from './dataSpecs' } from './dataSpecs'
import { ThinkModelProperty } from '@/api/iot/thinkmodel' import { ThingModelProperty } from '@/api/iot/thingmodel'
import { isEmpty } from '@/utils/is'
/** IoT 物模型数据 */ /** 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 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) => { const handleChange = (dataType: any) => {
property.value.dataSpecsList = [] property.value.dataSpecsList = []
@ -143,45 +142,6 @@ const handleChange = (dataType: any) => {
break 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -4,13 +4,13 @@
ref="formRef" ref="formRef"
v-loading="formLoading" v-loading="formLoading"
:model="formData" :model="formData"
:rules="formRules" :rules="ThingModelFormRules"
label-width="100px" label-width="100px"
> >
<el-form-item label="功能类型" prop="type"> <el-form-item label="功能类型" prop="type">
<el-radio-group v-model="formData.type"> <el-radio-group v-model="formData.type">
<el-radio-button <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" :key="dict.value"
:value="dict.value" :value="dict.value"
> >
@ -25,7 +25,7 @@
<el-input v-model="formData.identifier" placeholder="请输入标识符" /> <el-input v-model="formData.identifier" placeholder="请输入标识符" />
</el-form-item> </el-form-item>
<!-- 属性配置 --> <!-- 属性配置 -->
<ThinkModelDataSpecs <ThingModelDataSpecs
v-if="formData.type === ProductFunctionTypeEnum.PROPERTY" v-if="formData.type === ProductFunctionTypeEnum.PROPERTY"
v-model="formData.property" v-model="formData.property"
/> />
@ -40,15 +40,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ProductVO } from '@/api/iot/product/product' import { ProductVO } from '@/api/iot/product/product'
import ThinkModelDataSpecs from './ThinkModelDataSpecs.vue' import ThingModelDataSpecs from './ThingModelDataSpecs.vue'
import { ProductFunctionTypeEnum, ThinkModelApi, ThinkModelData } from '@/api/iot/thinkmodel' import { ProductFunctionTypeEnum, ThingModelApi, ThingModelData } from '@/api/iot/thingmodel'
import { IOT_PROVIDE_KEY } from '@/views/iot/utils/constants' import { IOT_PROVIDE_KEY } from '@/views/iot/utils/constants'
import { DataSpecsDataType } from './config' import { DataSpecsDataType, ThingModelFormRules } from './config'
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
/** IoT 物模型数据表单 */ /** IoT 物模型数据表单 */
defineOptions({ name: 'IoTProductThinkModelForm' }) defineOptions({ name: 'IoTProductThingModelForm' })
const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) // const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) //
@ -59,7 +59,7 @@ const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // const dialogTitle = ref('') //
const formLoading = ref(false) // 12 const formLoading = ref(false) // 12
const formType = ref('') // create - update - const formType = ref('') // create - update -
const formData = ref<ThinkModelData>({ const formData = ref<ThingModelData>({
type: ProductFunctionTypeEnum.PROPERTY, type: ProductFunctionTypeEnum.PROPERTY,
dataType: DataSpecsDataType.INT, dataType: DataSpecsDataType.INT,
property: { 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 const formRef = ref() // Ref
/** 打开弹窗 */ /** 打开弹窗 */
@ -117,7 +81,7 @@ const open = async (type: string, id?: number) => {
if (id) { if (id) {
formLoading.value = true formLoading.value = true
try { try {
formData.value = await ThinkModelApi.getThinkModel(id) formData.value = await ThingModelApi.getThingModel(id)
} finally { } finally {
formLoading.value = false formLoading.value = false
} }
@ -131,7 +95,7 @@ const submitForm = async () => {
await formRef.value.validate() await formRef.value.validate()
formLoading.value = true formLoading.value = true
try { try {
const data = cloneDeep(formData.value) as ThinkModelData const data = cloneDeep(formData.value) as ThingModelData
// //
data.productId = product!.value.id data.productId = product!.value.id
data.productKey = product!.value.productKey data.productKey = product!.value.productKey
@ -140,10 +104,10 @@ const submitForm = async () => {
data.property.identifier = data.identifier data.property.identifier = data.identifier
data.property.name = data.name data.property.name = data.name
if (formType.value === 'create') { if (formType.value === 'create') {
await ThinkModelApi.createThinkModel(data) await ThingModelApi.createThingModel(data)
message.success(t('common.createSuccess')) message.success(t('common.createSuccess'))
} else { } else {
await ThinkModelApi.updateThinkModel(data) await ThingModelApi.updateThingModel(data)
message.success(t('common.updateSuccess')) message.success(t('common.updateSuccess'))
} }
} finally { } finally {

View File

@ -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()
}

View File

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

View File

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

View File

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

View File

@ -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) // 12
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>

View File

@ -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
}

View File

@ -1,4 +1,3 @@
<!-- TODO 目录应该是 thinkModel -->
<template> <template>
<ContentWrap> <ContentWrap>
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
@ -17,7 +16,7 @@
placeholder="请选择功能类型" placeholder="请选择功能类型"
> >
<el-option <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" :key="dict.value"
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"
@ -50,7 +49,7 @@
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true"> <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
<el-table-column align="center" label="功能类型" prop="type"> <el-table-column align="center" label="功能类型" prop="type">
<template #default="scope"> <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> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="功能名称" prop="name" /> <el-table-column align="center" label="功能名称" prop="name" />
@ -97,23 +96,23 @@
</el-tabs> </el-tabs>
</ContentWrap> </ContentWrap>
<!-- 表单弹窗添加/修改 --> <!-- 表单弹窗添加/修改 -->
<ThinkModelForm ref="formRef" @success="getList" /> <ThingModelForm ref="formRef" @success="getList" />
</template> </template>
<script lang="ts" setup> <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 { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import ThinkModelForm from './ThinkModelForm.vue' import ThingModelForm from './ThingModelForm.vue'
import { ProductVO } from '@/api/iot/product/product' import { ProductVO } from '@/api/iot/product/product'
import { IOT_PROVIDE_KEY } from '@/views/iot/utils/constants' 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 { t } = useI18n() //
const message = useMessage() // const message = useMessage() //
const loading = ref(true) // const loading = ref(true) //
const list = ref<ThinkModelData[]>([]) // const list = ref<ThingModelData[]>([]) //
const total = ref(0) // const total = ref(0) //
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, pageNo: 1,
@ -131,7 +130,7 @@ const getList = async () => {
loading.value = true loading.value = true
try { try {
queryParams.productId = product?.value?.id || -1 queryParams.productId = product?.value?.id || -1
const data = await ThinkModelApi.getThinkModelPage(queryParams) const data = await ThingModelApi.getThingModelPage(queryParams)
list.value = data.list list.value = data.list
total.value = data.total total.value = data.total
} finally { } finally {
@ -163,7 +162,7 @@ const handleDelete = async (id: number) => {
// //
await message.delConfirm() await message.delConfirm()
// //
await ThinkModelApi.deleteThinkModel(id) await ThingModelApi.deleteThingModel(id)
message.success(t('common.delSuccess')) message.success(t('common.delSuccess'))
// //
await getList() await getList()

View File

@ -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
}

View File

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