效果演示

点击查看【bilibili】
说明:由于CE版没有权限功能,所以不能主题定制功能不能细分到每个用户一套主题。本文实现的是:定制主题绑定租户,租户管理员和客户用户继承所属租户的主题,且租户管理员可修改主题;不同租户下的租户管理员和客户用户登录后分别显示自己的主题。这个效果在视频中有演示。后面会发布关于登录界面的主题定制功能,可为每个租户设置专有域名和对应的登录界面。俩者结合就可以做到,对租户界面的特有定制,不同租户都拥有属于自己的UI界面。

资料

ngrx/store
angular canDeactivate
angular canDeactivate CSDN介绍
angular FormBuilder
angular FormGroup
Material Design 组件库 for Angular
material design icons
ThingsBoard图片上传组件拓展
添加自定义菜单
添加首页设置-展示仪表功能

思路

  1. 新建菜单组件,ui-ngx/src/app/modules/home/pages/custom-ui/custom-ui.component.ts,scss,html
  2. 添加菜单路由, ui-ngx/src/app/modules/home/pages/home-settings/home-settings-routing.module.ts

image.png
其中:canDeactivate: [ConfirmOnExitGuard] 是离开页面时,做出逻辑判断,即下图效果。
image.png
实现此效果需要在组件内实现HasDirtyFlag接口,并定义isDirty变量,值为true显示未保存提示,false不显示。自己根据表单控制。详细代码在文末。
image.png

  1. 在模块声明组件,ui-ngx/src/app/modules/home/pages/home-settings/home-settings.module.ts

image.png

  1. 组件表单内容构建,其中图片上传组件可以参考ThingsBoard图片上传组件拓展,采用的是tb源码内置的组件

