☰
ThingsBoard Widget Action 实战:用 customDialog 与服务注入实现“克隆设备“对话框
2026/10/3 8:22:57 网站建设 项目流程
  • 物联网
  • 后端
  • 数据可视化
  • 消息队列

【免费下载链接】thingsboard

All-in-one IoT Platform - Device management, data collection, processing and visualization.

项目地址:https://gitcode.com/GitHub_Trending/th/thingsboard
点击查看免费下载

本篇指南聚焦 ThingsBoard 前端(ui-ngx)中自定义 Widget Action 的高级用法,以官方帮助文档中"克隆设备(Clone Device)"示例为核心,完整讲解如何通过widgetContext的$injector与servicesMap注入deviceService、attributeService等 Angular 服务,配合customDialog动态渲染自定义对话框,并使用 RxJSmergeMap链实现"读取原设备 → 创建新设备 → 拷贝服务端属性 → 刷新数据源"的完整闭环。读完本篇,你将掌握在 ThingsBoard 表格/卡片等组件动作中编写可复用、可交互的自定义 JS Action 的完整方法论,并理解其底层运行原理。

一、应用场景:从表格行按钮到设备克隆

在 ThingsBoard 的仪表板中,表格(Table)、卡片(Card)、实体列表等 Widget 的"Actions"配置允许你在每一行数据上挂接自定义交互。本示例演示的场景是:点击某一台设备的行内按钮,弹出一个对话框,输入新设备名称,即可克隆该设备并复制其服务端属性。

这份示例代码位于仓库 custom_pretty_clone_device_js.md,同目录下还配套了对话框 HTML 模板文档 custom_pretty_clone_device_html.md。官方还提供了同风格的其他示例(创建实体对话框、编辑实体、创建用户、编辑图片等),可参考ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/目录下的其他*_js.md/*_html.md文件。

二、核心 API:WidgetContext 与依赖注入

示例代码的第一段就是整个脚本的运行基础:

const $injector = widgetContext.$scope.$injector; const customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); const attributeService = $injector.get(widgetContext.servicesMap.get('attributeService')); const deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); const rxjs = widgetContext.rxjs;

逐行说明:

表达式含义
widgetContext.$scope.$injector当前动态组件作用域的 AngularInjector,是获取服务的唯一入口。widgetContext.$scope即动态 Widget 组件的 scope(对应源码中的IDynamicWidgetComponent)
widgetContext.servicesMap.get('customDialog')从服务映射表中取出服务类(Token),servicesMap是Map<string, Type<any>>,把字符串名映射到服务类型
$injector.get(...)通过 Angular 依赖注入真正拿到服务实例
widgetContext.rxjs由源码 widget-component.models.ts 定义的rxjs对象,它展开合并了rxjs与rxjs/operators的全部导出,因此mergeMap、of等操作符可直接以rxjs.mergeMap(...)形式调用,无需再单独 import

从源码WidgetContext类定义(widget-component.models.ts)可以看到,servicesMap字段类型为Map<string, Type<any>>,$injector类型为Injector,$scope为动态组件 scope。官方自带的完整示例custom-sample-js.raw(见 custom-sample-js.raw)中也大量采用$injector.get(widgetContext.servicesMap.get('xxxService'))这一标准写法,可见这是 ThingsBoard 自定义 Action 获取后端服务的事实标准模式。

本示例注入了三个服务:

  • customDialog:动态对话框服务,源码位于 custom-dialog.service.ts,其核心方法签名如下:
customDialog(template: string, controller: (instance: CustomDialogComponent) => void, data?: any, config?: MatDialogConfig): Observable<any>

它接收一段HTML 模板字符串和一个控制器函数,通过dynamicComponentFactoryService.createDynamicComponent在运行时编译模板、打开MatDialog对话框,并在关闭后销毁动态组件。

  • deviceService:设备 CRUD 服务(源码位于ui-ngx/src/app/core/http/device.service.ts),提供getDevice、saveDevice等方法。
  • attributeService:属性服务(源码位于ui-ngx/src/app/core/http/attribute.service.ts),提供getEntityAttributes、saveEntityAttributes等方法。

三、打开克隆对话框

openCloneDeviceDialog(); function openCloneDeviceDialog() { customDialog.customDialog(htmlTemplate, CloneDeviceDialogController).subscribe(); }

htmlTemplate是你在 Action 的HTML 模板编辑区里写的字符串(即姐妹文档 custom_pretty_clone_device_html.md 提供的那份表单模板)。customDialog()返回一个Observable,subscribe()触发对话框打开,对话框关闭后该流完成。

提示:htmlTemplate变量名需与 HTML 模板编辑区中的命名一致,ThingsBoard 会把两个编辑区的内容在同一作用域内求值。官方示例在 HTML 侧同样命名为htmlTemplate。

四、对话框控制器:表单初始化与按钮逻辑

控制器函数在对话框组件实例上挂载逻辑:

function CloneDeviceDialogController(instance) { let vm = instance; vm.deviceName = entityName; vm.cloneDeviceFormGroup = vm.fb.group({ cloneName: ['', [vm.validators.required]] }); vm.save = function() { /* ... 见下节 ... */ }; vm.cancel = function() { vm.dialogRef.close(null); }; }

要点:

  • instance(vm)是CustomDialogComponent实例,自带fb(FormBuilder)、validators(Angular Validators)、dialogRef(MatDialogRef)等可选项,直接挂载属性与方法即可被模板引用。
  • entityName是当前行实体的名称,由 Widget Action 上下文自动注入,可直接作为变量使用(无需声明);同理entityId是当前行实体 ID。它们与数据行数据entityId/entityName一一对应,源码中WidgetContext相关定义可参见 widget-component.models.ts 中实体参数的注入逻辑。
  • 表单校验规则:cloneName必填(vm.validators.required),与 HTML 模板中的required输入框及hasError('required')错误提示配合使用。

