在 ThingsBoard 中,要讓“Entities hierarchy”部件(左側樹形導航)與右側的數據表格實現聯動——即點擊左側某個節點后,右側表格立刻按該節點對應的實體類型/層級進行過濾——需要把“數據源別名(Alias)+ 儀表板狀態(State)+ 部件動作(Action)”三件事串起來做。下面給出可直接落地的完整步驟(PE/CE 通用)。
一、準備數據模型
給所有需要被分類的實體(設備/資產)打上統一的屬性或關系。
例如:屬性
region
: “華北”、“華南”關系
Contains
:Asset → Device
在“資產”里建一個樹形結構,如
根 Asset → 區域 Asset → 子區域 Asset → 設備 Device。
二、做左側“Entities hierarchy”部件
進入儀表板編輯模式 → 添加部件 → Cards → Entities hierarchy
數據源選“實體列表”或“資產查詢”別名(見下一步),讓它把整個資產樹一次性展開。
在部件設置 → Actions(動作) → 添加動作
觸發源:
On node selected
動作類型:
Update current dashboard state
狀態參數:
entityId: ${entityId} entityType: ${entityType} entityName: ${entityName}
保存。
三、做右側“Entity table”部件
添加部件 → Cards → Entities table
關鍵在數據源別名:
打開“Entity aliases” → 新建別名
類型選 Relations query(關系查詢)
起點:
Dashboard state entity
(即左側點擊的節點)方向:
From
關系類型:
Contains
最大層級:按需要(一般 1 層即可)
目標實體過濾:可再按類型(Device/Asset)或屬性過濾
保存別名后回到表格部件,數據源選擇剛創建的別名。
表格列配置完后保存。
四、測試
退出編輯,刷新儀表板。
在左側樹形列表點任意節點 → 右側表格立即只顯示該節點“包含”的設備或子資產。
若要多級聯動(點區域 → 子區域 → 設備),把層級設成 2 或 3 即可。
ngOnInit(): void {this.ctx.$scope.entitiesTableWidget = this;this.settings = this.ctx.settings;this.widgetConfig = this.ctx.widgetConfig;this.subscription = this.ctx.defaultSubscription;this.initializeConfig();this.updateDatasources();this.ctx.updateWidgetParams();if (this.displayPagination) {this.widgetResize$ = new ResizeObserver(() => {this.ngZone.run(() => {const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue;if (showHidePageSize !== this.hidePageSize) {this.hidePageSize = showHidePageSize;this.cd.markForCheck();}});});this.widgetResize$.observe(this.elementRef.nativeElement);}function decodeState(raw: string | null): any {if (!raw) return null;try {// 1?? URL 解碼const urlDecoded = decodeURIComponent(raw);// 2?? BASE64 解碼 → Latin-1 字符串const latin1 = atob(urlDecoded);// 3?? Latin-1 → UTF-8const utf8 = new TextDecoder('utf-8').decode(Uint8Array.from(latin1, c => c.charCodeAt(0)));// 4?? JSON 解析return JSON.parse(utf8);} catch (e) {console.error('state 解碼失敗', e);return null;}}var collectParamObj=null;// 監聽路由參數變化(不刷新頁面)this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe(params => {const state = decodeState(params['state']);//console.log('監聽路由state=', JSON.stringify(state));if (!state) return ;if (state.length > 0 && state[0]?.params) {//const stateId = state[0].id;collectParamObj = state?.[0]?.params?.collect_param;if (collectParamObj) {// 把整個 collect_param 對象傳進去this.handleUrlParamsChange(collectParamObj);}}});}private handleUrlParamsChange(collectParam:any) {// 根據參數重新加載數據或更新過濾條件// 示例:更新 pageLink 的過濾條件//this.pageLink.textSearch = params.entityId || null;//console.log('filter entityId=', params);if (collectParam){const entityId = collectParam?.entityId?.id;const entityType = collectParam?.entityId?.entityType;const entityName = collectParam?.entityName;const entityLabel = collectParam?.entityLabel;const param={entityType,entityId,entityName,entityLabel};// 延后一幀再執行setTimeout(() => this.updateData(param));}}private buildKeyFiltersFromQueryParams(params: Params): KeyFilter[] {const filters: KeyFilter[] = [];if (!params) {return filters; // 返回空數組而不是 null,避免下游遍歷出錯}const entityType = params['entityType'];const entityId = params['entityId'];const entityName = params['entityName'];const entityLabel = params['entityLabel'];if (entityType =="ENTITY_VIEW"){if (entityLabel) {filters.push({key: {type: EntityKeyType.ENTITY_FIELD,key: 'label'},valueType: EntityKeyValueType.STRING,predicate: {type: FilterPredicateType.STRING,operation: StringOperation.EQUAL,value: {defaultValue: entityLabel,dynamicValue: null},ignoreCase: false // ? 添加這一行}});}}else if (entityType =="DEVICE"){if (entityName) {filters.push({key: {type: EntityKeyType.ENTITY_FIELD,key: 'name'},valueType: EntityKeyValueType.STRING,predicate: {type: FilterPredicateType.STRING,operation: StringOperation.EQUAL,value: {defaultValue: entityName,dynamicValue: null},ignoreCase: false // ? 添加這一行}});}}//console.log("filters",filters);return filters;}