image.png

  1. 国际化,ui-ngx/src/assets/locale/locale.constant-zh_CN.json中加入国际化信息
  2. 后端接口开发,无需额外建表,主题信息保存到租户的额外信息中。
  3. 前端请求服务开发,为了方便,还是定义到了ui-ngx/src/app/core/http/dashboard.service.ts 和文章添加首页设置-展示仪表功能的位置一样
  4. store相关开发,ui-ngx/src/app/core/core.state.ts,src/app/core/ui。
    1. 在app.component.ts初始化时请求当前登录用户对应租户的主题信息,没有则展示原始主题。并通过store订阅主题状态,改变则重新渲染。
    2. 登录成功后也需要请求一次后端主题信息,因为退出登录到重新登录,app组件不会再次初始化。
    3. home.component.ts也通过store订阅主题状态,用于改变logo和左下角的平台信息

      具体代码

      添加下面的代码即可实现此功能。
      为减少文章篇幅,优化阅读体验,代码请滚动查看。
      菜单部分在文章添加首页设置-展示仪表功能的基础上开发,也可以参考添加自定义菜单按自己想法定义菜单。
      内容确实有点多,如果发现有整理遗漏的地方,请联系告知看我

      新建菜单组件

      ui-ngx/src/app/modules/home/pages/custom-ui/下新建
      custom-ui.component.ts ```typescript import { AfterViewInit, Component, OnInit } from ‘@angular/core’; import { FormBuilder, FormGroup } from ‘@angular/forms’; import { environment as env } from ‘@env/environment’; import { TranslateService } from ‘@ngx-translate/core’; import { DashboardService } from ‘@core/http/dashboard.service’; import { UIInfo } from ‘@shared/models/dashboard.models’; import { Store } from ‘@ngrx/store’; import { AppState } from ‘@core/core.state’; import { ActionTenantUIChangeAll } from ‘@core/ui/tenant-ui.actions’; import { HasDirtyFlag } from ‘@core/guards/confirm-on-exit.guard’; import { TenantUIState } from ‘@core/ui/tenant-ui.models’; import { PageComponent } from ‘@shared/components/page.component’; import { initialState } from ‘@core/ui/tenant-ui.reducer’;

@Component({ selector: ‘tb-custom-ui’, templateUrl: ‘./custom-ui.component.html’, styleUrls: [‘./custom-ui.component.scss’] }) export class CustomUiComponent extends PageComponent implements OnInit, HasDirtyFlag,AfterViewInit { isDirty = false; faviconMaxKBytes = 256; logoMaxKBytes = 4096; customUiFormGroup: FormGroup; initData: any; previousData: any;

constructor( protected store: Store, private translate: TranslateService, private dashboardService: DashboardService, private fb: FormBuilder ) { super(store); this.initForm(); this.writeFormByHttp(); }

ngAfterViewInit() { }

ngOnInit(): void { this.customUiFormGroup.valueChanges.subscribe(data => { Reflect.ownKeys(data).forEach(key => data[key.toString()] = data[key.toString()] === ‘’ ? null : data[key.toString()]); if(JSON.stringify(this.initData) !== JSON.stringify(data)){ this.isDirty = true; this.previousData = data; this.store.dispatch(new ActionTenantUIChangeAll(data)); }else{ this.isDirty = false; if(JSON.stringify(this.previousData) !== JSON.stringify(data)){ this.store.dispatch(new ActionTenantUIChangeAll(data)); } } }); }

writeFormByHttp() { this.dashboardService.getTenantUIInfo().subscribe(ui => { this.patchFormValue(ui); this.initData = this.customUiFormGroup.value; this.previousData = this.customUiFormGroup.value; }); }

patchFormValue(ui: UIInfo | TenantUIState) { this.customUiFormGroup.get(‘applicationTitle’).patchValue(ui.applicationTitle); this.customUiFormGroup.get(‘iconImageUrl’).patchValue(ui.iconImageUrl); this.customUiFormGroup.get(‘logoImageUrl’).patchValue(ui.logoImageUrl); this.customUiFormGroup.get(‘logoImageHeight’).patchValue(ui.logoImageHeight); this.customUiFormGroup.get(‘platformMainColor’).patchValue(ui.platformMainColor); this.customUiFormGroup.get(‘platformTextMainColor’).patchValue(ui.platformTextMainColor); this.customUiFormGroup.get(‘platformButtonColor’).patchValue(ui.platformButtonColor); this.customUiFormGroup.get(‘platformMenuColorActive’).patchValue(ui.platformMenuColorActive); this.customUiFormGroup.get(‘platformMenuColorHover’).patchValue(ui.platformMenuColorHover); this.customUiFormGroup.get(‘showNameVersion’).patchValue(ui.showNameVersion); this.customUiFormGroup.get(‘platformName’).patchValue(ui.platformName); this.customUiFormGroup.get(‘platformVersion’).patchValue(ui.platformVersion); }

//恢复到tb原始设置 reset($event: Event) { if ($event) { $event.stopPropagation(); } this.patchFormValue(initialState); this.store.dispatch(new ActionTenantUIChangeAll(this.customUiFormGroup.value)); this.isDirty = true; } //撤销本次操作 cancel($event: Event) { if ($event) { $event.stopPropagation(); } this.writeFormByHttp(); }

//初始化表单 initForm() { this.customUiFormGroup = this.fb.group({ applicationTitle: [null, []], iconImageUrl: [null, []], logoImageUrl: [null, []], logoImageHeight: [null, []], platformMainColor: [null, []], platformTextMainColor: [null, []], platformButtonColor: [null, []], platformMenuColorActive: [null, []], platformMenuColorHover: [null, []], showNameVersion: [false, []], platformName: [env.appTitle, []], platformVersion: [env.tbVersion, []] }); this.initData = this.customUiFormGroup.value; this.previousData = this.customUiFormGroup.value; }

submit($event: Event) { if ($event) { $event.stopPropagation(); } this.dashboardService.saveTenantUIInfo(this.customUiFormGroup.value as UIInfo).subscribe(res => { }); this.store.dispatch(new ActionTenantUIChangeAll(this.customUiFormGroup.value)); this.isDirty = false; }

formatSlider(value: number) { return value + ‘px’; }

// advancedCssClick() { // // } }

  1. custom-ui.component.scss
  2. ```css
  3. @media screen and (min-width: 1280px){
  4. .mat-card.settings-card {
  5. width: 60%;
  6. }
  7. }
  8. .mat-card.settings-card {
  9. margin: .5rem;
  10. }
  11. .mat-headline{
  12. font: 400 1.5rem/2rem Roboto,Helvetica Neue,sans-serif;
  13. letter-spacing: normal;
  14. margin: 0 0 1rem;
  15. }
  16. .mat-card-content{
  17. padding-top: 1rem;
  18. }

custom-ui.component.tml