Commit d41ce65fdaeedf7ad5a3806d3c5c79fcd4e0d7ab

Authored by xp.Huang
2 parents 84ebe0b7 77d82afe

Merge branch 'perf/device-object-model' into 'main_dev'

pref: 优化设备详情物模型数据,结构体展示

See merge request yunteng/thingskit-front!1083
@@ -12,6 +12,7 @@ import { PageEnum } from '/@/enums/pageEnum'; @@ -12,6 +12,7 @@ import { PageEnum } from '/@/enums/pageEnum';
12 import { useGo } from '/@/hooks/web/usePage'; 12 import { useGo } from '/@/hooks/web/usePage';
13 import { useAuthDeviceDetail } from '../hook/useAuthDeviceDetail'; 13 import { useAuthDeviceDetail } from '../hook/useAuthDeviceDetail';
14 import { findDictItemByCode } from '/@/api/system/dict'; 14 import { findDictItemByCode } from '/@/api/system/dict';
  15 +import { isNullOrUnDef } from '/@/utils/is';
15 16
16 // 设备详情的描述 17 // 设备详情的描述
17 export const descSchema = (emit: EmitType): DescItem[] => { 18 export const descSchema = (emit: EmitType): DescItem[] => {
@@ -104,8 +105,11 @@ export const realTimeDataColumns: BasicColumn[] = [ @@ -104,8 +105,11 @@ export const realTimeDataColumns: BasicColumn[] = [
104 }, 105 },
105 { 106 {
106 title: '值', 107 title: '值',
107 - dataIndex: 'value', 108 + dataIndex: 'rawValue',
108 width: 160, 109 width: 160,
  110 + format(text) {
  111 + return isNullOrUnDef(text) ? '--' : text;
  112 + },
109 }, 113 },
110 { 114 {
111 title: '最后更新时间', 115 title: '最后更新时间',
  1 +import { DeviceModelOfMatterAttrs } from '/@/api/device/model/deviceModel';
  2 +import { DataType, Specs } from '/@/api/device/model/modelOfMatterModel';
  3 +import { DataTypeEnum } from '/@/enums/objectModelEnum';
  4 +import { isArray } from '/@/utils/is';
  5 +
  6 +export interface StructValueItemType extends BaseAdditionalInfo {
  7 + name: string;
  8 + key: string;
  9 + value?: string | number | boolean;
  10 +}
  11 +
  12 +export interface BaseAdditionalInfo {
  13 + unit?: string;
  14 + boolClose?: string;
  15 + boolOpen?: string;
  16 + unitName?: string;
  17 +}
  18 +
  19 +export interface SocketInfoDataSourceItemType extends BaseAdditionalInfo {
  20 + accessMode: string;
  21 + key: string;
  22 + name: string;
  23 + type: DataTypeEnum;
  24 + detail: DeviceModelOfMatterAttrs;
  25 + time?: number;
  26 + value?: string | number | boolean | Record<string, StructValueItemType>;
  27 + expand?: boolean;
  28 + showHistoryDataButton?: boolean;
  29 + rawValue?: any;
  30 +}
  31 +
  32 +export function buildTableDataSourceByObjectModel(
  33 + models: DeviceModelOfMatterAttrs[]
  34 +): SocketInfoDataSourceItemType[] {
  35 + function getAdditionalInfoByDataType(dataType?: DataType) {
  36 + const { specs } = dataType || {};
  37 + if (isArray(specs)) return {};
  38 + const { unit, boolClose, boolOpen, unitName } = (specs as Partial<Specs>) || {};
  39 + return { unit, boolClose, boolOpen, unitName };
  40 + }
  41 +
  42 + return models.map((item) => {
  43 + const { accessMode, identifier, name, detail } = item;
  44 + const { dataType } = detail;
  45 + const { type, specs } = dataType || {};
  46 +
  47 + const res = {
  48 + accessMode,
  49 + name,
  50 + key: identifier,
  51 + type: type!,
  52 + detail: item,
  53 + };
  54 +
  55 + if (isArray(specs) && type === DataTypeEnum.STRUCT) {
  56 + const value: Record<string, StructValueItemType> = specs
  57 + .filter((item) => item.dataType?.type !== DataTypeEnum.STRUCT)
  58 + .reduce((prev, next) => {
  59 + const { identifier, functionName, dataType } = next;
  60 + return {
  61 + ...prev,
  62 + [identifier]: {
  63 + key: identifier!,
  64 + name: functionName!,
  65 + type: dataType?.type,
  66 + ...getAdditionalInfoByDataType(dataType),
  67 + },
  68 + };
  69 + }, {});
  70 +
  71 + Object.assign(res, { value });
  72 + } else {
  73 + Object.assign(res, getAdditionalInfoByDataType(dataType));
  74 + }
  75 +
  76 + return res;
  77 + });
  78 +}
@@ -13,8 +13,7 @@ @@ -13,8 +13,7 @@
13 import HistoryData from './HistoryData.vue'; 13 import HistoryData from './HistoryData.vue';
14 import { BasicModal, useModal } from '/@/components/Modal'; 14 import { BasicModal, useModal } from '/@/components/Modal';
15 import { getDeviceAttrs } from '/@/api/device/deviceManager'; 15 import { getDeviceAttrs } from '/@/api/device/deviceManager';
16 - import { DeviceModelOfMatterAttrs, DeviceRecord } from '/@/api/device/model/deviceModel';  
17 - import { Specs, StructJSON } from '/@/api/device/model/modelOfMatterModel'; 16 + import { DeviceRecord } from '/@/api/device/model/deviceModel';
18 import { isArray, isNull, isObject } from '/@/utils/is'; 17 import { isArray, isNull, isObject } from '/@/utils/is';
19 import { useGlobSetting } from '/@/hooks/setting'; 18 import { useGlobSetting } from '/@/hooks/setting';
20 import { ModeSwitchButton, EnumTableCardMode } from '/@/components/Widget'; 19 import { ModeSwitchButton, EnumTableCardMode } from '/@/components/Widget';
@@ -23,8 +22,14 @@ @@ -23,8 +22,14 @@
23 import { ObjectModelCommandDeliveryModal } from './ObjectModelCommandDeliveryModal'; 22 import { ObjectModelCommandDeliveryModal } from './ObjectModelCommandDeliveryModal';
24 import { ModalParamsType } from '/#/utils'; 23 import { ModalParamsType } from '/#/utils';
25 import { AreaChartOutlined } from '@ant-design/icons-vue'; 24 import { AreaChartOutlined } from '@ant-design/icons-vue';
26 - import { SvgIcon } from '/@/components/Icon'; 25 + import { SvgIcon, Icon } from '/@/components/Icon';
27 import { DataTypeEnum } from '/@/enums/objectModelEnum'; 26 import { DataTypeEnum } from '/@/enums/objectModelEnum';
  27 + import {
  28 + buildTableDataSourceByObjectModel,
  29 + SocketInfoDataSourceItemType,
  30 + StructValueItemType,
  31 + } from './ModelOfMatter.config';
  32 + import { useJsonParse } from '/@/hooks/business/useJsonParse';
28 33
29 interface ReceiveMessage { 34 interface ReceiveMessage {
30 data: { 35 data: {
@@ -32,17 +37,6 @@ @@ -32,17 +37,6 @@
32 }; 37 };
33 } 38 }
34 39
35 - interface DataSource {  
36 - key?: string;  
37 - value?: string;  
38 - time?: number;  
39 - type?: string;  
40 - boolClose?: string;  
41 - boolOpen?: string;  
42 - name?: string;  
43 - detail: DeviceModelOfMatterAttrs;  
44 - }  
45 -  
46 const props = defineProps<{ 40 const props = defineProps<{
47 deviceDetail: DeviceRecord; 41 deviceDetail: DeviceRecord;
48 }>(); 42 }>();
@@ -70,13 +64,14 @@ @@ -70,13 +64,14 @@
70 cmdId: 0, 64 cmdId: 0,
71 origin: `${socketUrl}${token}`, 65 origin: `${socketUrl}${token}`,
72 attr: undefined as string | undefined, 66 attr: undefined as string | undefined,
73 - dataSource: [] as DataSource[], 67 + dataSource: [] as SocketInfoDataSourceItemType[],
  68 + rawDataSource: [] as SocketInfoDataSourceItemType[],
74 message: {} as ReceiveMessage['data'], 69 message: {} as ReceiveMessage['data'],
75 - attrKeys: [] as DeviceModelOfMatterAttrs[],  
76 - filterAttrKeys: [] as DeviceModelOfMatterAttrs[], 70 + attrKeys: [] as string[],
  71 + filterAttrKeys: [] as string[],
77 }); 72 });
78 73
79 - const getPaginationAttrkey = computed<DeviceModelOfMatterAttrs[]>(() => { 74 + const getPaginationAttrkey = computed(() => {
80 const { current = 1, pageSize = 10 } = pagination; 75 const { current = 1, pageSize = 10 } = pagination;
81 return ( 76 return (
82 socketInfo.filterAttrKeys && socketInfo.filterAttrKeys.length 77 socketInfo.filterAttrKeys && socketInfo.filterAttrKeys.length
@@ -104,9 +99,7 @@ @@ -104,9 +99,7 @@
104 entityId: props.deviceDetail!.tbDeviceId, 99 entityId: props.deviceDetail!.tbDeviceId,
105 scope: 'LATEST_TELEMETRY', 100 scope: 'LATEST_TELEMETRY',
106 cmdId: socketInfo.cmdId, 101 cmdId: socketInfo.cmdId,
107 - keys: unref(getPaginationAttrkey)  
108 - .map((item) => item.identifier)  
109 - .join(','), 102 + keys: unref(getPaginationAttrkey).join(','),
110 }, 103 },
111 ], 104 ],
112 }; 105 };
@@ -120,9 +113,7 @@ @@ -120,9 +113,7 @@
120 entityId: props.deviceDetail!.tbDeviceId, 113 entityId: props.deviceDetail!.tbDeviceId,
121 scope: 'LATEST_TELEMETRY', 114 scope: 'LATEST_TELEMETRY',
122 cmdId: socketInfo.cmdId, 115 cmdId: socketInfo.cmdId,
123 - keys: unref(getPaginationAttrkey)  
124 - .map((item) => item.identifier)  
125 - .join(','), 116 + keys: unref(getPaginationAttrkey).join(','),
126 }, 117 },
127 ], 118 ],
128 }; 119 };
@@ -150,12 +141,14 @@ @@ -150,12 +141,14 @@
150 pagination.current = 1; 141 pagination.current = 1;
151 142
152 socketInfo.filterAttrKeys = value 143 socketInfo.filterAttrKeys = value
153 - ? unref(socketInfo.attrKeys).filter(  
154 - (item) =>  
155 - item.identifier?.toUpperCase().includes(value.toUpperCase()) ||  
156 - item.name?.toUpperCase().includes(value.toUpperCase())  
157 - )  
158 - : socketInfo.attrKeys; 144 + ? unref(socketInfo.rawDataSource)
  145 + .filter(
  146 + (item) =>
  147 + item.key?.toUpperCase().includes(value.toUpperCase()) ||
  148 + item.name?.toUpperCase().includes(value.toUpperCase())
  149 + )
  150 + .map((item) => item.key)
  151 + : socketInfo.rawDataSource.map((item) => item.key);
