效果演示

点击查看【bilibili】
说明:主要实现同一个前端项目,给不同租户分配不同域名和对应的自定义登录主题,租户访问自己的域名时,展示自己的自定义登录页面。

资料

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

思路

  1. 创建自定义菜单,可参考添加自定义菜单
  2. 创建表单,包含登录页主题修改需要的字段。涉及TB自定义的组件:tb-image-input图片上传组件,tb-color-input颜色选择组件。
  3. 加载登录页面时根据domain(域名)获取对应的主题参数,并通过store通知app.component.ts修改应用名称和icon。
  4. 定义后端接口,并将登录界面表单元素储存到租户的额外信息中。
  5. 国际化,ui-ngx/src/assets/locale/locale.constant-zh_CN.json中加入国际化信息。
  6. 修改nginx配置,代理多个域名指向同一个前端地址。
  7. 修改angular.json中custom-webpack的配置,添加授权host。

    1. webpack-dev-server 2.4.3后新增对host header的正确性检测,以屏蔽未经授权的访问。官方提供了两个解决方案:
      • 执行 webpack-dev-server 命令时手动添加 —public 选项,取值为授权的 host,这是官方建议的做法,目的是为了安全。
      • 设置 webpack-dev-server 的配置项 disableHostCheck 为 true 以禁用这一检测,如果开发者使用了代理,或在开发环境中不 care 这些安全问题,该设置可以直接斩草除根。

        具体代码

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

        新建菜单组件

        ui-ngx/src/app/modules/home/pages/login-ui/下新建
        login-ui.component.ts ```typescript import { AfterViewInit, Component, OnInit } from ‘@angular/core’; import { FormBuilder, FormGroup, Validators } from ‘@angular/forms’; import { TranslateService } from ‘@ngx-translate/core’; import { DashboardService } from ‘@core/http/dashboard.service’; import { UIInfo } from ‘@shared/models/dashboard.models’; import { select, Store } from ‘@ngrx/store’; import { AppState } from ‘@core/core.state’; import { HasDirtyFlag } from ‘@core/guards/confirm-on-exit.guard’; import { LoginUIState } from ‘@core/ui/tenant-ui.models’; import { PageComponent } from ‘@shared/components/page.component’; import { initialLoginUIState } from ‘@core/ui/tenant-ui.reducer’; import { ActionLoginUIChange } from ‘@core/ui/tenant-ui.actions’; import { AttributeService } from ‘@core/http/attribute.service’; import { TenantId } from ‘@shared/models/id/tenant-id’; import { selectUserDetails } from ‘@core/auth/auth.selectors’; @Component({ selector: ‘tb-login-ui’, templateUrl: ‘./login-ui.component.html’, styleUrls: [‘./login-ui.component.scss’] }) export class LoginUiComponent extends PageComponent implements OnInit, HasDirtyFlag, AfterViewInit {

    isDirty = false; bgMaxKBytes = 10240; faviconMaxKBytes = 256; logoMaxKBytes = 4096; loginUiFormGroup: FormGroup; tenantId: TenantId;

    constructor( protected store: Store, private translate: TranslateService, private dashboardService: DashboardService, private attributeService: AttributeService, private fb: FormBuilder ) { super(store); this.initForm(); this.store.pipe(select(selectUserDetails)).subscribe(user => { if(user){

    1. this.tenantId = user.tenantId;
    2. this.writeFormByHttp();

    } }); }

    ngAfterViewInit() { }

    ngOnInit(): void { this.loginUiFormGroup.valueChanges.subscribe(data => { this.isDirty = true; Reflect.ownKeys(data).forEach(key => data[key.toString()] = data[key.toString()] === ‘’ ? null : data[key.toString()]); this.store.dispatch(new ActionLoginUIChange(data)); }); }

    writeFormByHttp() { this.dashboardService.getTenantLoginUIInfo(undefined,this.tenantId.id).subscribe(ui => this.patchFormValue(ui)); }

    patchFormValue(ui: UIInfo | LoginUIState) { this.loginUiFormGroup.get(‘loginDomainName’).patchValue(ui.loginDomainName); this.loginUiFormGroup.get(‘loginAppTitle’).patchValue(ui.loginAppTitle); this.loginUiFormGroup.get(‘loginIconImageUrl’).patchValue(ui.loginIconImageUrl); this.loginUiFormGroup.get(‘loginBGImage’).patchValue(ui.loginBGImage); this.loginUiFormGroup.get(‘loginLogoImageUrl’).patchValue(ui.loginLogoImageUrl); this.loginUiFormGroup.get(‘loginLogoImageHeight’).patchValue(ui.loginLogoImageHeight); this.loginUiFormGroup.get(‘loginBGColor’).patchValue(ui.loginBGColor); this.loginUiFormGroup.get(‘loginFormBGColor’).patchValue(ui.loginFormBGColor); this.loginUiFormGroup.get(‘loginFormTextColor’).patchValue(ui.loginFormTextColor); this.loginUiFormGroup.get(‘loginFormIconColor’).patchValue(ui.loginFormIconColor); this.loginUiFormGroup.get(‘loginFormInputColor’).patchValue(ui.loginFormInputColor); this.loginUiFormGroup.get(‘loginButtonColor’).patchValue(ui.loginButtonColor); this.loginUiFormGroup.get(‘loginButtonTextColor’).patchValue(ui.loginButtonTextColor); }

    //恢复到tb原始设置 reset($event: Event) { if ($event) { $event.stopPropagation(); } this.patchFormValue(initialLoginUIState); this.store.dispatch(new ActionLoginUIChange(this.loginUiFormGroup.value)); this.isDirty = true; }

    //撤销本次操作 cancel($event: Event) { if ($event) { $event.stopPropagation(); } this.writeFormByHttp(); }

    //初始化表单 initForm() { this.loginUiFormGroup = this.fb.group({ loginDomainName: [null, [Validators.required]], loginAppTitle: [null, []], loginIconImageUrl: [null, []], loginBGImage: [null, []], loginLogoImageUrl: [null, []], loginLogoImageHeight: [null, []], loginBGColor: [null, []], loginFormBGColor: [null, []], loginFormTextColor: [false, []], loginFormIconColor: [false, []], loginFormInputColor: [false, []], loginButtonColor: [null, []], loginButtonTextColor: [null, []] }); }

    submit($event: Event) { if ($event) { $event.stopPropagation(); } this.dashboardService.saveTenantLoginUIInfo(this.loginUiFormGroup.value as UIInfo).subscribe(); this.store.dispatch(new ActionLoginUIChange(this.loginUiFormGroup.value)); this.isDirty = false; }

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

    1. login-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. }