- 物联网
- 后端
- 数据可视化
- 消息队列
【免费下载链接】thingsboard
All-in-one IoT Platform - Device management, data collection, processing and visualization.
导读
本文围绕 ThingsBoard(开源版 IoT 平台)Dashboard 中「自定义动作(Custom Action)」的核心能力,以官方帮助文档提供的**「编辑设备或资产」示例**(custom_pretty_edit_dialog_js.md+custom_pretty_edit_dialog_html.md)为主线,完整讲解如何在 Widget 中通过 JavaScript 函数与 HTML 模板组合出一个可运行的「自定义对话框」,实现对目标实体的名称/标签、服务端属性、实体关系的新增、修改与删除。读完本文,你将掌握widgetContext、servicesMap、customDialog的调用方式,理解其底层运行原理,并能够把这段示例改造为适合自己的业务对话框。
一、示例文档在项目中的位置与定位
在 ThingsBoard 前端的帮助文档体系中,本示例位于:
- JavaScript 函数:ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_dialog_js.md
- HTML 模板:ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_dialog_html.md
它是「Custom action (with HTML template) function」帮助主题下的五个官方示例之一(其余还包括创建对话框、创建用户、编辑图片属性、克隆设备等,同目录下均有_js与_html两个文件)。这些示例被 Dashboard 的 Widget 动作配置界面(Action 弹窗中的tb-help-popup)直接引用,用户在界面上即可打开查看与复制。
示例对应的动作函数签名如下(见 custom_pretty_action_fn.md):
function ($event, widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel): void其中htmlTemplate参数即为用户在「HTML」标签页中定义的模板字符串,用于渲染自定义对话框;entityId、entityName、entityLabel为动作触发时由 Widget 传入的目标实体信息。
二、函数骨架:从 widgetContext 获取服务
示例 JavaScript 的第一步,是从widgetContext取出 Angular 注入器与相关服务:
let $injector = widgetContext.$scope.$injector; let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); let entityService = $injector.get(widgetContext.servicesMap.get('entityService')); 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'));原理说明:widgetContext是 ThingsBoard Widget 运行时提供给动作函数的核心对象,其类型定义见 widget-component.models.ts,其中:
servicesMap:Map<string, Type<any>>,预注册的服务类型映射表;$injector:AngularInjector,用于按类型实例化服务;$scope:IDynamicWidgetComponent动态组件作用域,可借此访问其$injector;rxjs:{...RxJS, ...RxJSOperators},即 rxjs 全量 API 及操作符集合,本示例中的forkJoin、of均来自该对象。
也就是说,示例中的取服务方式等价于先通过servicesMap查得服务类,再通过$injector.get()创建实例。这些服务对应后端 REST API 封装,其定义位于 ui-ngx/src/app/core/http 目录下。
三、入口函数与控制器:customDialog 的调用模式
openEditEntityDialog(); function openEditEntityDialog() { customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe(); } function EditEntityDialogController(instance) { let vm = instance; // ... 控制器逻辑 }customDialog.customDialog(template, controller, data?, config?)是服务公开的唯一入口,其实现见 custom-dialog.service.ts。调用流程为:
- 将
sharedModule、CommonModule及各 Home 组件模块作为 imports,调用DynamicComponentFactoryService.createDynamicComponent()把htmlTemplate编译成一个继承自CustomDialogComponent的动态组件类型; - 将
controller与动态组件类型一起封装进CustomDialogContainerData,以disableClose: true、全屏面板类tb-dialog/tb-fullscreen-dialog打开MatDialog对话框; - 对话框关闭后通过
tap()销毁动态组件,避免内存泄漏。
而控制器EditEntityDialogController(instance)接收的instance就是CustomDialogComponent实例。从 custom-dialog.component.ts 可以看到该基类内置了:
dialogRef:MatDialogRef,控制器中vm.dialogRef.close(...)即用它关闭对话框;fb:UntypedFormBuilder,用于构建响应式表单;validators:AngularValidators对象;data:CUSTOM_DIALOG_DATA注入令牌。
构造函数末尾会立即调用this.data.controller(this),把实例交给控制器初始化。
四、响应式表单结构:编辑对话框的字段设计
控制器用vm.fb.group()构建了表单模型:
vm.editEntityFormGroup = vm.fb.group({ entityName: ['', [vm.validators.required]], entityType: [null], 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: [false] }), oldRelations: vm.fb.array([]), relations: vm.fb.array([]) });字段说明:
| 字段 | 类型 | 校验 | 用途 |
|---|---|---|---|
entityName | 文本 | required | 实体名称,示例中设为只读 |
entityType | 文本 | 无 | 实体类型(DEVICE / ASSET),只读 |
entityLabel | 文本 | 无 | 实体标签,可编辑 |
type | 文本 | required | 实体类型(设备/资产所属自定义类型),只读 |
attributes.latitude/longitude | 数值 | 无 | 服务端属性,经纬度 |
attributes.address/owner | 文本 | 无 | 服务端属性 |
attributes.number | 数值 | pattern(/^-?[0-9]+$/) | 整数属性,非法值会触发校验错误 |
attributes.booleanValue | 布尔 | 无 | 布尔属性 |
oldRelations | FormArray | — | 已存在的实体关系(只读回显) |
relations | FormArray | — | 待新增的实体关系 |
关系条目也通过表单组约束,新增关系必须填写相关实体、关系类型与方向:
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.entitySearchDirection = {from: "FROM", to: "TO"},即 ThingsBoard 关系模型中的FROM(当前实体为源)与TO(当前实体为目标)。
五、数据加载:forkJoin 并发拉取实体信息
对话框打开后,控制器调用getEntityInfo()通过widgetContext.rxjs.forkJoin并行发起四个请求:
widgetContext.rxjs.forkJoin([ entityRelationService.findInfoByFrom(entityId), // 当前实体作为 FROM 源的关系 entityRelationService.findInfoByTo(entityId), // 当前实体作为 TO 目标的关系 attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE'), // 服务端属性 entityService.getEntity(entityId.entityType, entityId.id) // 实体详情 ]).subscribe(...)在回调中:
getEntityRelations(data.slice(0, 2)):分别遍历relationsFrom(FROM 方向)与relationsTo(TO 方向),把每条关系转换为{direction, relationType, relatedEntity}结构存入vm.oldRelationsData,并调用addOldRelation()往oldRelationsFormArray 追加一条禁用状态的条目(已存在关系不允许在表单里改方向与类型,只能删除);getEntityAttributes(data[2]):把服务端属性数组转换为{key: value}对象存入vm.attributes,作为后续比对「哪些属性被修改」的基线;vm.entity = data[3]:保存实体对象;vm.editEntityFormGroup.patchValue({...}, {emitEvent: false}):一次性回填表单,emitEvent: false避免回填过程触发校验与订阅通知。
注意attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE')与saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray)使用的都是SERVER_SCOPE(服务端作用域),即这些属性只能由平台/规则引擎修改、对设备不可见,适合存放经纬度、地址等元数据。该作用域枚举在 attribute.service.ts 中定义。
六、保存逻辑:属性、关系、实体的三段式提交
vm.save()同样使用forkJoin合并三类保存任务:
vm.save = function() { vm.editEntityFormGroup.markAsPristine(); widgetContext.rxjs.forkJoin([ saveAttributes(entityId), saveRelations(entityId), saveEntity() ]).subscribe(function () { widgetContext.updateAliases(); vm.dialogRef.close(null); }); };1. 保存属性:差量更新
saveAttributes遍历表单attributes子组的每个 key,与加载时保存的vm.attributes基线比对,只把值发生变化的属性组装成{key, value}数组提交,避免无谓写入:
for (let key in attributes) { if (attributes[key] !== vm.attributes[key]) { attributesArray.push({key: key, value: attributes[key]}); } } if (attributesArray.length > 0) { return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray); } return widgetContext.rxjs.of([]); // 无变化时返回空 Observable,保证 forkJoin 可正常完成widgetContext.rxjs.of([])是关键细节:forkJoin需要所有 Observable 都发出值才能汇聚,因此「没有需要保存的内容」时也要返回一个空流。
2. 保存关系:新增 + 删除
saveRelations处理两类任务:
- 新增关系:遍历
relationsFormArray 的值,根据direction确定from/to端点(FROM表示当前实体是关系源,TO表示当前实体是关系目标),补上typeGroup: 'COMMON'后调用entityRelationService.saveRelation(relation); - 删除旧关系:用户在对话框中点击旧关系条目的删除按钮时,
removeOldRelation(index, relation)会把该关系对象 push 进vm.relationsToDelete,保存时统一调用entityRelationService.deleteRelation(relation.from, relation.type, relation.to)。
这些 REST 方法定义在 entity-relation.service.ts:saveRelation对应关系新增/更新接口,deleteRelation按(from, type, to)三元组精确删除。
3. 保存实体:仅更新标签
saveEntity只关心表单里可编辑的entityLabel:如果标签发生了变化,才按实体类型分别调用assetService.saveAsset(vm.entity)或deviceService.saveDevice(vm.entity)提交实体对象;否则返回空流。这也是为什么entityName、entityType、type在表单中一律设为readonly——这些字段由平台管理,示例刻意不允许用户在动作中篡改。
七、HTML 模板要点:表单与对话框的绑定
配套的 HTML 模板(custom_pretty_edit_dialog_html.md)核心绑定关系如下:
<form #editEntityForm="ngForm" [formGroup]="editEntityFormGroup" (ngSubmit)="save()">:根表单,提交时触发保存;mat-toolbar标题动态拼接Edit {{entityType.toLowerCase()}} {{entityName}};mat-progress-bar通过isLoading$ | async控制加载进度条(该订阅变量由CustomDialogComponent基类的PageComponent提供);- 顶层
mat-form-field使用formControlName="entityName" / "entityLabel" / "entityType" / "type"与表单组绑定; - 属性区使用
formGroupName="attributes"分组,latitude/longitude/address/owner/number/booleanValue各有对应输入控件,其中number字段在hasError('pattern')时显示「Invalid integer value.」错误提示; - 关系区使用
formArrayName="oldRelations"与formArrayName="relations"遍历两条 FormArray,每个条目内用formGroupName="i"索引绑定direction(mat-select+entitySearchDirection)、relationType(tb-relation-type-autocomplete关系类型自动补全组件)、relatedEntity(tb-entity-select实体选择组件); - 旧关系条目的删除按钮调用
removeOldRelation(i, relation.value),新关系条目的删除按钮调用removeRelation(i),「Add」按钮调用addRelation(); - 底部操作区:
Cancel按钮调用cancel()(vm.dialogRef.close(null)),Save提交按钮的禁用条件是editEntityForm.invalid || !editEntityForm.dirty,即表单非法或未做任何修改时不可提交。
模板中使用的tb-relation-type-autocomplete、tb-entity-select是 ThingsBoard 自带的复合表单控件,它们在运行时由CustomDialogService注入的sharedHomeComponentsModule/homeComponentsModule/widgetComponentsModule等模块提供,这正解释了为什么对话框里能直接用这些组件——它们是动态组件编译时被合并进 imports 的。
八、从源码验证完整调用链
结合源码可以把整个动作的运行时链路串起来:
- Dashboard 中 Widget 配置的 JS 动作函数被执行时,框架注入
widgetContext(widget-component.models.ts); - 函数从
$injector取出customDialog(CustomDialogService)等服务; CustomDialogService.customDialog()调用DynamicComponentFactoryService把 HTML 模板实时编译为 Angular 组件(custom-dialog.service.ts);- 动态组件继承
CustomDialogComponent,其构造函数执行this.data.controller(this)即调用我们的EditEntityDialogController(custom-dialog.component.ts); - 控制器内通过
vm.fb、vm.validators、vm.dialogRef完成表单构建、校验与关闭; - 保存时经
attributeService/entityRelationService/assetService/deviceService(位于 ui-ngx/src/app/core/http)调用后端 REST API; - 全部保存完成后调用
widgetContext.updateAliases()刷新 Widget 的实体别名数据,再dialogRef.close(null)关闭对话框。
与示例同目录的其他文件(custom_pretty_create_dialog_js.md、custom_pretty_clone_device_js.md 等)复用同一套customDialog + 控制器模式,可对比学习新增、克隆等不同业务场景的写法。此外,Widget 动作配置内置的示例模板 custom-sample-js.raw 也给出了customDialog.customDialog(htmlTemplate, ...)的最小可运行骨架,可作为改造起点。
九、改造与复用建议
基于本示例,你可以按以下思路进行定制:
- 调整属性集合:修改
attributes子组的字段与对应 HTML 输入控件,即可编辑任意服务端属性;如需编辑共享/客户端作用域属性,把两处'SERVER_SCOPE'换成'SHARED_SCOPE'或'CLIENT_SCOPE'即可(作用域枚举见 attribute.service.ts); - 扩展校验规则:在表单字段上追加
vm.validators.required、pattern、min/max等,并同步在 HTML 中增加mat-error提示,参考number字段的整数校验写法; - 开放更多实体字段:若希望允许修改实体名称等字段,可去掉对应输入框的
readonly,并在saveEntity中按需提交entity.name; - 更换目标实体类型:把
saveEntity中的分支扩展为其他实体(如entityService.saveEntity或专门的 service)即可支持更多实体类型,后端统一封装入口可参考 entity.service.ts; - 对话框层级与行为:
customDialog()的第四个参数config可覆盖MatDialogConfig(如disableClose、width),需要非全屏或可点击遮罩关闭时可在此调整。
小结
本文完整还原了 ThingsBoard「编辑设备/资产」自定义动作的官方示例:从widgetContext获取服务、customDialog动态编译 HTML 模板、控制器初始化响应式表单,到属性差量保存、关系新增/删除、实体标签更新,再到底层CustomDialogService、CustomDialogComponent与各 HTTP 服务类的源码佐证。掌握这套「JS 函数 + HTML 模板 + 控制器」三段式写法后,你可以在 ThingsBoard Dashboard 中自由构建属于自己的编辑、创建、克隆类业务对话框,而无需改动平台本体代码。
- 物联网
- 后端
- 数据可视化
- 消息队列
【免费下载链接】thingsboard
All-in-one IoT Platform - Device management, data collection, processing and visualization.
相关推荐
ThingsBoard 自定义操作实战:用 HTML 模板构建美化版编辑对话框(编辑设备/资产)
ThingsBoard 自定义操作实战:用 HTML 模板构建美化版编辑对话框(编辑设备/资产) 本篇指南讲解 ThingsBoard 物联网平台中「自定义操作
物联网后端数据可视化消息队列Fleet 后端开发模式指南:API 输入校验、Go 与 MySQL 工程实践与 GitOps 落地
Fleet 后端开发模式指南:API 输入校验、Go 与 MySQL 工程实践与 GitOps 落地 Fleet 是一套开源的设备管理平台(Open devic
物联网后端数据可视化消息队列Task 模板引擎全解析:从变量插值到函数库的 Templating Reference 实战指南
Task 模板引擎全解析:从变量插值到函数库的 Templating Reference 实战指南 导读 Task(本项目为 GitHub 加速计划 / ta
物联网后端数据可视化消息队列
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考