159 152
160 await nextTick(); 153 await nextTick();
161 154
@@ -206,58 +199,10 @@ @@ -206,58 +199,10 @@
206 199
207 const { createMessage } = useMessage(); 200 const { createMessage } = useMessage();
208 201
209 - const getUnit = (record: StructJSON) => {  
210 - const { dataType } = record;  
211 - const { specs, type } = dataType! || {};  
212 - const unitName = isObject(specs)  
213 - ? (specs as Specs).unitName && (specs as Specs).unit  
214 - ? `${(specs as Specs).unitName}`  
215 - : ''  
216 - : '';  
217 - const { boolClose, boolOpen } = (specs || {}) as Specs;  
218 -  
219 - return { unit: unitName, boolClose, boolOpen, type };  
220 - };  
221 -  
222 - const isStructAndTextType = (type: DataTypeEnum) => {  
223 - return [DataTypeEnum.STRUCT, DataTypeEnum.STRING].includes(type);  
224 - };  
225 -  
226 const setDataSource = () => { 202 const setDataSource = () => {
227 - socketInfo.dataSource = (  
228 - socketInfo.filterAttrKeys && socketInfo.filterAttrKeys.length  
229 - ? socketInfo.filterAttrKeys  
230 - : socketInfo.attrKeys  
231 - ).map((item) => {  
232 - const { identifier: key, name, detail, accessMode } = item;  
233 - const { unit, boolClose, boolOpen, type } = getUnit(detail);  
234 - const dataInfo = socketInfo.filterAttrKeys.find((item) => item.identifier === key);  
235 -  
236 - let time: number | undefined;  
237 - let value: any | undefined;  
238 - const message = socketInfo.message[key];  
239 - if (message) {  
240 - const [attrTime, attrValue] = message.at(0) || [];  
241 - time = attrTime;  
242 - value = attrValue;  
243 - }  
244 -  
245 - return {  
246 - key,  
247 - value,  
248 - time,  
249 - name,  
250 - unit,  
251 - type,  
252 - boolClose,  
253 - boolOpen,  
254 - accessMode,  
255 - showHistoryDataButton: !isStructAndTextType(  
256 - dataInfo?.detail.dataType?.type as unknown as DataTypeEnum  
257 - ),  
258 - detail: toRaw(item),  
259 - };  
260 - }); 203 + socketInfo.dataSource = socketInfo.filterAttrKeys.length
  204 + ? socketInfo.rawDataSource.filter((item) => socketInfo.filterAttrKeys.includes(item.key))
  205 + : socketInfo.rawDataSource;
261 }; 206 };
262 207
263 const { send, close, data, open } = useWebSocket(socketInfo.origin, { 208 const { send, close, data, open } = useWebSocket(socketInfo.origin, {
@@ -272,9 +217,36 @@ @@ -272,9 +217,36 @@
272 if (value) { 217 if (value) {
273 const { data } = value; 218 const { data } = value;
274 const keys = Object.keys(data); 219 const keys = Object.keys(data);
275 - keys.forEach((key) => {  
276 - socketInfo.message[key] = value.data[key];  
277 - }); 220 +
  221 + for (const key of keys) {
  222 + const item = socketInfo.rawDataSource.find((item) => item.key === key);
  223 + if (!item) continue;
  224 + const { type } = item;
  225 + const [firstItem] = data[key] || [];
  226 + const [time, value] = firstItem;
  227 +
  228 + item.rawValue = value;
  229 +
  230 + if (type === DataTypeEnum.STRUCT) {
  231 + const { flag, value: structJSON } = useJsonParse(value);
  232 + if (!flag || !isObject(item.value)) continue;
  233 +
  234 + const structKeys = Object.keys(structJSON);
  235 +
  236 + structKeys.forEach((key) => {
  237 + if ((item.value as Record<string, StructValueItemType>)?.[key]) {
  238 + (item.value as Record<string, StructValueItemType>)[key].value = structJSON[key];
  239 + }
  240 + });
  241 +
  242 + item.time = time;
  243 +
  244 + continue;
  245 + }
  246 +
  247 + item.value = value;
  248 + item.time = time;
  249 + }
278 250
279 setDataSource(); 251 setDataSource();
280 252
@@ -296,7 +268,7 @@ @@ -296,7 +268,7 @@
296 setTableData(socketInfo.dataSource.filter((item) => !isNull(item.value))); 268 setTableData(socketInfo.dataSource.filter((item) => !isNull(item.value)));
297 } 269 }
298 270
299 - const handleShowDetail = (record: DataSource) => { 271 + const handleShowDetail = (record: SocketInfoDataSourceItemType) => {
300 const { key } = record; 272 const { key } = record;
301 socketInfo.attr = key; 273 socketInfo.attr = key;
302 openModal(true); 274 openModal(true);
@@ -305,23 +277,24 @@ @@ -305,23 +277,24 @@
305 onMounted(async () => { 277 onMounted(async () => {
306 const { deviceProfileId } = props.deviceDetail; 278 const { deviceProfileId } = props.deviceDetail;
307 const value = await getDeviceAttrs({ deviceProfileId }); 279 const value = await getDeviceAttrs({ deviceProfileId });
308 - socketInfo.attrKeys = isArray(value) ? value : []; 280 + socketInfo.attrKeys = isArray(value) ? value.map((item) => item.identifier) : [];
  281 + socketInfo.rawDataSource = buildTableDataSourceByObjectModel(value);
309 setDataSource(); 282 setDataSource();
310 open(); 283 open();
311 }); 284 });
312 285
313 - const formatValue = (item: DataSource) => { 286 + const formatValue = (item: SocketInfoDataSourceItemType) => {
314 return item.type === DataTypeEnum.BOOL 287 return item.type === DataTypeEnum.BOOL
315 ? !isNull(item.value) 288 ? !isNull(item.value)
316 ? !!Number(item.value) 289 ? !!Number(item.value)
317 ? item.boolOpen 290 ? item.boolOpen
318 : item.boolClose 291 : item.boolClose
319 : '--' 292 : '--'
320 - : item.value || '--'; 293 + : (item.value as string) || '--';
321 }; 294 };
322 295
323 const [register, { openModal: openSendCommandModal }] = useModal(); 296 const [register, { openModal: openSendCommandModal }] = useModal();
324 - const handleSendCommandModal = (data: DataSource) => { 297 + const handleSendCommandModal = (data: SocketInfoDataSourceItemType) => {
325 openSendCommandModal(true, { 298 openSendCommandModal(true, {
326 mode: DataActionModeEnum.READ, 299 mode: DataActionModeEnum.READ,
327 record: { ...toRaw(data.detail), deviceDetail: props.deviceDetail as any }, 300 record: { ...toRaw(data.detail), deviceDetail: props.deviceDetail as any },
@@ -369,7 +342,7 @@ @@ -369,7 +342,7 @@
369 :grid="grid" 342 :grid="grid"
370 :pagination="pagination" 343 :pagination="pagination"
371 > 344 >
372 - <template #renderItem="{ item }"> 345 + <template #renderItem="{ item }: { item: SocketInfoDataSourceItemType }">
373 <List.Item> 346 <List.Item>
374 <Card class="shadow-md"> 347 <Card class="shadow-md">
375 <template #title> 348 <template #title>
@@ -377,6 +350,16 @@ @@ -377,6 +350,16 @@
377 </template> 350 </template>
378 <template #extra> 351 <template #extra>
379 <Space> 352 <Space>
  353 + <Tooltip
  354 + :title="item.expand ? '收起' : '展开'"
  355 + v-if="item.type === DataTypeEnum.STRUCT"
  356 + >
  357 + <Icon
  358 + :icon="item.expand ? 'ant-design:up-outlined' : 'ant-design:down-outlined'"
  359 + class="cursor-pointer svg:text-blue-500"
  360 + @click="item.expand = !item.expand"
  361 + />
  362 + </Tooltip>
380 <Tooltip title="属性下发"> 363 <Tooltip title="属性下发">
381 <SvgIcon 364 <SvgIcon
382 name="send-command" 365 name="send-command"
@@ -396,14 +379,49 @@ @@ -396,14 +379,49 @@
396 </Space> 379 </Space>
397 </template> 380 </template>
398 <section class="min-h-16 flex flex-col justify-between"> 381 <section class="min-h-16 flex flex-col justify-between">
399 - <div class="flex font-bold text-lg mb-4 gap-2"> 382 + <div
  383 + v-if="item.type !== DataTypeEnum.STRUCT"
  384 + class="flex font-bold text-lg mb-4 gap-2"
  385 + >
400 <Tooltip :title="formatValue(item)" placement="topLeft"> 386 <Tooltip :title="formatValue(item)" placement="topLeft">
401 <div class="truncate">{{ formatValue(item) }}</div> 387 <div class="truncate">{{ formatValue(item) }}</div>
402 </Tooltip> 388 </Tooltip>
403 - <div class="text-xs flex items-center">{{ item.unit }}</div> 389 + <div class="text-xs flex items-center">{{ item.unitName }}</div>
  390 + </div>
  391 + <div
  392 + class="mb-4 overflow-hidden flex flex-col gap-2 relative"
  393 + v-if="item.type == DataTypeEnum.STRUCT && isObject(item.value)"
  394 + :style="{ height: item.expand ? 'fit-content' : '28px' }"
  395 + >
  396 + <div
  397 + v-for="key in Object.keys(item.value)"
  398 + :key="key"
  399 + class="cursor-pointer flex justify-between items-center gap-2"
  400 + >
  401 + <div class="font-medium truncate">{{ item.value[key].name }}</div>
  402 + <div class="flex-auto font-bold text-lg text-left ml-4 truncate">
  403 + <Tooltip :title="formatValue(item)" placement="topLeft">
  404 + <span>
  405 + {{ formatValue(item.value[key] as SocketInfoDataSourceItemType) }}
  406 + </span>
  407 + </Tooltip>
  408 + <span class="text-xs font-normal ml-2">
  409 + {{ item.value[key].unitName }}
  410 + </span>
  411 + </div>
  412 + </div>
  413 + <Tooltip title="点击展开">
  414 + <div
  415 + v-show="!item.expand"
  416 + class="absolute top-2 right-0 text-blue-400 cursor-pointer text-xs"
  417 + @click="item.expand = !item.expand"
  418 + >
  419 + 更多
  420 + </div>
  421 + </Tooltip>
404 </div> 422 </div>
405 <div class="text-dark-800 text-xs"> 423 <div class="text-dark-800 text-xs">
406 - {{ item.value ? formatToDateTime(item.time, 'YYYY-MM-DD HH:mm:ss') : '--' }} 424 + {{ item.time ? formatToDateTime(item.time, 'YYYY-MM-DD HH:mm:ss') : '--' }}
407 </div> 425 </div>
408 </section> 426 </section>
409 </Card> 427 </Card>
@@ -461,6 +479,26 @@ @@ -461,6 +479,26 @@
461 .device-things-model-table-mode:deep(.ant-table-placeholder) { 479 .device-things-model-table-mode:deep(.ant-table-placeholder) {
462 height: auto; 480 height: auto;
463 } 481 }
  482 +
  483 + .model-collapse {
  484 + border: none;
  485 +
  486 + :deep(.ant-collapse-header) {
  487 + display: none;
  488 + }
  489 +
  490 + :deep(.ant-collapse-item) {
  491 + border: none;
  492 + }
  493 +
  494 + :deep(.ant-collapse-content) {
  495 + border: none;
  496 + }
  497 +
  498 + :deep(.ant-collapse-content-box) {
  499 + padding: 0;
  500 + }
  501 + }
464 </style> 502 </style>
465 503
466 <style> 504 <style>