物联网后端数据可视化消息队列【免费下载链接】thingsboardAll-in-one IoT Platform - Device management, data collection, processing and visualization.项目地址https://gitcode.com/GitHub_Trending/th/thingsboard点击查看免费下载本文围绕 ThingsBoard 前端ui-ngx提供的“自定义 ActionCustom Action”机制完整讲解如何用 JavaScript 配合 HTML 模板实现一个可复用的“创建设备或资产”对话框。你将掌握widgetContext.servicesMap服务注入、customDialog动态对话框 API、Angular 响应式表单校验、服务器端属性与实体关系Relation批量保存等核心技能并能直接照抄示例改造为自己的业务对话框。示例源码位于仓库的 自定义 Action 帮助文档目录配套 HTML 模板见 custom_pretty_create_dialog_html.md。一、示例定位带 HTML 模板的自定义 Action在 ThingsBoard 中可以为仪表板Dashboard上的部件Widget配置“Action”。其中一类 Action 允许用户直接编写JavaScript 函数 自定义 HTML 模板从而弹出一个完全自定义的对话框。根据 custom_pretty_action_fn.md 的说明这类函数的标准签名是function ($event, widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel): void各参数含义参数类型说明$eventMouseEvent鼠标点击事件对象通常是触发 Action 的点击事件widgetContextWidgetContext当前部件实例的上下文引用封装了部件所需的全部 API 与数据entityIdstring可选的目标实体 IDentityNamestring可选的目标实体名称htmlTemplatestring在 Action 配置的HTML页签中定义的模板字符串用于渲染自定义对话框additionalParams{[key: string]: any}可选的附加参数键值对象entityLabelstring可选的目标实体标签本文示例的核心逻辑是函数内部读取用户输入的实体名称、类型、标签、自定义属性与关系调用后端 REST API 依次保存实体本身、服务器端属性SERVER_SCOPE以及实体间关系完成后刷新部件别名并关闭对话框。二、完整 JavaScript 实现以下代码即关联文档 custom_pretty_create_dialog_js.md 的完整正文可直接复制到自定义 Action 的JavaScript页签中使用let $injector widgetContext.$scope.$injector; let customDialog $injector.get(widgetContext.servicesMap.get(customDialog)); let assetService $injector.get(widgetContext.servicesMap.get(assetService)); let deviceService $injector.get(widgetContext.servicesMap.get(deviceService)); let attributeService $injector.get(widgetContext.servicesMap.get(attributeService)); let entityRelationService $injector.get(widgetContext.servicesMap.get(entityRelationService)); openAddEntityDialog(); function openAddEntityDialog() { customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe(); } function AddEntityDialogController(instance) { let vm instance; vm.allowedEntityTypes [ASSET, DEVICE]; vm.entitySearchDirection { from: FROM, to: TO } vm.addEntityFormGroup vm.fb.group({ entityName: [, [vm.validators.required]], entityType: [DEVICE], entityLabel: [null], type: [, [vm.validators.required]], attributes: vm.fb.group({ latitude: [null], longitude: [null], address: [null], owner: [null], number: [null, [vm.validators.pattern(/^-?[0-9]$/)]], booleanValue: [null] }), relations: vm.fb.array([]) }); vm.cancel function () { vm.dialogRef.close(null); }; vm.relations function () { return vm.addEntityFormGroup.get(relations); }; vm.addRelation function () { vm.relations().push(vm.fb.group({ relatedEntity: [null, [vm.validators.required]], relationType: [null, [vm.validators.required]], direction: [null, [vm.validators.required]] })); }; vm.removeRelation function (index) { vm.relations().removeAt(index); vm.relations().markAsDirty(); }; vm.save function () { vm.addEntityFormGroup.markAsPristine(); saveEntityObservable().subscribe( function (entity) { widgetContext.rxjs.forkJoin([ saveAttributes(entity.id), saveRelations(entity.id) ]).subscribe( function () { widgetContext.updateAliases(); vm.dialogRef.close(null); } ); } ); }; function saveEntityObservable() { const formValues vm.addEntityFormGroup.value; let entity { name: formValues.entityName, type: formValues.type, label: formValues.entityLabel }; if (formValues.entityType ASSET) { return assetService.saveAsset(entity); } else if (formValues.entityType DEVICE) { return deviceService.saveDevice(entity); } } function saveAttributes(entityId) { let attributes vm.addEntityFormGroup.get(attributes).value; let attributesArray []; for (let key in attributes) { if (attributes[key] ! null) { attributesArray.push({key: key, value: attributes[key]}); } } if (attributesArray.length 0) { return attributeService.saveEntityAttributes(entityId, SERVER_SCOPE, attributesArray); } return widgetContext.rxjs.of([]); } function saveRelations(entityId) { let relations vm.addEntityFormGroup.get(relations).value; let tasks []; for (let i 0; i relations.length; i) { let relation { type: relations[i].relationType, typeGroup: COMMON }; if (relations[i].direction FROM) { relation.to relations[i].relatedEntity; relation.from entityId; } else { relation.to entityId; relation.from relations[i].relatedEntity; } tasks.push(entityRelationService.saveRelation(relation)); } if (tasks.length 0) { return widgetContext.rxjs.forkJoin(tasks); } return widgetContext.rxjs.of([]); } }三、代码逐段解读3.1 通过$injectorservicesMap获取服务let $injector widgetContext.$scope.$injector; let customDialog $injector.get(widgetContext.servicesMap.get(customDialog)); let assetService $injector.get(widgetContext.servicesMap.get(assetService)); let deviceService $injector.get(widgetContext.servicesMap.get(deviceService)); let attributeService $injector.get(widgetContext.servicesMap.get(attributeService)); let entityRelationService $injector.get(widgetContext.servicesMap.get(entityRelationService));这是 ThingsBoard 自定义 Action 中获取 Angular 服务的标准姿势widgetContext.servicesMap是一个把“服务别名”映射到“服务类”的 Map定义在 services.map.ts 中。从源码可以看出该 Map 中可用别名非常丰富除本文用到的之外还包括alarmService、customerService、dashboardService、userService、entityViewService、telemetryWsService、notificationService、dialogs、router、http、translate、date、utils、importExport等数十个服务。拿到类型后再通过widgetContext.$scope.$injector.get(...)获取真实服务实例。这样做的意义在于自定义 Action 运行在受控的沙箱环境中不能直接import服务只能通过servicesMap白名单注入既保证了灵活性又限制了权限边界。本示例注入的服务对应关系servicesMap 别名实际服务职责customDialogCustomDialogService打开基于 HTML 模板的动态对话框assetServiceAssetService保存资产AssetdeviceServiceDeviceService保存设备DeviceattributeServiceAttributeService读写实体服务器端属性entityRelationServiceEntityRelationService保存/删除实体间关系3.2 打开对话框customDialog.customDialog(...)function openAddEntityDialog() { customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe(); }CustomDialogService.customDialog(template, controller, data?, config?)是核心 API其真实实现在 custom-dialog.service.ts它把传入的 HTML 模板字符串通过dynamicComponentFactoryService.createDynamicComponent(...)动态编译成一个 Angular 组件组件实例会继承CustomDialogComponent基类然后将该组件与controller一起封装进CustomDialogContainerData用 Angular Material 的MatDialog.open弹出对话框关闭后会销毁动态组件避免内存泄漏。其中htmlTemplate即自定义 Action 配置里HTML 页签中粘贴的模板字符串本示例配套模板见 custom_pretty_create_dialog_html.md。3.3 控制器初始化与响应式表单构建对话框的控制器函数AddEntityDialogController(instance)在CustomDialogComponent构造函数中被调用参见 custom-dialog.component.ts。控制器通过instance代码中记为vm访问基类注入的能力vm.fbAngularUntypedFormBuilder用于构建响应式表单vm.validatorsAngularValidators提供required、pattern等内置校验器vm.dialogRefMatDialogRef用于关闭对话框vm.data可选的注入数据本示例未使用。表单结构如下vm.addEntityFormGroup vm.fb.group({ entityName: [, [vm.validators.required]], // 实体名称必填 entityType: [DEVICE], // 实体类型默认 DEVICE entityLabel: [null], // 实体标签可选 type: [, [vm.validators.required]], // 实体子类型Profile必填 attributes: vm.fb.group({ ... }), // 嵌套属性组 relations: vm.fb.array([]) // 关系列表可动态增删 });值得注意的两个校验细节number字段使用了vm.validators.pattern(/^-?[0-9]$/)只允许可选的负号加纯整数防止非法数字入库entityType默认值为DEVICE即用户打开对话框时默认创建设备也可通过界面上的类型选择器切换为ASSET由vm.allowedEntityTypes [ASSET, DEVICE]约束可选范围。3.4 动态关系FormArray管理示例允许用户在一个对话框里同时为新建实体配置多条关系Relationvm.addRelation function () { vm.relations().push(vm.fb.group({ relatedEntity: [null, [vm.validators.required]], relationType: [null, [vm.validators.required]], direction: [null, [vm.validators.required]] })); }; vm.removeRelation function (index) { vm.relations().removeAt(index); vm.relations().markAsDirty(); };addRelation()向FormArray中push一个新的FormGroup其中relatedEntity关联实体、relationType关系类型、direction方向三者均必填removeRelation(index)按索引移除某行并通过markAsDirty()标记表单为脏状态从而触发按钮disabled状态的联动方向direction的可选值来自vm.entitySearchDirection { from: FROM, to: TO }分别表示“当前实体作为关系的起点/终点”。3.5 保存流程实体 → 属性 → 关系vm.save()采用“先建实体、再补属性与关系”的分阶段事务式编排vm.save function () { vm.addEntityFormGroup.markAsPristine(); saveEntityObservable().subscribe( function (entity) { widgetContext.rxjs.forkJoin([ saveAttributes(entity.id), saveRelations(entity.id) ]).subscribe( function () { widgetContext.updateAliases(); vm.dialogRef.close(null); } ); } ); };markAsPristine()清除表单脏标记避免后续校验逻辑误判saveEntityObservable()根据entityType分发到assetService.saveAsset(entity)或deviceService.saveDevice(entity)两者返回的 Observable 会携带创建成功后的实体对象含entity.id拿到entity.id后用widgetContext.rxjs.forkJoin并行执行saveAttributes(entity.id)与saveRelations(entity.id)forkJoin等待全部异步任务完成后才回调全部成功后调用widgetContext.updateAliases()刷新部件的数据源别名让新实体立刻反映在仪表板上最后vm.dialogRef.close(null)关闭对话框。saveAttributes只收集值非null的属性字段将其转为{key, value}数组后调用attributeService.saveEntityAttributes(entityId, SERVER_SCOPE, attributesArray);其中SERVER_SCOPE表示写入服务器端属性作用域。若没有非空属性则返回widgetContext.rxjs.of([])空 Observable保证流程一致。saveRelations为每条关系构造{type, typeGroup: COMMON, from, to}结构方向为FROM时to指向关联实体、from指向新建实体方向为TO时相反。所有保存任务同样通过forkJoin并行执行。四、配套 HTML 模板要点对话框的可视化界面来自 HTML 页签中的模板完整内容见 custom_pretty_create_dialog_html.md。它使用 Angular Material 组件与 ThingsBoard 自有表单控件与控制器代码一一对应核心结构包括表单根节点form #addEntityFormngForm [formGroup]addEntityFormGroup (ngSubmit)save()将模板与控制器中的addEntityFormGroup绑定提交时触发save()工具栏与关闭按钮mat-toolbarmat-icon-button点击调用cancel()加载指示mat-progress-bar *ngIfisLoading$ | async由基类提供的isLoading$流控制实体类型与子类型tb-entity-type-select受[allowedEntityTypes]约束与tb-entity-subtype-autocomplete根据entityType的值动态切换ASSET/DEVICE的 Profile 自动补全属性区formGroupNameattributes包裹经纬度、地址、归属、整数与布尔值等输入控件整数输入框使用step1并显示pattern校验错误信息关系区formArrayNamerelations*ngFor渲染动态行每行含方向下拉mat-select、关系类型自动补全tb-relation-type-autocomplete、实体选择器tb-entity-select与删除按钮底部有“Add”按钮调用addRelation()操作按钮Cancel 调用cancel()Create 为typesubmit并在addEntityForm.invalid || !addEntityForm.dirty时禁用保证必填项齐全且表单被修改后才允许提交。模板与控制器通过控件名称formControlName严格对齐修改任何一边都要同步另一侧这是自定义对话框最易出错、也最需要注意的耦合点。五、底层原理customDialog 如何动态工作要真正理解这段示例值得看一眼CustomDialogService的源码实现custom-dialog.service.ts对话框模板不是预编译的静态组件而是通过DynamicComponentFactoryService在运行时把字符串模板编译为动态组件再放入CustomDialogContainerComponent见 custom-dialog-container.component.ts中渲染动态组件实例继承 CustomDialogComponent 基类该基类在构造函数中执行this.data.controller(this)这就是控制器函数接收instance并能访问vm.fb、vm.validators、vm.dialogRef的原因对话框关闭后服务会调用destroyDynamicComponent销毁组件实例防止资源泄漏。因此从“控制器逻辑 HTML 模板视图 servicesMap 服务能力”三者组合的架构可以看出ThingsBoard 自定义对话框本质上是一种动态组件工厂 响应式表单 REST 服务调用的组合模式示例中的保存编排先建实体、再并行存属性与关系、成功后刷新别名是该模式下最典型、最值得复用的写法。六、延伸对比编辑对话框的差异仓库中还有同一系列“编辑设备/资产”对话框示例 custom_pretty_edit_dialog_js.md与创建示例对比可更清晰理解本示例的边界编辑示例通过entityId、entityName参数预填表单用entityService.getEntity(...)、attributeService.getEntityAttributes(entityId, SERVER_SCOPE)、entityRelationService.findInfoByFrom/findInfoByTo(...)加载存量数据再patchValue回填编辑示例额外维护oldRelations只读展示的既有关系与relationsToDelete待删除关系保存时用entityRelationService.deleteRelation(from, type, to)处理被移除的关系创建示例本文无需回填与删除逻辑只负责“新增实体 新增属性 新增关系”是理解整套 customDialog 模式的最佳起点。七、配置入口与使用建议在仪表板编辑模式中为目标部件添加 Action类型选择“自定义 ActionCustom Action”或选择支持 HTML 模板的自定义动作类型将本文第二节的 JavaScript 完整粘贴到JavaScript页签将 HTML 模板 中的代码粘贴到HTML页签保存后点击该部件对应的触发元素如表行、按钮、菜单项即可弹出“Add entity”对话框。实践要点总结模板与控制器强耦合formControlName、函数名save/cancel/addRelation/removeRelation必须与控制器中的定义一一对应服务只能通过servicesMap获取需要其他能力时先查阅 services.map.ts 中存在的别名再$injector.get(...)不要凭空构造服务名保存顺序不可颠倒必须先保存实体拿到id才能写入依附于实体的属性与关系及时刷新视图保存成功后调用widgetContext.updateAliases()否则仪表板上可能出现旧数据复用而非重复需要类似“编辑”“克隆”“创建用户”等场景时可直接参考同目录下的其他示例编辑对话框、克隆设备等改造复用。通过本文示例你已经掌握了一套可在 ThingsBoard 任意部件上复用的“自定义创建对话框”完整方案从服务注入、动态对话框打开到响应式表单校验、分阶段异步保存与视图刷新全部有仓库源码可循、有配套模板可抄可直接落地到真实仪表板中。赞分享物联网后端数据可视化消息队列【免费下载链接】thingsboardAll-in-one IoT Platform - Device management, data collection, processing and visualization.项目地址https://gitcode.com/GitHub_Trending/th/thingsboard点击查看免费下载相关推荐ThingsBoard 仪表板自定义动作实战用 customDialog 与 HTML 模板实现设备/资产创建对话框ThingsBoard 仪表板自定义动作实战用 customDialog 与 HTML 模板实现设备/资产创建对话框 本指南聚焦 ThingsBoard 仪表物联网后端数据可视化消息队列ThingsBoard Widget Action 实战用 customDialog 与服务注入实现克隆设备对话框ThingsBoard Widget Action 实战用 customDialog 与服务注入实现克隆设备对话框 本篇指南聚焦 ThingsBoard物联网后端数据可视化消息队列Task 模板引擎全解析从变量插值到函数库的 Templating Reference 实战指南Task 模板引擎全解析从变量插值到函数库的 Templating Reference 实战指南 导读 Task本项目为 GitHub 加速计划 / ta物联网后端数据可视化消息队列上一篇Windows取证必备SRUM-DUMP如何助力安全分析师追踪30天内可疑进程活动下一篇LightTrack模型训练全攻略从数据准备到Pose Estimation模块优化创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考