在 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;}