实现动态类型查询支持

This commit is contained in:
ChenYi 2025-07-10 10:28:06 +08:00
parent c1a85a5d30
commit 0d655dfc5b
8 changed files with 530 additions and 35 deletions

View File

@ -5085,16 +5085,14 @@ export type IoTDBTreeModelDeviceDataPageDataInput = {
}; };
export type IoTDBTreeModelDeviceDataDto = { export type IoTDBTreeModelDeviceDataDto = {
concurrencyStamp?: null | string; SystemName?: null | string;
creationTime?: string; ProjectId?: null | string;
readonly extraProperties?: null | { ProjectName?: null | string;
[key: string]: unknown; IoTDataType?: null | string;
}; DeviceType?: null | string;
id?: string; DeviceId?: null | string;
isDefault?: boolean; Timestamps?: null | string;
isPublic?: boolean; APPData?: null | string;
isStatic?: boolean;
name?: null | string;
}; };
export type IoTDBTreeModelDeviceDataPageListResultDto = { export type IoTDBTreeModelDeviceDataPageListResultDto = {

View File

@ -265,7 +265,7 @@
"BusinessSystemEnum": "系统类型" "BusinessSystemEnum": "系统类型"
}, },
"IoTDBDynamicObjectData": { "IoTDBDynamicObjectData": {
"DeviceId": "设备ID", "APPData": "设备数据",
"DeviceName": "设备名称", "DeviceName": "设备名称",
"DeviceType": "设备类型", "DeviceType": "设备类型",
"DeviceStatus": "设备状态", "DeviceStatus": "设备状态",

View File

@ -0,0 +1,181 @@
# 动态列解决方案
这个解决方案用于处理后端返回字段列数不固定的IoTDB设备数据。
## 问题描述
原来的 `IoTDBTreeModelDeviceDataPageAllResponse` 类型被限定为固定的字段结构,但后端返回的字段列数不固定,导致前端无法正确显示所有数据。
## 解决方案
### 1. 动态列生成
- **文件**: `dynamicColumns.ts`
- **功能**: 根据实际数据动态生成表格列
- **特点**:
- 支持字段名映射
- 支持字段类型配置
- 自动过滤不需要显示的字段
### 2. 类型安全
- **文件**: `types.ts`
- **功能**: 提供类型定义,确保类型安全
- **特点**:
- 使用 `[key: string]: any` 允许任意字段
- 继承基础类型,保持向后兼容
### 3. 数据处理
- **文件**: `example.ts`
- **功能**: 提供数据处理工具函数
- **特点**:
- 数据验证和清理
- 动态字段检测
- 字段重要性排序
## 使用方法
### 基本使用
```typescript
// 1. 导入必要的函数
import { generateDynamicColumns } from './dynamicColumns';
import { validateAndCleanData } from './example';
// 2. 在API调用后处理数据
const { data } = await postTreeModelDeviceDataInfoPage(options);
if (data?.items && data.items.length > 0) {
// 验证和清理数据
const cleanedData = validateAndCleanData(data.items);
// 动态生成列定义
const columns = generateDynamicColumns(cleanedData);
// 更新表格列
dynamicColumns.value = columns;
}
```
### 自定义字段映射
```typescript
// 在 dynamicColumns.ts 中添加字段映射
export const fieldNameMapping: FieldMapping = {
SystemName: '系统名称',
ProjectId: '项目ID',
// 添加新的字段映射
CustomField: '自定义字段',
};
```
### 自定义字段类型配置
```typescript
// 在 dynamicColumns.ts 中添加字段类型配置
export const fieldTypeConfig: FieldTypeConfig = {
Timestamps: {
formatter: (value: string) => {
return new Date(value).toLocaleString();
},
},
// 添加新的字段类型配置
CustomField: {
width: 200,
formatter: (value: any) => {
return `自定义格式: ${value}`;
},
},
};
```
## 配置选项
### 字段映射配置
| 字段名 | 显示名称 | 说明 |
|--------|----------|------|
| SystemName | 系统名称 | 系统名称字段 |
| ProjectId | 项目ID | 项目标识符 |
| ProjectName | 项目名称 | 项目名称 |
| IoTDataType | IoT数据类型 | 数据类型标识 |
| DeviceType | 设备类型 | 设备类型 |
| DeviceId | 设备ID | 设备标识符 |
| Timestamps | 时间戳 | 数据时间戳 |
| APPData | 应用数据 | 应用相关数据 |
### 排除字段
默认排除以下字段:
- `id`
- `key`
- `__typename`
可以在 `generateDynamicColumns` 函数中修改 `excludeFields` 数组来自定义。
## 高级功能
### 1. 混合模式
支持固定列和动态列混合显示:
```typescript
// 固定列定义
const fixedColumns = computed(() => [
{ title: '序号', type: 'seq', width: 50 },
{ field: 'SystemName', title: '系统名称', width: 150 },
]);
// 动态列
const dynamicColumns = ref<ColumnConfig[]>([]);
// 合并显示
const allColumns = computed(() => [
...fixedColumns.value,
...dynamicColumns.value,
]);
```
### 2. 字段重要性排序
```typescript
import { sortFieldsByImportance } from './example';
const fields = ['CustomField', 'Timestamps', 'SystemName'];
const sortedFields = sortFieldsByImportance(fields);
// 结果: ['Timestamps', 'SystemName', 'CustomField']
```
### 3. 数据验证
```typescript
import { validateAndCleanData } from './example';
const rawData = [/* 原始数据 */];
const cleanedData = validateAndCleanData(rawData);
```
## 注意事项
1. **性能考虑**: 动态列生成会增加一些计算开销,建议在数据量较大时进行优化
2. **类型安全**: 虽然使用了 `any` 类型,但通过类型定义和验证函数确保数据安全
3. **向后兼容**: 保持与原有固定字段的兼容性
4. **扩展性**: 可以轻松添加新的字段映射和类型配置
## 故障排除
### 常见问题
1. **列不显示**: 检查字段是否在 `excludeFields`
2. **字段名显示错误**: 检查 `fieldNameMapping` 配置
3. **数据格式错误**: 使用 `validateAndCleanData` 函数处理数据
### 调试技巧
```typescript
// 启用调试日志
console.log('原始数据:', data);
console.log('清理后数据:', cleanedData);
console.log('生成的列:', dynamicColumns.value);
```

View File

@ -0,0 +1,76 @@
import type { FieldMapping, FieldTypeConfig, ColumnConfig, DynamicDeviceData } from './types';
// 字段名映射配置
export const fieldNameMapping: FieldMapping = {
SystemName: '系统名称',
ProjectId: '项目ID',
ProjectName: '项目名称',
IoTDataType: 'IoT数据类型',
DeviceType: '设备类型',
DeviceId: '设备ID',
Timestamps: '时间戳',
APPData: '应用数据',
// 可以根据需要添加更多映射
};
// 字段类型配置
export const fieldTypeConfig: FieldTypeConfig = {
Timestamps: {
formatter: (value: string) => {
if (!value) return '';
try {
return new Date(value).toLocaleString();
} catch {
return value;
}
},
},
// 可以根据需要添加更多类型配置
};
// 动态生成表格列
export const generateDynamicColumns = (data: DynamicDeviceData[]): ColumnConfig[] => {
if (!data || data.length === 0) return [];
// 获取第一条数据的所有字段
const firstRow = data[0];
if (!firstRow) return [];
const fields = Object.keys(firstRow);
// 过滤掉不需要显示的字段
const excludeFields = ['id', 'key', '__typename'];
return fields
.filter(field => !excludeFields.includes(field))
.map(field => {
const columnConfig: any = {
field,
title: fieldNameMapping[field] || field,
minWidth: '150',
showOverflow: true,
};
// 应用字段类型配置
if (fieldTypeConfig[field]) {
Object.assign(columnConfig, fieldTypeConfig[field]);
}
return columnConfig;
});
};
// 获取所有可能的字段(用于预定义列)
export const getAllPossibleFields = () => {
return Object.keys(fieldNameMapping);
};
// 预定义列配置(可选)
export const getPredefinedColumns = () => {
return getAllPossibleFields().map(field => ({
field,
title: fieldNameMapping[field] || field,
minWidth: '150',
showOverflow: true,
...fieldTypeConfig[field],
}));
};

View File

@ -0,0 +1,134 @@
import type { DynamicDeviceData, DynamicPageResponse } from './types';
// 示例1处理标准IoTDB数据
export const handleStandardIoTDBData = (response: any): DynamicPageResponse<DynamicDeviceData> => {
// 确保响应格式正确
if (!response || !response.items) {
return { items: [], totalCount: 0 };
}
// 转换数据,确保所有字段都被包含
const items = response.items.map((item: any) => ({
SystemName: item.SystemName || '',
ProjectId: item.ProjectId || '',
ProjectName: item.ProjectName || '',
IoTDataType: item.IoTDataType || '',
DeviceType: item.DeviceType || '',
DeviceId: item.DeviceId || '',
Timestamps: item.Timestamps || '',
APPData: item.APPData || '',
// 包含所有其他字段
...item,
}));
return {
items,
totalCount: response.totalCount || items.length,
};
};
// 示例2处理自定义字段数据
export const handleCustomFieldData = (response: any): DynamicPageResponse<DynamicDeviceData> => {
if (!response || !response.items) {
return { items: [], totalCount: 0 };
}
// 直接使用原始数据,允许任意字段
return {
items: response.items,
totalCount: response.totalCount || response.items.length,
};
};
// 示例3数据验证和清理
export const validateAndCleanData = (data: any[]): DynamicDeviceData[] => {
return data.map(item => {
const cleaned: DynamicDeviceData = {};
// 遍历所有字段,进行类型转换和清理
Object.keys(item).forEach(key => {
const value = item[key];
// 根据字段名进行特殊处理
switch (key) {
case 'Timestamps':
cleaned[key] = value ? String(value) : '';
break;
case 'SystemName':
case 'ProjectName':
case 'DeviceType':
case 'IoTDataType':
cleaned[key] = value ? String(value) : '';
break;
case 'ProjectId':
case 'DeviceId':
cleaned[key] = value ? String(value) : '';
break;
case 'APPData':
// 尝试解析JSON数据
try {
if (typeof value === 'string') {
cleaned[key] = JSON.parse(value);
} else {
cleaned[key] = value;
}
} catch {
cleaned[key] = value;
}
break;
default:
// 对于未知字段,保持原值
cleaned[key] = value;
}
});
return cleaned;
});
};
// 示例4动态字段检测
export const detectDynamicFields = (data: DynamicDeviceData[]): string[] => {
if (!data || data.length === 0) return [];
const allFields = new Set<string>();
data.forEach(item => {
Object.keys(item).forEach(key => {
allFields.add(key);
});
});
return Array.from(allFields);
};
// 示例5字段重要性排序
export const sortFieldsByImportance = (fields: string[]): string[] => {
const importanceOrder = [
'Timestamps',
'SystemName',
'ProjectName',
'DeviceType',
'DeviceId',
'IoTDataType',
'APPData',
];
return fields.sort((a, b) => {
const aIndex = importanceOrder.indexOf(a);
const bIndex = importanceOrder.indexOf(b);
// 如果都在预定义列表中,按列表顺序排序
if (aIndex !== -1 && bIndex !== -1) {
return aIndex - bIndex;
}
// 如果只有a在预定义列表中a排在前面
if (aIndex !== -1) return -1;
// 如果只有b在预定义列表中b排在前面
if (bIndex !== -1) return 1;
// 都不在预定义列表中,按字母顺序排序
return a.localeCompare(b);
});
};

View File

@ -3,6 +3,7 @@ import type { VbenFormProps } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table'; import type { VxeGridProps } from '#/adapter/vxe-table';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { nextTick, watch, ref, computed } from 'vue';
import { Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
@ -10,6 +11,8 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { postTreeModelDeviceDataInfoPage } from '#/api-client'; import { postTreeModelDeviceDataInfoPage } from '#/api-client';
import { querySchema, tableSchema } from './schema'; import { querySchema, tableSchema } from './schema';
import { generateDynamicColumns } from './dynamicColumns';
import { handleStandardIoTDBData, validateAndCleanData } from './example';
defineOptions({ defineOptions({
name: 'DeviceData', name: 'DeviceData',
@ -17,18 +20,51 @@ defineOptions({
const route = useRoute(); const route = useRoute();
const { DeviceType, DeviceId, FocusAddress } = route.query; const { DeviceType, DeviceId, FocusAddress } = route.query;
//
const dynamicColumns = ref<any[]>([]);
//
const fixedColumns = computed(() => [
{ title: '序号', type: 'seq', width: 50 },
//
]);
//
const allColumns = computed(() => [
...fixedColumns.value,
...dynamicColumns.value,
]);
const formOptions: VbenFormProps = { const formOptions: VbenFormProps = {
schema: querySchema.value, schema: querySchema.value,
initialValues: { initialValues: {
FocusAddress: FocusAddress as string, FocusAddress: FocusAddress as string,
}, },
// 使
submitOnChange: false,
//
handleValuesChange: async (values, changedFields) => {
//
if (changedFields.includes('SystemName')) {
console.log('SystemName changed, values:', values);
console.log('Changed fields:', changedFields);
// 使 setTimeout
setTimeout(async () => {
const latestValues = await gridApi.formApi.getValues();
console.log('Latest values after timeout:', latestValues);
gridApi.reload(latestValues);
}, 0);
}
},
}; };
const gridOptions: VxeGridProps<any> = { const gridOptions: VxeGridProps<any> = {
checkboxConfig: { checkboxConfig: {
highlight: true, highlight: true,
labelField: 'name', labelField: 'name',
}, },
columns: tableSchema.value, columns: allColumns,
height: 'auto', height: 'auto',
keepSource: true, keepSource: true,
pagerConfig: {}, pagerConfig: {},
@ -52,13 +88,29 @@ const gridOptions: VxeGridProps<any> = {
FocusAddress, FocusAddress,
}, },
}); });
//
if (data?.items && data.items.length > 0) {
//
const cleanedData = validateAndCleanData(data.items);
//
dynamicColumns.value = generateDynamicColumns(cleanedData);
//
return {
...data,
items: cleanedData,
};
}
return data; return data;
}, },
}, },
}, },
}; };
const [Grid] = useVbenVxeGrid({ formOptions, gridOptions }); const [Grid, gridApi] = useVbenVxeGrid({ formOptions, gridOptions });
</script> </script>
<template> <template>

