diff --git a/apps/web-antd/src/views/iot/rule/scene/form/inputs/json-params-input.vue b/apps/web-antd/src/views/iot/rule/scene/form/inputs/json-params-input.vue index 191d2d7e6..fdc6eb47c 100644 --- a/apps/web-antd/src/views/iot/rule/scene/form/inputs/json-params-input.vue +++ b/apps/web-antd/src/views/iot/rule/scene/form/inputs/json-params-input.vue @@ -386,6 +386,9 @@ watch( // 使用 nextTick 确保在下一个 tick 中处理数据 await nextTick(); + if ((newValue || '') === paramsJson.value) { + return; + } handleDataDisplay(newValue || ''); }, { immediate: true }, diff --git a/apps/web-antd/src/views/iot/rule/scene/modules/form.vue b/apps/web-antd/src/views/iot/rule/scene/modules/form.vue index e34113d8c..dad109163 100644 --- a/apps/web-antd/src/views/iot/rule/scene/modules/form.vue +++ b/apps/web-antd/src/views/iot/rule/scene/modules/form.vue @@ -12,6 +12,7 @@ import { IotRuleSceneTriggerTypeEnum, isDeviceTrigger, } from '@vben/constants'; +import { CronUtils } from '@vben/utils'; import { Form, message } from 'ant-design-vue'; @@ -158,12 +159,15 @@ function validateTriggers(_rule: any, value: any, callback: any) { } } } - if ( - trigger.type === IotRuleSceneTriggerTypeEnum.TIMER && - !trigger.cronExpression - ) { - callback(new Error(`触发器 ${i + 1}:CRON 表达式不能为空`)); - return; + if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) { + if (!trigger.cronExpression) { + callback(new Error(`触发器 ${i + 1}:CRON 表达式不能为空`)); + return; + } + if (!CronUtils.validate(trigger.cronExpression)) { + callback(new Error(`触发器 ${i + 1}:CRON 表达式格式不正确`)); + return; + } } // 递归校验 conditionGroups(嵌套条件组) if (trigger.conditionGroups?.length) { diff --git a/apps/web-antd/src/views/iot/thingmodel/modules/data-specs/array.vue b/apps/web-antd/src/views/iot/thingmodel/modules/data-specs/array.vue index 1c411a109..bb31417fd 100644 --- a/apps/web-antd/src/views/iot/thingmodel/modules/data-specs/array.vue +++ b/apps/web-antd/src/views/iot/thingmodel/modules/data-specs/array.vue @@ -28,13 +28,11 @@ const childDataTypeOptions = getDataTypeOptions().filter( const dataSpecs = useVModel(props, 'modelValue', emits) as Ref; -/** 元素类型切到 struct 时,初始化 dataSpecsList 占位 */ +/** 元素类型切换时,清理旧子类型的结构体属性配置 */ function handleChange(e: any) { const val = e?.target?.value ?? e; - if (val !== IoTDataSpecsDataTypeEnum.STRUCT) { - return; - } dataSpecs.value.dataSpecsList = []; + dataSpecs.value.childDataType = val; } diff --git a/apps/web-antd/src/views/iot/thingmodel/modules/input-output-param.vue b/apps/web-antd/src/views/iot/thingmodel/modules/input-output-param.vue index 911a26a92..275504339 100644 --- a/apps/web-antd/src/views/iot/thingmodel/modules/input-output-param.vue +++ b/apps/web-antd/src/views/iot/thingmodel/modules/input-output-param.vue @@ -8,13 +8,17 @@ import { IoTDataSpecsDataTypeEnum } from '@vben/constants'; import { cloneDeep, isEmpty } from '@vben/utils'; import { useVModel } from '@vueuse/core'; -import { Button, Divider, Form, Input } from 'ant-design-vue'; +import { Button, Divider, Form, Input, message } from 'ant-design-vue'; import { ThingModelFormRules } from '#/api/iot/thingmodel'; import ThingModelProperty from './property.vue'; -const props = defineProps<{ direction: string; modelValue: any }>(); +const props = defineProps<{ + direction: string; + existingIdentifiers?: string[]; + modelValue: any; +}>(); const emits = defineEmits(['update:modelValue']); const thingModelParams = useVModel(props, 'modelValue', emits) as Ref; @@ -33,6 +37,13 @@ const [Modal, modalApi] = useVbenModal({ } // 组装表单 const data = formData.value; + if ( + data.identifier && + props.existingIdentifiers?.includes(data.identifier) + ) { + message.warning('输入参数和输出参数标识符不能重复'); + return; + } const item = { identifier: data.identifier, name: data.name, diff --git a/apps/web-antd/src/views/iot/thingmodel/modules/service.vue b/apps/web-antd/src/views/iot/thingmodel/modules/service.vue index dc0ab038a..c75732587 100644 --- a/apps/web-antd/src/views/iot/thingmodel/modules/service.vue +++ b/apps/web-antd/src/views/iot/thingmodel/modules/service.vue @@ -29,6 +29,17 @@ watch( (service.value.callType = IoTThingModelServiceCallTypeEnum.ASYNC.value), { immediate: true }, ); + +/** 提取参数标识符列表,用于输入 / 输出参数跨表去重 */ +function getParamIdentifiers(params?: any[]) { + const identifiers: string[] = []; + for (const item of params || []) { + if (item.identifier) { + identifiers.push(item.identifier); + } + } + return identifiers; +} diff --git a/apps/web-antd/src/views/mes/cal/team/components/cal-team-select-dialog.vue b/apps/web-antd/src/views/mes/cal/team/components/cal-team-select-dialog.vue new file mode 100644 index 000000000..afad7f188 --- /dev/null +++ b/apps/web-antd/src/views/mes/cal/team/components/cal-team-select-dialog.vue @@ -0,0 +1,141 @@ + + + diff --git a/apps/web-antd/src/views/mes/cal/team/components/cal-team-select.vue b/apps/web-antd/src/views/mes/cal/team/components/cal-team-select.vue new file mode 100644 index 000000000..73a276e69 --- /dev/null +++ b/apps/web-antd/src/views/mes/cal/team/components/cal-team-select.vue @@ -0,0 +1,84 @@ + + + diff --git a/apps/web-antd/src/views/mes/cal/team/components/index.ts b/apps/web-antd/src/views/mes/cal/team/components/index.ts new file mode 100644 index 000000000..092b24a19 --- /dev/null +++ b/apps/web-antd/src/views/mes/cal/team/components/index.ts @@ -0,0 +1,2 @@ +export { default as CalTeamSelectDialog } from './cal-team-select-dialog.vue'; +export { default as CalTeamSelect } from './cal-team-select.vue'; diff --git a/apps/web-antd/src/views/mes/cal/team/data.ts b/apps/web-antd/src/views/mes/cal/team/data.ts new file mode 100644 index 000000000..3c0f83f96 --- /dev/null +++ b/apps/web-antd/src/views/mes/cal/team/data.ts @@ -0,0 +1,188 @@ +import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; +import type { MesCalTeamApi } from '#/api/mes/cal/team'; + +import { h } from 'vue'; + +import { DICT_TYPE } from '@vben/constants'; +import { getDictOptions } from '@vben/hooks'; + +import { Button } from 'ant-design-vue'; + +import { z } from '#/adapter/form'; +import { generateAutoCode } from '#/api/mes/md/autocode/record'; +import { MesAutoCodeRuleCode } from '#/views/mes/utils/constants'; + +/** 新增/修改班组的表单 */ +export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { + return [ + { + fieldName: 'id', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'code', + label: '班组编码', + component: 'Input', + componentProps: { + maxLength: 64, + placeholder: '请输入班组编码', + }, + rules: z.string().min(1, '班组编码不能为空').max(64), + suffix: () => + h( + Button, + { + type: 'default', + onClick: async () => { + try { + const code = await generateAutoCode(MesAutoCodeRuleCode.CAL_TEAM_CODE); + await formApi?.setFieldValue('code', code); + } catch (error) { + console.error(error); + } + }, + }, + { default: () => '生成' }, + ), + }, + { + fieldName: 'name', + label: '班组名称', + component: 'Input', + componentProps: { + maxLength: 100, + placeholder: '请输入班组名称', + }, + rules: z.string().min(1, '班组名称不能为空').max(100), + }, + { + fieldName: 'calendarType', + label: '班组类型', + component: 'RadioGroup', + componentProps: { + buttonStyle: 'solid', + optionType: 'button', + options: getDictOptions(DICT_TYPE.MES_CAL_CALENDAR_TYPE, 'number'), + }, + rules: 'selectRequired', + }, + { + fieldName: 'remark', + label: '备注', + component: 'Textarea', + formItemClass: 'col-span-3', + componentProps: { + maxLength: 250, + placeholder: '请输入备注', + rows: 3, + }, + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'code', + label: '班组编码', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入班组编码', + }, + }, + { + fieldName: 'name', + label: '班组名称', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入班组名称', + }, + }, + { + fieldName: 'calendarType', + label: '班组类型', + component: 'Select', + componentProps: { + allowClear: true, + options: getDictOptions(DICT_TYPE.MES_CAL_CALENDAR_TYPE, 'number'), + placeholder: '请选择班组类型', + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'code', + title: '班组编码', + minWidth: 150, + slots: { + default: 'code', + }, + }, + { field: 'name', title: '班组名称', minWidth: 150 }, + { + field: 'calendarType', + title: '班组类型', + width: 140, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.MES_CAL_CALENDAR_TYPE }, + }, + }, + { field: 'remark', title: '备注', minWidth: 180 }, + { field: 'createTime', title: '创建时间', width: 180, formatter: 'formatDateTime' }, + { + title: '操作', + width: 180, + fixed: 'right', + slots: { + default: 'actions', + }, + }, + ]; +} + +/** 班组选择弹窗搜索表单 */ +export function useTeamSelectGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'code', + label: '班组编码', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入班组编码', + }, + }, + { + fieldName: 'name', + label: '班组名称', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入班组名称', + }, + }, + ]; +} + +/** 班组选择弹窗字段 */ +export function useTeamSelectGridColumns(): VxeTableGridOptions['columns'] { + return [ + { type: 'checkbox', width: 50 }, + { field: 'code', title: '班组编码', minWidth: 140 }, + { field: 'name', title: '班组名称', minWidth: 140 }, + { field: 'remark', title: '备注', minWidth: 160 }, + ]; +} diff --git a/apps/web-antd/src/views/mes/cal/team/index.vue b/apps/web-antd/src/views/mes/cal/team/index.vue new file mode 100644 index 000000000..42336c7da --- /dev/null +++ b/apps/web-antd/src/views/mes/cal/team/index.vue @@ -0,0 +1,147 @@ + + + diff --git a/apps/web-antd/src/views/mes/cal/team/modules/form.vue b/apps/web-antd/src/views/mes/cal/team/modules/form.vue new file mode 100644 index 000000000..0a3d1bbb8 --- /dev/null +++ b/apps/web-antd/src/views/mes/cal/team/modules/form.vue @@ -0,0 +1,116 @@ + + + diff --git a/apps/web-antd/src/views/mes/cal/team/modules/member-list.vue b/apps/web-antd/src/views/mes/cal/team/modules/member-list.vue new file mode 100644 index 000000000..0630dd10d --- /dev/null +++ b/apps/web-antd/src/views/mes/cal/team/modules/member-list.vue @@ -0,0 +1,194 @@ + + + diff --git a/apps/web-ele/src/views/iot/rule/scene/form/inputs/json-params-input.vue b/apps/web-ele/src/views/iot/rule/scene/form/inputs/json-params-input.vue index 20fe82344..41f36d631 100644 --- a/apps/web-ele/src/views/iot/rule/scene/form/inputs/json-params-input.vue +++ b/apps/web-ele/src/views/iot/rule/scene/form/inputs/json-params-input.vue @@ -386,6 +386,9 @@ watch( // 使用 nextTick 确保在下一个 tick 中处理数据 await nextTick(); + if ((newValue || '') === paramsJson.value) { + return; + } handleDataDisplay(newValue || ''); }, { immediate: true }, diff --git a/apps/web-ele/src/views/iot/rule/scene/modules/form.vue b/apps/web-ele/src/views/iot/rule/scene/modules/form.vue index 6c9324d12..ffff4c55a 100644 --- a/apps/web-ele/src/views/iot/rule/scene/modules/form.vue +++ b/apps/web-ele/src/views/iot/rule/scene/modules/form.vue @@ -12,6 +12,7 @@ import { IotRuleSceneTriggerTypeEnum, isDeviceTrigger, } from '@vben/constants'; +import { CronUtils } from '@vben/utils'; import { ElForm, ElMessage } from 'element-plus'; @@ -158,12 +159,15 @@ function validateTriggers(_rule: any, value: any, callback: any) { } } } - if ( - trigger.type === IotRuleSceneTriggerTypeEnum.TIMER && - !trigger.cronExpression - ) { - callback(new Error(`触发器 ${i + 1}:CRON 表达式不能为空`)); - return; + if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) { + if (!trigger.cronExpression) { + callback(new Error(`触发器 ${i + 1}:CRON 表达式不能为空`)); + return; + } + if (!CronUtils.validate(trigger.cronExpression)) { + callback(new Error(`触发器 ${i + 1}:CRON 表达式格式不正确`)); + return; + } } // 递归校验 conditionGroups(嵌套条件组) if (trigger.conditionGroups?.length) { diff --git a/apps/web-ele/src/views/iot/thingmodel/modules/data-specs/array.vue b/apps/web-ele/src/views/iot/thingmodel/modules/data-specs/array.vue index 3aa262609..89b273896 100644 --- a/apps/web-ele/src/views/iot/thingmodel/modules/data-specs/array.vue +++ b/apps/web-ele/src/views/iot/thingmodel/modules/data-specs/array.vue @@ -28,12 +28,10 @@ const childDataTypeOptions = getDataTypeOptions().filter( const dataSpecs = useVModel(props, 'modelValue', emits) as Ref; -/** 元素类型切到 struct 时,初始化 dataSpecsList 占位 */ +/** 元素类型切换时,清理旧子类型的结构体属性配置 */ function handleChange(val: any) { - if (val !== IoTDataSpecsDataTypeEnum.STRUCT) { - return; - } dataSpecs.value.dataSpecsList = []; + dataSpecs.value.childDataType = val; } diff --git a/apps/web-ele/src/views/iot/thingmodel/modules/input-output-param.vue b/apps/web-ele/src/views/iot/thingmodel/modules/input-output-param.vue index 63e67e5a0..555877f6d 100644 --- a/apps/web-ele/src/views/iot/thingmodel/modules/input-output-param.vue +++ b/apps/web-ele/src/views/iot/thingmodel/modules/input-output-param.vue @@ -14,13 +14,18 @@ import { ElForm, ElFormItem, ElInput, + ElMessage, } from 'element-plus'; import { ThingModelFormRules } from '#/api/iot/thingmodel'; import ThingModelProperty from './property.vue'; -const props = defineProps<{ direction: string; modelValue: any }>(); +const props = defineProps<{ + direction: string; + existingIdentifiers?: string[]; + modelValue: any; +}>(); const emits = defineEmits(['update:modelValue']); const thingModelParams = useVModel(props, 'modelValue', emits) as Ref; @@ -39,6 +44,13 @@ const [Modal, modalApi] = useVbenModal({ } // 组装表单 const data = formData.value; + if ( + data.identifier && + props.existingIdentifiers?.includes(data.identifier) + ) { + ElMessage.warning('输入参数和输出参数标识符不能重复'); + return; + } const item = { identifier: data.identifier, name: data.name, diff --git a/apps/web-ele/src/views/iot/thingmodel/modules/service.vue b/apps/web-ele/src/views/iot/thingmodel/modules/service.vue index 4dd5ec47a..15d261724 100644 --- a/apps/web-ele/src/views/iot/thingmodel/modules/service.vue +++ b/apps/web-ele/src/views/iot/thingmodel/modules/service.vue @@ -29,6 +29,17 @@ watch( (service.value.callType = IoTThingModelServiceCallTypeEnum.ASYNC.value), { immediate: true }, ); + +/** 提取参数标识符列表,用于输入 / 输出参数跨表去重 */ +function getParamIdentifiers(params?: any[]) { + const identifiers: string[] = []; + for (const item of params || []) { + if (item.identifier) { + identifiers.push(item.identifier); + } + } + return identifiers; +} diff --git a/apps/web-ele/src/views/mes/cal/team/components/cal-team-select-dialog.vue b/apps/web-ele/src/views/mes/cal/team/components/cal-team-select-dialog.vue new file mode 100644 index 000000000..d3c03f7e6 --- /dev/null +++ b/apps/web-ele/src/views/mes/cal/team/components/cal-team-select-dialog.vue @@ -0,0 +1,138 @@ + + + diff --git a/apps/web-ele/src/views/mes/cal/team/components/cal-team-select.vue b/apps/web-ele/src/views/mes/cal/team/components/cal-team-select.vue new file mode 100644 index 000000000..30a946224 --- /dev/null +++ b/apps/web-ele/src/views/mes/cal/team/components/cal-team-select.vue @@ -0,0 +1,81 @@ + + + diff --git a/apps/web-ele/src/views/mes/cal/team/components/index.ts b/apps/web-ele/src/views/mes/cal/team/components/index.ts new file mode 100644 index 000000000..092b24a19 --- /dev/null +++ b/apps/web-ele/src/views/mes/cal/team/components/index.ts @@ -0,0 +1,2 @@ +export { default as CalTeamSelectDialog } from './cal-team-select-dialog.vue'; +export { default as CalTeamSelect } from './cal-team-select.vue'; diff --git a/apps/web-ele/src/views/mes/cal/team/data.ts b/apps/web-ele/src/views/mes/cal/team/data.ts new file mode 100644 index 000000000..cf1521481 --- /dev/null +++ b/apps/web-ele/src/views/mes/cal/team/data.ts @@ -0,0 +1,186 @@ +import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; +import type { MesCalTeamApi } from '#/api/mes/cal/team'; + +import { h } from 'vue'; + +import { DICT_TYPE } from '@vben/constants'; +import { getDictOptions } from '@vben/hooks'; + +import { ElButton } from 'element-plus'; + +import { z } from '#/adapter/form'; +import { generateAutoCode } from '#/api/mes/md/autocode/record'; +import { MesAutoCodeRuleCode } from '#/views/mes/utils/constants'; + +/** 新增/修改班组的表单 */ +export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { + return [ + { + fieldName: 'id', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'code', + label: '班组编码', + component: 'Input', + componentProps: { + maxLength: 64, + placeholder: '请输入班组编码', + }, + rules: z.string().min(1, '班组编码不能为空').max(64), + suffix: () => + h( + ElButton, + { + type: 'default', + onClick: async () => { + try { + const code = await generateAutoCode(MesAutoCodeRuleCode.CAL_TEAM_CODE); + await formApi?.setFieldValue('code', code); + } catch (error) { + console.error(error); + } + }, + }, + { default: () => '生成' }, + ), + }, + { + fieldName: 'name', + label: '班组名称', + component: 'Input', + componentProps: { + maxLength: 100, + placeholder: '请输入班组名称', + }, + rules: z.string().min(1, '班组名称不能为空').max(100), + }, + { + fieldName: 'calendarType', + label: '班组类型', + component: 'RadioGroup', + componentProps: { + options: getDictOptions(DICT_TYPE.MES_CAL_CALENDAR_TYPE, 'number'), + }, + rules: 'selectRequired', + }, + { + fieldName: 'remark', + label: '备注', + component: 'Textarea', + formItemClass: 'col-span-3', + componentProps: { + maxLength: 250, + placeholder: '请输入备注', + rows: 3, + }, + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'code', + label: '班组编码', + component: 'Input', + componentProps: { + clearable: true, + placeholder: '请输入班组编码', + }, + }, + { + fieldName: 'name', + label: '班组名称', + component: 'Input', + componentProps: { + clearable: true, + placeholder: '请输入班组名称', + }, + }, + { + fieldName: 'calendarType', + label: '班组类型', + component: 'Select', + componentProps: { + clearable: true, + options: getDictOptions(DICT_TYPE.MES_CAL_CALENDAR_TYPE, 'number'), + placeholder: '请选择班组类型', + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'code', + title: '班组编码', + minWidth: 150, + slots: { + default: 'code', + }, + }, + { field: 'name', title: '班组名称', minWidth: 150 }, + { + field: 'calendarType', + title: '班组类型', + width: 140, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.MES_CAL_CALENDAR_TYPE }, + }, + }, + { field: 'remark', title: '备注', minWidth: 180 }, + { field: 'createTime', title: '创建时间', width: 180, formatter: 'formatDateTime' }, + { + title: '操作', + width: 180, + fixed: 'right', + slots: { + default: 'actions', + }, + }, + ]; +} + +/** 班组选择弹窗搜索表单 */ +export function useTeamSelectGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'code', + label: '班组编码', + component: 'Input', + componentProps: { + clearable: true, + placeholder: '请输入班组编码', + }, + }, + { + fieldName: 'name', + label: '班组名称', + component: 'Input', + componentProps: { + clearable: true, + placeholder: '请输入班组名称', + }, + }, + ]; +} + +/** 班组选择弹窗字段 */ +export function useTeamSelectGridColumns(): VxeTableGridOptions['columns'] { + return [ + { type: 'checkbox', width: 50 }, + { field: 'code', title: '班组编码', minWidth: 140 }, + { field: 'name', title: '班组名称', minWidth: 140 }, + { field: 'remark', title: '备注', minWidth: 160 }, + ]; +} diff --git a/apps/web-ele/src/views/mes/cal/team/index.vue b/apps/web-ele/src/views/mes/cal/team/index.vue new file mode 100644 index 000000000..700bc0b2b --- /dev/null +++ b/apps/web-ele/src/views/mes/cal/team/index.vue @@ -0,0 +1,145 @@ + + + diff --git a/apps/web-ele/src/views/mes/cal/team/modules/form.vue b/apps/web-ele/src/views/mes/cal/team/modules/form.vue new file mode 100644 index 000000000..618e10d1e --- /dev/null +++ b/apps/web-ele/src/views/mes/cal/team/modules/form.vue @@ -0,0 +1,112 @@ + + + diff --git a/apps/web-ele/src/views/mes/cal/team/modules/member-list.vue b/apps/web-ele/src/views/mes/cal/team/modules/member-list.vue new file mode 100644 index 000000000..8dc4f4200 --- /dev/null +++ b/apps/web-ele/src/views/mes/cal/team/modules/member-list.vue @@ -0,0 +1,192 @@ + + +