fix: 修复 IoT 迁移页面多处交互与契约问题

- 修复告警记录产品/设备筛选联动
- 清理设备详情延迟刷新 timer,避免卸载后触发查询
- 优化 OTA 固件编辑态只读展示与任务列表分页重置
- 修复场景联动值输入回显和布尔值类型
- 修复设备模拟器输入串台、数值类型提交和服务参数校验
- 更新 IoT bug 归档与迁移说明
pull/348/head
YunaiV 2026-05-24 19:41:15 +08:00
parent eb0f2a5ff2
commit 48547bc53b
20 changed files with 508 additions and 61 deletions

View File

@ -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',

View File

@ -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();

View File

@ -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"

View File

@ -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,

View File

@ -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,

View File

@ -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'),
},
},
];

View File

@ -44,7 +44,7 @@ async function handleRefresh() {
/** 按任务名称搜索 */
async function handleSearch() {
await gridApi.query();
await gridApi.reload();
}
/** 新增任务 */

View File

@ -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();
}
},
);

View File

@ -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>
<!-- 枚举值选择 -->

View File

@ -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);

View File

@ -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',

View File

@ -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();

View File

@ -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"

View File

@ -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,

View File

@ -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,

View File

@ -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'),
},
},
];

View File

@ -44,7 +44,7 @@ async function handleRefresh() {
/** 按任务名称搜索 */
async function handleSearch() {
await gridApi.query();
await gridApi.reload();
}
/** 新增任务 */

View File

@ -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();
}
},
);

View File

@ -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>
<!-- 枚举值选择 -->

View File

@ -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);