写点什么

ThingsBoard 前端项目轮播图部件开发

作者:echeverra
  • 2023-12-13
    天津
  • 本文字数:10533 字

    阅读完需:约 35 分钟

ThingsBoard 前端项目轮播图部件开发

前言

ThingsBoard 是目前 Github 上最流行的开源物联网平台(14.6k Star),可以实现物联网项目的快速开发、管理和扩展, 是中小微企业物联网平台的不二之选。


本文介绍如何在 ThingsBoard 前端项目中开发轮播图部件。

产品需求

最近接到产品经理一个需求,在 TB 仪表板中添加轮播图部件,支持基本的轮播图管理和设置,可以点击跳转等。


我一想这简单啊,轮播图是一种比较常见前端组件,直接拿来引用改改就好,可大跌我眼镜(好吧,我不带眼镜)的是 TB 使用的前端 UI 框架 Material 竟然没有轮播图组件,是的!没有!我看两边是真的没有!实在是想不通难道老外不习惯轮播图这种方式么- -。

解决方案

没办法只得引用三方轮播图插件了,这里我踩了个坑- -,之前我开发导航菜单部件时引用了 NG-ZORRO 前端框架,正好在这个框架中找到了 Carousel 走马灯组件。但开发完后发现了个很奇怪的问题。从仪表板库列表进入仪表板轮播图不显示,改变下窗口大小就好了,效果如下:



这个问题我 Debug 了好久,最终只能是认定兼容问题,遂放弃 NG-ZORRO。轮播图最有名的插件莫过于 swiper,但遗憾我引入出现了问题,没有成功。最终在第三次尝试终于成功了,它就是大名鼎鼎的 Layui。

轮播图部件

高级设置

首先还是开发部件的高级设置功能。


首先我将轮播图部件定义为 Cards 部件库的一种,所以我在 ui-ngx\src\app\modules\home\components\widget\lib\settings\cards 目录下创建部件设置文件 carousel-widget-settings.component.htmlcarousel-widget-settings.component.tscarousel-widget-settings.component.scss


首先讲解 carousel-widget-settings.component.ts 文件的代码:


import { Component, OnInit } from '@angular/core';import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';import { AbstractControl, FormArray, FormBuilder, FormGroup } from '@angular/forms';import { Store } from '@ngrx/store';import { AppState } from '@core/core.state';import { CdkDragDrop } from '@angular/cdk/drag-drop';import { UtilsService } from '@core/services/utils.service';
@Component({ selector: 'tb-carousel-widget-settings', templateUrl: './carousel-widget-settings.component.html', styleUrls: ['./../widget-settings.scss', './carousel-widget-settings.scss']})export class CarouselWidgetSettingsComponent extends WidgetSettingsComponent implements OnInit {
/*FormGroup表单*/ carouselWidgetSettingsForm: FormGroup;
constructor(protected store: Store<AppState>, private utils: UtilsService, private fb: FormBuilder) { super(store); }
protected settingsForm(): FormGroup { return this.carouselWidgetSettingsForm; }
/*初始化数据字段*/ protected defaultSettings(): WidgetSettings { return { carousels: [], autoPlaySpeed: 3000 }; }
/*数据字段设置*/ protected onSettingsSet(settings: WidgetSettings) { this.carouselWidgetSettingsForm = this.fb.group({ carousels: this.prepareCarouselsFormArray(settings.carousels), autoPlaySpeed: [settings.autoPlaySpeed, []], }); }

protected doUpdateSettings(settingsForm: FormGroup, settings: WidgetSettings) { settingsForm.setControl('carousels', this.prepareCarouselsFormArray(settings.carousels), {emitEvent: false}); }

private prepareCarouselsFormArray(carousels: any | undefined): FormArray { const carouselsControls: Array<AbstractControl> = []; if (carousels) { carousels.forEach((item) => { console.log('item', item); /*处理数据*/ const tempFormGroup = this.fb.group({ name: item.name, navId: item.navId, expanded: item.expanded, carouselId: item.carouselId, imageUrl: item.imageUrl }); carouselsControls.push(tempFormGroup); }); } return this.fb.array(carouselsControls); }
/*获取轮播图*/ carouselsFormArray(): FormArray { return this.carouselWidgetSettingsForm.get('carousels') as FormArray; }
public trackByCarouselControl(index: number, carouselControl: AbstractControl): any { return carouselControl; }
/*删除轮播图*/ public removeCarousel(index) { (this.carouselWidgetSettingsForm.get('carousels') as FormArray).removeAt(index); }
/*添加轮播图*/ public addCarousel() { const carouselsArray = this.carouselWidgetSettingsForm.get('carousels') as FormArray; const carouselGroup = this.fb.group({ name: '', carouselId: 'carousel-' + this.utils.guid(), expanded: true, imageUrl: '' }); carouselsArray.push(carouselGroup); console.log('carouselsArray', carouselsArray); this.carouselWidgetSettingsForm.updateValueAndValidity(); }
/*轮播图拖动排序*/ carouselDrop(event: CdkDragDrop<string[]>) { const carouselsArray = this.carouselWidgetSettingsForm.get('carousels') as FormArray; const label = carouselsArray.at(event.previousIndex); carouselsArray.removeAt(event.previousIndex); carouselsArray.insert(event.currentIndex, label); }
}
复制代码


