iot产品管理问题

1.修复物模型列表无限加载的问题
2.修复物模型管理页面添加,TSL,编辑,删除,功能类型选项功能不用问题
3.修复TSL按钮物模型接口没有的问题
4.修复物模型新增编辑页面的属性不能正常编辑修改问题美化显示
iot设备管理问题
1.修复新增编辑页面缺少字段相关组件
2.修复设备详情中子页面不显示问题
3.修复设备详情子页面物模型数据页面不显示问题
4.修复模拟设备右侧不显示问题 右侧溢出,改为上下分栏

Signed-off-by: Administrator <425053404@qq.com>
This commit is contained in:
Administrator
2025-10-17 00:13:48 +08:00
parent 22bd8b8f45
commit 54afd4555d
37 changed files with 1060 additions and 822 deletions

View File

@@ -2,8 +2,6 @@
<script lang="ts" setup>
import type { Ref } from 'vue';
import type { ThingModelEvent } from '#/api/iot/thingmodel';
import { watch } from 'vue';
import { isEmpty } from '@vben/utils';
@@ -27,7 +25,7 @@ const thingModelEvent = useVModel(
props,
'modelValue',
emits,
) as Ref<ThingModelEvent>;
) as Ref<any>;
// 默认选中INFO 信息
watch(
@@ -43,9 +41,9 @@ watch(
<Form.Item
:rules="[{ required: true, message: '请选择事件类型', trigger: 'change' }]"
label="事件类型"
prop="event.type"
name="event.type"
>
<Radio.Group v-model="thingModelEvent.type">
<Radio.Group v-model:value="thingModelEvent.type">
<Radio
v-for="eventType in Object.values(IoTThingModelEventTypeEnum)"
:key="eventType.value"
@@ -57,7 +55,7 @@ watch(
</Form.Item>
<Form.Item label="输出参数">
<ThingModelInputOutputParam
v-model="thingModelEvent.outputData"
v-model="thingModelEvent.outputParams"
:direction="IoTThingModelParamDirectionEnum.OUTPUT"
/>
</Form.Item>

View File

@@ -12,7 +12,7 @@ import { getDictOptions } from '@vben/hooks';
import { $t } from '@vben/locales';
import { cloneDeep } from '@vben/utils';
import { Button, Form, Input, message, Radio } from 'ant-design-vue';
import { Form, Input, message, Modal, Radio } from 'ant-design-vue';
import {
createThingModel,
@@ -40,8 +40,8 @@ const dialogVisible = ref(false); // 弹窗的是否展示
const dialogTitle = ref(''); // 弹窗的标题
const formLoading = ref(false); // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref(''); // 表单的类型create - 新增update - 修改
const formData = ref<ThingModelData>({
type: IoTThingModelTypeEnum.PROPERTY.toString(),
const formData = ref<any>({
type: IoTThingModelTypeEnum.PROPERTY,
dataType: IoTDataSpecsDataTypeEnum.INT,
property: {
dataType: IoTDataSpecsDataTypeEnum.INT,
@@ -49,8 +49,13 @@ const formData = ref<ThingModelData>({
dataType: IoTDataSpecsDataTypeEnum.INT,
},
},
service: {},
event: {},
service: {
inputParams: [],
outputParams: [],
},
event: {
outputParams: [],
},
});
const formRef = ref(); // 表单 Ref
@@ -58,13 +63,19 @@ const formRef = ref(); // 表单 Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true;
dialogTitle.value = $t(`action.${type}`);
// 设置标题create -> 新增update -> 编辑
dialogTitle.value = type === 'create' ? $t('page.action.add') : $t('page.action.edit');
formType.value = type;
resetForm();
if (id) {
formLoading.value = true;
try {
formData.value = await getThingModel(id);
const result = await getThingModel(id);
// 转换类型为数字
formData.value = {
...result,
type: Number(result.type),
};
// 情况一:属性初始化
if (
!formData.value.property ||
@@ -77,20 +88,50 @@ const open = async (type: string, id?: number) => {
dataType: IoTDataSpecsDataTypeEnum.INT,
},
};
} else {
// 确保 dataSpecs 和 dataSpecsList 存在
if (!formData.value.property.dataSpecs) {
formData.value.property.dataSpecs = {};
}
if (!formData.value.property.dataSpecsList) {
formData.value.property.dataSpecsList = [];
}
// 如果 property.dataType 不存在,设置为默认值
if (!formData.value.property.dataType) {
formData.value.property.dataType = IoTDataSpecsDataTypeEnum.INT;
}
}
// 情况二:服务初始化
if (
!formData.value.service ||
Object.keys(formData.value.service).length === 0
) {
formData.value.service = {};
formData.value.service = {
inputParams: [],
outputParams: [],
};
} else {
// 确保参数数组存在
if (!formData.value.service.inputParams) {
formData.value.service.inputParams = [];
}
if (!formData.value.service.outputParams) {
formData.value.service.outputParams = [];
}
}
// 情况三:事件初始化
if (
!formData.value.event ||
Object.keys(formData.value.event).length === 0
) {
formData.value.event = {};
formData.value.event = {
outputParams: [],
};
} else {
// 确保参数数组存在
if (!formData.value.event.outputParams) {
formData.value.event.outputParams = [];
}
}
} finally {
formLoading.value = false;
@@ -137,6 +178,13 @@ function fillExtraAttributes(data: any) {
data.dataType = data.service.dataType;
data.service.identifier = data.identifier;
data.service.name = data.name;
// 保留输入输出参数,但如果为空数组则删除
if (!data.service.inputParams || data.service.inputParams.length === 0) {
delete data.service.inputParams;
}
if (!data.service.outputParams || data.service.outputParams.length === 0) {
delete data.service.outputParams;
}
delete data.property;
delete data.event;
}
@@ -146,6 +194,10 @@ function fillExtraAttributes(data: any) {
data.dataType = data.event.dataType;
data.event.identifier = data.identifier;
data.event.name = data.name;
// 保留输出参数,但如果为空数组则删除
if (!data.event.outputParams || data.event.outputParams.length === 0) {
delete data.event.outputParams;
}
delete data.property;
delete data.service;
}
@@ -164,7 +216,7 @@ function removeDataSpecs(val: any) {
/** 重置表单 */
function resetForm() {
formData.value = {
type: IoTThingModelTypeEnum.PROPERTY.toString(),
type: IoTThingModelTypeEnum.PROPERTY,
dataType: IoTDataSpecsDataTypeEnum.INT,
property: {
dataType: IoTDataSpecsDataTypeEnum.INT,
@@ -172,18 +224,27 @@ function resetForm() {
dataType: IoTDataSpecsDataTypeEnum.INT,
},
},
service: {},
event: {},
service: {
inputParams: [],
outputParams: [],
},
event: {
outputParams: [],
},
};
formRef.value?.resetFields();
}
</script>
<template>
<Modal v-model="dialogVisible" :title="dialogTitle">
<Modal
v-model:open="dialogVisible"
:title="dialogTitle"
:confirm-loading="formLoading"
@ok="submitForm"
>
<Form
ref="formRef"
:loading="formLoading"
:model="formData"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
@@ -191,12 +252,9 @@ function resetForm() {
<Form.Item label="功能类型" name="type">
<Radio.Group v-model:value="formData.type">
<Radio.Button
v-for="(dict, index) in getDictOptions(
DICT_TYPE.IOT_THING_MODEL_TYPE,
'number',
)"
:key="index"
:value="dict.value"
v-for="dict in getDictOptions(DICT_TYPE.IOT_THING_MODEL_TYPE)"
:key="String(dict.value)"
:value="Number(dict.value)"
>
{{ dict.label }}
</Radio.Button>
@@ -210,17 +268,17 @@ function resetForm() {
</Form.Item>
<!-- 属性配置 -->
<ThingModelProperty
v-if="formData.type === IoTThingModelTypeEnum.PROPERTY.toString()"
v-if="formData.type === IoTThingModelTypeEnum.PROPERTY"
v-model="formData.property"
/>
<!-- 服务配置 -->
<ThingModelService
v-if="formData.type === IoTThingModelTypeEnum.SERVICE.toString()"
v-if="formData.type === IoTThingModelTypeEnum.SERVICE"
v-model="formData.service"
/>
<!-- 事件配置 -->
<ThingModelEvent
v-if="formData.type === IoTThingModelTypeEnum.EVENT.toString()"
v-if="formData.type === IoTThingModelTypeEnum.EVENT"
v-model="formData.event"
/>
<Form.Item label="描述" name="desc">
@@ -232,12 +290,5 @@ function resetForm() {
/>
</Form.Item>
</Form>
<template #footer>
<Button :disabled="formLoading" type="primary" @click="submitForm">
确 定
</Button>
<Button @click="dialogVisible = false"> </Button>
</template>
</Modal>
</template>

View File

@@ -29,6 +29,7 @@ const formData = ref<any>({
dataSpecs: {
dataType: IoTDataSpecsDataTypeEnum.INT,
},
dataSpecsList: [],
},
});
@@ -40,16 +41,22 @@ function openParamForm(val: any) {
return;
}
// 编辑时回显数据
const valData = val as any;
formData.value = {
identifier: val.identifier,
name: val.name,
description: val.description,
identifier: valData?.identifier || '',
name: valData?.name || '',
description: valData?.description || '',
property: {
dataType: val.dataType,
dataSpecs: val.dataSpecs,
dataSpecsList: val.dataSpecsList,
dataType: valData?.dataType || IoTDataSpecsDataTypeEnum.INT,
dataSpecs: valData?.dataSpecs ?? {},
dataSpecsList: valData?.dataSpecsList ?? [],
},
};
// 确保 property.dataType 有值
if (!formData.value.property.dataType) {
formData.value.property.dataType = IoTDataSpecsDataTypeEnum.INT;
}
}
/** 删除 param 项 */
@@ -108,6 +115,7 @@ function resetForm() {
dataSpecs: {
dataType: IoTDataSpecsDataTypeEnum.INT,
},
dataSpecsList: [],
},
};
paramFormRef.value?.resetFields();
@@ -122,35 +130,34 @@ function resetForm() {
>
<span>参数名称{{ item.name }}</span>
<div class="btn">
<Button link type="primary" @click="openParamForm(item)"> 编辑 </Button>
<Divider direction="vertical" />
<Button link danger @click="deleteParamItem(index)"> 删除 </Button>
<Button type="link" @click="openParamForm(item)">编辑</Button>
<Divider type="vertical" />
<Button type="link" danger @click="deleteParamItem(index)">删除</Button>
</div>
</div>
<Button link type="primary" @click="openParamForm(null)"> +新增参数 </Button>
<Button type="link" @click="openParamForm(null)">+新增参数</Button>
<!-- param 表单 -->
<Modal v-model="dialogVisible" title="新增参数" append-to-body>
<Modal
v-model:open="dialogVisible"
title="新增参数"
:confirm-loading="formLoading"
@ok="submitForm"
>
<Form
ref="paramFormRef"
v-loading="formLoading"
:model="formData"
label-width="100px"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<Form.Item label="参数名称" prop="name">
<Input v-model="formData.name" placeholder="请输入功能名称" />
<Form.Item label="参数名称" name="name">
<Input v-model:value="formData.name" placeholder="请输入功能名称" />
</Form.Item>
<Form.Item label="标识符" prop="identifier">
<Input v-model="formData.identifier" placeholder="请输入标识符" />
<Form.Item label="标识符" name="identifier">
<Input v-model:value="formData.identifier" placeholder="请输入标识符" />
</Form.Item>
<!-- 属性配置 -->
<ThingModelProperty v-model="formData.property" is-params />
</Form>
<template #footer>
<Button :disabled="formLoading" type="primary" @click="submitForm">
</Button>
<Button @click="dialogVisible = false"> </Button>
</template>
</Modal>
</template>

View File

@@ -11,7 +11,6 @@ import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Form, Input, Radio, Select } from 'ant-design-vue';
import { validateBoolName } from '#/api/iot/thingmodel';
import {
getDataTypeOptions,
IoTDataSpecsDataTypeEnum,
@@ -43,13 +42,8 @@ const getDataTypeOptions2 = computed(() => {
if (!props.isStructDataSpecs) {
return getDataTypeOptions();
}
const excludedTypes = new Set([
IoTDataSpecsDataTypeEnum.ARRAY,
IoTDataSpecsDataTypeEnum.STRUCT,
]);
return getDataTypeOptions().filter(
(item: any) => !excludedTypes.has(item.value),
);
const excludedTypes = [IoTDataSpecsDataTypeEnum.STRUCT, IoTDataSpecsDataTypeEnum.ARRAY];
return getDataTypeOptions().filter((item: any) => !excludedTypes.includes(item.value));
}); // 获得数据类型列表
/** 属性值的数据类型切换时初始化相关数据 */
@@ -58,11 +52,19 @@ function handleChange(dataType: any) {
property.value.dataSpecsList = [];
// 不是列表型数据才设置 dataSpecs.dataType
![
IoTDataSpecsDataTypeEnum.BOOL,
IoTDataSpecsDataTypeEnum.ENUM,
IoTDataSpecsDataTypeEnum.BOOL,
IoTDataSpecsDataTypeEnum.STRUCT,
].includes(dataType) && (property.value.dataSpecs.dataType = dataType);
switch (dataType) {
case IoTDataSpecsDataTypeEnum.ENUM: {
property.value.dataSpecsList.push({
dataType: IoTDataSpecsDataTypeEnum.ENUM,
name: '', // 枚举项的名称
value: undefined, // 枚举值
});
break;
}
case IoTDataSpecsDataTypeEnum.BOOL: {
for (let i = 0; i < 2; i++) {
property.value.dataSpecsList.push({
@@ -73,15 +75,8 @@ function handleChange(dataType: any) {
}
break;
}
case IoTDataSpecsDataTypeEnum.ENUM: {
property.value.dataSpecsList.push({
dataType: IoTDataSpecsDataTypeEnum.ENUM,
name: '', // 枚举项的名称
value: undefined, // 枚举值
});
break;
}
}
// useVModel 会自动同步数据到父组件,不需要手动 emit
}
/** 默认选中读写 */
@@ -91,9 +86,9 @@ watch(
if (props.isStructDataSpecs || props.isParams) {
return;
}
isEmpty(val) &&
(property.value.accessMode =
IoTThingModelAccessModeEnum.READ_WRITE.value);
if (isEmpty(val)) {
property.value.accessMode = IoTThingModelAccessModeEnum.READ_WRITE.value;
}
},
{ immediate: true },
);
@@ -101,12 +96,10 @@ watch(
<template>
<Form.Item
:rules="[{ required: true, message: '请选择数据类型', trigger: 'change' }]"
label="数据类型"
prop="property.dataType"
>
<Select
v-model="property.dataType"
v-model:value="property.dataType"
placeholder="请选择数据类型"
@change="handleChange"
>
@@ -114,9 +107,10 @@ watch(
<Select.Option
v-for="option in getDataTypeOptions2"
:key="option.value"
:label="`${option.value}(${option.label})`"
:value="option.value"
/>
>
{{ `${option.value}(${option.label})` }}
</Select.Option>
</Select>
</Form.Item>
<!-- 数值型配置 -->
@@ -140,24 +134,17 @@ watch(
v-if="property.dataType === IoTDataSpecsDataTypeEnum.BOOL"
label="布尔值"
>
<template v-for="(item, index) in property.dataSpecsList" :key="item.value">
<div class="w-1/1 mb-5px flex items-center justify-start">
<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>
<Form.Item
:prop="`property.dataSpecsList[${index}].name`"
:rules="[
{ required: true, message: '枚举描述不能为空' },
{ validator: validateBoolName, trigger: 'blur' },
]"
class="mb-0 flex-1"
>
<div class="flex-1">
<Input
v-model="item.name"
v-model:value="item.name"
:placeholder="`如:${item.value === 0 ? '关' : '开'}`"
class="w-255px!"
/>
</Form.Item>
</div>
</div>
</template>
</Form.Item>
@@ -165,21 +152,21 @@ watch(
<Form.Item
v-if="property.dataType === IoTDataSpecsDataTypeEnum.TEXT"
label="数据长度"
prop="property.dataSpecs.length"
name="property.dataSpecs.length"
>
<Input
v-model="property.dataSpecs.length"
v-model:value="property.dataSpecs.length"
class="w-255px!"
placeholder="请输入文本字节长度"
>
<template #append>字节</template>
<template #addonAfter>字节</template>
</Input>
</Form.Item>
<!-- 时间型配置 -->
<Form.Item
v-if="property.dataType === IoTDataSpecsDataTypeEnum.DATE"
label="时间格式"
prop="date"
name="date"
>
<Input
class="w-255px!"
@@ -200,13 +187,13 @@ watch(
<Form.Item
v-if="!isStructDataSpecs && !isParams"
label="读写类型"
prop="property.accessMode"
name="property.accessMode"
>
<Radio.Group v-model="property.accessMode">
<Radio.Group v-model:value="property.accessMode">
<Radio
v-for="accessMode in Object.values(IoTThingModelAccessModeEnum)"
:key="accessMode.value"
:label="accessMode.value"
:value="accessMode.value"
>
{{ accessMode.label }}
</Radio>

View File

@@ -2,8 +2,6 @@
<script lang="ts" setup>
import type { Ref } from 'vue';
import type { ThingModelService } from '#/api/iot/thingmodel';
import { watch } from 'vue';
import { isEmpty } from '@vben/utils';
@@ -23,7 +21,7 @@ defineOptions({ name: 'ThingModelService' });
const props = defineProps<{ isStructDataSpecs?: boolean; modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const service = useVModel(props, 'modelValue', emits) as Ref<ThingModelService>;
const service = useVModel(props, 'modelValue', emits) as Ref<any>;
/** 默认选中ASYNC 异步 */
watch(
@@ -39,9 +37,9 @@ watch(
<Form.Item
:rules="[{ required: true, message: '请选择调用方式', trigger: 'change' }]"
label="调用方式"
prop="service.callType"
name="service.callType"
>
<Radio.Group v-model="service.callType">
<Radio.Group v-model:value="service.callType">
<Radio
v-for="callType in Object.values(IoTThingModelServiceCallTypeEnum)"
:key="callType.value"
@@ -53,13 +51,13 @@ watch(
</Form.Item>
<Form.Item label="输入参数">
<ThingModelInputOutputParam
v-model="service.inputData"
v-model="service.inputParams"
:direction="IoTThingModelParamDirectionEnum.INPUT"
/>
</Form.Item>
<Form.Item label="输出参数">
<ThingModelInputOutputParam
v-model="service.outputData"
v-model="service.outputParams"
:direction="IoTThingModelParamDirectionEnum.OUTPUT"
/>
</Form.Item>

View File

@@ -3,58 +3,115 @@ import type { Ref } from 'vue';
import type { IotProductApi } from '#/api/iot/product/product';
import { inject, onMounted, ref } from 'vue';
import { computed, inject, ref, watch } from 'vue';
import { Modal, Radio } from 'ant-design-vue';
import hljs from 'highlight.js'; // 导入代码高亮文件
import json from 'highlight.js/lib/languages/json';
import { Modal, Radio, Textarea } from 'ant-design-vue';
import { getThingModelListByProductId } from '#/api/iot/thingmodel';
import { getThingModelTSL } from '#/api/iot/thingmodel';
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants';
import 'highlight.js/styles/github.css'; // 导入代码高亮样式
defineOptions({ name: 'ThingModelTSL' });
const dialogVisible = ref(false); // 弹窗的是否展示
const dialogTitle = ref('物模型 TSL'); // 弹窗的标题
const product = inject<Ref<IotProductApi.Product>>(IOT_PROVIDE_KEY.PRODUCT); // 注入产品信息
const viewMode = ref('code'); // 查看模式:code-代码视图editor-编辑器视图
const viewMode = ref('view'); // 查看模式:view-代码视图editor-编辑器视图
/** 打开弹窗 */
function open() {
const open = async () => {
dialogVisible.value = true;
}
await getTsl();
};
defineExpose({ open });
/** 获取 TSL */
const thingModelTSL = ref({});
async function getTsl() {
thingModelTSL.value = await getThingModelListByProductId(
product?.value?.id || 0,
);
}
const thingModelTSL = ref<any>({});
const tslString = ref(''); // 用于编辑器的字符串格式
/** 初始化 */
onMounted(async () => {
// 注册代码高亮的各种语言
hljs.registerLanguage('json', json);
await getTsl();
const getTsl = async () => {
try {
thingModelTSL.value = await getThingModelTSL(product?.value?.id || 0);
// 将对象转换为格式化的 JSON 字符串
tslString.value = JSON.stringify(thingModelTSL.value, null, 2);
} catch (error) {
console.error('获取 TSL 失败:', error);
thingModelTSL.value = {};
tslString.value = '{}';
}
};
/** 格式化的 TSL 用于只读展示 */
const formattedTSL = computed(() => {
try {
if (typeof thingModelTSL.value === 'string') {
return JSON.stringify(JSON.parse(thingModelTSL.value), null, 2);
}
return JSON.stringify(thingModelTSL.value, null, 2);
} catch {
return JSON.stringify(thingModelTSL.value, null, 2);
}
});
/** 监听编辑器内容变化,实时更新数据 */
watch(tslString, (newValue) => {
try {
thingModelTSL.value = JSON.parse(newValue);
} catch {
// JSON 解析失败时保持原值
}
});
</script>
<template>
<Modal v-model="dialogVisible" :title="dialogTitle">
<JsonEditor
v-model="thingModelTSL"
:mode="viewMode === 'editor' ? 'code' : 'view'"
height="600px"
/>
<template #footer>
<Radio.Group v-model="viewMode" size="small">
<Radio.Button label="code">代码视图</Radio.Button>
<Radio.Button label="editor">编辑器视图</Radio.Button>
<Modal
v-model:open="dialogVisible"
:title="dialogTitle"
:footer="null"
width="800px"
>
<div class="mb-4">
<Radio.Group v-model:value="viewMode" size="small">
<Radio.Button value="view">代码视图</Radio.Button>
<Radio.Button value="editor">编辑器视图</Radio.Button>
</Radio.Group>
</template>
</div>
<!-- 代码视图 - 只读展示 -->
<div v-if="viewMode === 'view'" class="json-viewer-container">
<pre class="json-code"><code>{{ formattedTSL }}</code></pre>
</div>
<!-- 编辑器视图 - 可编辑 -->
<Textarea
v-else
v-model:value="tslString"
:rows="20"
placeholder="请输入 JSON 格式的物模型 TSL"
class="json-editor"
/>
</Modal>
</template>
<style scoped>
.json-viewer-container {
background-color: #f5f5f5;
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 12px;
max-height: 600px;
overflow-y: auto;
}
.json-code {
margin: 0;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-size: 13px;
line-height: 1.5;
color: #333;
white-space: pre-wrap;
word-wrap: break-word;
}
.json-editor {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-size: 13px;
}
</style>

View File

@@ -1,6 +1,10 @@
<script lang="ts" setup>
import type { ThingModelData } from '#/api/iot/thingmodel';
import { computed } from 'vue';
import { Tooltip } from 'ant-design-vue';
import {
getEventTypeLabel,
getThingModelServiceCallTypeLabel,
@@ -11,12 +15,32 @@ import {
/** 数据定义展示组件 */
defineOptions({ name: 'DataDefinition' });
defineProps<{ data: ThingModelData }>();
const props = defineProps<{ data: ThingModelData }>();
// 格式化布尔值和枚举值列表为字符串
const formattedDataSpecsList = computed(() => {
if (!props.data.property?.dataSpecsList || props.data.property.dataSpecsList.length === 0) {
return '';
}
return props.data.property.dataSpecsList
.map(item => `${item.value}-${item.name}`)
.join('、');
});
// 显示的简短文本(第一个值)
const shortText = computed(() => {
if (!props.data.property?.dataSpecsList || props.data.property.dataSpecsList.length === 0) {
return '-';
}
const first = props.data.property.dataSpecsList[0];
const count = props.data.property.dataSpecsList.length;
return count > 1 ? `${first.value}-${first.name}${count}` : `${first.value}-${first.name}`;
});
</script>
<template>
<!-- 属性 -->
<template v-if="data.type === IoTThingModelTypeEnum.PROPERTY.toString()">
<template v-if="Number(data.type) === IoTThingModelTypeEnum.PROPERTY">
<!-- 非列表型数值 -->
<div
v-if="
@@ -28,12 +52,12 @@ defineProps<{ data: ThingModelData }>();
"
>
取值范围:{{
`${data.property?.dataSpecs.min}~${data.property?.dataSpecs.max}`
`${data.property?.dataSpecs?.min}~${data.property?.dataSpecs?.max}`
}}
</div>
<!-- 非列表型:文本 -->
<div v-if="IoTDataSpecsDataTypeEnum.TEXT === data.property?.dataType">
数据长度:{{ data.property?.dataSpecs.length }}
数据长度:{{ data.property?.dataSpecs?.length }}
</div>
<!-- 列表型: 数组、结构、时间(特殊) -->
<div
@@ -55,28 +79,37 @@ defineProps<{ data: ThingModelData }>();
)
"
>
<div>
{{
IoTDataSpecsDataTypeEnum.BOOL === data.property?.dataType
? '布尔值'
: '枚举值'
}}
</div>
<div v-for="item in data.property?.dataSpecsList" :key="item.value">
{{ `${item.name}-${item.value}` }}
</div>
<Tooltip :title="formattedDataSpecsList" placement="topLeft">
<span class="data-specs-text">
{{
IoTDataSpecsDataTypeEnum.BOOL === data.property?.dataType
? '布尔值'
: '枚举值'
}}{{ shortText }}
</span>
</Tooltip>
</div>
</template>
<!-- 服务 -->
<div v-if="data.type === IoTThingModelTypeEnum.SERVICE.toString()">
<div v-if="Number(data.type) === IoTThingModelTypeEnum.SERVICE">
调用方式:{{
getThingModelServiceCallTypeLabel(data.service?.callType as any)
}}
</div>
<!-- 事件 -->
<div v-if="data.type === IoTThingModelTypeEnum.EVENT.toString()">
<div v-if="Number(data.type) === IoTThingModelTypeEnum.EVENT">
事件类型:{{ getEventTypeLabel(data.event?.type as any) }}
</div>
</template>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.data-specs-text {
cursor: help;
border-bottom: 1px dashed #d9d9d9;
&:hover {
border-bottom-color: #1890ff;
color: #1890ff;
}
}
</style>

View File

@@ -29,8 +29,8 @@ function handleChange(val: any) {
</script>
<template>
<Form.Item label="元素类型" prop="property.dataSpecs.childDataType">
<Radio.Group v-model="dataSpecs.childDataType" @change="handleChange">
<Form.Item label="元素类型" name="property.dataSpecs.childDataType">
<Radio.Group v-model:value="dataSpecs.childDataType" @change="handleChange">
<template v-for="item in getDataTypeOptions()" :key="item.value">
<Radio
v-if="
@@ -50,8 +50,8 @@ function handleChange(val: any) {
</template>
</Radio.Group>
</Form.Item>
<Form.Item label="元素个数" prop="property.dataSpecs.size">
<Input v-model="dataSpecs.size" placeholder="请输入数组中的元素个数" />
<Form.Item label="元素个数" name="property.dataSpecs.size">
<Input v-model:value="dataSpecs.size" placeholder="请输入数组中的元素个数" />
</Form.Item>
<!-- Struct 型配置-->
<ThingModelStructDataSpecs

View File

@@ -2,31 +2,22 @@
<script lang="ts" setup>
import type { Ref } from 'vue';
import type { DataSpecsEnumOrBoolData } from '#/api/iot/thingmodel';
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Button, Form, Input, message } from 'ant-design-vue';
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
/** 枚举型的 dataSpecs 配置组件 */
defineOptions({ name: 'ThingModelEnumDataSpecs' });
const props = defineProps<{ modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<
DataSpecsEnumOrBoolData[]
>;
const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<any[]>;
/** 添加枚举项 */
function addEnum() {
dataSpecsList.value.push({
dataType: IoTDataSpecsDataTypeEnum.ENUM,
name: '', // 枚举项的名称
value: '', // 枚举值
});
} as any);
}
/** 删除枚举项 */
@@ -38,92 +29,10 @@ function deleteEnum(index: number) {
dataSpecsList.value.splice(index, 1);
}
/** 校验枚举值 */
function validateEnumValue(_: any, value: any, callback: any) {
if (isEmpty(value)) {
callback(new Error('枚举值不能为空'));
return;
}
if (Number.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();
}
/** 校验枚举描述 */
function validateEnumName(_: any, value: string, callback: any) {
if (isEmpty(value)) {
callback(new Error('枚举描述不能为空'));
return;
}
// 检查开头字符
if (!/^[\u4E00-\u9FA5a-z0-9]/i.test(value)) {
callback(new Error('枚举描述必须以中文、英文字母或数字开头'));
return;
}
// 检查整体格式
if (!/^[\u4E00-\u9FA5a-z0-9][\w\u4E00-\u9FA5-]*$/i.test(value)) {
callback(new Error('枚举描述只能包含中文、英文字母、数字、下划线和短划线'));
return;
}
// 检查长度(一个中文算一个字符)
if (value.length > 20) {
callback(new Error('枚举描述长度不能超过20个字符'));
return;
}
callback();
}
/** 校验整个枚举列表 */
function 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) =>
Number.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>
<template>
<Form.Item
:rules="[
{ required: true, validator: validateEnumList, trigger: 'change' },
]"
label="枚举项"
>
<Form.Item label="枚举项">
<div class="flex flex-col">
<div class="flex items-center">
<span class="flex-1"> 参数值 </span>
@@ -132,41 +41,27 @@ function validateEnumList(_: any, __: any, callback: any) {
<div
v-for="(item, index) in dataSpecsList"
:key="index"
class="mb-5px flex items-center justify-between"
class="flex items-center justify-between mb-5px"
>
<Form.Item
:prop="`property.dataSpecsList[${index}].value`"
:rules="[
{ required: true, message: '枚举值不能为空' },
{ validator: validateEnumValue, trigger: 'blur' },
]"
class="mb-0 flex-1"
>
<Input v-model="item.value" placeholder="请输入枚举值,如'0'" />
</Form.Item>
<div class="flex-1">
<Input v-model:value="item.value" placeholder="请输入枚举值,如'0'" />
</div>
<span class="mx-2">~</span>
<Form.Item
:prop="`property.dataSpecsList[${index}].name`"
:rules="[
{ required: true, message: '枚举描述不能为空' },
{ validator: validateEnumName, trigger: 'blur' },
]"
class="mb-0 flex-1"
>
<Input v-model="item.name" placeholder="对该枚举项的描述" />
</Form.Item>
<Button class="ml-10px" link type="primary" @click="deleteEnum(index)">
<div class="flex-1">
<Input v-model:value="item.name" placeholder="对该枚举项的描述" />
</div>
<Button class="ml-10px" type="link" @click="deleteEnum(index)">
删除
</Button>
</div>
<Button link type="primary" @click="addEnum">+添加枚举项</Button>
<Button type="link" @click="addEnum">+添加枚举项</Button>
</div>
</Form.Item>
</template>
<style lang="scss" scoped>
:deep(.el-form-item) {
.el-form-item {
:deep(.ant-form-item) {
.ant-form-item {
margin-bottom: 0;
}
}

View File

@@ -1,14 +1,55 @@
<!-- dataTypenumber 数组类型 -->
<template>
<Form.Item label="取值范围">
<div class="flex items-center justify-between">
<div class="flex-1">
<Input v-model:value="dataSpecs.min" placeholder="请输入最小值" />
</div>
<span class="mx-2">~</span>
<div class="flex-1">
<Input v-model:value="dataSpecs.max" placeholder="请输入最大值" />
</div>
</div>
</Form.Item>
<Form.Item label="步长">
<Input v-model:value="dataSpecs.step" placeholder="请输入步长" />
</Form.Item>
<Form.Item label="单位">
<Select
:model-value="
dataSpecs.unit ? `${dataSpecs.unitName}-${dataSpecs.unit}` : ''
"
show-search
placeholder="请选择单位"
class="w-1/1"
@change="unitChange"
>
<Select.Option
v-for="(item, index) in getDictOptions(
DICT_TYPE.IOT_THING_MODEL_UNIT,
'string',
)"
:key="index"
:value="`${item.label}-${item.value}`"
>
{{ `${item.label}-${item.value}` }}
</Select.Option>
</Select>
</Form.Item>
</template>
<script lang="ts" setup>
import type { Ref } from 'vue';
import type { DataSpecsNumberData } from '#/api/iot/thingmodel';
import { useVModel } from '@vueuse/core';
import { Form, Input, Select } from 'ant-design-vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { useVModel } from '@vueuse/core';
/** 数值型的 dataSpecs 配置组件 */
defineOptions({ name: 'ThingModelNumberDataSpecs' });
@@ -21,132 +62,17 @@ const dataSpecs = useVModel(
) as Ref<DataSpecsNumberData>;
/** 单位发生变化时触发 */
const unitChange = (UnitSpecs: string) => {
const [unitName, unit] = UnitSpecs.split('-');
const unitChange = (UnitSpecs: any) => {
if (!UnitSpecs) return;
const [unitName, unit] = String(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 (Number.isNaN(min)) {
callback(new Error('请输入有效的数值'));
return;
}
if (max !== undefined && !Number.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 (Number.isNaN(max)) {
callback(new Error('请输入有效的数值'));
return;
}
if (min !== undefined && !Number.isNaN(min) && max <= min) {
callback(new Error('最大值必须大于最小值'));
return;
}
callback();
};
/** 校验步长 */
const validateStep = (_: any, __: any, callback: any) => {
const step = Number(dataSpecs.value.step);
if (Number.isNaN(step)) {
callback(new Error('请输入有效的数值'));
return;
}
if (step <= 0) {
callback(new Error('步长必须大于0'));
return;
}
const min = Number(dataSpecs.value.min);
const max = Number(dataSpecs.value.max);
if (!Number.isNaN(min) && !Number.isNaN(max) && step > max - min) {
callback(new Error('步长不能大于最大值和最小值的差值'));
return;
}
callback();
};
</script>
<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="请选择单位"
class="w-1/1"
@change="unitChange"
>
<el-option
v-for="(item, index) in getDictOptions(
DICT_TYPE.IOT_THING_MODEL_UNIT,
'string',
)"
:key="index"
:label="`${item.label}-${item.value}`"
:value="`${item.label}-${item.value}`"
/>
</el-select>
</el-form-item>
</template>
<style lang="scss" scoped>
:deep(.el-form-item) {
.el-form-item {
:deep(.ant-form-item) {
.ant-form-item {
margin-bottom: 0;
}
}

View File

@@ -29,6 +29,7 @@ const formData = ref<any>({
dataSpecs: {
dataType: IoTDataSpecsDataTypeEnum.INT,
},
dataSpecsList: [],
},
});
@@ -40,16 +41,22 @@ function openStructForm(val: any) {
return;
}
// 编辑时回显数据
const valData = val as any;
formData.value = {
identifier: val.identifier,
name: val.name,
description: val.description,
identifier: valData?.identifier || '',
name: valData?.name || '',
description: valData?.description || '',
property: {
dataType: val.childDataType,
dataSpecs: val.dataSpecs,
dataSpecsList: val.dataSpecsList,
dataType: valData?.childDataType || IoTDataSpecsDataTypeEnum.INT,
dataSpecs: valData?.dataSpecs ?? {},
dataSpecsList: valData?.dataSpecsList ?? [],
},
};
// 确保 property.dataType 有值
if (!formData.value.property.dataType) {
formData.value.property.dataType = IoTDataSpecsDataTypeEnum.INT;
}
}
/** 删除 struct 项 */
@@ -102,19 +109,12 @@ function resetForm() {
dataSpecs: {
dataType: IoTDataSpecsDataTypeEnum.INT,
},
dataSpecsList: [],
},
};
structFormRef.value?.resetFields();
}
/** 校验 struct 不能为空 */
function validateList(_: any, __: any, callback: any) {
if (isEmpty(dataSpecsList.value)) {
callback(new Error('struct 不能为空'));
return;
}
callback();
}
/** 组件初始化 */
onMounted(async () => {
@@ -126,51 +126,49 @@ onMounted(async () => {
<template>
<!-- struct 数据展示 -->
<Form.Item
:rules="[{ required: true, validator: validateList, trigger: 'change' }]"
label="JSON 对象"
>
<Form.Item label="属性对象">
<div
v-for="(item, index) in dataSpecsList"
:key="index"
class="px-10px mb-10px flex w-full justify-between bg-gray-100"
>
<span>参数名称{{ item.name }}</span>
<span>参数{{ item.name }}</span>
<div class="btn">
<Button link type="primary" @click="openStructForm(item)">
<Button type="link" @click="openStructForm(item)">
编辑
</Button>
<Divider direction="vertical" />
<Button link danger @click="deleteStructItem(index)"> 删除 </Button>
<Divider type="vertical" />
<Button type="link" danger @click="deleteStructItem(index)">
删除
</Button>
</div>
</div>
<Button link type="primary" @click="openStructForm(null)">
<Button type="link" @click="openStructForm(null)">
+新增参数
</Button>
</Form.Item>
<!-- struct 表单 -->
<Modal v-model="dialogVisible" :title="dialogTitle" append-to-body>
<Modal
v-model:open="dialogVisible"
:title="dialogTitle"
:confirm-loading="formLoading"
@ok="submitForm"
>
<Form
ref="structFormRef"
v-loading="formLoading"
:model="formData"
label-width="100px"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<Form.Item label="参数名称" prop="name">
<Input v-model="formData.name" placeholder="请输入功能名称" />
<Form.Item label="参数名称" name="name">
<Input v-model:value="formData.name" placeholder="请输入功能名称" />
</Form.Item>
<Form.Item label="标识符" prop="identifier">
<Input v-model="formData.identifier" placeholder="请输入标识符" />
<Form.Item label="标识符" name="identifier">
<Input v-model:value="formData.identifier" placeholder="请输入标识符" />
</Form.Item>
<!-- 属性配置 -->
<ThingModelProperty v-model="formData.property" is-struct-data-specs />
</Form>
<template #footer>
<Button :disabled="formLoading" type="primary" @click="submitForm">
</Button>
<Button @click="dialogVisible = false"> </Button>
</template>
</Modal>
</template>