diff --git a/apps/web-antd/package.json b/apps/web-antd/package.json index 904f69a77..a3ae11913 100644 --- a/apps/web-antd/package.json +++ b/apps/web-antd/package.json @@ -60,6 +60,7 @@ "pinia": "catalog:", "steady-xml": "catalog:", "tinymce": "catalog:", + "tyme4ts": "catalog:", "video.js": "catalog:", "vue": "catalog:", "vue-dompurify-html": "catalog:", diff --git a/apps/web-antd/src/api/iot/rule/scene/index.ts b/apps/web-antd/src/api/iot/rule/scene/index.ts index acbfbac42..23549f1ad 100644 --- a/apps/web-antd/src/api/iot/rule/scene/index.ts +++ b/apps/web-antd/src/api/iot/rule/scene/index.ts @@ -24,7 +24,7 @@ export namespace RuleSceneApi { operator?: string; value?: any; cronExpression?: string; - conditionGroups?: TriggerCondition[][]; + conditionGroups?: TriggerCondition[][]; // 后端结构:List>;外层「或」、组内「且」 } /** 场景联动规则的触发条件 */ @@ -80,13 +80,6 @@ export function deleteSceneRule(id: number) { return requestClient.delete(`/iot/scene-rule/delete?id=${id}`); } -/** 批量删除场景联动规则 */ -export function deleteSceneRuleList(ids: number[]) { - return requestClient.delete('/iot/scene-rule/delete-list', { - params: { ids: ids.join(',') }, - }); -} - /** 更新场景联动规则状态 */ export function updateSceneRuleStatus(id: number, status: number) { return requestClient.put(`/iot/scene-rule/update-status`, { diff --git a/apps/web-antd/src/api/iot/thingmodel/index.ts b/apps/web-antd/src/api/iot/thingmodel/index.ts index 2fb80be2e..88665ebc9 100644 --- a/apps/web-antd/src/api/iot/thingmodel/index.ts +++ b/apps/web-antd/src/api/iot/thingmodel/index.ts @@ -148,8 +148,8 @@ export const ThingModelFormRules: Record = { identifier: [ { required: true, message: '标识符不能为空', trigger: 'blur' }, { - pattern: /^\w{1,50}$/, - message: '支持大小写字母、数字和下划线,不超过 50 个字符', + pattern: /^[a-zA-Z][a-zA-Z0-9_]{0,31}$/, + message: '支持大小写字母、数字和下划线,必须以字母开头,不超过 32 个字符', trigger: 'blur', }, { diff --git a/apps/web-antd/src/api/mes/md/autocode/rule/index.ts b/apps/web-antd/src/api/mes/md/autocode/rule/index.ts index d33e6e0fc..6955e7304 100644 --- a/apps/web-antd/src/api/mes/md/autocode/rule/index.ts +++ b/apps/web-antd/src/api/mes/md/autocode/rule/index.ts @@ -51,7 +51,5 @@ export function deleteAutoCodeRule(id: number) { /** 导出编码规则 */ export function exportAutoCodeRule(params: PageParam) { - return requestClient.download('/mes/md/auto-code-rule/export-excel', { - params, - }); + return requestClient.download('/mes/md/auto-code-rule/export-excel', { params }); } diff --git a/apps/web-antd/src/api/mes/md/item/index.ts b/apps/web-antd/src/api/mes/md/item/index.ts index f2a23c0e6..9d3b107f8 100644 --- a/apps/web-antd/src/api/mes/md/item/index.ts +++ b/apps/web-antd/src/api/mes/md/item/index.ts @@ -34,9 +34,7 @@ export namespace MesMdItemApi { /** 查询物料产品分页 */ export function getItemPage(params: PageParam) { - return requestClient.get>('/mes/md/item/page', { - params, - }); + return requestClient.get>('/mes/md/item/page', { params }); } /** 查询物料产品详情 */ diff --git a/apps/web-antd/src/api/mes/pro/process/index.ts b/apps/web-antd/src/api/mes/pro/process/index.ts index 5a3893b18..a4ff1ea64 100644 --- a/apps/web-antd/src/api/mes/pro/process/index.ts +++ b/apps/web-antd/src/api/mes/pro/process/index.ts @@ -5,7 +5,7 @@ import { requestClient } from '#/api/request'; export namespace MesProProcessApi { /** MES 生产工序 */ export interface Process { - id: number; + id?: number; code?: string; name?: string; attention?: string; @@ -36,3 +36,23 @@ export function getProcess(id: number) { `/mes/pro/process/get?id=${id}`, ); } + +/** 新增生产工序 */ +export function createProcess(data: MesProProcessApi.Process) { + return requestClient.post('/mes/pro/process/create', data); +} + +/** 修改生产工序 */ +export function updateProcess(data: MesProProcessApi.Process) { + return requestClient.put('/mes/pro/process/update', data); +} + +/** 删除生产工序 */ +export function deleteProcess(id: number) { + return requestClient.delete(`/mes/pro/process/delete?id=${id}`); +} + +/** 导出生产工序 Excel */ +export function exportProcess(params: any) { + return requestClient.download('/mes/pro/process/export-excel', { params }); +} diff --git a/apps/web-antd/src/assets/imgs/iot/device.png b/apps/web-antd/src/assets/imgs/iot/device.png new file mode 100644 index 000000000..79339cdf4 Binary files /dev/null and b/apps/web-antd/src/assets/imgs/iot/device.png differ diff --git a/apps/web-antd/src/assets/svgs/iot/cube.svg b/apps/web-antd/src/assets/svgs/iot/cube.svg new file mode 100644 index 000000000..200ac1b1c --- /dev/null +++ b/apps/web-antd/src/assets/svgs/iot/cube.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web-antd/src/components/shortcut-date-range-picker/shortcut-date-range-picker.vue b/apps/web-antd/src/components/shortcut-date-range-picker/shortcut-date-range-picker.vue index 5a3ce0526..3dc3f15eb 100644 --- a/apps/web-antd/src/components/shortcut-date-range-picker/shortcut-date-range-picker.vue +++ b/apps/web-antd/src/components/shortcut-date-range-picker/shortcut-date-range-picker.vue @@ -4,6 +4,7 @@ import type { Dayjs } from 'dayjs'; import { onMounted, ref } from 'vue'; import { DatePicker, Radio, RadioGroup } from 'ant-design-vue'; +import dayjs from 'dayjs'; import { getRangePickerDefaultProps } from '#/utils/rangePickerProps'; @@ -19,8 +20,20 @@ const times = ref<[Dayjs, Dayjs]>(); // 日期范围 const rangePickerProps = getRangePickerDefaultProps(); const timeRangeOptions = [ rangePickerProps.presets[3]!, // 昨天 - rangePickerProps.presets[1]!, // 最近 7 天 - rangePickerProps.presets[2]!, // 最近 30 天 + { + label: rangePickerProps.presets[1]!.label, + value: [ + dayjs().subtract(7, 'day').startOf('day'), + dayjs().subtract(1, 'day').endOf('day'), + ], + }, + { + label: rangePickerProps.presets[2]!.label, + value: [ + dayjs().subtract(30, 'day').startOf('day'), + dayjs().subtract(1, 'day').endOf('day'), + ], + }, ]; const timeRangeType = ref(timeRangeOptions[1]!.label); // 默认选中第一个选项 diff --git a/apps/web-antd/src/components/upload/file-upload.vue b/apps/web-antd/src/components/upload/file-upload.vue index 3838642ea..85afc3b9d 100644 --- a/apps/web-antd/src/components/upload/file-upload.vue +++ b/apps/web-antd/src/components/upload/file-upload.vue @@ -32,6 +32,7 @@ const props = withDefaults(defineProps(), { multiple: false, api: undefined, resultField: '', + returnText: false, showDescription: false, }); const emit = defineEmits([ @@ -147,9 +148,6 @@ function handleUploadError(error: any) { * @returns 是否允许上传 */ async function beforeUpload(file: File) { - const fileContent = await file.text(); - emit('returnText', fileContent); - // 检查文件数量限制 if (fileList.value!.length >= props.maxNumber) { message.error($t('ui.upload.maxNumber', [props.maxNumber])); @@ -176,6 +174,10 @@ async function beforeUpload(file: File) { // 只有在验证通过后才增加计数器 uploadNumber.value++; + if (props.returnText) { + const fileContent = await file.text(); + emit('returnText', fileContent); + } return true; } diff --git a/apps/web-antd/src/components/upload/input-upload.vue b/apps/web-antd/src/components/upload/input-upload.vue index 90b2f4f7c..88f00f1cf 100644 --- a/apps/web-antd/src/components/upload/input-upload.vue +++ b/apps/web-antd/src/components/upload/input-upload.vue @@ -58,6 +58,7 @@ const textareaProps = computed(() => { const fileUploadProps = computed(() => { return { ...props.fileUploadProps, + returnText: true, }; }); diff --git a/apps/web-antd/src/components/upload/typing.ts b/apps/web-antd/src/components/upload/typing.ts index f3c16bc4d..990aa7d8a 100644 --- a/apps/web-antd/src/components/upload/typing.ts +++ b/apps/web-antd/src/components/upload/typing.ts @@ -27,6 +27,7 @@ export interface FileUploadProps { maxSize?: number; // 文件最大多少MB multiple?: boolean; // 是否支持多选 resultField?: string; // support xxx.xxx.xx + returnText?: boolean; // 是否返回文件文本内容 showDescription?: boolean; // 是否显示下面的描述 value?: string | string[]; } diff --git a/apps/web-antd/src/router/routes/modules/iot.ts b/apps/web-antd/src/router/routes/modules/iot.ts index 04cf97d35..a1d2203b9 100644 --- a/apps/web-antd/src/router/routes/modules/iot.ts +++ b/apps/web-antd/src/router/routes/modules/iot.ts @@ -12,7 +12,7 @@ const routes: RouteRecordRaw[] = [ }, children: [ { - path: 'product/detail/:id', + path: 'product/product/detail/:id', name: 'IoTProductDetail', meta: { title: '产品详情', @@ -30,11 +30,11 @@ const routes: RouteRecordRaw[] = [ component: () => import('#/views/iot/device/device/detail/index.vue'), }, { - path: 'ota/firmware/detail/:id', + path: 'ota/operation/firmware/detail/:id', name: 'IoTOtaFirmwareDetail', meta: { title: '固件详情', - activePath: '/iot/ota', + activePath: '/iot/operation/ota/firmware', }, component: () => import('#/views/iot/ota/firmware/detail/index.vue'), }, diff --git a/apps/web-antd/src/views/iot/alert/record/data.ts b/apps/web-antd/src/views/iot/alert/record/data.ts index 0d5e6deb1..2bc0af490 100644 --- a/apps/web-antd/src/views/iot/alert/record/data.ts +++ b/apps/web-antd/src/views/iot/alert/record/data.ts @@ -62,13 +62,27 @@ export function useGridFormSchema(): VbenFormSchema[] { label: '设备', component: 'ApiSelect', componentProps: { - api: getSimpleDeviceList, + api: (params?: { productId?: number }) => + getSimpleDeviceList(undefined, params?.productId), labelField: 'deviceName', valueField: 'id', placeholder: '请选择设备', allowClear: true, showSearch: true, }, + dependencies: { + triggerFields: ['productId'], + componentProps: (values) => { + return { + params: { productId: values.productId }, + }; + }, + trigger: (values, formApi) => { + if (values.deviceId !== undefined) { + void formApi.setFieldValue('deviceId', undefined); + } + }, + }, }, { fieldName: 'processStatus', diff --git a/apps/web-antd/src/views/iot/alert/record/index.vue b/apps/web-antd/src/views/iot/alert/record/index.vue index d3d8f9d17..b5c59a7a8 100644 --- a/apps/web-antd/src/views/iot/alert/record/index.vue +++ b/apps/web-antd/src/views/iot/alert/record/index.vue @@ -45,10 +45,6 @@ function handleProcess(row: AlertRecordApi.AlertRecord) { }), ]), async onOk() { - if (!processRemark.value) { - message.warning('请输入处理原因'); - throw new Error('请输入处理原因'); - } const hideLoading = message.loading({ content: '正在处理...', duration: 0, diff --git a/apps/web-antd/src/views/iot/device/device/data.ts b/apps/web-antd/src/views/iot/device/device/data.ts index 3eb5ebb1b..ea7f0fc9b 100644 --- a/apps/web-antd/src/views/iot/device/device/data.ts +++ b/apps/web-antd/src/views/iot/device/device/data.ts @@ -79,10 +79,18 @@ export function useAdvancedFormSchema(): VbenFormSchema[] { }, rules: z .string() - .min(4, '备注名称长度限制为 4~64 个字符') - .max(64, '备注名称长度限制为 4~64 个字符') - .regex( - /^[\u4E00-\u9FA5\u3040-\u30FF\w]+$/, + .refine( + (value) => { + const length = value.replaceAll( + /[\u4E00-\u9FA5\u3040-\u30FF]/g, + 'aa', + ).length; + return length >= 4 && length <= 64; + }, + '备注名称长度限制为 4~64 个字符,中文及日文算 2 个字符', + ) + .refine( + (value) => /^[\u4E00-\u9FA5\u3040-\u30FF\w]+$/.test(value), '备注名称只能包含中文、英文字母、日文、数字和下划线(_)', ) .optional() @@ -276,6 +284,7 @@ export function useGridColumns(): VxeTableGridOptions['colu field: 'deviceName', title: 'DeviceName', minWidth: 150, + slots: { default: 'deviceName' }, }, { field: 'nickname', diff --git a/apps/web-antd/src/views/iot/device/device/detail/index.vue b/apps/web-antd/src/views/iot/device/device/detail/index.vue index 26457759e..4d7c29889 100644 --- a/apps/web-antd/src/views/iot/device/device/detail/index.vue +++ b/apps/web-antd/src/views/iot/device/device/detail/index.vue @@ -97,7 +97,7 @@ onMounted(async () => { @@ -127,7 +127,7 @@ onMounted(async () => { { @@ -151,7 +151,7 @@ onMounted(async () => { tab="Modbus 配置" > ( ); const mapDialogRef = ref>(); -/** 是否有位置信息 */ +/** 是否有位置信息(合法经纬度 0 不应视为空) */ const hasLocation = computed(() => { - return !!(props.device.longitude && props.device.latitude); + return props.device.longitude != null && props.device.latitude != null; }); /** 打开地图弹窗 */ diff --git a/apps/web-antd/src/views/iot/device/device/detail/modules/message.vue b/apps/web-antd/src/views/iot/device/device/detail/modules/message.vue index 965d5eb24..16efec5cf 100644 --- a/apps/web-antd/src/views/iot/device/device/detail/modules/message.vue +++ b/apps/web-antd/src/views/iot/device/device/detail/modules/message.vue @@ -31,6 +31,7 @@ const queryParams = reactive({ const autoRefresh = ref(false); // 自动刷新开关 let autoRefreshTimer: any = null; // 自动刷新定时器 +let refreshTimer: ReturnType | undefined; // 延迟刷新定时器 /** 消息方法选项 */ const methodOptions = computed(() => { @@ -150,6 +151,10 @@ onBeforeUnmount(() => { clearInterval(autoRefreshTimer); autoRefreshTimer = null; } + if (refreshTimer) { + clearTimeout(refreshTimer); + refreshTimer = undefined; + } }); /** 初始化 */ @@ -161,9 +166,14 @@ onMounted(() => { /** 刷新消息列表 */ function refresh(delay = 0) { + if (refreshTimer) { + clearTimeout(refreshTimer); + refreshTimer = undefined; + } if (delay > 0) { - setTimeout(() => { + refreshTimer = setTimeout(() => { gridApi.query(); + refreshTimer = undefined; }, delay); } else { gridApi.query(); diff --git a/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config-form.vue b/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config-form.vue index 669b01fbf..fe3a7afdb 100644 --- a/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config-form.vue +++ b/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config-form.vue @@ -58,11 +58,13 @@ const [Form, formApi] = useVbenForm({ componentProps: { placeholder: '请输入 Modbus 服务器 IP 地址', }, + // Client 模式专有字段:必填;Server 模式不显示也不校验 dependencies: { triggerFields: [''], - show: () => isClient.value, // Client 模式专有字段:IP 地址 + show: () => isClient.value, + rules: () => + isClient.value ? z.string().min(1, '请输入 IP 地址') : null, }, - rules: z.string().min(1, '请输入 IP 地址').optional(), }, { fieldName: 'port', @@ -76,9 +78,12 @@ const [Form, formApi] = useVbenForm({ }, dependencies: { triggerFields: [''], - show: () => isClient.value, // Client 模式专有字段:端口 + show: () => isClient.value, + rules: () => + isClient.value + ? z.number({ message: '请输入端口' }).min(1).max(65_535) + : null, }, - rules: z.number().min(1).max(65_535).optional(), defaultValue: 502, }, { @@ -106,9 +111,12 @@ const [Form, formApi] = useVbenForm({ }, dependencies: { triggerFields: [''], - show: () => isClient.value, // Client 模式专有字段:连接超时 + show: () => isClient.value, + rules: () => + isClient.value + ? z.number({ message: '请输入连接超时时间' }).min(1000) + : null, }, - rules: z.number().min(1000).optional(), defaultValue: 3000, }, { @@ -123,9 +131,12 @@ const [Form, formApi] = useVbenForm({ }, dependencies: { triggerFields: [''], - show: () => isClient.value, // Client 模式专有字段:重试间隔 + show: () => isClient.value, + rules: () => + isClient.value + ? z.number({ message: '请输入重试间隔' }).min(1000) + : null, }, - rules: z.number().min(1000).optional(), defaultValue: 10_000, }, { diff --git a/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config.vue b/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config.vue index 672915a01..ce4d907d8 100644 --- a/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config.vue +++ b/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-config.vue @@ -14,7 +14,7 @@ import { computed, h, onMounted, ref } from 'vue'; import { confirm, useVbenModal } from '@vben/common-ui'; import { DICT_TYPE, ModbusFunctionCodeOptions } from '@vben/constants'; -import { Button, message } from 'ant-design-vue'; +import { message } from 'ant-design-vue'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { getModbusConfig } from '#/api/iot/device/modbus/config'; @@ -307,7 +307,16 @@ onMounted(async () => { @@ -320,6 +329,7 @@ onMounted(async () => { label: '新增点位', type: 'primary', icon: ACTION_ICON.ADD, + auth: ['iot:device:create'], onClick: handleAddPoint, }, ]" @@ -331,12 +341,14 @@ onMounted(async () => { { label: '编辑', type: 'link', + auth: ['iot:device:update'], onClick: () => handleEditPoint(row), }, { label: '删除', type: 'link', danger: true, + auth: ['iot:device:delete'], popConfirm: { title: `确定要删除点位【${row.name}】吗?`, confirm: () => handleDeletePoint(row), diff --git a/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-point-form.vue b/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-point-form.vue index cce6c75f1..6f91a6973 100644 --- a/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-point-form.vue +++ b/apps/web-antd/src/views/iot/device/device/detail/modules/modbus-point-form.vue @@ -246,10 +246,20 @@ const [Form, formApi] = useVbenForm({ if (option && option.registerCount > 0) { await formApi.setFieldValue('registerCount', option.registerCount); } - // 重置字节序为第一个选项 + // 字节序:仅在当前值为空或不属于新 rawDataType 时才重置为第一个选项; + // 编辑回填时 setValues 会触发本回调,无条件重置会覆盖已保存字节序 const byteOrderOptions = getByteOrderOptions(rawDataType); if (byteOrderOptions.length > 0) { - await formApi.setFieldValue('byteOrder', byteOrderOptions[0]!.value); + const currentByteOrder = values.byteOrder; + const isCurrentValid = + !!currentByteOrder && + byteOrderOptions.some((opt) => opt.value === currentByteOrder); + if (!isCurrentValid) { + await formApi.setFieldValue( + 'byteOrder', + byteOrderOptions[0]!.value, + ); + } } } } diff --git a/apps/web-antd/src/views/iot/device/device/detail/modules/simulator.vue b/apps/web-antd/src/views/iot/device/device/detail/modules/simulator.vue index 3af6c6216..8a58ce5f7 100644 --- a/apps/web-antd/src/views/iot/device/device/detail/modules/simulator.vue +++ b/apps/web-antd/src/views/iot/device/device/detail/modules/simulator.vue @@ -6,11 +6,12 @@ import type { IotDeviceApi } from '#/api/iot/device/device'; import type { IotProductApi } from '#/api/iot/product/product'; import type { ThingModelApi } from '#/api/iot/thingmodel'; -import { computed, ref } from 'vue'; +import { computed, ref, watch } from 'vue'; import { ContentWrap } from '@vben/common-ui'; import { DeviceStateEnum, + IoTDataSpecsDataTypeEnum, IotDeviceMessageMethodEnum, IoTThingModelTypeEnum, } from '@vben/constants'; @@ -21,8 +22,10 @@ import { Card, Col, Input, + InputNumber, message, Row, + Select, Table, Tabs, Textarea, @@ -51,7 +54,7 @@ const debugCollapsed = ref(false); // 指令调试区域折叠状态 const messageCollapsed = ref(false); // 设备消息区域折叠状态 // 表单数据:存储用户输入的模拟值 -const formData = ref>({}); +const formData = ref>({}); // 根据类型过滤物模型数据 const getFilteredThingModelList = (type: number) => { @@ -184,21 +187,101 @@ const serviceColumns = [ // 获取表单值 function getFormValue(identifier: string) { - return formData.value[identifier] || ''; + return formData.value[identifier] ?? ''; } // 设置表单值 -function setFormValue(identifier: string, value: string) { +function setFormValue(identifier: string, value: any) { formData.value[identifier] = value; } +/** 获取属性数据类型 */ +function getPropertyDataType(row: ThingModelApi.ThingModel) { + return row.property?.dataType; +} + +/** 判断属性是否为数值类型 */ +function isNumberProperty(row: ThingModelApi.ThingModel) { + return [ + IoTDataSpecsDataTypeEnum.DOUBLE, + IoTDataSpecsDataTypeEnum.FLOAT, + IoTDataSpecsDataTypeEnum.INT, + ].includes(getPropertyDataType(row) as any); +} + +/** 判断属性是否使用下拉选项 */ +function isSelectProperty(row: ThingModelApi.ThingModel) { + return [ + IoTDataSpecsDataTypeEnum.BOOL, + IoTDataSpecsDataTypeEnum.ENUM, + ].includes(getPropertyDataType(row) as any); +} + +/** 获取属性选项 */ +function getPropertyOptions(row: ThingModelApi.ThingModel) { + const list = row.property?.dataSpecsList || []; + if (list.length > 0) { + return list.map((item: any) => ({ + label: item.name || item.label || String(item.value), + value: String(item.value), + })); + } + if (getPropertyDataType(row) === IoTDataSpecsDataTypeEnum.BOOL) { + return [ + { label: '真 (true)', value: 'true' }, + { label: '假 (false)', value: 'false' }, + ]; + } + return []; +} + +/** 获取物模型选项原始值 */ +function getMatchedPropertyOption(row: ThingModelApi.ThingModel, value: any) { + return row.property?.dataSpecsList?.find( + (item: any) => String(item.value) === String(value), + ); +} + +/** 按物模型数据类型转换属性值 */ +function normalizePropertyValue(row: ThingModelApi.ThingModel, value: any) { + if (value === undefined || value === null || value === '') { + return undefined; + } + const dataType = getPropertyDataType(row); + if (isNumberProperty(row)) { + return Number(value); + } + if ( + [IoTDataSpecsDataTypeEnum.BOOL, IoTDataSpecsDataTypeEnum.ENUM].includes( + dataType as any, + ) + ) { + const option = getMatchedPropertyOption(row, value); + if (option) { + return option.value; + } + } + if (dataType === IoTDataSpecsDataTypeEnum.BOOL) { + if (String(value) === 'true') { + return true; + } + if (String(value) === 'false') { + return false; + } + } + return value; +} + // 属性上报 async function handlePropertyPost() { try { const params: Record = {}; propertyList.value.forEach((item) => { - const value = formData.value[item.identifier!]; - if (value) { + const value = normalizePropertyValue( + item, + formData.value[item.identifier!], + ); + if (value !== undefined) { params[item.identifier!] = value; } }); @@ -229,13 +312,15 @@ async function handleEventPost(row: ThingModelApi.ThingModel) { const valueStr = formData.value[row.identifier!]; let eventValue: any; - if (valueStr) { - try { - eventValue = JSON.parse(valueStr); - } catch { - message.error('事件参数格式错误,请输入有效的JSON格式'); - return; - } + if (valueStr === undefined || valueStr === null || valueStr === '') { + message.warning('请输入事件参数'); + return; + } + try { + eventValue = JSON.parse(valueStr); + } catch { + message.error('事件参数格式错误,请输入有效的JSON格式'); + return; } // 与后端 IotDeviceEventPostReqDTO 对齐 :{ identifier, value, time } @@ -281,8 +366,11 @@ async function handlePropertySet() { try { const params: Record = {}; propertyList.value.forEach((item) => { - const value = formData.value[item.identifier!]; - if (value) { + const value = normalizePropertyValue( + item, + formData.value[item.identifier!], + ); + if (value !== undefined) { params[item.identifier!] = value; } }); @@ -313,13 +401,23 @@ async function handleServiceInvoke(row: ThingModelApi.ThingModel) { const valueStr = formData.value[row.identifier!]; let inputParams: any = {}; - if (valueStr) { - try { - inputParams = JSON.parse(valueStr); - } catch { - message.error('服务参数格式错误,请输入有效的JSON格式'); - return; - } + if (valueStr === undefined || valueStr === null || valueStr === '') { + message.warning('请输入服务参数'); + return; + } + try { + inputParams = JSON.parse(valueStr); + } catch { + message.error('服务参数格式错误,请输入有效的JSON格式'); + return; + } + if ( + typeof inputParams !== 'object' || + inputParams === null || + Array.isArray(inputParams) + ) { + message.error('服务参数必须是 JSON 对象'); + return; } // 与后端 IotDeviceServiceInvokeReqDTO 对齐 :{ identifier, inputParams } @@ -340,6 +438,11 @@ async function handleServiceInvoke(row: ThingModelApi.ThingModel) { console.error(error); } } + +/** 切换调试方法时清空输入,避免不同方法之间串台提交 */ +watch([activeTab, upstreamTab, downstreamTab], () => { + formData.value = {}; +});