效果演示
点击查看【bilibili】
说明:由于CE版没有权限功能,所以不能主题定制功能不能细分到每个用户一套主题。本文实现的是:定制主题绑定租户,租户管理员和客户用户继承所属租户的主题,且租户管理员可修改主题;不同租户下的租户管理员和客户用户登录后分别显示自己的主题。这个效果在视频中有演示。后面会发布关于登录界面的主题定制功能,可为每个租户设置专有域名和对应的登录界面。俩者结合就可以做到,对租户界面的特有定制,不同租户都拥有属于自己的UI界面。
资料
ngrx/store
angular canDeactivate
angular canDeactivate CSDN介绍
angular FormBuilder
angular FormGroup
Material Design 组件库 for Angular
material design icons
ThingsBoard图片上传组件拓展
添加自定义菜单
添加首页设置-展示仪表功能
思路
- 新建菜单组件,ui-ngx/src/app/modules/home/pages/custom-ui/custom-ui.component.ts,scss,html
- 添加菜单路由, ui-ngx/src/app/modules/home/pages/home-settings/home-settings-routing.module.ts
其中:canDeactivate: [ConfirmOnExitGuard] 是离开页面时,做出逻辑判断,即下图效果。
实现此效果需要在组件内实现HasDirtyFlag
接口,并定义isDirty
变量,值为true
显示未保存提示,false
不显示。自己根据表单控制。详细代码在文末。
- 在模块声明组件,ui-ngx/src/app/modules/home/pages/home-settings/home-settings.module.ts
- 组件表单内容构建,其中图片上传组件可以参考ThingsBoard图片上传组件拓展,采用的是tb源码内置的组件
- 国际化,ui-ngx/src/assets/locale/locale.constant-zh_CN.json中加入国际化信息
- 后端接口开发,无需额外建表,主题信息保存到租户的额外信息中。
- 前端请求服务开发,为了方便,还是定义到了ui-ngx/src/app/core/http/dashboard.service.ts 和文章添加首页设置-展示仪表功能的位置一样
- store相关开发,ui-ngx/src/app/core/core.state.ts,src/app/core/ui。
- 在app.component.ts初始化时请求当前登录用户对应租户的主题信息,没有则展示原始主题。并通过store订阅主题状态,改变则重新渲染。
- 登录成功后也需要请求一次后端主题信息,因为退出登录到重新登录,app组件不会再次初始化。
- 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
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() { // // } }
custom-ui.component.scss
```css
@media screen and (min-width: 1280px){
.mat-card.settings-card {
width: 60%;
}
}
.mat-card.settings-card {
margin: .5rem;
}
.mat-headline{
font: 400 1.5rem/2rem Roboto,Helvetica Neue,sans-serif;
letter-spacing: normal;
margin: 0 0 1rem;
}
.mat-card-content{
padding-top: 1rem;
}
custom-ui.component.tml