feat(wms):优化 antd、ele 的 order receipt 迁移
parent
f8c2d4b1ff
commit
b42e9b36e5
|
|
@ -1,53 +1,9 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace ThingModelApi {
|
||||
/** IoT 物模型数据 VO */
|
||||
export interface ThingModel {
|
||||
id?: number;
|
||||
productId?: number;
|
||||
productKey?: string;
|
||||
identifier: string;
|
||||
name: string;
|
||||
desc?: string;
|
||||
type: string;
|
||||
property?: ThingModelProperty;
|
||||
event?: ThingModelEvent;
|
||||
service?: ThingModelService;
|
||||
}
|
||||
|
||||
/** IoT 物模型属性 */
|
||||
export interface Property {
|
||||
identifier: string;
|
||||
name: string;
|
||||
accessMode: string;
|
||||
dataType: string;
|
||||
dataSpecs?: any;
|
||||
dataSpecsList?: any[];
|
||||
desc?: string;
|
||||
}
|
||||
|
||||
/** IoT 物模型服务 */
|
||||
export interface Service {
|
||||
identifier: string;
|
||||
name: string;
|
||||
callType: string;
|
||||
inputData?: any[];
|
||||
outputData?: any[];
|
||||
desc?: string;
|
||||
}
|
||||
|
||||
/** IoT 物模型事件 */
|
||||
export interface Event {
|
||||
identifier: string;
|
||||
name: string;
|
||||
type: string;
|
||||
outputData?: any[];
|
||||
desc?: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** IoT 物模型数据 */
|
||||
export interface ThingModelData {
|
||||
id?: number;
|
||||
|
|
@ -55,9 +11,9 @@ export interface ThingModelData {
|
|||
productKey?: string;
|
||||
identifier?: string;
|
||||
name?: string;
|
||||
desc?: string;
|
||||
type?: string;
|
||||
description?: string;
|
||||
dataType?: string;
|
||||
type?: number; // 参见 IoTThingModelTypeEnum 枚举类
|
||||
property?: ThingModelProperty;
|
||||
event?: ThingModelEvent;
|
||||
service?: ThingModelService;
|
||||
|
|
@ -68,29 +24,45 @@ export interface ThingModelProperty {
|
|||
identifier?: string;
|
||||
name?: string;
|
||||
accessMode?: string;
|
||||
required?: boolean;
|
||||
dataType?: string;
|
||||
description?: string;
|
||||
dataSpecs?: any;
|
||||
dataSpecsList?: any[];
|
||||
desc?: string;
|
||||
}
|
||||
|
||||
/** IoT 物模型服务 */
|
||||
export interface ThingModelService {
|
||||
identifier?: string;
|
||||
name?: string;
|
||||
required?: boolean;
|
||||
callType?: string;
|
||||
inputData?: any[];
|
||||
outputData?: any[];
|
||||
desc?: string;
|
||||
description?: string;
|
||||
inputParams?: ThingModelParam[];
|
||||
outputParams?: ThingModelParam[];
|
||||
method?: string;
|
||||
}
|
||||
|
||||
/** IoT 物模型事件 */
|
||||
export interface ThingModelEvent {
|
||||
identifier?: string;
|
||||
name?: string;
|
||||
required?: boolean;
|
||||
type?: string;
|
||||
outputData?: any[];
|
||||
desc?: string;
|
||||
description?: string;
|
||||
outputParams?: ThingModelParam[];
|
||||
method?: string;
|
||||
}
|
||||
|
||||
/** IoT 物模型参数 */
|
||||
export interface ThingModelParam {
|
||||
identifier?: string;
|
||||
name?: string;
|
||||
direction?: string;
|
||||
paraOrder?: number;
|
||||
dataType?: string;
|
||||
dataSpecs?: any;
|
||||
dataSpecsList?: any[];
|
||||
}
|
||||
|
||||
/** IoT 数据定义(数值型) */
|
||||
|
|
@ -108,23 +80,119 @@ export interface DataSpecsEnumOrBoolData {
|
|||
name: string;
|
||||
}
|
||||
|
||||
/** IoT 物模型表单校验规则 */
|
||||
export interface ThingModelFormRules {
|
||||
[key: string]: any;
|
||||
/** 生成「必填 + 数字」类校验器:拼到 size / length / 枚举值上 */
|
||||
function buildRequiredNumberValidator(label: string) {
|
||||
return (_rule: any, value: any, callback: any) => {
|
||||
if (isEmpty(value)) {
|
||||
callback(new Error(`${label}不能为空`));
|
||||
return;
|
||||
}
|
||||
if (Number.isNaN(Number(value))) {
|
||||
callback(new Error(`${label}必须是数字`));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
};
|
||||
}
|
||||
|
||||
/** 验证布尔型名称 */
|
||||
export function validateBoolName(_rule: any, value: any, callback: any) {
|
||||
if (value) {
|
||||
/** 生成「标识符样式」名称校验器:开头需为中文 / 英文 / 数字,整体仅允许中文、英文、数字、下划线、短划线,长度 ≤ 20 */
|
||||
export function buildIdentifierLikeNameValidator(label: string) {
|
||||
return (_rule: any, value: string, callback: any) => {
|
||||
if (isEmpty(value)) {
|
||||
callback(new Error(`${label}不能为空`));
|
||||
return;
|
||||
}
|
||||
if (!/^[一-龥A-Za-z0-9]/.test(value)) {
|
||||
callback(new Error(`${label}必须以中文、英文字母或数字开头`));
|
||||
return;
|
||||
}
|
||||
if (!/^[一-龥A-Za-z0-9][\w一-龥-]*$/.test(value)) {
|
||||
callback(
|
||||
new Error(`${label}只能包含中文、英文字母、数字、下划线和短划线`),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (value.length > 20) {
|
||||
callback(new Error(`${label}长度不能超过 20 个字符`));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
} else {
|
||||
callback(new Error('枚举描述不能为空'));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** IoT 物模型表单校验规则 */
|
||||
export const ThingModelFormRules = {
|
||||
name: [
|
||||
{ required: true, message: '功能名称不能为空', trigger: 'blur' },
|
||||
{
|
||||
pattern: /^[一-龥A-Za-z0-9][一-龥A-Za-z0-9\-_/.]{0,29}$/,
|
||||
message:
|
||||
'支持中文、大小写字母、日文、数字、短划线、下划线、斜杠和小数点,必须以中文、英文或数字开头,不超过 30 个字符',
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
type: [{ required: true, message: '功能类型不能为空', trigger: 'blur' }],
|
||||
identifier: [
|
||||
{ required: true, message: '标识符不能为空', trigger: 'blur' },
|
||||
{
|
||||
pattern: /^\w{1,50}$/,
|
||||
message: '支持大小写字母、数字和下划线,不超过 50 个字符',
|
||||
trigger: 'blur',
|
||||
},
|
||||
{
|
||||
validator: (_rule: 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 是系统保留字段,不能用于标识符定义',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (/^\d+$/.test(value)) {
|
||||
callback(new Error('标识符不能是纯数字'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
childDataType: [{ required: true, message: '元素类型不能为空' }],
|
||||
size: [
|
||||
{
|
||||
required: true,
|
||||
validator: buildRequiredNumberValidator('元素个数'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
length: [
|
||||
{
|
||||
required: true,
|
||||
validator: buildRequiredNumberValidator('文本长度'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
accessMode: [{ required: true, message: '请选择读写类型', trigger: 'change' }],
|
||||
callType: [{ required: true, message: '请选择调用方式', trigger: 'change' }],
|
||||
eventType: [{ required: true, message: '请选择事件类型', trigger: 'change' }],
|
||||
};
|
||||
|
||||
/** 校验布尔值名称 */
|
||||
export const validateBoolName = buildIdentifierLikeNameValidator('布尔值名称');
|
||||
|
||||
/** 查询产品物模型分页 */
|
||||
export function getThingModelPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<ThingModelApi.ThingModel>>(
|
||||
return requestClient.get<PageResult<ThingModelData>>(
|
||||
'/iot/thing-model/page',
|
||||
{ params },
|
||||
);
|
||||
|
|
@ -132,17 +200,14 @@ export function getThingModelPage(params: PageParam) {
|
|||
|
||||
/** 查询产品物模型详情 */
|
||||
export function getThingModel(id: number) {
|
||||
return requestClient.get<ThingModelApi.ThingModel>(
|
||||
`/iot/thing-model/get?id=${id}`,
|
||||
);
|
||||
return requestClient.get<ThingModelData>(`/iot/thing-model/get?id=${id}`);
|
||||
}
|
||||
|
||||
/** 根据产品 ID 查询物模型列表 */
|
||||
export function getThingModelListByProductId(productId: number) {
|
||||
return requestClient.get<ThingModelApi.ThingModel[]>(
|
||||
'/iot/thing-model/list',
|
||||
{ params: { productId } },
|
||||
);
|
||||
return requestClient.get<ThingModelData[]>('/iot/thing-model/list', {
|
||||
params: { productId },
|
||||
});
|
||||
}
|
||||
|
||||
/** 新增物模型 */
|
||||
|
|
@ -162,25 +227,7 @@ export function deleteThingModel(id: number) {
|
|||
|
||||
/** 获取物模型 TSL */
|
||||
export function getThingModelTSL(productId: number) {
|
||||
return requestClient.get<ThingModelApi.ThingModel[]>(
|
||||
'/iot/thing-model/get-tsl',
|
||||
{ params: { productId } },
|
||||
);
|
||||
}
|
||||
|
||||
/** 导入物模型 TSL
|
||||
export function importThingModelTSL(productId: number, tslData: any) {
|
||||
return requestClient.post('/iot/thing-model/import-tsl', {
|
||||
productId,
|
||||
tslData,
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
/** 导出物模型 TSL
|
||||
export function exportThingModelTSL(productId: number) {
|
||||
return requestClient.get<any>('/iot/thing-model/export-tsl', {
|
||||
return requestClient.get<any>('/iot/thing-model/get-tsl', {
|
||||
params: { productId },
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { message, Tabs } from 'ant-design-vue';
|
|||
import { getDeviceCount } from '#/api/iot/device/device';
|
||||
import { getProduct } from '#/api/iot/product/product';
|
||||
import IoTProductThingModel from '#/views/iot/thingmodel/index.vue';
|
||||
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants';
|
||||
|
||||
import ProductDetailsHeader from './modules/header.vue';
|
||||
import ProductDetailsInfo from './modules/info.vue';
|
||||
|
|
@ -25,7 +26,8 @@ const loading = ref(true);
|
|||
const product = ref<IotProductApi.Product>({} as IotProductApi.Product);
|
||||
const activeTab = ref('info');
|
||||
|
||||
provide('product', product); // 提供产品信息给子组件
|
||||
/** 向子组件提供产品信息 */
|
||||
provide(IOT_PROVIDE_KEY.PRODUCT, product);
|
||||
|
||||
/** 获取产品详情 */
|
||||
async function getProductData(productId: number) {
|
||||
|
|
@ -82,10 +84,7 @@ onMounted(async () => {
|
|||
<ProductDetailsInfo v-if="activeTab === 'info'" :product="product" />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="thingModel" tab="物模型(功能定义)">
|
||||
<IoTProductThingModel
|
||||
v-if="activeTab === 'thingModel'"
|
||||
:product-id="id"
|
||||
/>
|
||||
<IoTProductThingModel v-if="activeTab === 'thingModel'" />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Page>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
|||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { getDataTypeOptionsLabel } from '#/views/iot/utils/constants';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
|
|
@ -27,7 +29,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
|||
{
|
||||
field: 'type',
|
||||
title: '功能类型',
|
||||
minWidth: 20,
|
||||
minWidth: 100,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.IOT_THING_MODEL_TYPE },
|
||||
|
|
@ -41,17 +43,16 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
|||
{
|
||||
field: 'identifier',
|
||||
title: '标识符',
|
||||
minWidth: 20,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'dataType',
|
||||
title: '数据类型',
|
||||
minWidth: 50,
|
||||
slots: { default: 'dataType' },
|
||||
minWidth: 100,
|
||||
formatter: ({ row }) =>
|
||||
getDataTypeOptionsLabel(row.property?.dataType) || '-',
|
||||
},
|
||||
{
|
||||
field: 'property',
|
||||
title: '属性',
|
||||
title: '数据定义',
|
||||
minWidth: 200,
|
||||
slots: { default: 'dataDefinition' },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,99 +1,90 @@
|
|||
<script setup lang="ts">
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||
|
||||
import { onMounted, provide, ref } from 'vue';
|
||||
import { computed, inject } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getProduct } from '#/api/iot/product/product';
|
||||
import { deleteThingModel, getThingModelPage } from '#/api/iot/thingmodel';
|
||||
import { $t } from '#/locales';
|
||||
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants';
|
||||
|
||||
import { getDataTypeOptionsLabel, IOT_PROVIDE_KEY } from '../utils/constants';
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import { DataDefinition } from './modules/components';
|
||||
import ThingModelForm from './modules/thing-model-form.vue';
|
||||
import ThingModelTsl from './modules/thing-model-tsl.vue';
|
||||
import Form from './modules/form.vue';
|
||||
import Tsl from './modules/tsl.vue';
|
||||
|
||||
defineOptions({ name: 'IoTThingModel' });
|
||||
|
||||
const props = defineProps<{
|
||||
productId: number;
|
||||
}>();
|
||||
const product = inject<Ref<IotProductApi.Product>>(IOT_PROVIDE_KEY.PRODUCT);
|
||||
const productId = computed(() => product?.value?.id);
|
||||
|
||||
const product = ref<IotProductApi.Product>({} as IotProductApi.Product); // 产品信息
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
provide(IOT_PROVIDE_KEY.PRODUCT, product); // 提供产品信息给子组件
|
||||
const [TslModal, tslModalApi] = useVbenModal({
|
||||
connectedComponent: Tsl,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
// TODO @haohao:form 是不是用 web-antd/src/views/system/user/index.vue 里 open 的风格;
|
||||
const thingModelFormRef = ref();
|
||||
// TODO @haohao:thingModelTSLRef 应该是个 modal,也可以调整下风格;
|
||||
const thingModelTSLRef = ref();
|
||||
|
||||
// TODO @haohao:方法的顺序、注释、调整的和别的模块一致。
|
||||
|
||||
// 新增功能
|
||||
function handleCreate() {
|
||||
thingModelFormRef.value?.open('create');
|
||||
}
|
||||
|
||||
// 编辑功能
|
||||
function handleEdit(row: any) {
|
||||
thingModelFormRef.value?.open('update', row.id);
|
||||
}
|
||||
|
||||
// 删除功能
|
||||
async function handleDelete(row: any) {
|
||||
// TODO @haohao:应该有个 loading,类似别的模块写法;
|
||||
try {
|
||||
await deleteThingModel(row.id);
|
||||
message.success('删除成功');
|
||||
gridApi.reload();
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 打开 TSL
|
||||
function handleOpenTSL() {
|
||||
thingModelTSLRef.value?.open();
|
||||
}
|
||||
|
||||
// 获取数据类型标签
|
||||
// TODO @haohao:可以直接在 data.ts 就写掉这个逻辑;
|
||||
function getDataTypeLabel(row: any) {
|
||||
return getDataTypeOptionsLabel(row.property?.dataType) || '-';
|
||||
}
|
||||
|
||||
// 刷新表格
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
gridApi.reload();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
// 获取产品信息
|
||||
async function getProductData() {
|
||||
/** 新增物模型 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑物模型 */
|
||||
function handleEdit(row: ThingModelData) {
|
||||
formModalApi.setData({ id: row.id }).open();
|
||||
}
|
||||
|
||||
/** 删除物模型 */
|
||||
async function handleDelete(row: ThingModelData) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
product.value = await getProduct(props.productId);
|
||||
} catch (error) {
|
||||
console.error('获取产品信息失败:', error);
|
||||
await deleteThingModel(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO @haohao:字段的顺序,调整成别的模块一直;
|
||||
/** 打开 TSL 弹窗 */
|
||||
function handleOpenTsl() {
|
||||
tslModalApi.open();
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }: any, formValues: any) => {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getThingModelPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
productId: props.productId,
|
||||
productId: productId.value,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
|
|
@ -108,64 +99,55 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||
search: true,
|
||||
},
|
||||
},
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
});
|
||||
|
||||
// 初始化
|
||||
onMounted(async () => {
|
||||
await getProductData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<Grid>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<TslModal />
|
||||
|
||||
<Grid table-title="物模型列表">
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '添加功能',
|
||||
label: $t('ui.actionTitle.create', ['物模型']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['iot:thing-model:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: 'TSL',
|
||||
type: 'default',
|
||||
color: 'success', // TODO @haohao:貌似 color 可以去掉?应该是不生效的哈。ps:另外,也给搞个 icon?
|
||||
onClick: handleOpenTSL,
|
||||
type: 'primary',
|
||||
auth: ['iot:thing-model:query'],
|
||||
onClick: handleOpenTsl,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<!-- 数据类型列 -->
|
||||
<template #dataType="{ row }">
|
||||
<span>{{ getDataTypeLabel(row) }}</span>
|
||||
</template>
|
||||
<!-- 数据定义列 -->
|
||||
<!-- TODO @haohao:可以在 data.ts 就写掉这个逻辑; -->
|
||||
<template #dataDefinition="{ row }">
|
||||
<DataDefinition :data="row" />
|
||||
</template>
|
||||
<!-- 操作列 -->
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '编辑',
|
||||
label: $t('common.edit'),
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.EDIT,
|
||||
auth: ['iot:thing-model:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
label: $t('common.delete'),
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
auth: ['iot:thing-model:delete'],
|
||||
popConfirm: {
|
||||
title: '确认删除该功能吗?',
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
|
|
@ -173,10 +155,5 @@ onMounted(async () => {
|
|||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
<!-- 物模型表单 -->
|
||||
<ThingModelForm ref="thingModelFormRef" @success="handleRefresh" />
|
||||
<!-- TSL 弹窗 -->
|
||||
<ThingModelTsl ref="thingModelTSLRef" />
|
||||
</Page>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
<!-- TODO @haohao:如果是模块内用的,就用 modules 里。(等后面点在看,优先级:低) -->
|
||||
<script lang="ts" setup>
|
||||
import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||
|
||||
|
|
@ -13,83 +12,63 @@ import {
|
|||
IoTThingModelTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
/** 数据定义展示组件 */
|
||||
defineOptions({ name: 'DataDefinition' });
|
||||
const NUMBER_TYPES = new Set<string>([
|
||||
IoTDataSpecsDataTypeEnum.INT,
|
||||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||
]);
|
||||
const PLACEHOLDER_TYPES = new Set<string>([
|
||||
IoTDataSpecsDataTypeEnum.ARRAY,
|
||||
IoTDataSpecsDataTypeEnum.STRUCT,
|
||||
IoTDataSpecsDataTypeEnum.DATE,
|
||||
]);
|
||||
const LIST_TYPES = new Set<string>([
|
||||
IoTDataSpecsDataTypeEnum.BOOL,
|
||||
IoTDataSpecsDataTypeEnum.ENUM,
|
||||
]);
|
||||
|
||||
const props = defineProps<{ data: ThingModelData }>();
|
||||
|
||||
const formattedDataSpecsList = computed(() => {
|
||||
if (
|
||||
!props.data.property?.dataSpecsList ||
|
||||
props.data.property.dataSpecsList.length === 0
|
||||
) {
|
||||
if (!props.data.property?.dataSpecsList?.length) {
|
||||
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
|
||||
) {
|
||||
const list = props.data.property?.dataSpecsList;
|
||||
if (!list?.length) {
|
||||
return '-';
|
||||
}
|
||||
const first = props.data.property.dataSpecsList[0];
|
||||
const count = props.data.property.dataSpecsList.length;
|
||||
return count > 1
|
||||
? `${first.value}-${first.name} 等${count}项`
|
||||
const first = list[0];
|
||||
return list.length > 1
|
||||
? `${first.value}-${first.name} 等 ${list.length} 项`
|
||||
: `${first.value}-${first.name}`;
|
||||
}); // 显示的简短文本(第一个值)
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 属性 -->
|
||||
<template v-if="Number(data.type) === IoTThingModelTypeEnum.PROPERTY">
|
||||
<!-- 非列表型:数值 -->
|
||||
<div
|
||||
v-if="
|
||||
[
|
||||
IoTDataSpecsDataTypeEnum.INT,
|
||||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||
].includes(data.property?.dataType as any)
|
||||
"
|
||||
>
|
||||
<template v-if="data.type === IoTThingModelTypeEnum.PROPERTY">
|
||||
<div v-if="NUMBER_TYPES.has(data.property?.dataType as any)">
|
||||
取值范围:{{
|
||||
`${data.property?.dataSpecs?.min}~${data.property?.dataSpecs?.max}`
|
||||
}}
|
||||
</div>
|
||||
<!-- 非列表型:文本 -->
|
||||
<div v-if="IoTDataSpecsDataTypeEnum.TEXT === data.property?.dataType">
|
||||
<div v-if="data.property?.dataType === IoTDataSpecsDataTypeEnum.TEXT">
|
||||
数据长度:{{ data.property?.dataSpecs?.length }}
|
||||
</div>
|
||||
<!-- 列表型: 数组、结构、时间(特殊) -->
|
||||
<div
|
||||
v-if="
|
||||
[
|
||||
IoTDataSpecsDataTypeEnum.ARRAY,
|
||||
IoTDataSpecsDataTypeEnum.STRUCT,
|
||||
IoTDataSpecsDataTypeEnum.DATE,
|
||||
].includes(data.property?.dataType as any)
|
||||
"
|
||||
>
|
||||
-
|
||||
</div>
|
||||
<!-- 列表型: 布尔值、枚举 -->
|
||||
<div
|
||||
v-if="
|
||||
[IoTDataSpecsDataTypeEnum.BOOL, IoTDataSpecsDataTypeEnum.ENUM].includes(
|
||||
data.property?.dataType as any,
|
||||
)
|
||||
"
|
||||
>
|
||||
<div v-if="PLACEHOLDER_TYPES.has(data.property?.dataType as any)">-</div>
|
||||
<div v-if="LIST_TYPES.has(data.property?.dataType as any)">
|
||||
<Tooltip :title="formattedDataSpecsList" placement="topLeft">
|
||||
<span class="data-specs-text">
|
||||
<span
|
||||
class="cursor-help border-b border-dashed border-gray-300 hover:border-blue-500 hover:text-blue-500"
|
||||
>
|
||||
{{
|
||||
IoTDataSpecsDataTypeEnum.BOOL === data.property?.dataType
|
||||
data.property?.dataType === IoTDataSpecsDataTypeEnum.BOOL
|
||||
? '布尔值'
|
||||
: '枚举值'
|
||||
}}:{{ shortText }}
|
||||
|
|
@ -98,25 +77,12 @@ const shortText = computed(() => {
|
|||
</div>
|
||||
</template>
|
||||
<!-- 服务 -->
|
||||
<div v-if="Number(data.type) === IoTThingModelTypeEnum.SERVICE">
|
||||
<div v-if="data.type === IoTThingModelTypeEnum.SERVICE">
|
||||
调用方式:
|
||||
{{ getThingModelServiceCallTypeLabel(data.service?.callType as any) }}
|
||||
</div>
|
||||
<!-- 事件 -->
|
||||
<div v-if="Number(data.type) === IoTThingModelTypeEnum.EVENT">
|
||||
<div v-if="data.type === IoTThingModelTypeEnum.EVENT">
|
||||
事件类型:{{ getEventTypeLabel(data.event?.type as any) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/** TODO @haohao:tindwind */
|
||||
.data-specs-text {
|
||||
cursor: help;
|
||||
border-bottom: 1px dashed #d9d9d9;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
border-bottom-color: #1890ff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -5,21 +5,29 @@ import type { Ref } from 'vue';
|
|||
import { useVModel } from '@vueuse/core';
|
||||
import { Form, Input, Radio } from 'ant-design-vue';
|
||||
|
||||
import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
||||
import {
|
||||
getDataTypeOptions,
|
||||
IoTDataSpecsDataTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
import ThingModelStructDataSpecs from './thing-model-struct-data-specs.vue';
|
||||
import ThingModelStructDataSpecs from './struct.vue';
|
||||
|
||||
/** 数组型的 dataSpecs 配置组件 */
|
||||
defineOptions({ name: 'ThingModelArrayDataSpecs' });
|
||||
/** 数组元素禁止选择的类型 */
|
||||
const EXCLUDED_CHILD_TYPES = new Set<string>([
|
||||
IoTDataSpecsDataTypeEnum.ENUM,
|
||||
IoTDataSpecsDataTypeEnum.ARRAY,
|
||||
IoTDataSpecsDataTypeEnum.DATE,
|
||||
]);
|
||||
const childDataTypeOptions = getDataTypeOptions().filter(
|
||||
(item) => !EXCLUDED_CHILD_TYPES.has(item.value),
|
||||
);
|
||||
|
||||
const props = defineProps<{ modelValue: any }>();
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
const dataSpecs = useVModel(props, 'modelValue', emits) as Ref<any>;
|
||||
|
||||
/** 元素类型改变时间。当值为 struct 时,对 dataSpecs 中的 dataSpecsList 进行初始化 */
|
||||
/** 元素类型切到 struct 时,初始化 dataSpecsList 占位 */
|
||||
function handleChange(val: any) {
|
||||
if (val !== IoTDataSpecsDataTypeEnum.STRUCT) {
|
||||
return;
|
||||
|
|
@ -31,29 +39,25 @@ function handleChange(val: any) {
|
|||
<template>
|
||||
<Form.Item
|
||||
:name="['property', 'dataSpecs', 'childDataType']"
|
||||
:rules="ThingModelFormRules.childDataType"
|
||||
label="元素类型"
|
||||
>
|
||||
<Radio.Group v-model:value="dataSpecs.childDataType" @change="handleChange">
|
||||
<template v-for="item in getDataTypeOptions()" :key="item.value">
|
||||
<Radio
|
||||
v-if="
|
||||
!(
|
||||
[
|
||||
IoTDataSpecsDataTypeEnum.ENUM,
|
||||
IoTDataSpecsDataTypeEnum.ARRAY,
|
||||
IoTDataSpecsDataTypeEnum.DATE,
|
||||
] as any[]
|
||||
).includes(item.value)
|
||||
"
|
||||
:value="item.value"
|
||||
class="w-1/3"
|
||||
>
|
||||
{{ `${item.value}(${item.label})` }}
|
||||
</Radio>
|
||||
</template>
|
||||
<Radio
|
||||
v-for="item in childDataTypeOptions"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
class="w-1/3"
|
||||
>
|
||||
{{ `${item.value}(${item.label})` }}
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item :name="['property', 'dataSpecs', 'size']" label="元素个数">
|
||||
<Form.Item
|
||||
:name="['property', 'dataSpecs', 'size']"
|
||||
:rules="ThingModelFormRules.size"
|
||||
label="元素个数"
|
||||
>
|
||||
<Input
|
||||
v-model:value="dataSpecs.size"
|
||||
placeholder="请输入数组中的元素个数"
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
<!-- dataType:enum 数组类型 -->
|
||||
<script lang="ts" setup>
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Button, Form, Input, message } from 'ant-design-vue';
|
||||
|
||||
import { buildIdentifierLikeNameValidator } from '#/api/iot/thingmodel';
|
||||
|
||||
const props = defineProps<{ modelValue: any }>();
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<any[]>;
|
||||
|
||||
const validateEnumName = buildIdentifierLikeNameValidator('枚举描述');
|
||||
|
||||
/** 添加枚举项 */
|
||||
function addEnum() {
|
||||
dataSpecsList.value.push({ name: '', value: '' } as any);
|
||||
}
|
||||
|
||||
/** 删除枚举项 */
|
||||
function deleteEnum(index: number) {
|
||||
if (dataSpecsList.value.length === 1) {
|
||||
message.warning('至少需要一个枚举项');
|
||||
return;
|
||||
}
|
||||
dataSpecsList.value.splice(index, 1);
|
||||
}
|
||||
|
||||
/** 校验单项枚举值:必填、数字、不重复 */
|
||||
function validateEnumValue(_rule: any, value: any, callback: any) {
|
||||
if (isEmpty(value)) {
|
||||
callback(new Error('枚举值不能为空'));
|
||||
return;
|
||||
}
|
||||
if (Number.isNaN(Number(value))) {
|
||||
callback(new Error('枚举值必须是数字'));
|
||||
return;
|
||||
}
|
||||
const sameCount = dataSpecsList.value.filter((it) => it.value === value)
|
||||
.length;
|
||||
if (sameCount > 1) {
|
||||
callback(new Error('枚举值不能重复'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
/** 校验整个枚举列表:非空、无空项、无非法数字、无重复 */
|
||||
function validateEnumList(_rule: any, _value: any, callback: any) {
|
||||
if (isEmpty(dataSpecsList.value)) {
|
||||
callback(new Error('请至少添加一个枚举项'));
|
||||
return;
|
||||
}
|
||||
const hasEmpty = dataSpecsList.value.some(
|
||||
(item) => isEmpty(item.value) || isEmpty(item.name),
|
||||
);
|
||||
if (hasEmpty) {
|
||||
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);
|
||||
if (new Set(values).size !== values.length) {
|
||||
callback(new Error('存在重复的枚举值'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form.Item
|
||||
:rules="[{ validator: validateEnumList, trigger: 'change' }]"
|
||||
label="枚举项"
|
||||
>
|
||||
<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="mb-[5px] flex items-center justify-between"
|
||||
>
|
||||
<Form.Item
|
||||
:name="['property', 'dataSpecsList', index, 'value']"
|
||||
:rules="[
|
||||
{ required: true, message: '枚举值不能为空', trigger: 'blur' },
|
||||
{ validator: validateEnumValue, trigger: 'blur' },
|
||||
]"
|
||||
class="mb-0 flex-1"
|
||||
>
|
||||
<Input v-model:value="item.value" placeholder="请输入枚举值,如「0」" />
|
||||
</Form.Item>
|
||||
<span class="mx-2">~</span>
|
||||
<Form.Item
|
||||
:name="['property', 'dataSpecsList', index, 'name']"
|
||||
:rules="[
|
||||
{ required: true, message: '枚举描述不能为空', trigger: 'blur' },
|
||||
{ validator: validateEnumName, trigger: 'blur' },
|
||||
]"
|
||||
class="mb-0 flex-1"
|
||||
>
|
||||
<Input v-model:value="item.name" placeholder="对该枚举项的描述" />
|
||||
</Form.Item>
|
||||
<Button class="ml-2.5" type="link" @click="deleteEnum(index)">
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
<Button type="link" @click="addEnum">+ 添加枚举项</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.ant-form-item) {
|
||||
.ant-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export { default as ThingModelArrayDataSpecs } from './array.vue';
|
||||
export { default as ThingModelEnumDataSpecs } from './enum.vue';
|
||||
export { default as ThingModelNumberDataSpecs } from './number.vue';
|
||||
export { default as ThingModelStructDataSpecs } from './struct.vue';
|
||||
|
|
@ -10,9 +10,6 @@ import { getDictOptions } from '@vben/hooks';
|
|||
import { useVModel } from '@vueuse/core';
|
||||
import { Form, Input, Select } from 'ant-design-vue';
|
||||
|
||||
/** 数值型的 dataSpecs 配置组件 */
|
||||
defineOptions({ name: 'ThingModelNumberDataSpecs' });
|
||||
|
||||
const props = defineProps<{ modelValue: any }>();
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
const dataSpecs = useVModel(
|
||||
|
|
@ -21,13 +18,15 @@ const dataSpecs = useVModel(
|
|||
emits,
|
||||
) as Ref<DataSpecsNumberData>;
|
||||
|
||||
/** 单位发生变化时触发 */
|
||||
const unitChange = (UnitSpecs: any) => {
|
||||
if (!UnitSpecs) return;
|
||||
const [unitName, unit] = String(UnitSpecs).split('-');
|
||||
/** 单位下拉变化时,拆出 unitName 与 unit 回写 */
|
||||
function unitChange(unitSpecs: any) {
|
||||
if (!unitSpecs) {
|
||||
return;
|
||||
}
|
||||
const [unitName, unit] = String(unitSpecs).split('-');
|
||||
dataSpecs.value.unitName = unitName;
|
||||
dataSpecs.value.unit = unit;
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -52,7 +51,7 @@ const unitChange = (UnitSpecs: any) => {
|
|||
"
|
||||
show-search
|
||||
placeholder="请选择单位"
|
||||
class="w-1/1"
|
||||
class="w-full"
|
||||
@change="unitChange"
|
||||
>
|
||||
<Select.Option
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Button, Divider, Form, Input } from 'ant-design-vue';
|
||||
|
||||
import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
||||
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
import ThingModelProperty from '../property.vue';
|
||||
|
||||
const props = defineProps<{ modelValue: any }>();
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<any[]>;
|
||||
|
||||
const structFormRef = ref();
|
||||
const formData = ref<any>(buildEmptyFormData());
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
try {
|
||||
await structFormRef.value?.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const data = formData.value;
|
||||
const item = {
|
||||
identifier: data.identifier,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
dataType: IoTDataSpecsDataTypeEnum.STRUCT,
|
||||
childDataType: data.property.dataType,
|
||||
dataSpecs:
|
||||
!isEmpty(data.property.dataSpecs) &&
|
||||
Object.keys(data.property.dataSpecs).length > 1
|
||||
? data.property.dataSpecs
|
||||
: undefined,
|
||||
dataSpecsList: isEmpty(data.property.dataSpecsList)
|
||||
? undefined
|
||||
: data.property.dataSpecsList,
|
||||
};
|
||||
const existingIndex = dataSpecsList.value.findIndex(
|
||||
(spec) => spec.identifier === data.identifier,
|
||||
);
|
||||
if (existingIndex === -1) {
|
||||
dataSpecsList.value.push(item);
|
||||
} else {
|
||||
dataSpecsList.value[existingIndex] = item;
|
||||
}
|
||||
await modalApi.close();
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
formData.value = buildEmptyFormData();
|
||||
structFormRef.value?.clearValidate?.();
|
||||
const data = modalApi.getData<any>();
|
||||
if (isEmpty(data)) {
|
||||
return;
|
||||
}
|
||||
formData.value = {
|
||||
identifier: data.identifier ?? '',
|
||||
name: data.name ?? '',
|
||||
description: data.description ?? '',
|
||||
property: {
|
||||
dataType: data.childDataType ?? IoTDataSpecsDataTypeEnum.INT,
|
||||
dataSpecs: data.dataSpecs ?? {},
|
||||
dataSpecsList: data.dataSpecsList ?? [],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/** 构造空白结构体表单 */
|
||||
function buildEmptyFormData() {
|
||||
return {
|
||||
identifier: '',
|
||||
name: '',
|
||||
description: '',
|
||||
property: {
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
dataSpecs: { dataType: IoTDataSpecsDataTypeEnum.INT },
|
||||
dataSpecsList: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 打开结构体表单 */
|
||||
function openStructForm(val: any) {
|
||||
modalApi.setData(val).open();
|
||||
}
|
||||
|
||||
/** 删除结构体项 */
|
||||
function deleteStructItem(index: number) {
|
||||
dataSpecsList.value.splice(index, 1);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (isEmpty(dataSpecsList.value)) {
|
||||
dataSpecsList.value = [];
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form.Item label="属性对象">
|
||||
<div
|
||||
v-for="(item, index) in dataSpecsList"
|
||||
:key="index"
|
||||
class="mb-2.5 flex w-full justify-between bg-gray-100 px-2.5 dark:bg-gray-800"
|
||||
>
|
||||
<span>参数:{{ item.name }}</span>
|
||||
<div>
|
||||
<Button type="link" @click="openStructForm(item)">编辑</Button>
|
||||
<Divider type="vertical" />
|
||||
<Button danger type="link" @click="deleteStructItem(index)">
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="link" @click="openStructForm(null)">+ 新增参数</Button>
|
||||
</Form.Item>
|
||||
|
||||
<!-- 结构体参数表单 -->
|
||||
<Modal class="w-2/5" title="结构体参数">
|
||||
<Form
|
||||
ref="structFormRef"
|
||||
:label-col="{ span: 6 }"
|
||||
:model="formData"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
class="mx-4"
|
||||
>
|
||||
<Form.Item
|
||||
:rules="ThingModelFormRules.name"
|
||||
label="参数名称"
|
||||
name="name"
|
||||
>
|
||||
<Input v-model:value="formData.name" placeholder="请输入参数名称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
:rules="ThingModelFormRules.identifier"
|
||||
label="标识符"
|
||||
name="identifier"
|
||||
>
|
||||
<Input v-model:value="formData.identifier" placeholder="请输入标识符" />
|
||||
</Form.Item>
|
||||
<!-- 属性配置 -->
|
||||
<ThingModelProperty v-model="formData.property" is-struct-data-specs />
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
export { default as ThingModelArrayDataSpecs } from './thing-model-array-data-specs.vue';
|
||||
export { default as ThingModelEnumDataSpecs } from './thing-model-enum-data-specs.vue';
|
||||
export { default as ThingModelNumberDataSpecs } from './thing-model-number-data-specs.vue';
|
||||
export { default as ThingModelStructDataSpecs } from './thing-model-struct-data-specs.vue';
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
<!-- dataType:enum 数组类型 -->
|
||||
<script lang="ts" setup>
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Button, Form, Input, message } from 'ant-design-vue';
|
||||
|
||||
/** 枚举型的 dataSpecs 配置组件 */
|
||||
defineOptions({ name: 'ThingModelEnumDataSpecs' });
|
||||
|
||||
const props = defineProps<{ modelValue: any }>();
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<any[]>;
|
||||
|
||||
/** 添加枚举项 */
|
||||
function addEnum() {
|
||||
dataSpecsList.value.push({
|
||||
name: '', // 枚举项的名称
|
||||
value: '', // 枚举值
|
||||
} as any);
|
||||
}
|
||||
|
||||
/** 删除枚举项 */
|
||||
function deleteEnum(index: number) {
|
||||
if (dataSpecsList.value.length === 1) {
|
||||
message.warning('至少需要一个枚举项');
|
||||
return;
|
||||
}
|
||||
dataSpecsList.value.splice(index, 1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form.Item label="枚举项">
|
||||
<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="mb-5px flex items-center justify-between"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<Input v-model:value="item.value" placeholder="请输入枚举值,如'0'" />
|
||||
</div>
|
||||
<span class="mx-2">~</span>
|
||||
<div class="flex-1">
|
||||
<Input v-model:value="item.name" placeholder="对该枚举项的描述" />
|
||||
</div>
|
||||
<Button class="ml-10px" type="link" @click="deleteEnum(index)">
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
<Button type="link" @click="addEnum">+添加枚举项</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.ant-form-item) {
|
||||
.ant-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
<!-- dataType:struct 数组类型 -->
|
||||
<script lang="ts" setup>
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { nextTick, onMounted, ref, unref } from 'vue';
|
||||
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Button, Divider, Form, Input, Modal } from 'ant-design-vue';
|
||||
|
||||
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
import ThingModelProperty from '../thing-model-property.vue';
|
||||
|
||||
/** Struct 型的 dataSpecs 配置组件 */
|
||||
defineOptions({ name: 'ThingModelStructDataSpecs' });
|
||||
|
||||
const props = defineProps<{ modelValue: any }>();
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<any[]>;
|
||||
const dialogVisible = ref(false); // 弹窗的是否展示
|
||||
const dialogTitle = ref('新增参数'); // 弹窗的标题
|
||||
const formLoading = ref(false); // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const structFormRef = ref(); // 表单 ref
|
||||
const formData = ref<any>({
|
||||
property: {
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
dataSpecs: {
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
},
|
||||
dataSpecsList: [],
|
||||
},
|
||||
});
|
||||
|
||||
/** 打开 struct 表单 */
|
||||
function openStructForm(val: any) {
|
||||
dialogVisible.value = true;
|
||||
resetForm();
|
||||
if (isEmpty(val)) {
|
||||
return;
|
||||
}
|
||||
// 编辑时回显数据
|
||||
const valData = val as any;
|
||||
formData.value = {
|
||||
identifier: valData?.identifier || '',
|
||||
name: valData?.name || '',
|
||||
description: valData?.description || '',
|
||||
property: {
|
||||
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 项 */
|
||||
function deleteStructItem(index: number) {
|
||||
dataSpecsList.value.splice(index, 1);
|
||||
}
|
||||
|
||||
/** 添加参数 */
|
||||
async function submitForm() {
|
||||
await structFormRef.value.validate();
|
||||
|
||||
try {
|
||||
const data = unref(formData);
|
||||
// 构建数据对象
|
||||
const item = {
|
||||
identifier: data.identifier,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
dataType: IoTDataSpecsDataTypeEnum.STRUCT,
|
||||
childDataType: data.property.dataType,
|
||||
dataSpecs:
|
||||
!!data.property.dataSpecs &&
|
||||
Object.keys(data.property.dataSpecs).length > 1
|
||||
? data.property.dataSpecs
|
||||
: undefined,
|
||||
dataSpecsList: isEmpty(data.property.dataSpecsList)
|
||||
? undefined
|
||||
: data.property.dataSpecsList,
|
||||
};
|
||||
|
||||
// 新增或修改同 identifier 的参数
|
||||
const existingIndex = dataSpecsList.value.findIndex(
|
||||
(spec) => spec.identifier === data.identifier,
|
||||
);
|
||||
if (existingIndex === -1) {
|
||||
dataSpecsList.value.push(item);
|
||||
} else {
|
||||
dataSpecsList.value[existingIndex] = item;
|
||||
}
|
||||
} finally {
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
function resetForm() {
|
||||
formData.value = {
|
||||
property: {
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
dataSpecs: {
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
},
|
||||
dataSpecsList: [],
|
||||
},
|
||||
};
|
||||
structFormRef.value?.resetFields();
|
||||
}
|
||||
|
||||
/** 组件初始化 */
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
// 预防 dataSpecsList 空指针
|
||||
isEmpty(dataSpecsList.value) && (dataSpecsList.value = []);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- struct 数据展示 -->
|
||||
<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>
|
||||
<div class="btn">
|
||||
<Button type="link" @click="openStructForm(item)"> 编辑 </Button>
|
||||
<Divider type="vertical" />
|
||||
<Button type="link" danger @click="deleteStructItem(index)">
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="link" @click="openStructForm(null)"> +新增参数 </Button>
|
||||
</Form.Item>
|
||||
|
||||
<!-- struct 表单 -->
|
||||
<Modal
|
||||
v-model:open="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
:confirm-loading="formLoading"
|
||||
@ok="submitForm"
|
||||
>
|
||||
<Form
|
||||
ref="structFormRef"
|
||||
:model="formData"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<Form.Item label="参数名称" name="name">
|
||||
<Input v-model:value="formData.name" placeholder="请输入功能名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="标识符" name="identifier">
|
||||
<Input v-model:value="formData.identifier" placeholder="请输入标识符" />
|
||||
</Form.Item>
|
||||
<!-- 属性配置 -->
|
||||
<ThingModelProperty v-model="formData.property" is-struct-data-specs />
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -9,17 +9,15 @@ import { isEmpty } from '@vben/utils';
|
|||
import { useVModel } from '@vueuse/core';
|
||||
import { Form, Radio } from 'ant-design-vue';
|
||||
|
||||
import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
||||
import {
|
||||
IoTThingModelEventTypeEnum,
|
||||
IoTThingModelParamDirectionEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
import ThingModelInputOutputParam from './thing-model-input-output-param.vue';
|
||||
import ThingModelInputOutputParam from './input-output-param.vue';
|
||||
|
||||
/** IoT 物模型事件 */
|
||||
defineOptions({ name: 'ThingModelEvent' });
|
||||
|
||||
const props = defineProps<{ isStructDataSpecs?: boolean; modelValue: any }>();
|
||||
const props = defineProps<{ modelValue: any }>();
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
const thingModelEvent = useVModel(props, 'modelValue', emits) as Ref<any>;
|
||||
|
||||
|
|
@ -36,7 +34,7 @@ watch(
|
|||
<template>
|
||||
<Form.Item
|
||||
:name="['event', 'type']"
|
||||
:rules="[{ required: true, message: '请选择事件类型', trigger: 'change' }]"
|
||||
:rules="ThingModelFormRules.eventType"
|
||||
label="事件类型"
|
||||
>
|
||||
<Radio.Group v-model:value="thingModelEvent.type">
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||
|
||||
import { computed, inject, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { $t } from '@vben/locales';
|
||||
import { cloneDeep, isEmpty } from '@vben/utils';
|
||||
|
||||
import { Form, Input, message, Radio } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
createThingModel,
|
||||
getThingModel,
|
||||
ThingModelFormRules,
|
||||
updateThingModel,
|
||||
} from '#/api/iot/thingmodel';
|
||||
import {
|
||||
IOT_PROVIDE_KEY,
|
||||
IoTDataSpecsDataTypeEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
import ThingModelEvent from './event.vue';
|
||||
import ThingModelProperty from './property.vue';
|
||||
import ThingModelService from './service.vue';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const product = inject<Ref<IotProductApi.Product>>(IOT_PROVIDE_KEY.PRODUCT);
|
||||
|
||||
const formRef = ref();
|
||||
const formData = ref<ThingModelData>(buildEmptyFormData());
|
||||
|
||||
const getTitle = computed(() =>
|
||||
formData.value.id
|
||||
? $t('ui.actionTitle.edit', ['物模型'])
|
||||
: $t('ui.actionTitle.create', ['物模型']),
|
||||
);
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
try {
|
||||
await formRef.value?.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
const data = cloneDeep(formData.value);
|
||||
data.productId = product!.value.id;
|
||||
data.productKey = product!.value.productKey;
|
||||
fillExtraAttributes(data);
|
||||
await (data.id ? updateThingModel(data) : createThingModel(data));
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
// 每次打开都先重置到空白,避免上一次的状态残留
|
||||
formData.value = buildEmptyFormData();
|
||||
formRef.value?.clearValidate?.();
|
||||
const data = modalApi.getData<{ id?: number }>();
|
||||
if (!data?.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
const result = await getThingModel(data.id);
|
||||
formData.value = normalizeFormData(result);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 构造空白表单数据 */
|
||||
function buildEmptyFormData(): ThingModelData {
|
||||
return {
|
||||
type: IoTThingModelTypeEnum.PROPERTY,
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
property: {
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
dataSpecs: {
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
},
|
||||
},
|
||||
service: {
|
||||
inputParams: [],
|
||||
outputParams: [],
|
||||
},
|
||||
event: {
|
||||
outputParams: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 回显数据时,规整各分支字段确保子表单可绑定 */
|
||||
function normalizeFormData(result: ThingModelData): ThingModelData {
|
||||
const next: any = { ...result, type: Number(result.type) };
|
||||
if (isEmpty(next.property)) {
|
||||
next.dataType = IoTDataSpecsDataTypeEnum.INT;
|
||||
next.property = {
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
dataSpecs: { dataType: IoTDataSpecsDataTypeEnum.INT },
|
||||
};
|
||||
} else {
|
||||
next.property.dataSpecs ??= {};
|
||||
next.property.dataSpecsList ??= [];
|
||||
next.property.dataType ??= IoTDataSpecsDataTypeEnum.INT;
|
||||
}
|
||||
if (isEmpty(next.service)) {
|
||||
next.service = { inputParams: [], outputParams: [] };
|
||||
} else {
|
||||
next.service.inputParams ??= [];
|
||||
next.service.outputParams ??= [];
|
||||
}
|
||||
if (isEmpty(next.event)) {
|
||||
next.event = { outputParams: [] };
|
||||
} else {
|
||||
next.event.outputParams ??= [];
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
/** 按功能类型将子表单数据回写到顶层,并清理无关分支 */
|
||||
function fillExtraAttributes(data: any) {
|
||||
if (data.type === IoTThingModelTypeEnum.PROPERTY) {
|
||||
removeDataSpecs(data.property);
|
||||
data.dataType = data.property.dataType;
|
||||
data.property.identifier = data.identifier;
|
||||
data.property.name = data.name;
|
||||
delete data.service;
|
||||
delete data.event;
|
||||
} else if (data.type === IoTThingModelTypeEnum.SERVICE) {
|
||||
removeDataSpecs(data.service);
|
||||
data.dataType = data.service.dataType;
|
||||
data.service.identifier = data.identifier;
|
||||
data.service.name = data.name;
|
||||
if (isEmpty(data.service.inputParams)) {
|
||||
delete data.service.inputParams;
|
||||
}
|
||||
if (isEmpty(data.service.outputParams)) {
|
||||
delete data.service.outputParams;
|
||||
}
|
||||
delete data.property;
|
||||
delete data.event;
|
||||
} else if (data.type === IoTThingModelTypeEnum.EVENT) {
|
||||
removeDataSpecs(data.event);
|
||||
data.dataType = data.event.dataType;
|
||||
data.event.identifier = data.identifier;
|
||||
data.event.name = data.name;
|
||||
if (isEmpty(data.event.outputParams)) {
|
||||
delete data.event.outputParams;
|
||||
}
|
||||
delete data.property;
|
||||
delete data.service;
|
||||
}
|
||||
}
|
||||
|
||||
/** 清理空的 dataSpecs / dataSpecsList */
|
||||
function removeDataSpecs(val: any) {
|
||||
if (isEmpty(val.dataSpecs)) {
|
||||
delete val.dataSpecs;
|
||||
}
|
||||
if (isEmpty(val.dataSpecsList)) {
|
||||
delete val.dataSpecsList;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-2/5">
|
||||
<Form
|
||||
ref="formRef"
|
||||
:label-col="{ span: 6 }"
|
||||
:model="formData"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
class="mx-4"
|
||||
>
|
||||
<Form.Item
|
||||
:rules="ThingModelFormRules.type"
|
||||
label="功能类型"
|
||||
name="type"
|
||||
>
|
||||
<Radio.Group v-model:value="formData.type">
|
||||
<Radio.Button
|
||||
v-for="dict in getDictOptions(DICT_TYPE.IOT_THING_MODEL_TYPE)"
|
||||
:key="String(dict.value)"
|
||||
:value="Number(dict.value)"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
:rules="ThingModelFormRules.name"
|
||||
label="功能名称"
|
||||
name="name"
|
||||
>
|
||||
<Input v-model:value="formData.name" placeholder="请输入功能名称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
:rules="ThingModelFormRules.identifier"
|
||||
label="标识符"
|
||||
name="identifier"
|
||||
>
|
||||
<Input v-model:value="formData.identifier" placeholder="请输入标识符" />
|
||||
</Form.Item>
|
||||
<!-- 属性配置 -->
|
||||
<ThingModelProperty
|
||||
v-if="formData.type === IoTThingModelTypeEnum.PROPERTY"
|
||||
v-model="formData.property"
|
||||
/>
|
||||
<!-- 服务配置 -->
|
||||
<ThingModelService
|
||||
v-if="formData.type === IoTThingModelTypeEnum.SERVICE"
|
||||
v-model="formData.service"
|
||||
/>
|
||||
<!-- 事件配置 -->
|
||||
<ThingModelEvent
|
||||
v-if="formData.type === IoTThingModelTypeEnum.EVENT"
|
||||
v-model="formData.event"
|
||||
/>
|
||||
<Form.Item label="描述" name="description">
|
||||
<Input.TextArea
|
||||
v-model:value="formData.description"
|
||||
:maxlength="200"
|
||||
:rows="3"
|
||||
placeholder="请输入物模型描述"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Button, Divider, Form, Input } from 'ant-design-vue';
|
||||
|
||||
import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
||||
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
import ThingModelProperty from './property.vue';
|
||||
|
||||
const props = defineProps<{ direction: string; modelValue: any }>();
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
const thingModelParams = useVModel(props, 'modelValue', emits) as Ref<any[]>;
|
||||
|
||||
const paramFormRef = ref();
|
||||
const formData = ref<any>(buildEmptyFormData());
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
try {
|
||||
await paramFormRef.value?.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!thingModelParams.value) {
|
||||
thingModelParams.value = [];
|
||||
}
|
||||
const data = formData.value;
|
||||
const item = {
|
||||
identifier: data.identifier,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
dataType: data.property.dataType,
|
||||
paraOrder: 0,
|
||||
direction: props.direction,
|
||||
dataSpecs:
|
||||
!isEmpty(data.property.dataSpecs) &&
|
||||
Object.keys(data.property.dataSpecs).length > 1
|
||||
? data.property.dataSpecs
|
||||
: undefined,
|
||||
dataSpecsList: isEmpty(data.property.dataSpecsList)
|
||||
? undefined
|
||||
: data.property.dataSpecsList,
|
||||
};
|
||||
// 按 identifier 去重,存在则更新,否则追加
|
||||
const existingIndex = thingModelParams.value.findIndex(
|
||||
(spec) => spec.identifier === data.identifier,
|
||||
);
|
||||
if (existingIndex === -1) {
|
||||
thingModelParams.value.push(item);
|
||||
} else {
|
||||
thingModelParams.value[existingIndex] = item;
|
||||
}
|
||||
await modalApi.close();
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
formData.value = buildEmptyFormData();
|
||||
paramFormRef.value?.clearValidate?.();
|
||||
const data = modalApi.getData<any>();
|
||||
if (isEmpty(data)) {
|
||||
return;
|
||||
}
|
||||
formData.value = {
|
||||
identifier: data.identifier ?? '',
|
||||
name: data.name ?? '',
|
||||
description: data.description ?? '',
|
||||
property: {
|
||||
dataType: data.dataType ?? IoTDataSpecsDataTypeEnum.INT,
|
||||
dataSpecs: data.dataSpecs ?? {},
|
||||
dataSpecsList: data.dataSpecsList ?? [],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/** 构造空白参数表单 */
|
||||
function buildEmptyFormData() {
|
||||
return {
|
||||
identifier: '',
|
||||
name: '',
|
||||
description: '',
|
||||
property: {
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
dataSpecs: { dataType: IoTDataSpecsDataTypeEnum.INT },
|
||||
dataSpecsList: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 打开参数表单(新增或编辑) */
|
||||
function openParamForm(val: any) {
|
||||
modalApi.setData(val).open();
|
||||
}
|
||||
|
||||
/** 删除参数项 */
|
||||
function deleteParamItem(index: number) {
|
||||
thingModelParams.value.splice(index, 1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-for="(item, index) in thingModelParams"
|
||||
:key="index"
|
||||
class="mb-2.5 flex w-full justify-between bg-gray-100 px-2.5 dark:bg-gray-800"
|
||||
>
|
||||
<span>参数名称:{{ item.name }}</span>
|
||||
<div>
|
||||
<Button type="link" @click="openParamForm(item)">编辑</Button>
|
||||
<Divider type="vertical" />
|
||||
<Button danger type="link" @click="deleteParamItem(index)">删除</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="link" @click="openParamForm(null)">+ 新增参数</Button>
|
||||
|
||||
<!-- 参数表单 -->
|
||||
<Modal class="w-2/5" title="参数配置">
|
||||
<Form
|
||||
ref="paramFormRef"
|
||||
:label-col="{ span: 6 }"
|
||||
:model="formData"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
class="mx-4"
|
||||
>
|
||||
<Form.Item
|
||||
:rules="ThingModelFormRules.name"
|
||||
label="参数名称"
|
||||
name="name"
|
||||
>
|
||||
<Input v-model:value="formData.name" placeholder="请输入参数名称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
:rules="ThingModelFormRules.identifier"
|
||||
label="标识符"
|
||||
name="identifier"
|
||||
>
|
||||
<Input v-model:value="formData.identifier" placeholder="请输入标识符" />
|
||||
</Form.Item>
|
||||
<!-- 属性配置 -->
|
||||
<ThingModelProperty v-model="formData.property" is-params />
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -11,6 +11,7 @@ import { isEmpty } from '@vben/utils';
|
|||
import { useVModel } from '@vueuse/core';
|
||||
import { Form, Input, Radio, Select } from 'ant-design-vue';
|
||||
|
||||
import { ThingModelFormRules, validateBoolName } from '#/api/iot/thingmodel';
|
||||
import {
|
||||
getDataTypeOptions,
|
||||
IoTDataSpecsDataTypeEnum,
|
||||
|
|
@ -22,10 +23,22 @@ import {
|
|||
ThingModelEnumDataSpecs,
|
||||
ThingModelNumberDataSpecs,
|
||||
ThingModelStructDataSpecs,
|
||||
} from './dataSpecs';
|
||||
} from './data-specs';
|
||||
|
||||
/** IoT 物模型属性 */
|
||||
defineOptions({ name: 'ThingModelProperty' });
|
||||
/** 嵌套在结构体里时,禁止再选数组 / 结构体(最多支持两层嵌套) */
|
||||
const NESTED_EXCLUDED_TYPES = new Set<string>([
|
||||
IoTDataSpecsDataTypeEnum.ARRAY,
|
||||
IoTDataSpecsDataTypeEnum.STRUCT,
|
||||
]);
|
||||
const STRUCT_CHILD_OPTIONS = getDataTypeOptions().filter(
|
||||
(item) => !NESTED_EXCLUDED_TYPES.has(item.value),
|
||||
);
|
||||
/** 数值型数据类型集合 */
|
||||
const NUMERIC_TYPES = new Set<string>([
|
||||
IoTDataSpecsDataTypeEnum.INT,
|
||||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||
]);
|
||||
|
||||
const props = defineProps<{
|
||||
isParams?: boolean;
|
||||
|
|
@ -38,65 +51,54 @@ const property = useVModel(
|
|||
'modelValue',
|
||||
emits,
|
||||
) as Ref<ThingModelProperty>;
|
||||
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 dataTypeOptions = computed(() =>
|
||||
props.isStructDataSpecs ? STRUCT_CHILD_OPTIONS : getDataTypeOptions(),
|
||||
);
|
||||
|
||||
/** 数据类型切换时,重置 dataSpecs / dataSpecsList 并按新类型初始化 */
|
||||
function handleChange(dataType: any) {
|
||||
property.value.dataSpecs = {};
|
||||
property.value.dataSpecsList = [];
|
||||
// 不是列表型数据才设置 dataSpecs.dataType
|
||||
![
|
||||
// 数值 / 文本 / 时间 / 数组型把 dataType 同步到 dataSpecs;布尔 / 枚举 / 结构走 dataSpecsList
|
||||
const listLike = [
|
||||
IoTDataSpecsDataTypeEnum.BOOL,
|
||||
IoTDataSpecsDataTypeEnum.ENUM,
|
||||
IoTDataSpecsDataTypeEnum.STRUCT,
|
||||
].includes(dataType) && (property.value.dataSpecs.dataType = dataType);
|
||||
switch (dataType) {
|
||||
case IoTDataSpecsDataTypeEnum.BOOL: {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
property.value.dataSpecsList.push({
|
||||
dataType: IoTDataSpecsDataTypeEnum.BOOL,
|
||||
name: '', // 布尔值的名称
|
||||
value: i, // 布尔值
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case IoTDataSpecsDataTypeEnum.ENUM: {
|
||||
property.value.dataSpecsList.push({
|
||||
dataType: IoTDataSpecsDataTypeEnum.ENUM,
|
||||
name: '', // 枚举项的名称
|
||||
value: undefined, // 枚举值
|
||||
});
|
||||
break;
|
||||
}
|
||||
];
|
||||
if (!listLike.includes(dataType)) {
|
||||
property.value.dataSpecs.dataType = dataType;
|
||||
}
|
||||
if (dataType === IoTDataSpecsDataTypeEnum.BOOL) {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
property.value.dataSpecsList.push({
|
||||
dataType: IoTDataSpecsDataTypeEnum.BOOL,
|
||||
name: '', // 布尔描述
|
||||
value: i, // 布尔值
|
||||
});
|
||||
}
|
||||
} else if (dataType === IoTDataSpecsDataTypeEnum.ENUM) {
|
||||
property.value.dataSpecsList.push({
|
||||
dataType: IoTDataSpecsDataTypeEnum.ENUM,
|
||||
name: '', // 枚举项描述
|
||||
value: undefined, // 枚举值
|
||||
});
|
||||
}
|
||||
// useVModel 会自动同步数据到父组件,不需要手动 emit
|
||||
}
|
||||
|
||||
/** 默认选中读写 */
|
||||
watch(
|
||||
() => property.value.accessMode,
|
||||
(val: string | undefined) => {
|
||||
if (props.isStructDataSpecs || props.isParams) {
|
||||
return;
|
||||
}
|
||||
if (isEmpty(val)) {
|
||||
property.value.accessMode = IoTThingModelAccessModeEnum.READ_WRITE.value;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
/** 顶层属性表单首次进入时,默认选中「读写」 */
|
||||
if (!props.isStructDataSpecs && !props.isParams) {
|
||||
watch(
|
||||
() => property.value.accessMode,
|
||||
(val: string | undefined) => {
|
||||
if (isEmpty(val)) {
|
||||
property.value.accessMode =
|
||||
IoTThingModelAccessModeEnum.READ_WRITE.value;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -108,7 +110,7 @@ watch(
|
|||
>
|
||||
<!-- ARRAY 和 STRUCT 类型数据相互嵌套时,最多支持递归嵌套 2 层(父和子) -->
|
||||
<Select.Option
|
||||
v-for="option in getDataTypeOptions2"
|
||||
v-for="option in dataTypeOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
|
|
@ -118,13 +120,7 @@ watch(
|
|||
</Form.Item>
|
||||
<!-- 数值型配置 -->
|
||||
<ThingModelNumberDataSpecs
|
||||
v-if="
|
||||
[
|
||||
IoTDataSpecsDataTypeEnum.INT,
|
||||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||
].includes(property.dataType || '')
|
||||
"
|
||||
v-if="NUMERIC_TYPES.has(property.dataType || '')"
|
||||
v-model="property.dataSpecs"
|
||||
/>
|
||||
<!-- 枚举型配置 -->
|
||||
|
|
@ -137,17 +133,27 @@ watch(
|
|||
v-if="property.dataType === IoTDataSpecsDataTypeEnum.BOOL"
|
||||
label="布尔值"
|
||||
>
|
||||
<template v-for="item in property.dataSpecsList" :key="item.value">
|
||||
<div class="w-1/1 mb-5px flex items-center justify-start">
|
||||
<template
|
||||
v-for="(item, index) in property.dataSpecsList"
|
||||
:key="item.value"
|
||||
>
|
||||
<div class="mb-[5px] flex w-full items-center justify-start">
|
||||
<span>{{ item.value }}</span>
|
||||
<span class="mx-2">-</span>
|
||||
<div class="flex-1">
|
||||
<Form.Item
|
||||
:name="['property', 'dataSpecsList', index, 'name']"
|
||||
:rules="[
|
||||
{ required: true, message: '布尔描述不能为空', trigger: 'blur' },
|
||||
{ validator: validateBoolName, trigger: 'blur' },
|
||||
]"
|
||||
class="mb-0 flex-1"
|
||||
>
|
||||
<Input
|
||||
v-model:value="item.name"
|
||||
:placeholder="`如:${item.value === 0 ? '关' : '开'}`"
|
||||
class="w-255px!"
|
||||
class="!w-[255px]"
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</template>
|
||||
</Form.Item>
|
||||
|
|
@ -155,11 +161,12 @@ watch(
|
|||
<Form.Item
|
||||
v-if="property.dataType === IoTDataSpecsDataTypeEnum.TEXT"
|
||||
:name="['property', 'dataSpecs', 'length']"
|
||||
:rules="ThingModelFormRules.length"
|
||||
label="数据长度"
|
||||
>
|
||||
<Input
|
||||
v-model:value="property.dataSpecs.length"
|
||||
class="w-255px!"
|
||||
class="!w-[255px]"
|
||||
placeholder="请输入文本字节长度"
|
||||
>
|
||||
<template #addonAfter>字节</template>
|
||||
|
|
@ -172,7 +179,7 @@ watch(
|
|||
name="date"
|
||||
>
|
||||
<Input
|
||||
class="w-255px!"
|
||||
class="!w-[255px]"
|
||||
disabled
|
||||
placeholder="String 类型的 UTC 时间戳(毫秒)"
|
||||
/>
|
||||
|
|
@ -190,6 +197,7 @@ watch(
|
|||
<Form.Item
|
||||
v-if="!isStructDataSpecs && !isParams"
|
||||
:name="['property', 'accessMode']"
|
||||
:rules="ThingModelFormRules.accessMode"
|
||||
label="读写类型"
|
||||
>
|
||||
<Radio.Group v-model:value="property.accessMode">
|
||||
|
|
@ -9,17 +9,15 @@ import { isEmpty } from '@vben/utils';
|
|||
import { useVModel } from '@vueuse/core';
|
||||
import { Form, Radio } from 'ant-design-vue';
|
||||
|
||||
import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
||||
import {
|
||||
IoTThingModelParamDirectionEnum,
|
||||
IoTThingModelServiceCallTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
import ThingModelInputOutputParam from './thing-model-input-output-param.vue';
|
||||
import ThingModelInputOutputParam from './input-output-param.vue';
|
||||
|
||||
/** IoT 物模型服务 */
|
||||
defineOptions({ name: 'ThingModelService' });
|
||||
|
||||
const props = defineProps<{ isStructDataSpecs?: boolean; modelValue: any }>();
|
||||
const props = defineProps<{ modelValue: any }>();
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
const service = useVModel(props, 'modelValue', emits) as Ref<any>;
|
||||
|
||||
|
|
@ -36,7 +34,7 @@ watch(
|
|||
<template>
|
||||
<Form.Item
|
||||
:name="['service', 'callType']"
|
||||
:rules="[{ required: true, message: '请选择调用方式', trigger: 'change' }]"
|
||||
:rules="ThingModelFormRules.callType"
|
||||
label="调用方式"
|
||||
>
|
||||
<Radio.Group v-model:value="service.callType">
|
||||
|
|
@ -1,298 +0,0 @@
|
|||
<!-- 产品的物模型表单 -->
|
||||
<script lang="ts" setup>
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
// TODO @haohao:使用 form.vue;
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||
|
||||
import { inject, ref } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { $t } from '@vben/locales';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
import { Form, Input, message, Modal, Radio } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
createThingModel,
|
||||
getThingModel,
|
||||
updateThingModel,
|
||||
} from '#/api/iot/thingmodel';
|
||||
import {
|
||||
IOT_PROVIDE_KEY,
|
||||
IoTDataSpecsDataTypeEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
import ThingModelEvent from './thing-model-event.vue';
|
||||
import ThingModelProperty from './thing-model-property.vue';
|
||||
import ThingModelService from './thing-model-service.vue';
|
||||
|
||||
/** IoT 物模型数据表单 */
|
||||
defineOptions({ name: 'IoTThingModelForm' });
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']);
|
||||
const product = inject<Ref<IotProductApi.Product>>(IOT_PROVIDE_KEY.PRODUCT); // 注入产品信息
|
||||
|
||||
const dialogVisible = ref(false); // 弹窗的是否展示
|
||||
const dialogTitle = ref(''); // 弹窗的标题
|
||||
const formLoading = ref(false); // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const formType = ref(''); // 表单的类型:create - 新增;update - 修改
|
||||
const formData = ref<any>({
|
||||
type: IoTThingModelTypeEnum.PROPERTY,
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
property: {
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
dataSpecs: {
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
},
|
||||
},
|
||||
service: {
|
||||
inputParams: [],
|
||||
outputParams: [],
|
||||
},
|
||||
event: {
|
||||
outputParams: [],
|
||||
},
|
||||
});
|
||||
|
||||
const formRef = ref(); // 表单 Ref
|
||||
|
||||
/** 打开弹窗 */
|
||||
// TODO @haohao:Modal 的写法。
|
||||
async function open(type: string, id?: number) {
|
||||
dialogVisible.value = true;
|
||||
// 设置标题:create -> 新增,update -> 编辑
|
||||
dialogTitle.value =
|
||||
type === 'create' ? $t('page.action.add') : $t('page.action.edit');
|
||||
formType.value = type;
|
||||
resetForm();
|
||||
if (id) {
|
||||
formLoading.value = true;
|
||||
try {
|
||||
const result = await getThingModel(id);
|
||||
// 转换类型为数字
|
||||
formData.value = {
|
||||
...result,
|
||||
type: Number(result.type),
|
||||
};
|
||||
// 情况一:属性初始化
|
||||
if (
|
||||
!formData.value.property ||
|
||||
Object.keys(formData.value.property).length === 0
|
||||
) {
|
||||
formData.value.dataType = IoTDataSpecsDataTypeEnum.INT;
|
||||
formData.value.property = {
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
dataSpecs: {
|
||||
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 = {
|
||||
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 = {
|
||||
outputParams: [],
|
||||
};
|
||||
} else {
|
||||
// 确保参数数组存在
|
||||
if (!formData.value.event.outputParams) {
|
||||
formData.value.event.outputParams = [];
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
defineExpose({ open, close: () => (dialogVisible.value = false) });
|
||||
|
||||
async function submitForm() {
|
||||
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;
|
||||
fillExtraAttributes(data);
|
||||
await (formType.value === 'create'
|
||||
? createThingModel(data)
|
||||
: updateThingModel(data));
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
// 关闭弹窗
|
||||
dialogVisible.value = false;
|
||||
emit('success');
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 填写额外的属性(处理不同类型的情况) */
|
||||
function fillExtraAttributes(data: any) {
|
||||
// 属性
|
||||
if (data.type === IoTThingModelTypeEnum.PROPERTY) {
|
||||
removeDataSpecs(data.property);
|
||||
data.dataType = data.property.dataType;
|
||||
data.property.identifier = data.identifier;
|
||||
data.property.name = data.name;
|
||||
delete data.service;
|
||||
delete data.event;
|
||||
}
|
||||
// 服务
|
||||
if (data.type === IoTThingModelTypeEnum.SERVICE) {
|
||||
removeDataSpecs(data.service);
|
||||
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;
|
||||
}
|
||||
// 事件
|
||||
if (data.type === IoTThingModelTypeEnum.EVENT) {
|
||||
removeDataSpecs(data.event);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理 dataSpecs 为空的情况 */
|
||||
function removeDataSpecs(val: any) {
|
||||
if (!val.dataSpecs || Object.keys(val.dataSpecs).length === 0) {
|
||||
delete val.dataSpecs;
|
||||
}
|
||||
if (!val.dataSpecsList || val.dataSpecsList.length === 0) {
|
||||
delete val.dataSpecsList;
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
function resetForm() {
|
||||
formData.value = {
|
||||
type: IoTThingModelTypeEnum.PROPERTY,
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
property: {
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
dataSpecs: {
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
},
|
||||
},
|
||||
service: {
|
||||
inputParams: [],
|
||||
outputParams: [],
|
||||
},
|
||||
event: {
|
||||
outputParams: [],
|
||||
},
|
||||
};
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
v-model:open="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
:confirm-loading="formLoading"
|
||||
@ok="submitForm"
|
||||
>
|
||||
<!-- TODO @haohao:这个可以改造成 data.ts schema 形式么?可能是有一定成本,后续迁移 ele 版本,会容易很多。 -->
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<Form.Item label="功能类型" name="type">
|
||||
<Radio.Group v-model:value="formData.type">
|
||||
<Radio.Button
|
||||
v-for="dict in getDictOptions(DICT_TYPE.IOT_THING_MODEL_TYPE)"
|
||||
:key="String(dict.value)"
|
||||
:value="Number(dict.value)"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label="功能名称" name="name">
|
||||
<Input v-model:value="formData.name" placeholder="请输入功能名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="标识符" name="identifier">
|
||||
<Input v-model:value="formData.identifier" placeholder="请输入标识符" />
|
||||
</Form.Item>
|
||||
<!-- 属性配置 -->
|
||||
<ThingModelProperty
|
||||
v-if="formData.type === IoTThingModelTypeEnum.PROPERTY"
|
||||
v-model="formData.property"
|
||||
/>
|
||||
<!-- 服务配置 -->
|
||||
<ThingModelService
|
||||
v-if="formData.type === IoTThingModelTypeEnum.SERVICE"
|
||||
v-model="formData.service"
|
||||
/>
|
||||
<!-- 事件配置 -->
|
||||
<ThingModelEvent
|
||||
v-if="formData.type === IoTThingModelTypeEnum.EVENT"
|
||||
v-model="formData.event"
|
||||
/>
|
||||
<Form.Item label="描述" name="desc">
|
||||
<Input.TextArea
|
||||
v-model:value="formData.desc"
|
||||
:maxlength="200"
|
||||
:rows="3"
|
||||
placeholder="请输入属性描述"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
<!-- 产品的物模型表单(event、service 项里的参数) -->
|
||||
<script lang="ts" setup>
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { ref, unref } from 'vue';
|
||||
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Button, Divider, Form, Input, Modal } from 'ant-design-vue';
|
||||
|
||||
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
import ThingModelProperty from './thing-model-property.vue';
|
||||
|
||||
/** 输入输出参数配置组件 */
|
||||
defineOptions({ name: 'ThingModelInputOutputParam' });
|
||||
|
||||
const props = defineProps<{ direction: string; modelValue: any }>();
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
const thingModelParams = useVModel(props, 'modelValue', emits) as Ref<any[]>;
|
||||
const dialogVisible = ref(false); // 弹窗的是否展示
|
||||
const formLoading = ref(false); // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const paramFormRef = ref(); // 表单 ref
|
||||
const formData = ref<any>({
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
property: {
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
dataSpecs: {
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
},
|
||||
dataSpecsList: [],
|
||||
},
|
||||
});
|
||||
|
||||
/** 打开 param 表单 */
|
||||
function openParamForm(val: any) {
|
||||
dialogVisible.value = true;
|
||||
resetForm();
|
||||
if (isEmpty(val)) {
|
||||
return;
|
||||
}
|
||||
// 编辑时回显数据
|
||||
const valData = val as any;
|
||||
formData.value = {
|
||||
identifier: valData?.identifier || '',
|
||||
name: valData?.name || '',
|
||||
description: valData?.description || '',
|
||||
property: {
|
||||
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 项 */
|
||||
function deleteParamItem(index: number) {
|
||||
thingModelParams.value.splice(index, 1);
|
||||
}
|
||||
|
||||
/** 添加参数 */
|
||||
async function submitForm() {
|
||||
// 初始化参数列表
|
||||
if (isEmpty(thingModelParams.value)) {
|
||||
thingModelParams.value = [];
|
||||
}
|
||||
// 校验参数
|
||||
await paramFormRef.value.validate();
|
||||
try {
|
||||
// 构建数据对象
|
||||
const data = unref(formData);
|
||||
const item = {
|
||||
identifier: data.identifier,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
dataType: data.property.dataType,
|
||||
paraOrder: 0, // TODO @puhui999: 先写死默认看看后续
|
||||
direction: props.direction,
|
||||
dataSpecs:
|
||||
!!data.property.dataSpecs &&
|
||||
Object.keys(data.property.dataSpecs).length > 1
|
||||
? data.property.dataSpecs
|
||||
: undefined,
|
||||
dataSpecsList: isEmpty(data.property.dataSpecsList)
|
||||
? undefined
|
||||
: data.property.dataSpecsList,
|
||||
};
|
||||
|
||||
// 新增或修改同 identifier 的参数
|
||||
const existingIndex = thingModelParams.value.findIndex(
|
||||
(spec) => spec.identifier === data.identifier,
|
||||
);
|
||||
if (existingIndex === -1) {
|
||||
thingModelParams.value.push(item);
|
||||
} else {
|
||||
thingModelParams.value[existingIndex] = item;
|
||||
}
|
||||
} finally {
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
function resetForm() {
|
||||
formData.value = {
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
property: {
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
dataSpecs: {
|
||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||
},
|
||||
dataSpecsList: [],
|
||||
},
|
||||
};
|
||||
paramFormRef.value?.resetFields();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-for="(item, index) in thingModelParams"
|
||||
:key="index"
|
||||
class="w-1/1 px-10px mb-10px flex justify-between bg-gray-100"
|
||||
>
|
||||
<span>参数名称:{{ item.name }}</span>
|
||||
<div class="btn">
|
||||
<Button type="link" @click="openParamForm(item)">编辑</Button>
|
||||
<Divider type="vertical" />
|
||||
<Button type="link" danger @click="deleteParamItem(index)">删除</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="link" @click="openParamForm(null)">+新增参数</Button>
|
||||
|
||||
<!-- param 表单 -->
|
||||
<Modal
|
||||
v-model:open="dialogVisible"
|
||||
title="新增参数"
|
||||
:confirm-loading="formLoading"
|
||||
@ok="submitForm"
|
||||
>
|
||||
<Form
|
||||
ref="paramFormRef"
|
||||
:model="formData"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<Form.Item label="参数名称" name="name">
|
||||
<Input v-model:value="formData.name" placeholder="请输入功能名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="标识符" name="identifier">
|
||||
<Input v-model:value="formData.identifier" placeholder="请输入标识符" />
|
||||
</Form.Item>
|
||||
<!-- 属性配置 -->
|
||||
<ThingModelProperty v-model="formData.property" is-params />
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
|
||||
import { computed, inject, ref, watch } from 'vue';
|
||||
|
||||
import { Modal, Radio, Textarea } from 'ant-design-vue';
|
||||
|
||||
import { getThingModelTSL } from '#/api/iot/thingmodel';
|
||||
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants';
|
||||
|
||||
defineOptions({ name: 'ThingModelTsl' });
|
||||
|
||||
const dialogVisible = ref(false); // 弹窗的是否展示
|
||||
const dialogTitle = ref('物模型 TSL'); // 弹窗的标题
|
||||
const product = inject<Ref<IotProductApi.Product>>(IOT_PROVIDE_KEY.PRODUCT); // 注入产品信息
|
||||
const viewMode = ref('view'); // 查看模式:view-代码视图,editor-编辑器视图
|
||||
|
||||
/** 打开弹窗 */
|
||||
async function open() {
|
||||
dialogVisible.value = true;
|
||||
await getTsl();
|
||||
}
|
||||
defineExpose({ open });
|
||||
|
||||
/** 获取 TSL */
|
||||
const thingModelTSL = ref<any>({});
|
||||
const tslString = ref(''); // 用于编辑器的字符串格式
|
||||
|
||||
async function getTsl() {
|
||||
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: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>
|
||||
</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 {
|
||||
max-height: 600px;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
background-color: #f5f5f5;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.json-code {
|
||||
margin: 0;
|
||||
font-family: Monaco, Menlo, 'Ubuntu Mono', Consolas, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.json-editor {
|
||||
font-family: Monaco, Menlo, 'Ubuntu Mono', Consolas, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
<script setup lang="ts">
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import type { IotProductApi } from '#/api/iot/product/product';
|
||||
|
||||
import { computed, inject, ref, watch } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Radio, Textarea } from 'ant-design-vue';
|
||||
|
||||
import { getThingModelTSL } from '#/api/iot/thingmodel';
|
||||
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants';
|
||||
|
||||
const product = inject<Ref<IotProductApi.Product>>(IOT_PROVIDE_KEY.PRODUCT);
|
||||
|
||||
const viewMode = ref<'editor' | 'view'>('view');
|
||||
const thingModelTSL = ref<any>({});
|
||||
const tslString = ref('');
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
thingModelTSL.value = await getThingModelTSL(product?.value?.id || 0);
|
||||
tslString.value = JSON.stringify(thingModelTSL.value, null, 2);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 只读视图下,格式化后的 TSL 字符串 */
|
||||
const formattedTSL = computed(() =>
|
||||
JSON.stringify(thingModelTSL.value, null, 2),
|
||||
);
|
||||
|
||||
/** 编辑器内容变化时,同步到数据对象 */
|
||||
watch(tslString, (newValue) => {
|
||||
try {
|
||||
thingModelTSL.value = JSON.parse(newValue);
|
||||
} catch {
|
||||
// JSON 解析失败时保持原值
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :footer="false" class="w-3/5" title="物模型 TSL">
|
||||
<div class="mx-4">
|
||||
<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>
|
||||
</div>
|
||||
<!-- 代码视图:只读展示 -->
|
||||
<div
|
||||
v-if="viewMode === 'view'"
|
||||
class="max-h-[600px] overflow-y-auto rounded border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<pre
|
||||
class="m-0 whitespace-pre-wrap break-words font-mono text-[13px] leading-normal"
|
||||
><code>{{ formattedTSL }}</code></pre>
|
||||
</div>
|
||||
<!-- 编辑器视图:可编辑 -->
|
||||
<Textarea
|
||||
v-else
|
||||
v-model:value="tslString"
|
||||
:rows="20"
|
||||
class="font-mono text-[13px]"
|
||||
placeholder="请输入 JSON 格式的物模型 TSL"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
// TODO @AI:感觉这块,放到 biz-iot-enum 里好点。
|
||||
|
||||
/** 检查值是否为空 */
|
||||
const isEmpty = (value: any): boolean => {
|
||||
return value === null || value === undefined || value === '';
|
||||
};
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
/** IoT 依赖注入 KEY */
|
||||
export const IOT_PROVIDE_KEY = {
|
||||
|
|
@ -104,19 +101,19 @@ export const IoTDataSpecsDataTypeEnum = {
|
|||
ARRAY: 'array',
|
||||
};
|
||||
|
||||
export const getDataTypeOptions = () => {
|
||||
return [
|
||||
{ value: IoTDataSpecsDataTypeEnum.INT, label: '整数型' },
|
||||
{ value: IoTDataSpecsDataTypeEnum.FLOAT, label: '单精度浮点型' },
|
||||
{ value: IoTDataSpecsDataTypeEnum.DOUBLE, label: '双精度浮点型' },
|
||||
{ value: IoTDataSpecsDataTypeEnum.ENUM, label: '枚举型' },
|
||||
{ value: IoTDataSpecsDataTypeEnum.BOOL, label: '布尔型' },
|
||||
{ value: IoTDataSpecsDataTypeEnum.TEXT, label: '文本型' },
|
||||
{ value: IoTDataSpecsDataTypeEnum.DATE, label: '时间型' },
|
||||
{ value: IoTDataSpecsDataTypeEnum.STRUCT, label: '结构体' },
|
||||
{ value: IoTDataSpecsDataTypeEnum.ARRAY, label: '数组' },
|
||||
];
|
||||
};
|
||||
const DATA_TYPE_OPTIONS = Object.freeze([
|
||||
{ value: IoTDataSpecsDataTypeEnum.INT, label: '整数型' },
|
||||
{ value: IoTDataSpecsDataTypeEnum.FLOAT, label: '单精度浮点型' },
|
||||
{ value: IoTDataSpecsDataTypeEnum.DOUBLE, label: '双精度浮点型' },
|
||||
{ value: IoTDataSpecsDataTypeEnum.ENUM, label: '枚举型' },
|
||||
{ value: IoTDataSpecsDataTypeEnum.BOOL, label: '布尔型' },
|
||||
{ value: IoTDataSpecsDataTypeEnum.TEXT, label: '文本型' },
|
||||
{ value: IoTDataSpecsDataTypeEnum.DATE, label: '时间型' },
|
||||
{ value: IoTDataSpecsDataTypeEnum.STRUCT, label: '结构体' },
|
||||
{ value: IoTDataSpecsDataTypeEnum.ARRAY, label: '数组' },
|
||||
]);
|
||||
|
||||
export const getDataTypeOptions = () => DATA_TYPE_OPTIONS;
|
||||
|
||||
/** 获得物体模型数据类型配置项名称 */
|
||||
export const getDataTypeOptionsLabel = (value: string) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue