效果演示
点击查看【bilibili】
说明:主要实现同一个前端项目,给不同租户分配不同域名和对应的自定义登录主题,租户访问自己的域名时,展示自己的自定义登录页面。
资料
ngrx/store
angular canDeactivate
angular FormBuilder
angular FormGroup
Material Design 组件库 for Angular
material design icons
ThingsBoard图片上传组件拓展
添加自定义菜单
添加首页设置-展示仪表功能
思路
- 创建自定义菜单,可参考添加自定义菜单
- 创建表单,包含登录页主题修改需要的字段。涉及TB自定义的组件:
tb-image-input
图片上传组件,tb-color-input
颜色选择组件。 - 加载登录页面时根据domain(域名)获取对应的主题参数,并通过store通知app.component.ts修改应用名称和icon。
- 定义后端接口,并将登录界面表单元素储存到租户的额外信息中。
- 国际化,ui-ngx/src/assets/locale/locale.constant-zh_CN.json中加入国际化信息。
- 修改nginx配置,代理多个域名指向同一个前端地址。
修改angular.json中custom-webpack的配置,添加授权host。
- 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){ this.tenantId = user.tenantId;
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’; } }
login-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;
}
- webpack-dev-server 2.4.3后新增对host header的正确性检测,以屏蔽未经授权的访问。官方提供了两个解决方案: