fix: 修复 IoT 迁移页面多处交互与契约问题
- 修复告警记录产品/设备筛选联动 - 清理设备详情延迟刷新 timer,避免卸载后触发查询 - 优化 OTA 固件编辑态只读展示与任务列表分页重置 - 修复场景联动值输入回显和布尔值类型 - 修复设备模拟器输入串台、数值类型提交和服务参数校验 - 更新 IoT bug 归档与迁移说明pull/348/head
parent
eb0f2a5ff2
commit
48547bc53b
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ const queryParams = reactive({
|
|||
|
||||
const autoRefresh = ref(false); // 自动刷新开关
|
||||
let autoRefreshTimer: any = null; // 自动刷新定时器
|
||||
let refreshTimer: ReturnType<typeof setTimeout> | 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();
|
||||
|
|
|
|||
|
|
@ -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<Record<string, string>>({});
|
||||
const formData = ref<Record<string, any>>({});
|
||||
|
||||
// 根据类型过滤物模型数据
|
||||
const getFilteredThingModelList = (type: number) => {
|
||||
|
|
@ -184,21 +187,75 @@ 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 normalizePropertyValue(row: ThingModelApi.ThingModel, value: any) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return undefined;
|
||||
}
|
||||
if (isNumberProperty(row)) {
|
||||
return Number(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// 属性上报
|
||||
async function handlePropertyPost() {
|
||||
try {
|
||||
const params: Record<string, any> = {};
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
|
@ -281,8 +338,11 @@ async function handlePropertySet() {
|
|||
try {
|
||||
const params: Record<string, any> = {};
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
|
@ -320,6 +380,14 @@ async function handleServiceInvoke(row: ThingModelApi.ThingModel) {
|
|||
message.error('服务参数格式错误,请输入有效的JSON格式');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
typeof inputParams !== 'object' ||
|
||||
inputParams === null ||
|
||||
Array.isArray(inputParams)
|
||||
) {
|
||||
message.error('服务参数必须是 JSON 对象');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 与后端 IotDeviceServiceInvokeReqDTO 对齐 :{ identifier, inputParams }
|
||||
|
|
@ -340,6 +408,11 @@ async function handleServiceInvoke(row: ThingModelApi.ThingModel) {
|
|||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 切换调试方法时清空输入,避免不同方法之间串台提交 */
|
||||
watch([activeTab, upstreamTab, downstreamTab], () => {
|
||||
formData.value = {};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -392,7 +465,29 @@ async function handleServiceInvoke(row: ThingModelApi.ThingModel) {
|
|||
<DataDefinition :data="record" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<InputNumber
|
||||
v-if="isNumberProperty(record)"
|
||||
:value="getFormValue(record.identifier)"
|
||||
placeholder="输入值"
|
||||
size="small"
|
||||
class="w-full"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
"
|
||||
/>
|
||||
<Select
|
||||
v-else-if="isSelectProperty(record)"
|
||||
:value="getFormValue(record.identifier)"
|
||||
:options="getPropertyOptions(record)"
|
||||
placeholder="请选择值"
|
||||
size="small"
|
||||
class="w-full"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
"
|
||||
/>
|
||||
<Input
|
||||
v-else
|
||||
:value="getFormValue(record.identifier)"
|
||||
placeholder="输入值"
|
||||
size="small"
|
||||
|
|
@ -514,7 +609,29 @@ async function handleServiceInvoke(row: ThingModelApi.ThingModel) {
|
|||
<DataDefinition :data="record" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'value'">
|
||||
<InputNumber
|
||||
v-if="isNumberProperty(record)"
|
||||
:value="getFormValue(record.identifier)"
|
||||
placeholder="输入值"
|
||||
size="small"
|
||||
class="w-full"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
"
|
||||
/>
|
||||
<Select
|
||||
v-else-if="isSelectProperty(record)"
|
||||
:value="getFormValue(record.identifier)"
|
||||
:options="getPropertyOptions(record)"
|
||||
placeholder="请选择值"
|
||||
size="small"
|
||||
class="w-full"
|
||||
@update:value="
|
||||
setFormValue(record.identifier, $event)
|
||||
"
|
||||
/>
|
||||
<Input
|
||||
v-else
|
||||
:value="getFormValue(record.identifier)"
|
||||
placeholder="输入值"
|
||||
size="small"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||
|
||||
import { computed, onMounted, reactive, watch } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, watch } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import {
|
||||
|
|
@ -29,6 +29,7 @@ const queryParams = reactive({
|
|||
identifier: '',
|
||||
times: undefined as [string, string] | undefined,
|
||||
});
|
||||
let refreshTimer: ReturnType<typeof setTimeout> | undefined; // 延迟刷新定时器
|
||||
|
||||
/** 事件类型的物模型数据 */
|
||||
const eventThingModels = computed(() => {
|
||||
|
|
@ -153,8 +154,15 @@ function parseParams(params: string) {
|
|||
|
||||
/** 刷新列表 */
|
||||
function refresh(delay = 0) {
|
||||
if (refreshTimer) {
|
||||
clearTimeout(refreshTimer);
|
||||
refreshTimer = undefined;
|
||||
}
|
||||
if (delay > 0) {
|
||||
setTimeout(() => gridApi.query(), delay);
|
||||
refreshTimer = setTimeout(() => {
|
||||
gridApi.query();
|
||||
refreshTimer = undefined;
|
||||
}, delay);
|
||||
} else {
|
||||
gridApi.query();
|
||||
}
|
||||
|
|
@ -177,6 +185,14 @@ onMounted(() => {
|
|||
}
|
||||
});
|
||||
|
||||
/** 组件卸载时清除延迟刷新定时器 */
|
||||
onBeforeUnmount(() => {
|
||||
if (refreshTimer) {
|
||||
clearTimeout(refreshTimer);
|
||||
refreshTimer = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
/** 暴露方法给父组件 */
|
||||
defineExpose({
|
||||
refresh,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||
|
||||
import { computed, onMounted, reactive, watch } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, watch } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import {
|
||||
|
|
@ -29,6 +29,7 @@ const queryParams = reactive({
|
|||
identifier: '',
|
||||
times: undefined as [string, string] | undefined,
|
||||
});
|
||||
let refreshTimer: ReturnType<typeof setTimeout> | undefined; // 延迟刷新定时器
|
||||
|
||||
/** 服务类型的物模型数据 */
|
||||
const serviceThingModels = computed(() => {
|
||||
|
|
@ -167,8 +168,15 @@ function parseParams(params: string) {
|
|||
|
||||
/** 刷新列表 */
|
||||
function refresh(delay = 0) {
|
||||
if (refreshTimer) {
|
||||
clearTimeout(refreshTimer);
|
||||
refreshTimer = undefined;
|
||||
}
|
||||
if (delay > 0) {
|
||||
setTimeout(() => gridApi.query(), delay);
|
||||
refreshTimer = setTimeout(() => {
|
||||
gridApi.query();
|
||||
refreshTimer = undefined;
|
||||
}, delay);
|
||||
} else {
|
||||
gridApi.query();
|
||||
}
|
||||
|
|
@ -191,6 +199,14 @@ onMounted(() => {
|
|||
}
|
||||
});
|
||||
|
||||
/** 组件卸载时清除延迟刷新定时器 */
|
||||
onBeforeUnmount(() => {
|
||||
if (refreshTimer) {
|
||||
clearTimeout(refreshTimer);
|
||||
refreshTimer = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
/** 暴露方法给父组件 */
|
||||
defineExpose({
|
||||
refresh,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,11 @@ export function getProductName(productId?: number): string {
|
|||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{ field: 'name', label: '固件名称' },
|
||||
{ field: 'productName', label: '所属产品' },
|
||||
{
|
||||
field: 'productName',
|
||||
label: '所属产品',
|
||||
render: (val) => val || '-',
|
||||
},
|
||||
{ field: 'version', label: '固件版本' },
|
||||
{
|
||||
field: 'createTime',
|
||||
|
|
@ -67,10 +71,12 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
valueField: 'id',
|
||||
placeholder: '请选择产品',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
show: (values) => !values.id,
|
||||
componentProps: (values) => ({
|
||||
disabled: !!values.id,
|
||||
}),
|
||||
rules: (values) => (values.id ? null : 'required'),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -80,10 +86,12 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
componentProps: {
|
||||
placeholder: '请输入版本号',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
show: (values) => !values.id,
|
||||
componentProps: (values) => ({
|
||||
disabled: !!values.id,
|
||||
}),
|
||||
rules: (values) => (values.id ? null : 'required'),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -105,10 +113,12 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
maxSize: 50,
|
||||
helpText: '支持上传 .bin、.zip、.pdf 格式的固件文件,最大 50MB',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
show: (values) => !values.id,
|
||||
componentProps: (values) => ({
|
||||
disabled: !!values.id,
|
||||
}),
|
||||
rules: (values) => (values.id ? null : 'required'),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ async function handleRefresh() {
|
|||
|
||||
/** 按任务名称搜索 */
|
||||
async function handleSearch() {
|
||||
await gridApi.query();
|
||||
await gridApi.reload();
|
||||
}
|
||||
|
||||
/** 新增任务 */
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ const statusTabs = computed(() => {
|
|||
/** 切换标签 */
|
||||
async function handleTabChange(tabKey: number | string) {
|
||||
activeTab.value = String(tabKey);
|
||||
await gridApi.query();
|
||||
await gridApi.reload();
|
||||
}
|
||||
|
||||
/** 取消单条记录的升级 */
|
||||
|
|
@ -89,7 +89,7 @@ watch(
|
|||
async (val) => {
|
||||
if (val) {
|
||||
activeTab.value = '';
|
||||
await gridApi.query();
|
||||
await gridApi.reload();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -137,10 +137,46 @@ function handleNumberChange(value: any) {
|
|||
localValue.value = value == null ? '' : String(value);
|
||||
}
|
||||
|
||||
/** 根据外部值同步内部输入态 */
|
||||
function syncInternalValue(value = '') {
|
||||
const normalized = value;
|
||||
if (
|
||||
props.operator ===
|
||||
IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN.value
|
||||
) {
|
||||
const [start = '', end = ''] = normalized.split(',');
|
||||
rangeStart.value = start;
|
||||
rangeEnd.value = end;
|
||||
numberValue.value = undefined;
|
||||
dateValue.value = '';
|
||||
return;
|
||||
}
|
||||
rangeStart.value = '';
|
||||
rangeEnd.value = '';
|
||||
if (props.propertyType === IoTDataSpecsDataTypeEnum.DATE) {
|
||||
dateValue.value = normalized;
|
||||
numberValue.value = undefined;
|
||||
return;
|
||||
}
|
||||
if (isNumericType()) {
|
||||
const parsed = Number(normalized);
|
||||
numberValue.value =
|
||||
normalized === '' || Number.isNaN(parsed) ? undefined : parsed;
|
||||
dateValue.value = '';
|
||||
return;
|
||||
}
|
||||
numberValue.value = undefined;
|
||||
dateValue.value = '';
|
||||
}
|
||||
|
||||
/** 监听操作符变化 */
|
||||
watch(
|
||||
() => props.operator,
|
||||
() => {
|
||||
(_operator, oldOperator) => {
|
||||
if (oldOperator === undefined) {
|
||||
syncInternalValue(props.modelValue);
|
||||
return;
|
||||
}
|
||||
localValue.value = '';
|
||||
rangeStart.value = '';
|
||||
rangeEnd.value = '';
|
||||
|
|
@ -148,6 +184,12 @@ watch(
|
|||
numberValue.value = undefined;
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.propertyType] as const,
|
||||
([value]) => syncInternalValue(value),
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -159,8 +201,8 @@ watch(
|
|||
placeholder="请选择布尔值"
|
||||
class="w-full!"
|
||||
>
|
||||
<Select.Option :value="true">真 (true)</Select.Option>
|
||||
<Select.Option :value="false">假 (false)</Select.Option>
|
||||
<Select.Option value="true">真 (true)</Select.Option>
|
||||
<Select.Option value="false">假 (false)</Select.Option>
|
||||
</Select>
|
||||
|
||||
<!-- 枚举值选择 -->
|
||||
|
|
|
|||
|
|
@ -33,10 +33,7 @@ async function getList() {
|
|||
function handleChange(value: SelectValue) {
|
||||
const toolId = typeof value === 'number' ? value : undefined;
|
||||
emit('update:modelValue', toolId);
|
||||
emit(
|
||||
'change',
|
||||
list.value.find((item) => item.id === toolId),
|
||||
);
|
||||
emit('change', list.value.find((item) => item.id === toolId));
|
||||
}
|
||||
|
||||
onMounted(getList);
|
||||
|
|
|
|||
|
|
@ -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: '请选择设备',
|
||||
clearable: true,
|
||||
filterable: 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',
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ const queryParams = reactive({
|
|||
|
||||
const autoRefresh = ref(false); // 自动刷新开关
|
||||
let autoRefreshTimer: any = null; // 自动刷新定时器
|
||||
let refreshTimer: ReturnType<typeof setTimeout> | undefined; // 延迟刷新定时器
|
||||
|
||||
/** 消息方法选项 */
|
||||
const methodOptions = computed(() => {
|
||||
|
|
@ -156,6 +157,10 @@ onBeforeUnmount(() => {
|
|||
clearInterval(autoRefreshTimer);
|
||||
autoRefreshTimer = null;
|
||||
}
|
||||
if (refreshTimer) {
|
||||
clearTimeout(refreshTimer);
|
||||
refreshTimer = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
/** 初始化 */
|
||||
|
|
@ -167,9 +172,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();
|
||||
|
|
|
|||
|
|
@ -4,11 +4,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';
|
||||
|
|
@ -19,8 +20,11 @@ import {
|
|||
ElCard,
|
||||
ElCol,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElRow,
|
||||
ElSelect,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTabPane,
|
||||
|
|
@ -50,7 +54,7 @@ const debugCollapsed = ref(false); // 指令调试区域折叠状态
|
|||
const messageCollapsed = ref(false); // 设备消息区域折叠状态
|
||||
|
||||
// 表单数据:存储用户输入的模拟值
|
||||
const formData = ref<Record<string, string>>({});
|
||||
const formData = ref<Record<string, any>>({});
|
||||
|
||||
// 根据类型过滤物模型数据
|
||||
const getFilteredThingModelList = (type: number) => {
|
||||
|
|
@ -76,21 +80,72 @@ const serviceList = computed(() =>
|
|||
|
||||
// 获取表单值
|
||||
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 normalizePropertyValue(row: ThingModelApi.ThingModel, value: any) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return undefined;
|
||||
}
|
||||
if (isNumberProperty(row)) {
|
||||
return Number(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// 属性上报
|
||||
async function handlePropertyPost() {
|
||||
try {
|
||||
const params: Record<string, any> = {};
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
|
@ -173,8 +228,8 @@ async function handlePropertySet() {
|
|||
try {
|
||||
const params: Record<string, any> = {};
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
|
@ -212,6 +267,14 @@ async function handleServiceInvoke(row: ThingModelApi.ThingModel) {
|
|||
ElMessage.error('服务参数格式错误,请输入有效的JSON格式');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
typeof inputParams !== 'object' ||
|
||||
inputParams === null ||
|
||||
Array.isArray(inputParams)
|
||||
) {
|
||||
ElMessage.error('服务参数必须是 JSON 对象');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 与后端 IotDeviceServiceInvokeReqDTO 对齐 :{ identifier, inputParams }
|
||||
|
|
@ -232,6 +295,11 @@ async function handleServiceInvoke(row: ThingModelApi.ThingModel) {
|
|||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 切换调试方法时清空输入,避免不同方法之间串台提交 */
|
||||
watch([activeTab, upstreamTab, downstreamTab], () => {
|
||||
formData.value = {};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -297,7 +365,35 @@ async function handleServiceInvoke(row: ThingModelApi.ThingModel) {
|
|||
</ElTableColumn>
|
||||
<ElTableColumn label="值" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<ElInputNumber
|
||||
v-if="isNumberProperty(row)"
|
||||
:model-value="getFormValue(row.identifier)"
|
||||
placeholder="输入值"
|
||||
size="small"
|
||||
class="w-full"
|
||||
@update:model-value="
|
||||
setFormValue(row.identifier, $event)
|
||||
"
|
||||
/>
|
||||
<ElSelect
|
||||
v-else-if="isSelectProperty(row)"
|
||||
:model-value="getFormValue(row.identifier)"
|
||||
placeholder="请选择值"
|
||||
size="small"
|
||||
class="w-full"
|
||||
@update:model-value="
|
||||
setFormValue(row.identifier, $event)
|
||||
"
|
||||
>
|
||||
<ElOption
|
||||
v-for="option in getPropertyOptions(row)"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
<ElInput
|
||||
v-else
|
||||
:model-value="getFormValue(row.identifier)"
|
||||
placeholder="输入值"
|
||||
size="small"
|
||||
|
|
@ -448,7 +544,35 @@ async function handleServiceInvoke(row: ThingModelApi.ThingModel) {
|
|||
</ElTableColumn>
|
||||
<ElTableColumn label="值" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<ElInputNumber
|
||||
v-if="isNumberProperty(row)"
|
||||
:model-value="getFormValue(row.identifier)"
|
||||
placeholder="输入值"
|
||||
size="small"
|
||||
class="w-full"
|
||||
@update:model-value="
|
||||
setFormValue(row.identifier, $event)
|
||||
"
|
||||
/>
|
||||
<ElSelect
|
||||
v-else-if="isSelectProperty(row)"
|
||||
:model-value="getFormValue(row.identifier)"
|
||||
placeholder="请选择值"
|
||||
size="small"
|
||||
class="w-full"
|
||||
@update:model-value="
|
||||
setFormValue(row.identifier, $event)
|
||||
"
|
||||
>
|
||||
<ElOption
|
||||
v-for="option in getPropertyOptions(row)"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
<ElInput
|
||||
v-else
|
||||
:model-value="getFormValue(row.identifier)"
|
||||
placeholder="输入值"
|
||||
size="small"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||
|
||||
import { computed, onMounted, reactive, watch } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, watch } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import {
|
||||
|
|
@ -35,6 +35,7 @@ const queryParams = reactive({
|
|||
identifier: '',
|
||||
times: undefined as [string, string] | undefined,
|
||||
});
|
||||
let refreshTimer: ReturnType<typeof setTimeout> | undefined; // 延迟刷新定时器
|
||||
|
||||
/** 事件类型的物模型数据 */
|
||||
const eventThingModels = computed(() => {
|
||||
|
|
@ -159,8 +160,15 @@ function parseParams(params: string) {
|
|||
|
||||
/** 刷新列表 */
|
||||
function refresh(delay = 0) {
|
||||
if (refreshTimer) {
|
||||
clearTimeout(refreshTimer);
|
||||
refreshTimer = undefined;
|
||||
}
|
||||
if (delay > 0) {
|
||||
setTimeout(() => gridApi.query(), delay);
|
||||
refreshTimer = setTimeout(() => {
|
||||
gridApi.query();
|
||||
refreshTimer = undefined;
|
||||
}, delay);
|
||||
} else {
|
||||
gridApi.query();
|
||||
}
|
||||
|
|
@ -183,6 +191,14 @@ onMounted(() => {
|
|||
}
|
||||
});
|
||||
|
||||
/** 组件卸载时清除延迟刷新定时器 */
|
||||
onBeforeUnmount(() => {
|
||||
if (refreshTimer) {
|
||||
clearTimeout(refreshTimer);
|
||||
refreshTimer = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
/** 暴露方法给父组件 */
|
||||
defineExpose({
|
||||
refresh,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||
|
||||
import { computed, onMounted, reactive, watch } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, watch } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import {
|
||||
|
|
@ -35,6 +35,7 @@ const queryParams = reactive({
|
|||
identifier: '',
|
||||
times: undefined as [string, string] | undefined,
|
||||
});
|
||||
let refreshTimer: ReturnType<typeof setTimeout> | undefined; // 延迟刷新定时器
|
||||
|
||||
/** 服务类型的物模型数据 */
|
||||
const serviceThingModels = computed(() => {
|
||||
|
|
@ -173,8 +174,15 @@ function parseParams(params: string) {
|
|||
|
||||
/** 刷新列表 */
|
||||
function refresh(delay = 0) {
|
||||
if (refreshTimer) {
|
||||
clearTimeout(refreshTimer);
|
||||
refreshTimer = undefined;
|
||||
}
|
||||
if (delay > 0) {
|
||||
setTimeout(() => gridApi.query(), delay);
|
||||
refreshTimer = setTimeout(() => {
|
||||
gridApi.query();
|
||||
refreshTimer = undefined;
|
||||
}, delay);
|
||||
} else {
|
||||
gridApi.query();
|
||||
}
|
||||
|
|
@ -197,6 +205,14 @@ onMounted(() => {
|
|||
}
|
||||
});
|
||||
|
||||
/** 组件卸载时清除延迟刷新定时器 */
|
||||
onBeforeUnmount(() => {
|
||||
if (refreshTimer) {
|
||||
clearTimeout(refreshTimer);
|
||||
refreshTimer = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
/** 暴露方法给父组件 */
|
||||
defineExpose({
|
||||
refresh,
|
||||
|
|
|
|||
|
|
@ -67,10 +67,12 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
valueField: 'id',
|
||||
placeholder: '请选择产品',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
show: (values) => !values.id,
|
||||
componentProps: (values) => ({
|
||||
disabled: !!values.id,
|
||||
}),
|
||||
rules: (values) => (values.id ? null : 'required'),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -80,10 +82,12 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
componentProps: {
|
||||
placeholder: '请输入版本号',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
show: (values) => !values.id,
|
||||
componentProps: (values) => ({
|
||||
disabled: !!values.id,
|
||||
}),
|
||||
rules: (values) => (values.id ? null : 'required'),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -105,10 +109,12 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
maxSize: 50,
|
||||
helpText: '支持上传 .bin、.zip、.pdf 格式的固件文件,最大 50MB',
|
||||
},
|
||||
rules: 'required',
|
||||
dependencies: {
|
||||
triggerFields: ['id'],
|
||||
show: (values) => !values.id,
|
||||
componentProps: (values) => ({
|
||||
disabled: !!values.id,
|
||||
}),
|
||||
rules: (values) => (values.id ? null : 'required'),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ async function handleRefresh() {
|
|||
|
||||
/** 按任务名称搜索 */
|
||||
async function handleSearch() {
|
||||
await gridApi.query();
|
||||
await gridApi.reload();
|
||||
}
|
||||
|
||||
/** 新增任务 */
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ const statusTabs = computed(() => {
|
|||
/** 切换标签 */
|
||||
async function handleTabChange(tabKey: number | string) {
|
||||
activeTab.value = String(tabKey);
|
||||
await gridApi.query();
|
||||
await gridApi.reload();
|
||||
}
|
||||
|
||||
/** 取消单条记录的升级 */
|
||||
|
|
@ -89,7 +89,7 @@ watch(
|
|||
async (val) => {
|
||||
if (val) {
|
||||
activeTab.value = '';
|
||||
await gridApi.query();
|
||||
await gridApi.reload();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -146,10 +146,46 @@ function handleNumberChange(value: number | undefined) {
|
|||
localValue.value = value?.toString() || '';
|
||||
}
|
||||
|
||||
/** 根据外部值同步内部输入态 */
|
||||
function syncInternalValue(value = '') {
|
||||
const normalized = value;
|
||||
if (
|
||||
props.operator ===
|
||||
IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN.value
|
||||
) {
|
||||
const [start = '', end = ''] = normalized.split(',');
|
||||
rangeStart.value = start;
|
||||
rangeEnd.value = end;
|
||||
numberValue.value = undefined;
|
||||
dateValue.value = '';
|
||||
return;
|
||||
}
|
||||
rangeStart.value = '';
|
||||
rangeEnd.value = '';
|
||||
if (props.propertyType === IoTDataSpecsDataTypeEnum.DATE) {
|
||||
dateValue.value = normalized;
|
||||
numberValue.value = undefined;
|
||||
return;
|
||||
}
|
||||
if (isNumericType()) {
|
||||
const parsed = Number(normalized);
|
||||
numberValue.value =
|
||||
normalized === '' || Number.isNaN(parsed) ? undefined : parsed;
|
||||
dateValue.value = '';
|
||||
return;
|
||||
}
|
||||
numberValue.value = undefined;
|
||||
dateValue.value = '';
|
||||
}
|
||||
|
||||
/** 监听操作符变化 */
|
||||
watch(
|
||||
() => props.operator,
|
||||
() => {
|
||||
(_operator, oldOperator) => {
|
||||
if (oldOperator === undefined) {
|
||||
syncInternalValue(props.modelValue);
|
||||
return;
|
||||
}
|
||||
localValue.value = '';
|
||||
rangeStart.value = '';
|
||||
rangeEnd.value = '';
|
||||
|
|
@ -157,6 +193,12 @@ watch(
|
|||
numberValue.value = undefined;
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.propertyType] as const,
|
||||
([value]) => syncInternalValue(value),
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -168,8 +210,8 @@ watch(
|
|||
placeholder="请选择布尔值"
|
||||
class="w-full!"
|
||||
>
|
||||
<ElOption label="真 (true)" :value="true" />
|
||||
<ElOption label="假 (false)" :value="false" />
|
||||
<ElOption label="真 (true)" value="true" />
|
||||
<ElOption label="假 (false)" value="false" />
|
||||
</ElSelect>
|
||||
|
||||
<!-- 枚举值选择 -->
|
||||
|
|
|
|||
|
|
@ -31,10 +31,7 @@ async function getList() {
|
|||
function handleChange(value: number | string | undefined) {
|
||||
const toolId = typeof value === 'number' ? value : undefined;
|
||||
emit('update:modelValue', toolId);
|
||||
emit(
|
||||
'change',
|
||||
list.value.find((item) => item.id === toolId),
|
||||
);
|
||||
emit('change', list.value.find((item) => item.id === toolId));
|
||||
}
|
||||
|
||||
onMounted(getList);
|
||||
|
|
|
|||
Loading…
Reference in New Issue