赞
踩
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.html
、carousel-widget-settings.component.ts
和 carousel-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.ts
、carousel-widget.component.html
、carousel-widget.component.scss
和 carousel-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({...})
渲染轮播图,对应 id
为 my-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 部件包,右下角点击添加新的部件类型->创建新的部件类型->静态部件,进行轮播图部件初始化设置:
<tb-carousel-widget [ctx]="ctx"></tb-carousel-widget>
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,一起学习进步!不定时会有资源和福利相送哦!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。