请原谅我大幅粘贴- -,以上为完整代码,下面讲解代码核心内容。


首先在 defaultSettings() 在函数中声明两个重要变量,carousels: [] 用来存储轮播图片,autoPlaySpeed: 3000 用来设置轮播切换时间,默认 3 秒。


prepareCarouselsFormArray() 函数中对 carousels 数据进行格式处理,创建新的 FormGroup 实例以便在模板文件中获取。


新增轮播图 addCarousel() 函数,先获取 carousels 变量,插入新的轮播图 FormGroup,包含轮播图名称:name。轮播图 ID:carouselId,新增 id 是为了后面添加动作相关功能会使用到。展开标识:expanded,默认为 true 展开。轮播图链接:imageUrl,这里使用 TB 自带的组件,图片会以 base64 文本形式保存。



最后记得将 Class CarouselWidgetSettingsComponent 在部件设置模块文件 widget-settings.module.ts 中引入声明和导出。


import {  CarouselWidgetSettingsComponent} from '@home/components/widget/lib/settings/cards/carousel-widget-settings.component';
@NgModule({ declarations: [ ... CarouselWidgetSettingsComponent ], exports: [ ... CarouselWidgetSettingsComponent ]
export class WidgetSettingsModule {}
export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsComponent>} = { ... 'tb-carousel-widget-settings': CarouselWidgetSettingsComponent};
复制代码


接下来是 carousel-widget-settings.component.html 文件:


<section class="tb-widget-settings" [formGroup]="carouselWidgetSettingsForm">
<fieldset class="fields-group" > <legend class="group-title" translate>widgets.carousel.carousel-item</legend> <div fxLayout="column"> <div class="tb-control-list tb-drop-list" cdkDropList cdkDropListOrientation="vertical" (cdkDropListDropped)="carouselDrop($event)"> <div cdkDrag class="tb-draggable" *ngFor="let carouselControl of carouselsFormArray().controls; trackBy: trackByCarouselControl; let $index = index; last as isLast;" fxLayout="column" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}"> <mat-expansion-panel class="carousel-item" [formGroup]="carouselControl" [expanded]="carouselControl.get('expanded').value"> <mat-expansion-panel-header> <div fxFlex fxLayout="row" fxLayoutAlign="start center"> <mat-panel-title> <div fxLayout="row" fxFlex fxLayoutAlign="start center"> {{ carouselControl.get('name').value }} </div> </mat-panel-title> <span fxFlex></span> <button mat-icon-button style="min-width: 40px;" type="button" (click)="removeCarousel($index)" matTooltip="{{ 'action.remove' | translate }}" matTooltipPosition="above"> <mat-icon>delete</mat-icon> </button> </div> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <div fxLayout="column" fxLayoutGap="0.5em"> <mat-divider></mat-divider> <section class="tb-widget-settings" fxLayout="column"> <mat-form-field style="padding-bottom: 16px;"> <mat-label translate>widgets.carousel.name</mat-label> <input required matInput formControlName="name"> </mat-form-field> <tb-image-input required label="{{ 'widgets.carousel.imageUrl' | translate }}" formControlName="imageUrl"> </tb-image-input> </section> </div> </ng-template> </mat-expansion-panel>
</div> </div> <div *ngIf="!carouselsFormArray().controls.length"> <span translate fxLayoutAlign="center center" class="tb-prompt">widgets.carousel.no-carousels</span> </div> <div style="padding-top: 16px;"> <button mat-raised-button color="primary" type="button" (click)="addCarousel()"> <span translate>widgets.carousel.add-carousel</span> </button> </div> </div> </fieldset>
<fieldset class="fields-group" > <legend class="group-title" translate>widgets.carousel.carousel-settings</legend> <div fxLayout="column"> <!--切换时间(毫秒)--> <mat-form-field fxFlex> <mat-label translate>widgets.carousel.autoPlaySpeed</mat-label> <input matInput type="number" min="0" formControlName="autoPlaySpeed"> </mat-form-field> </div> </fieldset></section>
复制代码


