ThingsBoard 自定义动作实战:使用 HTML 模板 + JavaScript 控制器实现设备克隆对话框

发布时间:2026/10/3 2:03:15
ThingsBoard 自定义动作实战:使用 HTML 模板 + JavaScript 控制器实现设备克隆对话框
物联网后端数据可视化消息队列【免费下载链接】thingsboardAll-in-one IoT Platform - Device management, data collection, processing and visualization.项目地址https://gitcode.com/GitHub_Trending/th/thingsboard点击查看免费下载本篇技术指南聚焦 ThingsBoard 仪表板 widget 的“自定义动作Custom Action with HTML template”能力以官方帮助文档中的克隆设备对话框custom_pretty_clone_device_html.md与配套的custom_pretty_clone_device_js.md为完整案例逐行讲解其 HTML 模板结构与 JavaScript 控制器实现并结合ui-ngx前端源码剖析customDialog服务的动态编译原理与底层 REST API 调用链。读完本文你将能够独立编写创建/编辑/克隆设备、添加用户、编辑图片等自定义对话框动作并理解模板与控制器之间通过CustomDialogComponent实例桥接的机制。一、场景与前置知识什么是带 HTML 模板的自定义动作在 ThingsBoard 中widget 的动作配置支持两种自定义函数普通自定义动作Custom action function函数签名为function ($event, widgetContext, entityId, entityName, additionalParams, entityLabel): void定义见 custom_action_fn.md适合弹窗、跳转状态、复制令牌等轻量操作。带 HTML 模板的自定义动作Custom action with HTML template函数签名为function ($event, widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel): void定义见 custom_pretty_action_fn.md。与前者相比多出htmlTemplate参数——即你在动作配置的HTML页签里编写的一段 Angular 模板字符串用于渲染自定义对话框界面而JavaScript页签中的函数负责业务逻辑与数据读写。函数参数说明参数类型含义$eventMouseEvent触发动作的鼠标事件对象widgetContextWidgetContext当前 widget 实例上下文持有 API 与数据定义见 widget-component.models.tsentityIdstring目标实体 ID可选entityNamestring目标实体名称可选htmlTemplatestringHTML 页签中定义的模板字符串用于渲染自定义对话框additionalParams{[key: string]: any}附加实体参数按 widget 类型与动作来源不同而不同详见 custom_additional_params.mdentityLabelstring目标实体标签可选在官方帮助文档中这类漂亮对话框示例统一存放在 examples_custom_pretty 目录下包括创建/编辑设备或资产、创建用户、编辑实体属性图片以及本文重点讲解的克隆设备四组示例每组均包含 HTML 模板与 JS 控制器两个文件custom_pretty_create_dialog_html.md / custom_pretty_create_dialog_js.md —— 创建设备/资产custom_pretty_edit_dialog_html.md / custom_pretty_edit_dialog_js.md —— 编辑设备/资产custom_pretty_create_user_html.md / custom_pretty_create_user_js.md —— 创建用户custom_pretty_edit_image_html.md / custom_pretty_edit_image_js.md —— 编辑实体属性图片custom_pretty_clone_device_html.md / custom_pretty_clone_device_js.md ——克隆设备本文主题二、克隆设备对话框HTML 模板逐段解析以下代码来自 custom_pretty_clone_device_html.md是克隆设备对话框的完整 Angular 模板。把它原样粘贴到自定义动作配置的HTML页签即可form [formGroup]cloneDeviceFormGroup (ngSubmit)save() stylemin-width:320px; mat-toolbar classflex flex-row colorprimary h2Clone device: {{ deviceName }}/h2 span classflex-1/span button mat-icon-button (click)cancel() typebutton mat-icon classmaterial-iconsclose /mat-icon /button /mat-toolbar mat-progress-bar colorwarn modeindeterminate *ngIfisLoading$ | async /mat-progress-bar div styleheight: 4px; *ngIf!(isLoading$ | async)/div div mat-dialog-content classflex flex-col mat-form-field classmat-block flex-1 mat-labelClone device name/mat-label input matInput formControlNamecloneName required mat-error *ngIfcloneDeviceFormGroup.get(cloneName).hasError(required) Clone device name is required /mat-error /mat-form-field /div div mat-dialog-actions classflex flex-row items-center justify-end button mat-button colorprimary typebutton [disabled](isLoading$ | async) (click)cancel() cdkFocusInitial Cancel /button button mat-button mat-raised-button colorprimary typesubmit [disabled](isLoading$ | async) || cloneDeviceFormGroup.invalid || !cloneDeviceFormGroup.dirty Save /button /div /form2.1 表单根元素formGroup 与 ngSubmit 绑定form [formGroup]cloneDeviceFormGroup (ngSubmit)save() stylemin-width:320px;[formGroup]cloneDeviceFormGroup把模板绑定到控制器中由vm.fb.group({...})创建的表单组实例。模板中引用的cloneDeviceFormGroup就来自 JS 控制器里挂载在vm上的同名属性。(ngSubmit)save()表单提交回车或点击 submit 按钮时调用控制器中的vm.save函数。stylemin-width:320px;保证对话框在窄屏下也有可用宽度。2.2 标题工具栏mat-toolbarmat-toolbar classflex flex-row colorprimary h2Clone device: {{ deviceName }}/h2 span classflex-1/span button mat-icon-button (click)cancel() typebutton mat-icon classmaterial-iconsclose /mat-icon /button /mat-toolbarcolorprimary使用主题主色渲染工具栏。{{ deviceName }}插值表达式输出当前被克隆设备的名称该值由控制器中的vm.deviceName entityName提供entityName是自定义动作函数的入参。右侧的close图标按钮通过(click)cancel()调用vm.cancel关闭对话框typebutton确保它不会触发表单提交。2.3 加载指示mat-progress-barmat-progress-bar colorwarn modeindeterminate *ngIfisLoading$ | async /mat-progress-bar div styleheight: 4px; *ngIf!(isLoading$ | async)/div当isLoading$ | async为真时显示不确定进度条modeindeterminatecolorwarn提示用户克隆请求正在处理。isLoading$是一个 Observable由控制器或 PageComponent 基类维护当保存操作进行中时发出true。为了让进度条出现/消失不引起布局跳动模板用*ngIf的互补逻辑在非加载状态下渲染一个height: 4px的占位 div。值得注意虽然官方示例模板中引用了isLoading$但克隆设备示例的 JS 控制器并没有显式定义它——这说明isLoading$实际上由PageComponent基类提供loading$/isLoading$这类加载状态流在 page.component.ts 中定义这正是HTML 模板可用基类能力的体现。2.4 输入区域mat-form-field 与表单校验div mat-dialog-content classflex flex-col mat-form-field classmat-block flex-1 mat-labelClone device name/mat-label input matInput formControlNamecloneName required mat-error *ngIfcloneDeviceFormGroup.get(cloneName).hasError(required) Clone device name is required /mat-error /mat-form-field /divmat-dialog-contentMaterial 对话框内容容器指令提供标准的内边距与滚动行为。input matInput formControlNamecloneName required输入框绑定到表单组中的cloneName控件并通过required配合控制器中vm.validators.required强制非空。mat-error中的cloneDeviceFormGroup.get(cloneName).hasError(required)响应式校验表达式当克隆名称为空时显示 Clone device name is required 错误提示。Angular 响应式表单校验的状态invalid、dirty等在此被完整复用。2.5 操作按钮Cancel / Savediv mat-dialog-actions classflex flex-row items-center justify-end button mat-button colorprimary typebutton [disabled](isLoading$ | async) (click)cancel() cdkFocusInitial Cancel /button button mat-button mat-raised-button colorprimary typesubmit [disabled](isLoading$ | async) || cloneDeviceFormGroup.invalid || !cloneDeviceFormGroup.dirty Save /button /divCancel 按钮typebutton(click)cancel()加载期间禁用cdkFocusInitial使其获得初始焦点。Save 按钮typesubmit触发ngSubmitmat-raised-button提升视觉层级。禁用条件为三者的或(isLoading$ | async) || cloneDeviceFormGroup.invalid || !cloneDeviceFormGroup.dirty。其中dirty条件意味着用户未修改表单时 Save 不可点击避免产生无意义的克隆操作——这是该示例的一个实用细节。三、克隆设备对话框JavaScript 控制器逐段解析以下代码来自 custom_pretty_clone_device_js.md把它原样粘贴到自定义动作配置的JavaScript页签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; openCloneDeviceDialog(); function openCloneDeviceDialog() { customDialog.customDialog(htmlTemplate, CloneDeviceDialogController).subscribe(); } function CloneDeviceDialogController(instance) { let vm instance; vm.deviceName entityName; vm.cloneDeviceFormGroup vm.fb.group({ cloneName: [, [vm.validators.required]] }); 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); }); }; vm.cancel function() { vm.dialogRef.close(null); }; }3.1 依赖注入$injector 与 servicesMapconst $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是 Angular 根注入器widgetContext.servicesMap是 widget 上下文暴露的服务名 → 服务类型映射表定义于 widget-component.models.ts动作脚本通过$injector.get(servicesMap.get(xxx))取得对应服务的实例。本例注入三个关键依赖customDialogCustomDialogService负责把 HTML 模板动态编译为组件并打开 Material 对话框deviceServiceDeviceServicedevice.service.ts封装设备 REST APIattributeServiceAttributeServiceattribute.service.ts封装实体属性读写rxjs widgetContext.rxjswidget 上下文内置的 RxJS 与操作符合集定义见 widget-component.models.ts这里用到mergeMap串联异步请求。3.2 打开对话框customDialog.customDialog(htmlTemplate, controller)openCloneDeviceDialog(); function openCloneDeviceDialog() { customDialog.customDialog(htmlTemplate, CloneDeviceDialogController).subscribe(); }htmlTemplate即自定义动作函数的第四个入参对应 HTML 页签中的模板字符串。customDialog.customDialog(template, controller)返回一个Observable.subscribe()触发对话框打开。底层原理见 custom-dialog.service.tscustomDialog()方法通过DynamicComponentFactoryService.createDynamicComponent(...)把模板字符串编译成动态组件类CustomDialogComponentInstance继承自CustomDialogComponent将{controller, customComponentType, data}打包进CustomDialogContainerData再调用MatDialog.open打开CustomDialogContainerComponent并在对话框关闭后销毁动态组件。3.3 控制器回调CloneDeviceDialogController(instance)function CloneDeviceDialogController(instance) { let vm instance; vm.deviceName entityName; vm.cloneDeviceFormGroup vm.fb.group({ cloneName: [, [vm.validators.required]] }); ... }customDialog在动态组件实例化完成后会调用你传入的控制器函数并把CustomDialogComponent 实例作为instance传入见 custom-dialog.component.ts 中this.data.controller(this)。控制器内通过let vm instance约定俗成地把所有属性和方法挂到vm上供 HTML 模板引用。该基类实例已内置vm.fbUntypedFormBuilder用于构建响应式表单vm.validatorsAngularValidators集合required、pattern等vm.dialogRefMatDialogRefCustomDialogContainerComponent用于关闭对话框vm.data注入的自定义对话框数据。vm.deviceName entityName把动作函数的实体名入参写入控制器模板中的{{ deviceName }}便由此渲染。cloneDeviceFormGroup只含一个cloneName控件并施加required校验——与模板中的formControlNamecloneName及hasError(required)一一对应。3.4 保存流程三级 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); }); };整个克隆过程是一条严格有序的异步流水线用mergeMap把 4 个请求依次串起来读取原设备deviceService.getDevice(entityId.id)获取源设备完整信息。该调用对应 RESTGET /api/device/{deviceId}见 device.service.ts。创建克隆设备取原设备的type与表单输入的新名称构造cloneDevice {name, type}调用deviceService.saveDevice(cloneDevice)对应 RESTPOST /api/device见 device.service.ts。注意克隆仅复制名称 类型设备凭证Access Token、证书等不会被复制新设备需要重新配置凭证。读取原设备服务端属性attributeService.getEntityAttributes(origDevice.id, SERVER_SCOPE)拉取源设备SERVER_SCOPE服务端作用域下的全部属性对应 RESTGET /api/plugins/telemetry/{entityType}/{entityId}/values/attributes/SERVER_SCOPE见 attribute.service.ts。AttributeData为{key, value}键值对数组。写入克隆设备属性attributeService.saveEntityAttributes(newDevice.id, SERVER_SCOPE, origAttributes)把原属性批量写入新设备对应 RESTPOST /api/plugins/telemetry/{entityType}/{entityId}/SERVER_SCOPE见 attribute.service.ts。整条链完成后widgetContext.updateAliases()刷新 widget 数据源中的实体别名使表格/卡片等展示立即反映新创建的设备对应 widget-component.models.ts 中的updateAliases(aliasIds?)内部委托给aliasController.updateAliases。vm.dialogRef.close(null)关闭对话框。close(null)传入空结果表示本次操作无返回值交给订阅者。3.5 取消流程vm.cancel function() { vm.dialogRef.close(null); };Cancel 只做一件事直接关闭对话框不做任何数据变更。模板中的关闭图标按钮与 Cancel 按钮都指向它。四、模板与控制器如何桥接CustomDialog 动态编译机制要真正理解这套 HTML/JS 双页签机制需要看前端的动态编译链路。核心源码如下入口custom-dialog.service.ts ——customDialog(template, controller, data?, config?)将模板交给DynamicComponentFactoryService生成组件类然后把控制器、组件类型、附加数据装入CustomDialogContainerData用MatDialog打开容器组件对话框关闭后通过tap(() this.dynamicComponentFactoryService.destroyDynamicComponent(componentType))销毁动态组件避免内存泄漏。基类custom-dialog.component.ts —— 动态组件继承CustomDialogComponent派生自PageComponent通过inject()注入router、dialogRefMatDialogRefCustomDialogContainerComponent、dataCUSTOM_DIALOG_DATA与fbUntypedFormBuilder构造函数中执行this.data.controller(this)。这也解释了为什么模板里可以直接使用vm.fb、vm.validators、vm.dialogRef以及基类提供的加载状态流。容器custom-dialog-container.component.ts —— 容器组件构造时创建自定义注入器提供CUSTOM_DIALOG_DATA与MatDialogRef把动态组件实例化进视图若实例化失败会依据错误码NG0前缀为模板错误给出custom-pretty-template-error或custom-pretty-controller-error的本地化错误提示。从源码结构可以推断HTML 页签是视图JS 页签是控制器二者通过动态组件实例vm双向绑定——模板中的cloneDeviceFormGroup、deviceName、save()、cancel()、isLoading$都必须在控制器或基类上存在否则对话框会因模板编译/控制器执行错误而打开失败并弹出错误提示。五、底层 REST 调用链与属性作用域说明克隆设备动作虽然只写了 4 个服务方法但映射到后端 REST 接口是清晰且固定的步骤前端服务方法REST 接口源码位置1deviceService.getDevice(entityId.id)GET /api/device/{deviceId}device.service.ts2deviceService.saveDevice(cloneDevice)POST /api/devicedevice.service.ts3attributeService.getEntityAttributes(origDevice.id, SERVER_SCOPE)GET /api/plugins/telemetry/{entityType}/{entityId}/values/attributes/SERVER_SCOPEattribute.service.ts4attributeService.saveEntityAttributes(newDevice.id, SERVER_SCOPE, origAttributes)POST /api/plugins/telemetry/{entityType}/{entityId}/SERVER_SCOPEattribute.service.ts关于属性作用域需要特别说明示例固定使用SERVER_SCOPE服务端属性即只有设备存储于服务端的属性会被克隆客户端属性CLIENT_SCOPE、共享属性SHARED_SCOPE与遥测时序数据不在克隆范围内。saveEntityAttributes在 attribute.service.ts 中对值为null/未定义的属性会走删除分支其余属性以键值对形式打包提交——这意味着原设备中值为空的属性会在新设备上被删除而不是写入空值。entityId.id取自动作函数入参entityId形如{entityType: DEVICE, id: ...}因此该动作适用于任何提供实体上下文的自定义动作触发点行点击、操作单元格按钮等只要目标实体是设备即可。六、常见问题与使用建议对话框打开即报错若模板引用了控制器中不存在的属性/方法动态组件编译或执行会失败界面弹出custom-pretty-template-error模板语法错误以NG0开头或custom-pretty-controller-error控制器执行错误提示。排查时先保证模板里每一个绑定cloneDeviceFormGroup、deviceName、save、cancel在vm上都有定义。Save 按钮永远不可点注意模板的!cloneDeviceFormGroup.dirty条件——用户没有修改cloneName输入框时按钮保持禁用。这是刻意设计避免无输入的克隆请求。克隆后新设备不完整克隆只继承设备type与SERVER_SCOPE属性设备凭证、遥测历史、关系Relations、告警等均不复制。若需复制关系可参考 custom_pretty_edit_dialog_js.md 中entityRelationService的使用方式自行扩展。实体名重复ThingsBoard 设备名称在租户内唯一克隆时若cloneName与已有设备重名saveDevice对应的POST /api/device会返回冲突错误且整条mergeMap链会中断不会执行到属性复制步骤。复用模板结构本示例的mat-toolbar mat-progress-bar mat-dialog-content mat-dialog-actions骨架可复用到任意自定义对话框。官方同目录下的 custom_pretty_create_dialog_html.md、custom_pretty_create_user_html.md、custom_pretty_edit_image_html.md 均沿用这一结构配合各自的控制器即可实现创建、编辑、加人等场景。七、小结克隆设备自定义动作是 ThingsBoard 带 HTML 模板自定义动作的典型范例完整展示了三层能力视图层复用 Angular Material 组件mat-toolbar、mat-progress-bar、mat-form-field、mat-dialog-actions与响应式表单实现带校验、加载态、按钮状态控制的对话框控制层利用$injectorservicesMap注入CustomDialogService、DeviceService、AttributeService在CustomDialogComponent实例上组织表单、保存与取消逻辑数据层通过mergeMap串联读设备 → 创建设备 → 读属性 → 写属性四条 REST 调用最终刷新 widget 别名并关闭对话框。开发者可直接复制本文 HTML 与 JS 两份代码到任意实体类 widget 的自定义动作配置中即用也可以以此为模板扩展出克隆资产复制设备并迁移共享属性等更复杂的数据迁移动作。相关示例与源码均可在本仓库中继续查阅examples_custom_pretty 目录下的全部 10 个示例文件以及 custom-dialog.service.ts、custom-dialog.component.ts、device.service.ts、attribute.service.ts 等前端实现。赞分享物联网后端数据可视化消息队列【免费下载链接】thingsboardAll-in-one IoT Platform - Device management, data collection, processing and visualization.项目地址https://gitcode.com/GitHub_Trending/th/thingsboard点击查看免费下载相关推荐在 Lightning Fabric 训练循环中接入 Weights BiasesWandbLogger 使用指南在 Lightning Fabric 训练循环中接入 Weights BiasesWandbLogger 使用指南 本文以 Lightning Fabri物联网后端数据可视化消息队列ThingsBoard 仪表板自定义动作实战用 customDialog 与 HTML 模板实现设备/资产创建对话框ThingsBoard 仪表板自定义动作实战用 customDialog 与 HTML 模板实现设备/资产创建对话框 本指南聚焦 ThingsBoard 仪表物联网后端数据可视化消息队列Task 模板引擎全解析从变量插值到函数库的 Templating Reference 实战指南Task 模板引擎全解析从变量插值到函数库的 Templating Reference 实战指南 导读 Task本项目为 GitHub 加速计划 / ta物联网后端数据可视化消息队列上一篇Sidekiq-Cron版本升级指南从1.x到2.x的平滑迁移下一篇突破YOLO-World性能瓶颈从诊断到系统优化的实战指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考