配套 HTML 模板(节选自 custom_pretty_clone_device_html.md)通过[formGroup]="cloneDeviceFormGroup"绑定表单,(ngSubmit)="save()"提交保存,Save 按钮在cloneDeviceFormGroup.invalid或表单未变更(!dirty)时禁用,同时用isLoading$ | async控制进度条显示。

五、克隆主流程:RxJS mergeMap 链

保存逻辑是整个示例的核心:

vm.save = function() { deviceService.getDevice(entityId.id).pipe( rxjs.mergeMap((origDevice) => { let cloneDevice = { name: vm.cloneDeviceFormGroup.get('cloneName').value, type: origDevice.type }; return deviceService.saveDevice(cloneDevice).pipe( rxjs.mergeMap((newDevice) => { return attributeService.getEntityAttributes(origDevice.id, 'SERVER_SCOPE').pipe( rxjs.mergeMap((origAttributes) => { return attributeService.saveEntityAttributes(newDevice.id, 'SERVER_SCOPE', origAttributes); }) ); }) ); }) ).subscribe(() => { widgetContext.updateAliases(); vm.dialogRef.close(null); }); };

该链的执行顺序与数据流:

  1. deviceService.getDevice(entityId.id)—— 根据当前行实体的entityId.id读取原始设备完整信息;
  2. 基于原始设备构造克隆体:{ name: 新名称, type: 原类型 }(仅复制名称与类型,其余如配置文件、证书等不在本示例范围内);
  3. deviceService.saveDevice(cloneDevice)—— 保存新设备,返回新设备对象;
  4. attributeService.getEntityAttributes(origDevice.id, 'SERVER_SCOPE')—— 读取原设备服务端作用域(SERVER_SCOPE)的全部属性;
  5. attributeService.saveEntityAttributes(newDevice.id, 'SERVER_SCOPE', origAttributes)—— 把属性原样写入新设备;
  6. 链尾subscribe(() => {...})中调用widgetContext.updateAliases()刷新 Widget 数据源(让新设备立刻出现在当前表格/列表中),并关闭对话框。

采用嵌套mergeMap而非forkJoin的原因很直观:第 4、5 步强依赖第 3 步返回的newDevice,整个流程是严格的串行依赖,嵌套mergeMap是最贴合语义的表达方式。

六、属性拷贝范围与作用域说明

'SERVER_SCOPE'是 ThingsBoard 三种属性作用域之一(另两种为CLIENT_SCOPE与SHARED_SCOPE)。本示例只拷贝服务端属性。getEntityAttributes(origDevice.id, 'SERVER_SCOPE')返回该设备全部服务端属性数组,saveEntityAttributes(newDevice.id, 'SERVER_SCOPE', origAttributes)的第三参数直接接收该数组整体写入,attributeService底层会将其拆分为逐个属性保存请求。若需同时拷贝其他作用域,可按同样模式在 mergeMap 链中继续追加;若需跳过属性为空等边界情况,可在saveEntityAttributes前加rxjs.filter判断。

七、实战配置与注意事项

配置入口

在 ThingsBoard 仪表板编辑模式下打开目标 Widget 的Actions配置,选择行内动作(如Row click或自定义按钮),将 Action Type 设置为基于 JavaScript 的自定义动作,然后:

  • 在HTML编辑区粘贴 custom_pretty_clone_device_html.md 的模板;
  • 在JavaScript编辑区粘贴本示例代码;
  • 两处代码均以{:copy-code}标注,官方文档已内置一键复制功能。

代码风格约定

官方示例约定控制器函数采用 PascalCase 命名(如CloneDeviceDialogController),并在{:code-style="max-height: 400px;"}中设置代码块展示高度,这些约定在编辑器中可直接复用。

实践要点

  • 关闭对话框:vm.dialogRef.close(null)的入参null会作为customDialog().subscribe()的回传值,可用于通知调用方结果。
  • 界面刷新:保存成功后务必调用widgetContext.updateAliases(),否则新设备不会实时反映到当前 Widget 的数据源中(官方示例 custom-sample-js.raw 中也多处使用该调用)。
  • 表单校验:借助vm.validators.required与cloneDeviceFormGroup.invalid/dirty控制提交按钮状态,避免空名称入库。
  • 错误处理:示例未显式处理异常,生产环境建议在链尾追加rxjs.catchError,在失败时通过widgetContext.toastTargetId或widgetContext.dialogs给出提示并保持对话框打开。

八、小结

本示例虽短,却是 ThingsBad 自定义 Action 领域的"最小完整工程":它示范了服务注入(servicesMap + $injector)→ 动态对话框(customDialog)→ 表单(fb/validators)→ 串行数据流(rxjs.mergeMap)→ 界面联动(updateAliases)的完整范式。掌握这套组合拳后,你可以据此扩展出"克隆资产""批量创建用户""复制属性到其他实体"等更多场景。相关源码可继续深入阅读 widget-component.models.ts(WidgetContext 定义)、custom-dialog.service.ts(动态对话框实现)以及官方完整示例 custom-sample-js.raw。

  • 物联网
  • 后端
  • 数据可视化
  • 消息队列

【免费下载链接】thingsboard

All-in-one IoT Platform - Device management, data collection, processing and visualization.

项目地址:https://gitcode.com/GitHub_Trending/th/thingsboard
点击查看免费下载
上一篇:ComfyUI IPAdapter终极配置指南:3步解决模型加载失败问题
下一篇:Android钉钉自动打卡终极方案:告别迟到烦恼

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询