高级设置 html 文件展示分为两个区域 <fieldset>,轮播图管理和设置。


通过 [formGroup]="carouselWidgetSettingsForm" 指令管理一个表单组。通过 formControlName="key" 指令将 FormGroup 中的 FormControl 按名称同步到一个表单控制元素。


因为轮播图需要支持拖动排序,所以使用 cdkDrag 指令完成。


使用 <tb-image-input> 内置的组件上传轮播图片。


*ngFor="let carouselControl of carouselsFormArray().controls; 遍历所有的轮播图,并通过 carouselControl.get(key).value 的方式获取轮播图的各属性。


最后是 carousel-widget-settings.component.scss 文件:


:host {  display: block;  .mat-expansion-panel {    box-shadow: none;    &.carousel-item {      border: 1px groove rgba(0, 0, 0, .25);    }  }}
复制代码


样式主要是将拖动模块设置上边框,更加美观。


轮播图展示

轮播图展示我最终使用到的是 layui 插件,首先我们引入它。


ui-ngx/src/assets 目录下创建 layui 文件夹,将 layui 官网上下载的插件拖进来。



在入口文件 index.html 中通过标签的方式引入 css 和 js 文件。


<head>  <link rel="stylesheet" href="./assets/layui/css/layui.css" />  <script src="./assets/layui/layui.js"></script></head>
复制代码


这是第一步,我们在创建轮播图展示文件,在 ui-ngx\src\app\modules\home\components\widget\lib\ 目录下创建 carousel.models.tscarousel-widget.component.htmlcarousel-widget.component.scsscarousel-widget.component.ts


carousel.models.ts 文件中声明导入导出。


import { NgModule } from '@angular/core';import { CarouselWidgetComponent } from '@home/components/widget/lib/carousel-widget.component';import { RouterModule } from '@angular/router';import { CommonModule } from '@angular/common';import { SharedModule } from '@app/shared/shared.module';
@NgModule({ declarations: [ CarouselWidgetComponent ], imports: [ RouterModule, CommonModule, SharedModule ], exports: [ CarouselWidgetComponent ]})export class CarouselModule {}
复制代码


和高级设置文件一样,Class CarouselModule 需要在部件模块文件 widget-components.module.ts 中引入声明和导出。


