ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ThingsBoard 自定义操作实战:用 Custom Action(带 HTML 模板)为实体属性添加/编辑图片

ThingsBoard 自定义操作实战:用 Custom Action(带 HTML 模板)为实体属性添加/编辑图片 物联网后端数据可视化消息队列【免费下载链接】thingsboardAll-in-one IoT Platform - Device management, data collection, processing and visualization.项目地址https://gitcode.com/GitHub_Trending/th/thingsboard点击查看免费下载导读本文基于 ThingsBoard 官方帮助文档中的custom_pretty_edit_image_js示例完整讲解如何为仪表盘组件编写一个Custom Action带 HTML 模板类型的 JavaScript 操作函数实现通过弹窗为设备、资产等实体的服务器端属性添加或编辑图片。文章不仅逐行拆解官方示例代码还深入对应的 Angular 源码CustomDialogService、AttributeService、ServicesMap等讲解其底层运行原理帮助你举一反三掌握 ThingsBoard 自定义对话框操作的完整开发模式。一、功能定位这个示例脚本做了什么官方示例文档的标题为 Function displaying dialog to add/edit image in entity attribute即显示用于在实体属性中添加/编辑图片的对话框函数。它的业务场景非常典型仪表盘某个组件例如实体卡片、设备列表配置了一个带 HTML 模板的自定义操作按钮用户点击按钮后弹出一个 Material Design 风格对话框对话框内通过tb-image-input图片上传组件展示并编辑实体名为image的服务端属性SERVER_SCOPE点击Save时脚本会做差异比较仅把发生变化的属性提交到服务端点击Cancel或关闭图标时直接关闭对话框且不提交任何数据。该示例属于 custom_pretty_action_fn.md 主文档中 Examples 章节的第五个示例与创建设备/资产对话框、编辑设备/资产对话框、创建用户对话框、克隆设备对话框同一目录下的custom_pretty_create_dialog_js、custom_pretty_edit_dialog_js、custom_pretty_create_user_js、custom_pretty_clone_device_js等共同构成一套完整的自定义对话框开发样例均存放在 examples_custom_pretty 目录下。二、动作函数的签名与参数约定带 HTML 模板的自定义操作Custom Pretty Action本质是一个 JavaScript 函数官方在 custom_pretty_action_fn.md 中给出了标准签名function ($event, widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel): void各参数含义如下参数类型说明$eventMouseEvent触发操作的鼠标事件对象通常是点击事件的结果widgetContextWidgetContext组件实例上下文引用包含组件运行所需的全部 API 与数据entityIdstring目标实体的可选 ID示例中用于定位要编辑属性的实体entityNamestring目标实体的可选名称示例中用于对话框标题展示htmlTemplatestring在HTML选项卡中定义的模板字符串用于渲染自定义对话框additionalParams{[key: string]: any}可选键值对象携带附加实体参数entityLabelstring目标实体的可选标签widgetContext的完整类型定义可参考 widget-component.models.ts 中的WidgetContext类它暴露了$scope、servicesMap、$injector、rxjs、showSuccessToast、showErrorToast等一系列工具能力是自定义操作脚本与前端框架交互的核心桥梁。三、完整示例代码与逐行剖析3.1 完整 JavaScript 函数官方示例源码custom_pretty_edit_image_js.md如下let $injector widgetContext.$scope.$injector; let customDialog $injector.get(widgetContext.servicesMap.get(customDialog)); let assetService $injector.get(widgetContext.servicesMap.get(assetService)); let attributeService $injector.get(widgetContext.servicesMap.get(attributeService)); let entityService $injector.get(widgetContext.servicesMap.get(entityService)); openAddEntityDialog(); function openAddEntityDialog() { customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe(() {}); } function AddEntityDialogController(instance) { let vm instance; vm.entityName entityName; vm.attributes {}; vm.editEntity vm.fb.group({ attributes: vm.fb.group({ image: [null] }) }); getEntityInfo(); vm.cancel function() { vm.dialogRef.close(null); }; vm.save function() { vm.loading true; saveAttributes(entityId).subscribe( () { vm.dialogRef.close(null); }, () { vm.loading false; } ); }; function getEntityAttributes(attributes) { for (var i 0; i attributes.length; i) { vm.attributes[attributes[i].key] attributes[i].value; } } function getEntityInfo() { vm.loading true; attributeService.getEntityAttributes(entityId, SERVER_SCOPE).subscribe( function (data) { getEntityAttributes(data); vm.editEntity.patchValue({ attributes: vm.attributes }, {emitEvent: false}); vm.loading false; } ); } function saveAttributes(entityId) { let attributes vm.editEntity.get(attributes).value; let attributesArray []; 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([]); } }3.2 服务注入从servicesMap获取依赖let $injector widgetContext.$scope.$injector; let customDialog $injector.get(widgetContext.servicesMap.get(customDialog)); let assetService $injector.get(widgetContext.servicesMap.get(assetService)); let attributeService $injector.get(widgetContext.servicesMap.get(attributeService)); let entityService $injector.get(widgetContext.servicesMap.get(entityService));脚本通过widgetContext.$scope.$injector拿到 Angular 依赖注入器InjectorwidgetContext.servicesMap是一个服务名 → 服务类型的映射表在 services.map.ts 中定义。该映射覆盖了customDialogCustomDialogService、assetServiceAssetService、attributeServiceAttributeService、entityServiceEntityService等数十个服务均注册于 widget.component.ts 中this.widgetContext.servicesMap ServicesMap;因此$injector.get(...)相当于把前端框架中现成的服务实例注入到脚本作用域这是所有自定义操作脚本获取服务能力的标准姿势。需要说明的是本示例虽然注入了assetService和entityService但脚本主体只使用customDialog与attributeService。保留它们通常是为了便于在对话框控制器中扩展关联实体查询逻辑。3.3 打开对话框customDialog.customDialog(htmlTemplate, Controller)function openAddEntityDialog() { customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe(() {}); }CustomDialogService.customDialog的方法签名见 custom-dialog.service.ts为customDialog(template: string, controller: (instance: CustomDialogComponent) void, data?: any, config?: MatDialogConfig): Observableany它接收HTML 模板字符串与控制器函数底层通过DynamicComponentFactoryService把模板动态编译成一个组件实例再放入 Material 的MatDialog中打开。因此htmlTemplate参数必须与在动作配置的 HTML 选项卡中粘贴的模板保持一致下文第四节。3.4 控制器表单、初始化与回调钩子AddEntityDialogController(instance)是对话框控制器Angular 侧会把它注入到动态组件实例见 custom-dialog.component.ts因此控制器内部可以通过vm访问到框架预置的上下文成员vm.fbUntypedFormBuilder用于构建响应式表单vm.validatorsValidators工具集本示例未用到但在创建/编辑实体等示例中常用于必填、正则校验vm.dialogRefMatDialogRef用于关闭对话框vm.dataCUSTOM_DIALOG_DATA注入的数据customDialog方法的第三个参数data。构建表单控制器先创建表单组表单由attributes嵌套组构成其中只定义一个image控件vm.editEntity vm.fb.group({ attributes: vm.fb.group({ image: [null] }) });加载实体信息随后调用getEntityInfo()从服务端拉取该实体SERVER_SCOPE作用域下已有的属性并把值回填到表单function getEntityInfo() { vm.loading true; attributeService.getEntityAttributes(entityId, SERVER_SCOPE).subscribe( function (data) { getEntityAttributes(data); vm.editEntity.patchValue({ attributes: vm.attributes }, {emitEvent: false}); vm.loading false; } ); }细节要点getEntityAttributes(attributes)把服务端返回的属性数组展开成{key: value}映射存到vm.attributes中作为后续差异比较的基线数据patchValue(..., {emitEvent: false})在回填时不触发表单事件避免无谓的校验或联动vm.loading用于驱动 HTML 模板中的进度条mat-progress-bar。取消vm.cancel直接以null关闭对话框vm.cancel function() { vm.dialogRef.close(null); };保存vm.save先把vm.loading置为true调用saveAttributes(entityId)完成提交成功则关闭对话框失败则复位loading以便用户重试vm.save function() { vm.loading true; saveAttributes(entityId).subscribe( () { vm.dialogRef.close(null); }, () { vm.loading false; } ); };3.5 差异比较与按需提交saveAttributes是整个脚本中最体现工程技巧的部分function saveAttributes(entityId) { let attributes vm.editEntity.get(attributes).value; let attributesArray []; 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([]); }它把表单当前值与vm.attributes打开对话框时的服务端基线逐字段比较只把发生变化的属性组装成{key, value}数组提交避免全量写入如果没有任何字段发生变化例如用户只是打开又关闭则返回widgetContext.rxjs.of([])—— 一个空的 RxJS 流此时不会发起任何 HTTP 请求widgetContext.rxjs是WidgetContext暴露的 RxJS 命名空间...RxJS, ...RxJSOperators因此脚本无需额外引入 rxjs 依赖。四、配套 HTML 模板用tb-image-input承接图片上传对话框渲染依赖动作配置中的 HTML 选项卡。官方配套模板custom_pretty_edit_image_html.md如下form [formGroup]editEntity (ngSubmit)save() classedit-entity-form mat-toolbar classflex flex-row colorprimary h2Edit {{entityName}} image/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 *ngIf(isLoading$ | async) || loading /mat-progress-bar div styleheight: 4px; *ngIf!(isLoading$ | async) !loading/div div mat-dialog-content classflex flex-col div formGroupNameattributes classflex flex-col tb-image-input labelEntity image formControlNameimage /tb-image-input /div /div div mat-dialog-actions classflex flex-row items-center justify-end button mat-button mat-raised-button colorprimary typesubmit [disabled](isLoading$ | async) || editEntity.invalid || !editEntity.dirty Save /button button mat-button colorprimary typebutton [disabled](isLoading$ | async) (click)cancel() cdkFocusInitial Cancel /button /div /form逐段解读模板片段作用form [formGroup]editEntity (ngSubmit)save()绑定控制器中的表单组提交时触发save()mat-toolbarEdit {{entityName}} image标题栏动态展示实体名称右侧关闭按钮绑定cancel()mat-progress-bar *ngIf(isLoading$ \| async) \|\| loading加载/保存期间显示进度条div formGroupNameattributes与 JS 中vm.fb.group({attributes: ...})的嵌套结构对应tb-image-input labelEntity image formControlNameimage图片上传/预览组件绑定image控件Save 按钮的[disabled]当处于加载中、表单无效或**未发生任何修改!editEntity.dirty**时禁用Cancel 按钮绑定cancel()关闭对话框tb-image-input是 ThingsBoard 封装的上传组件实现在 image-input.component.ts 中。它实现了 Angular 的ControlValueAccessor支持拖拽上传、本地图片预览、清除图片等能力其输出值就是图片资源的引用通常是一段以image/png开头的 data URI 或资源引用因此可以直接以字符串形式存入实体属性JS 脚本无需关心文件上传细节。小提示模板中还引用了isLoading$一个可通过AsyncPipe订阅的 Observable。官方示例脚本本身没有定义它属于可选的扩展写法实际以vm.loading为主若你在 HTML 中保留isLoading$的引用可在控制器中自行定义该 Observable 以驱动更精细的加载状态。五、底层原理自定义对话框的完整调用链理解本示例的运行机制有助于排查问题与扩展功能。其完整链路如下动作触发组件渲染时widget.component.ts把动作描述符WidgetActionDescriptor登记到widgetContext.actionsApi用户点击时handleWidgetAction命中WidgetActionType.customPretty分支调用executeCustomPrettyActionwidget.component.ts模板与资源准备executeCustomPrettyAction读取动作描述符中的customHtml即 HTML 选项卡内容、customCss、customResources并调用widgetContext.customDialog.setAdditionalImports(...)注入额外模块随后把 JS 函数编译为可执行函数以($event, widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel)的顺序调用——这正是脚本开头能直接使用htmlTemplate、entityId、entityName等变量的原因动态编译脚本调用customDialog.customDialog(htmlTemplate, AddEntityDialogController)后CustomDialogService通过DynamicComponentFactoryService.createDynamicComponent把 HTML 模板编译成组件类custom-dialog.service.ts容器注入CustomDialogContainerComponent创建自定义组件实例并通过Injector.create提供CUSTOM_DIALOG_DATA与MatDialogRefcustom-dialog-container.component.ts模板若解析失败会抛出NG0类错误并被捕获弹出 custom-pretty-template-error 错误提示控制器执行CustomDialogComponent的构造函数中执行this.data.controller(this)即把AddEntityDialogController(instance)调用起来instance就是动态组件实例vm.fb、vm.dialogRef、vm.validators均来自这里custom-dialog.component.ts。由此可见JS 脚本只负责组装数据 打开对话框真正的 UI 渲染、表单能力与生命周期都由 Angular 框架提供二者通过controller函数和模板绑定无缝衔接。六、属性读写AttributeService与作用域示例中的属性读写由AttributeService完成其真实实现位于 attribute.service.ts6.1 读取属性getEntityAttributes(entityId: EntityId, attributeScope?: AttributeScope, keys?: Arraystring, config?: RequestConfig): ObservableArrayAttributeData底层请求为GET /api/plugins/telemetry/{entityType}/{entityId}/values/attributes/{scope}当传入keys时会附加?keyxxx查询参数只拉取指定键的属性不传则返回该作用域下全部属性。示例中调用getEntityAttributes(entityId, SERVER_SCOPE)即拉取实体的全部服务器端属性。6.2 保存属性saveEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: ArrayAttributeData, config?: RequestConfig): Observableany底层请求为POST /api/plugins/telemetry/{entityType}/{entityId}/{scope}实现上会对属性值做一次分流见 attribute.service.ts值非空isDefinedAndNotNull的属性 → 组装成{key: value}字典通过 POST 保存值为null/undefined的属性 → 归入删除列表通过 DELETE 请求删除对应键最终用forkJoin并行发出保存与删除请求。这意味着在自定义脚本中只要把某属性值设为null加入数组即可实现删除图片属性的效果。6.3 属性作用域AttributeScope枚举定义在 telemetry.models.tsexport enum AttributeScope { CLIENT_SCOPE CLIENT_SCOPE, SERVER_SCOPE SERVER_SCOPE, SHARED_SCOPE SHARED_SCOPE }作用域含义CLIENT_SCOPE客户端属性由设备端上报、维护服务端只读SERVER_SCOPE服务器端属性由服务端或此处的仪表盘操作脚本创建与维护SHARED_SCOPE共享属性服务端可读写设备可订阅获取示例使用SERVER_SCOPE是正确的选择图片属于服务端维护的实体描述数据且 UI 脚本拥有读写权限。如果你的业务需要设备读取该属性可改用SHARED_SCOPE。七、在仪表盘中配置该操作的步骤要将本示例落地到实际仪表盘操作步骤如下进入仪表盘编辑模式选中目标组件打开Actions操作配置选择合适的事件源如组件标题按钮、单元格点击或On node selected等添加新操作操作类型选择Custom action (with HTML template)即customPretty在JavaScript选项卡粘贴上文第三节的完整 JS 函数在HTML选项卡粘贴第四节配套模板若使用tb-image-input需保留formGroupNameattributes与formControlNameimage的嵌套结构可选在CSS选项卡补充样式、在Resources中加载额外 JS/CSS 模块保存后点击操作按钮即可看到Edit {实体名} image对话框上传图片后点击Save完成属性更新。配置完成后动作的执行链路会经由executeCustomPrettyAction动态编译并注入htmlTemplate因此JS 与 HTML 必须成对配置——只粘贴 JS 而不粘贴 HTML 会导致模板为空对话框无法正常渲染。八、同类示例一览与扩展思路examples_custom_pretty目录下还有其他官方示例可作为学习与二次开发的模板custom_pretty_create_dialog_js.md /custom_pretty_create_dialog_html.md弹窗创建设备或资产custom_pretty_edit_dialog_js.md /custom_pretty_edit_dialog_html.md弹窗编辑设备或资产含多个属性字段与校验custom_pretty_create_user_js.md /custom_pretty_create_user_html.md弹窗创建新用户custom_pretty_clone_device_js.md /custom_pretty_clone_device_html.md弹窗克隆设备。基于本示例的扩展方向包括在attributes嵌套组中增加更多表单控件如latitude、longitude、description配合vm.validators.required/pattern做输入校验保存逻辑无需改动差异比较是通用的将SERVER_SCOPE替换为SHARED_SCOPE让设备端也能订阅到图片属性在save()成功后调用widgetContext.showSuccessToast(...)反馈保存结果利用注入的assetService/entityService在打开对话框时动态查询并展示实体关联信息。九、注意事项与常见问题作用域字符串需一致读取getEntityAttributes与保存saveEntityAttributes必须使用同一作用域如SERVER_SCOPE否则会出现读到了但保存不上或反之的现象。表单结构必须与模板一一对应JS 中vm.fb.group({attributes: vm.fb.group({image: [null]})})与 HTML 中div formGroupNameattributesformControlNameimage是严格对应的任何一侧缺失都会导致表单绑定失效或模板编译报错。差异比较依赖基线数据vm.attributes必须在打开对话框时先于用户操作完成填充getEntityInfo在控制器中同步调用否则首次保存会把空值误判为变化。无变更时不发请求saveAttributes在无差异时返回widgetContext.rxjs.of([])配合 Save 按钮的!editEntity.dirty禁用逻辑从源头避免无效写入。对话框关闭约定cancel()与保存成功路径均通过vm.dialogRef.close(null)关闭这与CustomDialogContainerComponent的afterClosed()生命周期衔接确保动态组件资源被正确释放。十、小结本示例虽短却浓缩了 ThingsBoard 自定义操作带 HTML 模板的完整开发模式从servicesMap注入服务 → 用customDialog.customDialog(template, controller)打开动态编译的对话框 → 在控制器中用vm.fb构建表单并读写实体属性 → 通过AttributeService的差异提交完成保存。理解这条链路后你可以把同一套模式复用到任意实体的属性管理、业务弹窗、批量操作等场景构建真正贴合业务的自定义仪表盘交互。关键源码参考CustomDialogService · CustomDialogComponent · CustomDialogContainerComponent · AttributeService · ServicesMap · WidgetContext · 动作执行入口赞分享物联网后端数据可视化消息队列【免费下载链接】thingsboardAll-in-one IoT Platform - Device management, data collection, processing and visualization.项目地址https://gitcode.com/GitHub_Trending/th/thingsboard点击查看免费下载相关推荐ThingsBoard 自定义动作实战用 HTML 模板实现实体图片属性编辑对话框ThingsBoard 自定义动作实战用 HTML 模板实现实体图片属性编辑对话框 本篇技术指南以 ThingsBoard 内置帮助文档 custom_pre物联网后端数据可视化消息队列ThingsBoard 自定义操作实战用 Custom Action 弹窗展示实体信息Display Alert DialogThingsBoard 自定义操作实战用 Custom Action 弹窗展示实体信息Display Alert Dialog 导读 在 ThingsBo物联网后端数据可视化消息队列ThingsBoard 自定义操作实战用 HTML 模板构建美化版编辑对话框编辑设备/资产ThingsBoard 自定义操作实战用 HTML 模板构建美化版编辑对话框编辑设备/资产 本篇指南讲解 ThingsBoard 物联网平台中「自定义操作物联网后端数据可视化消息队列创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表