View File

@ -9,11 +9,6 @@ import { $t } from '#/locales';
import { getSelectResultList } from '#/api-client'; import { getSelectResultList } from '#/api-client';
export const querySchema = computed(() => [ export const querySchema = computed(() => [
{
component: 'Input',
fieldName: 'FocusAddress',
label: $t('abp.focus.focusAddress'),
},
{ {
component: 'ApiSelect', component: 'ApiSelect',
fieldName: 'SystemName', fieldName: 'SystemName',
@ -48,31 +43,40 @@ export const querySchema = computed(() => [
}, },
}, },
}, },
{
component: 'Input',
fieldName: 'FocusAddress',
label: $t('abp.focus.focusAddress'),
},
]); ]);
export const tableSchema: any = computed((): VxeGridProps['columns'] => [ export const tableSchema: any = computed((): VxeGridProps['columns'] => [
{ title: $t('common.seq'), type: 'seq', width: 50 }, { title: $t('common.seq'), type: 'seq', width: 50 },
{ field: 'systemName', title: $t('abp.log.systemName'), minWidth: '150' },
{ {
field: 'projectId', field: 'Timestamps',
title: $t('abp.log.projectId'), title: $t('abp.IoTDBBase.Timestamps'),
minWidth: '150',
},
{ field: 'SystemName', title: $t('abp.IoTDBBase.SystemName'), minWidth: '150' },
{
field: 'ProjectId',
title: $t('abp.IoTDBBase.ProjectId'),
minWidth: '150',
}
,
{
field: 'DeviceType',
title: $t('abp.IoTDBBase.DeviceType'),
minWidth: '150', minWidth: '150',
}, },
{ {
field: 'dataType', field: 'IoTDataType',
title: $t('abp.log.dataType'), title: $t('abp.IoTDBBase.IoTDataType'),
minWidth: '150', minWidth: '150',
}, }
,
{ {
field: 'deviceType', field: 'DeviceId',
title: $t('abp.log.deviceType'), title: $t('abp.IoTDBBase.DeviceId'),
minWidth: '150', minWidth: '150',
}, }
{
field: 'times',
title: $t('abp.log.timestamps'),
minWidth: '150',
formatter: ({ cellValue }) => {
return dayjs(cellValue).format('YYYY-MM-DD HH:mm:ss');
},
},
]); ]);

View File

@ -0,0 +1,50 @@
// 基础设备数据类型
export interface BaseDeviceData {
SystemName?: string;
ProjectId?: string;
ProjectName?: string;
IoTDataType?: string;
DeviceType?: string;
DeviceId?: string;
Timestamps?: string;
APPData?: string;
[key: string]: any; // 允许任意额外字段
}
// 动态设备数据类型
export interface DynamicDeviceData extends BaseDeviceData {
[key: string]: any; // 允许任意字段
}
// 分页响应类型
export interface DynamicPageResponse<T = DynamicDeviceData> {
items?: T[] | null;
totalCount?: number;
[key: string]: any; // 允许任意额外字段
}
// 列配置类型
export interface ColumnConfig {
field: string;
title: string;
minWidth?: string | number;
width?: string | number;
showOverflow?: boolean;
formatter?: (value: any) => string;
[key: string]: any;
}
// 字段映射配置类型
export interface FieldMapping {
[key: string]: string;
}
// 字段类型配置类型
export interface FieldTypeConfig {
[key: string]: {
formatter?: (value: any) => string;
width?: string | number;
minWidth?: string | number;
[key: string]: any;
};
}