import { CarouselModule } from '@home/components/widget/lib/carousel.models';
@NgModule({ declarations: [ ... CarouselModule ], exports: [ ... CarouselModule ]
export class WidgetComponentsModule {}
复制代码


然后是 carousel-widget.component.ts 文件:


import { ChangeDetectorRef, Component, Input, OnInit, AfterViewInit } from '@angular/core';import { PageComponent } from '@shared/components/page.component';import { WidgetContext } from '@home/models/widget-component.models';import { Store } from '@ngrx/store';import { AppState } from '@core/core.state';declare const layui: any;
interface CarouselWidgetSettings { carousels: Array<any>; autoPlaySpeed: number;}
@Component({ selector: 'tb-carousel-widget', templateUrl: './carousel-widget.component.html', styleUrls: ['./carousel-widget.component.scss']})export class CarouselWidgetComponent extends PageComponent implements OnInit, AfterViewInit {
settings: CarouselWidgetSettings;
@Input() ctx: WidgetContext;
constructor(protected store: Store<AppState>, protected cd: ChangeDetectorRef) { super(store); }
ngOnInit(): void { this.ctx.$scope.imageWidget = this; this.settings = this.ctx.settings; }
ngAfterViewInit() { layui.use(['carousel'], () => { const carousel = layui.carousel; // 常规轮播 carousel.render({ elem: '#my-carousel', arrow: 'hover', width: '100%', height: '100%', interval: this.settings.autoPlaySpeed, }); });
setTimeout(() => { /*添加轮播图动作事件*/ this.ctx.customCarouselActions.forEach((action, index) => { const ele = document.querySelector('#' + action.descriptor.carouselId.substr(16)); ele.addEventListener('click', () => { action.onAction(event); }); }); }, 50); }}
复制代码


因为 layui 不是 TypeScript 编写的,并且 TypeScript 可能无法识别 layui 的类型。所以我们使用 declare const layui: any这样的方式绕过。


声明选择器 tb-carousel-widget,这个一会创建新部件要用到。


所有的高级设置都在 this.settings 对象中,在 ngAfterViewInit() 页面加载完成后,进行轮播图渲染和点击事件绑定操作。


carousel.render({...}) 渲染轮播图,对应 idmy-carousel 的容器,arrow: 'hover' 设置轮播图前后箭头在鼠标悬浮后显示,轮播时间 interval 为高级设置中的 autoPlaySpeed 字段值。


后面的绑定事件需要通过 this.ctx.customCarouselActions 获取自定义事件,绑定到对应轮播图 id 上,这个在下文的轮播图添加动作中介绍。


carousel-widget.component.html 文件:


<div class="layui-carousel" id="my-carousel" lay-filter="my-carousel">  <div carousel-item="">    <div *ngFor="let item of settings.carousels">      <img class="carousel-img" id="{{ item.carouselId }}" src="{{item.imageUrl}}" alt="">    </div>  </div></div>
复制代码


根据 layui 轮播图写法,遍历所有轮播图数据 *ngFor="let item of settings.carousels"


carousel-widget.component.scss 文件:


:host {  display: flex;  width: 100%;  height: 100%;}
复制代码


没什么好说的,将轮播图片全部铺满展示。

导入部件部

想要看到最终效果,我们需要先将轮播图部件添加到部件库中,登录系统管理员账号 sysadmin@thingsboard.org / sysadmin,登录系统管理员账号操作是因为添加后会默认显示为系统部件包。


打开部件库菜单,打开 Cards 部件包,右下角点击添加新的部件类型->创建新的部件类型->静态部件,进行轮播图部件初始化设置:



  1. 设置部件标题,如“Carousel Widget”

  2. 设置 HTML :<tb-carousel-widget [ctx]="ctx"></tb-carousel-widget>

  3. 清空 JavaScript 内容

  4. widget.widget-settings 中 widget.settings-form-selector 设置为 tb-carousel-widget-settings


其中第 2 项中 [ctx]="ctx" 为组件传值必须项,不能省略;第 4 项的 tb-carousel-widget-settings 为部件高级设置选择器,不能填错。


添加好部件好,我们在仪表板中添加该部件。切换回 tenant@thingsboard.org / tenant 用户,仪表板中添加轮播图部件,添加轮播图图片。


最终效果如下:


添加动作

好了,难点来了,需求要求轮播图可以点击支持跳转,TB 部件内置支持只支部件顶部按钮添加跳转,但具体的每个内容点击跳转并不支持。



所以需要开发这部分内容,可以参考部件顶部按钮跳转功能。


首先在 widget-action-dialog.component.ts 部件添加动作窗口增加轮播图动作源:


constructor(...) {    console.log('data', data, this.widgetSettings);    // 轮播图部件动作源    if (this.widgetSettings.carousels) {      let pre = 0;      this.widgetSettings.carousels.forEach((item) => {        pre ++;        // z- 目的是为了排序在headerButton 部件顶部按钮之后        this.data.actionsData.actionSources[this.dealPreFix(pre, 'z-carousel') + '-' + item.carouselId] = {          name: item.name,          value: item.carouselId,          multiple: true,          hasShowCondition: true        };      });    }}
dealPreFix(pre, str): string { let preString = pre.toString(); while (preString.length < 5) { preString = '0' + preString; } return str + preString; }
复制代码


dealPreFix() 函数目的是为了使动作源排序正常,因为默认是按照字符串排序会比较奇怪。这样我们就可以在 actionSources 中添加所有的动作源。效果如下:



我们还需要改写下动作源添加后的列表名称显示,在 manage-widget-actions.component.ts 文件中新增:


ngOnInit(): void {    // 轮播图部件动作源名称    if (this.widgetSettings.carousels) {      this.widgetSettings.carousels.forEach((item) => {        this.widgetService.carouselIdTranslate[item.carouselId] = item.name;      });    }    console.log('carouselIdTranslate', this.widgetService.carouselIdTranslate);  }}
actionSourceName(actionSourceId): string { if (actionSourceId.indexOf('carousel-') !== -1){ return this.widgetService.carouselIdTranslate[actionSourceId.slice(16)]; }else { return actionSourceId; } }
复制代码


carouselIdTranslate 打印输出如下:



然后在 manage-widget-actions.component.html 模板文件中输出:


<div fxFlex class="table-container">      <table mat-table [dataSource]="dataSource"                 matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear>        <ng-container matColumnDef="actionSourceName">          <mat-header-cell *matHeaderCellDef mat-sort-header style="width: 20%"> {{ 'widget-config.action-source' | translate }} </mat-header-cell>          <mat-cell *matCellDef="let action">            <!--修改处-->            {{ actionSourceName(action.actionSourceName) }}          </mat-cell>        </ng-container>    </table></div>
复制代码


动作列表显示如下:



具体的轮播图添加动作的逻辑在文件 widget.component.ts


/*轮播图动作*/this.widgetContext.customCarouselActions = [];const carouselActionsDescriptors = this.getCarouselActionDescriptors(this.widgetContext);console.log('carouselActionsDescriptors', carouselActionsDescriptors);carouselActionsDescriptors.forEach((descriptor) =>{  let useShowWidgetCarouselActionFunction = descriptor.useShowWidgetActionFunction || false;  let showWidgetCarouselActionFunction: ShowWidgetCarouselActionFunction = null;  if (useShowWidgetCarouselActionFunction && isNotEmptyStr(descriptor.showWidgetActionFunction)) {    try {      showWidgetCarouselActionFunction =        new Function('widgetContext', 'data', descriptor.showWidgetActionFunction) as ShowWidgetCarouselActionFunction; // TODO    } catch (e) {      useShowWidgetCarouselActionFunction = false;    }  }  const carouselAction: WidgetCarouselAction = {    name: descriptor.name,    displayName: descriptor.displayName,    icon: descriptor.icon,    descriptor,    useShowWidgetCarouselActionFunction,    showWidgetCarouselActionFunction,    onAction: $event => {      const entityInfo = this.getActiveEntityInfo();      const entityId = entityInfo ? entityInfo.entityId : null;      const entityName = entityInfo ? entityInfo.entityName : null;      const entityLabel = entityInfo ? entityInfo.entityLabel : null;      console.log('carouselAction', descriptor);      this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel);    }  };  this.widgetContext.customCarouselActions.push(carouselAction);  console.log('this.widgetContext', this.widgetContext);});
private getCarouselActionDescriptors(widgetContext): Array<WidgetActionDescriptor> { let result = []; console.log('widgetContext', widgetContext.widget.config.actions); const allActions = widgetContext.widget.config.actions; for (const key in allActions) { if (allActions.hasOwnProperty(key)) { // console.log(key, allActions[key]); // 轮播图动作 if (key.indexOf('carousel-') !== -1 && allActions[key].length !== 0) { allActions[key].forEach((item, index) => { allActions[key][index].displayName = allActions[key][index].name; allActions[key][index].carouselId = key; }); result.push(allActions[key][allActions[key].length - 1]); } } } if (!result) { result = []; } console.log('getCarouselActionDescriptors', result); return result; }
复制代码


上述代码功能为将所有的轮播图动作 carouselAction 添加到自定义的轮播图动作数组 customCarouselActions 中,模仿原顶部动作 customHeaderActions 的写法。



最后在轮播图展示页面 carousel-widget.component.ts 中,通过绑定某一轮播图,点击触发其动作。


this.ctx.customCarouselActions.forEach((action, index) => {        const ele = document.querySelector('#' + action.descriptor.carouselId.substr(16));        ele.addEventListener('click', () => {          action.onAction(event);        });      });
复制代码


最终效果如下:



大功告成,Nice~

结语

本文展示了 99% 的实现源码,省略了部分中英翻译、变量声明等部分,大家可以自行补充。


由于 TB 的受众面很小,所以如果你没研究过 TB 看不懂这篇文章也是很正常的- -,跳过就好,TB 的相关文章更多的是作为本人的一个工作知识记录,如果能对一小部分人有所帮助那就更好啦~


好啦,以上就是 ThingsBoard 前端项目轮播图部件开发的全部内容,希望对你有所帮助,如有问题可通过我的博客 https://echeverra.cn 或微信公众号 echeverra 联系我。


你学“废”了么?


(完)




文章首发于我的博客 https://echeverra.cn/tb4,原创文章,转载请注明出处。


欢迎关注我的微信公众号 echeverra,一起学习进步!不定时会有资源和福利相送哦!




发布于: 刚刚阅读数: 2
用户头像

echeverra

关注

Let`s go, together. 2021-09-18 加入

个人博客:https://echeverra.cn 微信公众号:echeverra

评论

发布
暂无评论
ThingsBoard 前端项目轮播图部件开发_thingsboard_echeverra_InfoQ写作社区