[web-app] ng-alain模版工程初始化
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', pathMatch: 'full', redirectTo: '/welcome' },
|
||||
{ path: 'welcome', loadChildren: () => import('./pages/welcome/welcome.module').then(m => m.WelcomeModule) }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
@@ -1,54 +0,0 @@
|
||||
<nz-layout class="app-layout">
|
||||
<nz-sider class="menu-sidebar"
|
||||
nzCollapsible
|
||||
nzWidth="256px"
|
||||
nzBreakpoint="md"
|
||||
[(nzCollapsed)]="isCollapsed"
|
||||
[nzTrigger]="null">
|
||||
<div class="sidebar-logo">
|
||||
<a href="https://ng.ant.design/" target="_blank">
|
||||
<img src="https://ng.ant.design/assets/img/logo.svg" alt="logo">
|
||||
<h1>Ant Design Of Angular</h1>
|
||||
</a>
|
||||
</div>
|
||||
<ul nz-menu nzTheme="dark" nzMode="inline" [nzInlineCollapsed]="isCollapsed">
|
||||
<li nz-submenu nzOpen nzTitle="Dashboard" nzIcon="dashboard">
|
||||
<ul>
|
||||
<li nz-menu-item nzMatchRouter>
|
||||
<a routerLink="/welcome">Welcome</a>
|
||||
</li>
|
||||
<li nz-menu-item nzMatchRouter>
|
||||
<a>Monitor</a>
|
||||
</li>
|
||||
<li nz-menu-item nzMatchRouter>
|
||||
<a>Workplace</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li nz-submenu nzOpen nzTitle="Form" nzIcon="form">
|
||||
<ul>
|
||||
<li nz-menu-item nzMatchRouter>
|
||||
<a>Basic Form</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nz-sider>
|
||||
<nz-layout>
|
||||
<nz-header>
|
||||
<div class="app-header">
|
||||
<span class="header-trigger" (click)="isCollapsed = !isCollapsed">
|
||||
<i class="trigger"
|
||||
nz-icon
|
||||
[nzType]="isCollapsed ? 'menu-unfold' : 'menu-fold'"
|
||||
></i>
|
||||
</span>
|
||||
</div>
|
||||
</nz-header>
|
||||
<nz-content>
|
||||
<div class="inner-content">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</nz-content>
|
||||
</nz-layout>
|
||||
</nz-layout>
|
||||
@@ -1,80 +0,0 @@
|
||||
:host {
|
||||
display: flex;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.app-layout {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.menu-sidebar {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
min-height: 100vh;
|
||||
box-shadow: 2px 0 6px rgba(0,21,41,.35);
|
||||
}
|
||||
|
||||
.header-trigger {
|
||||
height: 64px;
|
||||
padding: 20px 24px;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
transition: all .3s,padding 0s;
|
||||
}
|
||||
|
||||
.trigger:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
position: relative;
|
||||
height: 64px;
|
||||
padding-left: 24px;
|
||||
overflow: hidden;
|
||||
line-height: 64px;
|
||||
background: #001529;
|
||||
transition: all .3s;
|
||||
}
|
||||
|
||||
.sidebar-logo img {
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.sidebar-logo h1 {
|
||||
display: inline-block;
|
||||
margin: 0 0 0 20px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
font-family: Avenir,Helvetica Neue,Arial,Helvetica,sans-serif;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
nz-header {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
position: relative;
|
||||
height: 64px;
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0,21,41,.08);
|
||||
}
|
||||
|
||||
nz-content {
|
||||
margin: 24px;
|
||||
}
|
||||
|
||||
.inner-content {
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterTestingModule
|
||||
],
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have as title 'web-app'`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('web-app');
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('.content span')?.textContent).toContain('web-app app is running!');
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,46 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, ElementRef, OnInit, Renderer2 } from '@angular/core';
|
||||
import { NavigationEnd, NavigationError, RouteConfigLoadStart, Router } from '@angular/router';
|
||||
import { TitleService, VERSION as VERSION_ALAIN } from '@delon/theme';
|
||||
import { environment } from '@env/environment';
|
||||
import { NzModalService } from 'ng-zorro-antd/modal';
|
||||
import { VERSION as VERSION_ZORRO } from 'ng-zorro-antd/version';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.less']
|
||||
template: ` <router-outlet></router-outlet> `
|
||||
})
|
||||
export class AppComponent {
|
||||
isCollapsed = false;
|
||||
export class AppComponent implements OnInit {
|
||||
constructor(
|
||||
el: ElementRef,
|
||||
renderer: Renderer2,
|
||||
private router: Router,
|
||||
private titleSrv: TitleService,
|
||||
private modalSrv: NzModalService
|
||||
) {
|
||||
renderer.setAttribute(el.nativeElement, 'ng-alain-version', VERSION_ALAIN.full);
|
||||
renderer.setAttribute(el.nativeElement, 'ng-zorro-version', VERSION_ZORRO.full);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
let configLoad = false;
|
||||
this.router.events.subscribe(ev => {
|
||||
if (ev instanceof RouteConfigLoadStart) {
|
||||
configLoad = true;
|
||||
}
|
||||
if (configLoad && ev instanceof NavigationError) {
|
||||
this.modalSrv.confirm({
|
||||
nzTitle: `提醒`,
|
||||
nzContent: environment.production ? `应用可能已发布新版本,请点击刷新才能生效。` : `无法加载路由:${ev.url}`,
|
||||
nzCancelDisabled: false,
|
||||
nzOkText: '刷新',
|
||||
nzCancelText: '忽略',
|
||||
nzOnOk: () => location.reload()
|
||||
});
|
||||
}
|
||||
if (ev instanceof NavigationEnd) {
|
||||
this.titleSrv.setTitle();
|
||||
this.modalSrv.closeAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,88 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
/* eslint-disable import/order */
|
||||
/* eslint-disable import/no-duplicates */
|
||||
import { HttpClient, HttpClientModule } from '@angular/common/http';
|
||||
import { APP_INITIALIZER, Injector, LOCALE_ID, NgModule, Type } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { NZ_I18N } from 'ng-zorro-antd/i18n';
|
||||
import { en_US } from 'ng-zorro-antd/i18n';
|
||||
import { registerLocaleData } from '@angular/common';
|
||||
import en from '@angular/common/locales/en';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { IconsProviderModule } from './icons-provider.module';
|
||||
import { NzLayoutModule } from 'ng-zorro-antd/layout';
|
||||
import { NzMenuModule } from 'ng-zorro-antd/menu';
|
||||
import { NzMessageModule } from 'ng-zorro-antd/message';
|
||||
import { NzNotificationModule } from 'ng-zorro-antd/notification';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
registerLocaleData(en);
|
||||
// #region default language
|
||||
// Reference: https://ng-alain.com/docs/i18n
|
||||
import { default as ngLang } from '@angular/common/locales/zh';
|
||||
import { DELON_LOCALE, zh_CN as delonLang } from '@delon/theme';
|
||||
import { zhCN as dateLang } from 'date-fns/locale';
|
||||
import { NZ_DATE_LOCALE, NZ_I18N, zh_CN as zorroLang } from 'ng-zorro-antd/i18n';
|
||||
const LANG = {
|
||||
abbr: 'zh',
|
||||
ng: ngLang,
|
||||
zorro: zorroLang,
|
||||
date: dateLang,
|
||||
delon: delonLang,
|
||||
};
|
||||
// register angular
|
||||
import { registerLocaleData } from '@angular/common';
|
||||
registerLocaleData(LANG.ng, LANG.abbr);
|
||||
const LANG_PROVIDES = [
|
||||
{ provide: LOCALE_ID, useValue: LANG.abbr },
|
||||
{ provide: NZ_I18N, useValue: LANG.zorro },
|
||||
{ provide: NZ_DATE_LOCALE, useValue: LANG.date },
|
||||
{ provide: DELON_LOCALE, useValue: LANG.delon },
|
||||
];
|
||||
// #endregion
|
||||
// #region i18n services
|
||||
import { ALAIN_I18N_TOKEN } from '@delon/theme';
|
||||
import { I18NService } from '@core';
|
||||
|
||||
const I18NSERVICE_PROVIDES = [
|
||||
{ provide: ALAIN_I18N_TOKEN, useClass: I18NService, multi: false }
|
||||
];
|
||||
// #region
|
||||
|
||||
// #region JSON Schema form (using @delon/form)
|
||||
import { JsonSchemaModule } from '@shared';
|
||||
const FORM_MODULES = [ JsonSchemaModule ];
|
||||
// #endregion
|
||||
|
||||
|
||||
// #region Http Interceptors
|
||||
import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { DefaultInterceptor } from '@core';
|
||||
import { SimpleInterceptor } from '@delon/auth';
|
||||
const INTERCEPTOR_PROVIDES = [
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: SimpleInterceptor, multi: true},
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true}
|
||||
];
|
||||
// #endregion
|
||||
|
||||
// #region global third module
|
||||
const GLOBAL_THIRD_MODULES: Array<Type<void>> = [];
|
||||
// #endregion
|
||||
|
||||
// #region Startup Service
|
||||
import { StartupService } from '@core';
|
||||
export function StartupServiceFactory(startupService: StartupService): () => Observable<void> {
|
||||
return () => startupService.load();
|
||||
}
|
||||
const APPINIT_PROVIDES = [
|
||||
StartupService,
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: StartupServiceFactory,
|
||||
deps: [StartupService],
|
||||
multi: true
|
||||
}
|
||||
];
|
||||
// #endregion
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { CoreModule } from './core/core.module';
|
||||
import { GlobalConfigModule } from './global-config.module';
|
||||
import { LayoutModule } from './layout/layout.module';
|
||||
import { RoutesModule } from './routes/routes.module';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
import { STWidgetModule } from './shared/st-widget/st-widget.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -22,15 +90,25 @@ registerLocaleData(en);
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
AppRoutingModule,
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
BrowserAnimationsModule,
|
||||
IconsProviderModule,
|
||||
NzLayoutModule,
|
||||
NzMenuModule
|
||||
HttpClientModule,
|
||||
GlobalConfigModule.forRoot(),
|
||||
CoreModule,
|
||||
SharedModule,
|
||||
LayoutModule,
|
||||
RoutesModule,
|
||||
STWidgetModule,
|
||||
NzMessageModule,
|
||||
NzNotificationModule,
|
||||
...FORM_MODULES,
|
||||
...GLOBAL_THIRD_MODULES
|
||||
],
|
||||
providers: [
|
||||
...LANG_PROVIDES,
|
||||
...INTERCEPTOR_PROVIDES,
|
||||
...I18NSERVICE_PROVIDES,
|
||||
...APPINIT_PROVIDES
|
||||
],
|
||||
providers: [{ provide: NZ_I18N, useValue: en_US }],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
||||
|
||||
5
web-app/src/app/core/README.md
Normal file
5
web-app/src/app/core/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
### CoreModule
|
||||
|
||||
**应** 仅只留 `providers` 属性。
|
||||
|
||||
**作用:** 一些通用服务,例如:用户消息、HTTP数据访问。
|
||||
15
web-app/src/app/core/core.module.ts
Normal file
15
web-app/src/app/core/core.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { NgModule, Optional, SkipSelf } from '@angular/core';
|
||||
import { throwIfAlreadyLoaded } from './module-import-guard';
|
||||
|
||||
import { I18NService } from './i18n/i18n.service';
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
I18NService
|
||||
]
|
||||
})
|
||||
export class CoreModule {
|
||||
constructor( @Optional() @SkipSelf() parentModule: CoreModule) {
|
||||
throwIfAlreadyLoaded(parentModule, 'CoreModule');
|
||||
}
|
||||
}
|
||||
84
web-app/src/app/core/i18n/i18n.service.spec.ts
Normal file
84
web-app/src/app/core/i18n/i18n.service.spec.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { TestBed, TestBedStatic } from '@angular/core/testing';
|
||||
import { DelonLocaleService, SettingsService } from '@delon/theme';
|
||||
import { NzSafeAny } from 'ng-zorro-antd/core/types';
|
||||
import { NzI18nService } from 'ng-zorro-antd/i18n';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { I18NService } from './i18n.service';
|
||||
|
||||
describe('Service: I18n', () => {
|
||||
let injector: TestBedStatic;
|
||||
let srv: I18NService;
|
||||
const MockSettingsService: NzSafeAny = {
|
||||
layout: {
|
||||
lang: null
|
||||
}
|
||||
};
|
||||
const MockNzI18nService = {
|
||||
setLocale: () => {},
|
||||
setDateLocale: () => {}
|
||||
};
|
||||
const MockDelonLocaleService = {
|
||||
setLocale: () => {}
|
||||
};
|
||||
const MockTranslateService = {
|
||||
getBrowserLang: jasmine.createSpy('getBrowserLang'),
|
||||
addLangs: () => {},
|
||||
setLocale: () => {},
|
||||
getDefaultLang: () => '',
|
||||
use: (lang: string) => of(lang),
|
||||
instant: jasmine.createSpy('instant')
|
||||
};
|
||||
|
||||
function genModule(): void {
|
||||
injector = TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
providers: [
|
||||
I18NService,
|
||||
{ provide: SettingsService, useValue: MockSettingsService },
|
||||
{ provide: NzI18nService, useValue: MockNzI18nService },
|
||||
{ provide: DelonLocaleService, useValue: MockDelonLocaleService }
|
||||
]
|
||||
});
|
||||
srv = TestBed.inject(I18NService);
|
||||
}
|
||||
|
||||
it('should working', () => {
|
||||
spyOnProperty(navigator, 'languages').and.returnValue(['zh-CN']);
|
||||
genModule();
|
||||
expect(srv).toBeTruthy();
|
||||
expect(srv.defaultLang).toBe('zh-CN');
|
||||
srv.fanyi('a');
|
||||
srv.fanyi('a', {});
|
||||
});
|
||||
|
||||
it('should be used layout as default language', () => {
|
||||
MockSettingsService.layout.lang = 'en-US';
|
||||
const navSpy = spyOnProperty(navigator, 'languages');
|
||||
genModule();
|
||||
expect(navSpy).not.toHaveBeenCalled();
|
||||
expect(srv.defaultLang).toBe('en-US');
|
||||
MockSettingsService.layout.lang = null;
|
||||
});
|
||||
|
||||
it('should be used browser as default language', () => {
|
||||
spyOnProperty(navigator, 'languages').and.returnValue(['zh-TW']);
|
||||
genModule();
|
||||
expect(srv.defaultLang).toBe('zh-TW');
|
||||
});
|
||||
|
||||
it('should be use default language when the browser language is not in the list', () => {
|
||||
spyOnProperty(navigator, 'languages').and.returnValue(['es-419']);
|
||||
genModule();
|
||||
expect(srv.defaultLang).toBe('zh-CN');
|
||||
});
|
||||
|
||||
it('should be trigger notify when changed language', () => {
|
||||
genModule();
|
||||
srv.use('en-US', {});
|
||||
srv.change.subscribe(lang => {
|
||||
expect(lang).toBe('en-US');
|
||||
});
|
||||
});
|
||||
});
|
||||
116
web-app/src/app/core/i18n/i18n.service.ts
Normal file
116
web-app/src/app/core/i18n/i18n.service.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
// 请参考:https://ng-alain.com/docs/i18n
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import { registerLocaleData } from '@angular/common';
|
||||
import ngEn from '@angular/common/locales/en';
|
||||
import ngZh from '@angular/common/locales/zh';
|
||||
import ngZhTw from '@angular/common/locales/zh-Hant';
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
DelonLocaleService,
|
||||
en_US as delonEnUS,
|
||||
SettingsService,
|
||||
zh_CN as delonZhCn,
|
||||
zh_TW as delonZhTw,
|
||||
_HttpClient,
|
||||
AlainI18nBaseService
|
||||
} from '@delon/theme';
|
||||
import { AlainConfigService } from '@delon/util/config';
|
||||
import { enUS as dfEn, zhCN as dfZhCn, zhTW as dfZhTw } from 'date-fns/locale';
|
||||
import { NzSafeAny } from 'ng-zorro-antd/core/types';
|
||||
import { en_US as zorroEnUS, NzI18nService, zh_CN as zorroZhCN, zh_TW as zorroZhTW } from 'ng-zorro-antd/i18n';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
interface LangConfigData {
|
||||
abbr: string;
|
||||
text: string;
|
||||
ng: NzSafeAny;
|
||||
zorro: NzSafeAny;
|
||||
date: NzSafeAny;
|
||||
delon: NzSafeAny;
|
||||
}
|
||||
|
||||
const DEFAULT = 'zh-CN';
|
||||
const LANGS: { [key: string]: LangConfigData } = {
|
||||
'zh-CN': {
|
||||
text: '简体中文',
|
||||
ng: ngZh,
|
||||
zorro: zorroZhCN,
|
||||
date: dfZhCn,
|
||||
delon: delonZhCn,
|
||||
abbr: '🇨🇳'
|
||||
},
|
||||
'zh-TW': {
|
||||
text: '繁体中文',
|
||||
ng: ngZhTw,
|
||||
zorro: zorroZhTW,
|
||||
date: dfZhTw,
|
||||
delon: delonZhTw,
|
||||
abbr: '🇭🇰'
|
||||
},
|
||||
'en-US': {
|
||||
text: 'English',
|
||||
ng: ngEn,
|
||||
zorro: zorroEnUS,
|
||||
date: dfEn,
|
||||
delon: delonEnUS,
|
||||
abbr: '🇬🇧'
|
||||
}
|
||||
};
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class I18NService extends AlainI18nBaseService {
|
||||
protected _defaultLang = DEFAULT;
|
||||
private _langs = Object.keys(LANGS).map(code => {
|
||||
const item = LANGS[code];
|
||||
return { code, text: item.text, abbr: item.abbr };
|
||||
});
|
||||
|
||||
constructor(
|
||||
private http: _HttpClient,
|
||||
private settings: SettingsService,
|
||||
private nzI18nService: NzI18nService,
|
||||
private delonLocaleService: DelonLocaleService,
|
||||
private platform: Platform,
|
||||
cogSrv: AlainConfigService
|
||||
) {
|
||||
super(cogSrv);
|
||||
|
||||
const defaultLang = this.getDefaultLang();
|
||||
this._defaultLang = this._langs.findIndex(w => w.code === defaultLang) === -1 ? DEFAULT : defaultLang;
|
||||
}
|
||||
|
||||
private getDefaultLang(): string {
|
||||
if (!this.platform.isBrowser) {
|
||||
return DEFAULT;
|
||||
}
|
||||
if (this.settings.layout.lang) {
|
||||
return this.settings.layout.lang;
|
||||
}
|
||||
let res = (navigator.languages ? navigator.languages[0] : null) || navigator.language;
|
||||
const arr = res.split('-');
|
||||
return arr.length <= 1 ? res : `${arr[0]}-${arr[1].toUpperCase()}`;
|
||||
}
|
||||
|
||||
loadLangData(lang: string): Observable<NzSafeAny> {
|
||||
return this.http.get(`assets/tmp/i18n/${lang}.json`);
|
||||
}
|
||||
|
||||
use(lang: string, data: Record<string, unknown>): void {
|
||||
if (this._currentLang === lang) return;
|
||||
|
||||
this._data = this.flatData(data, []);
|
||||
|
||||
const item = LANGS[lang];
|
||||
registerLocaleData(item.ng);
|
||||
this.nzI18nService.setLocale(item.zorro);
|
||||
this.nzI18nService.setDateLocale(item.date);
|
||||
this.delonLocaleService.setLocale(item.delon);
|
||||
this._currentLang = lang;
|
||||
|
||||
this._change$.next(lang);
|
||||
}
|
||||
|
||||
getLangs(): Array<{ code: string; text: string; abbr: string }> {
|
||||
return this._langs;
|
||||
}
|
||||
}
|
||||
4
web-app/src/app/core/index.ts
Normal file
4
web-app/src/app/core/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './i18n/i18n.service';
|
||||
export * from './module-import-guard';
|
||||
export * from './net/default.interceptor';
|
||||
export * from './startup/startup.service';
|
||||
6
web-app/src/app/core/module-import-guard.ts
Normal file
6
web-app/src/app/core/module-import-guard.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// https://angular.io/guide/styleguide#style-04-12
|
||||
export function throwIfAlreadyLoaded(parentModule: any, moduleName: string): void {
|
||||
if (parentModule) {
|
||||
throw new Error(`${moduleName} has already been loaded. Import Core modules in the AppModule only.`);
|
||||
}
|
||||
}
|
||||
261
web-app/src/app/core/net/default.interceptor.ts
Normal file
261
web-app/src/app/core/net/default.interceptor.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import {
|
||||
HttpErrorResponse,
|
||||
HttpEvent,
|
||||
HttpHandler,
|
||||
HttpHeaders,
|
||||
HttpInterceptor,
|
||||
HttpRequest,
|
||||
HttpResponseBase
|
||||
} from '@angular/common/http';
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
|
||||
import { ALAIN_I18N_TOKEN, _HttpClient } from '@delon/theme';
|
||||
import { environment } from '@env/environment';
|
||||
import { NzNotificationService } from 'ng-zorro-antd/notification';
|
||||
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
|
||||
import { catchError, filter, mergeMap, switchMap, take } from 'rxjs/operators';
|
||||
|
||||
const CODEMESSAGE: { [key: number]: string } = {
|
||||
200: '服务器成功返回请求的数据。',
|
||||
201: '新建或修改数据成功。',
|
||||
202: '一个请求已经进入后台排队(异步任务)。',
|
||||
204: '删除数据成功。',
|
||||
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
|
||||
401: '用户没有权限(令牌、用户名、密码错误)。',
|
||||
403: '用户得到授权,但是访问是被禁止的。',
|
||||
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
|
||||
406: '请求的格式不可得。',
|
||||
410: '请求的资源被永久删除,且不会再得到的。',
|
||||
422: '当创建一个对象时,发生一个验证错误。',
|
||||
500: '服务器发生错误,请检查服务器。',
|
||||
502: '网关错误。',
|
||||
503: '服务不可用,服务器暂时过载或维护。',
|
||||
504: '网关超时。'
|
||||
};
|
||||
|
||||
/**
|
||||
* 默认HTTP拦截器,其注册细节见 `app.module.ts`
|
||||
*/
|
||||
@Injectable()
|
||||
export class DefaultInterceptor implements HttpInterceptor {
|
||||
private refreshTokenEnabled = environment.api.refreshTokenEnabled;
|
||||
private refreshTokenType: 're-request' | 'auth-refresh' = environment.api.refreshTokenType;
|
||||
private refreshToking = false;
|
||||
private refreshToken$: BehaviorSubject<any> = new BehaviorSubject<any>(null);
|
||||
|
||||
constructor(private injector: Injector) {
|
||||
if (this.refreshTokenType === 'auth-refresh') {
|
||||
this.buildAuthRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
private get notification(): NzNotificationService {
|
||||
return this.injector.get(NzNotificationService);
|
||||
}
|
||||
|
||||
private get tokenSrv(): ITokenService {
|
||||
return this.injector.get(DA_SERVICE_TOKEN);
|
||||
}
|
||||
|
||||
private get http(): _HttpClient {
|
||||
return this.injector.get(_HttpClient);
|
||||
}
|
||||
|
||||
private goTo(url: string): void {
|
||||
setTimeout(() => this.injector.get(Router).navigateByUrl(url));
|
||||
}
|
||||
|
||||
private checkStatus(ev: HttpResponseBase): void {
|
||||
if ((ev.status >= 200 && ev.status < 300) || ev.status === 401) {
|
||||
return;
|
||||
}
|
||||
|
||||
const errortext = CODEMESSAGE[ev.status] || ev.statusText;
|
||||
this.notification.error(`请求错误 ${ev.status}: ${ev.url}`, errortext);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新 Token 请求
|
||||
*/
|
||||
private refreshTokenRequest(): Observable<any> {
|
||||
const model = this.tokenSrv.get();
|
||||
return this.http.post(`/api/auth/refresh`, null, null, { headers: { refresh_token: model?.refresh_token || '' } });
|
||||
}
|
||||
|
||||
// #region 刷新Token方式一:使用 401 重新刷新 Token
|
||||
|
||||
private tryRefreshToken(ev: HttpResponseBase, req: HttpRequest<any>, next: HttpHandler): Observable<any> {
|
||||
// 1、若请求为刷新Token请求,表示来自刷新Token可以直接跳转登录页
|
||||
if ([`/api/auth/refresh`].some(url => req.url.includes(url))) {
|
||||
this.toLogin();
|
||||
return throwError(ev);
|
||||
}
|
||||
// 2、如果 `refreshToking` 为 `true` 表示已经在请求刷新 Token 中,后续所有请求转入等待状态,直至结果返回后再重新发起请求
|
||||
if (this.refreshToking) {
|
||||
return this.refreshToken$.pipe(
|
||||
filter(v => !!v),
|
||||
take(1),
|
||||
switchMap(() => next.handle(this.reAttachToken(req)))
|
||||
);
|
||||
}
|
||||
// 3、尝试调用刷新 Token
|
||||
this.refreshToking = true;
|
||||
this.refreshToken$.next(null);
|
||||
|
||||
return this.refreshTokenRequest().pipe(
|
||||
switchMap(res => {
|
||||
// 通知后续请求继续执行
|
||||
this.refreshToking = false;
|
||||
this.refreshToken$.next(res);
|
||||
// 重新保存新 token
|
||||
this.tokenSrv.set(res);
|
||||
// 重新发起请求
|
||||
return next.handle(this.reAttachToken(req));
|
||||
}),
|
||||
catchError(err => {
|
||||
this.refreshToking = false;
|
||||
this.toLogin();
|
||||
return throwError(err);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新附加新 Token 信息
|
||||
*
|
||||
* > 由于已经发起的请求,不会再走一遍 `@delon/auth` 因此需要结合业务情况重新附加新的 Token
|
||||
*/
|
||||
private reAttachToken(req: HttpRequest<any>): HttpRequest<any> {
|
||||
// 以下示例是以 NG-ALAIN 默认使用 `SimpleInterceptor`
|
||||
const token = this.tokenSrv.get()?.token;
|
||||
return req.clone({
|
||||
setHeaders: {
|
||||
token: `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region 刷新Token方式二:使用 `@delon/auth` 的 `refresh` 接口
|
||||
|
||||
private buildAuthRefresh(): void {
|
||||
if (!this.refreshTokenEnabled) {
|
||||
return;
|
||||
}
|
||||
this.tokenSrv.refresh
|
||||
.pipe(
|
||||
filter(() => !this.refreshToking),
|
||||
switchMap(res => {
|
||||
console.log(res);
|
||||
this.refreshToking = true;
|
||||
return this.refreshTokenRequest();
|
||||
})
|
||||
)
|
||||
.subscribe(
|
||||
res => {
|
||||
// TODO: Mock expired value
|
||||
res.expired = +new Date() + 1000 * 60 * 5;
|
||||
this.refreshToking = false;
|
||||
this.tokenSrv.set(res);
|
||||
},
|
||||
() => this.toLogin()
|
||||
);
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
private toLogin(): void {
|
||||
this.notification.error(`未登录或登录已过期,请重新登录。`, ``);
|
||||
this.goTo(this.tokenSrv.login_url!);
|
||||
}
|
||||
|
||||
private handleData(ev: HttpResponseBase, req: HttpRequest<any>, next: HttpHandler): Observable<any> {
|
||||
this.checkStatus(ev);
|
||||
// 业务处理:一些通用操作
|
||||
switch (ev.status) {
|
||||
case 200:
|
||||
// 业务层级错误处理,以下是假定restful有一套统一输出格式(指不管成功与否都有相应的数据格式)情况下进行处理
|
||||
// 例如响应内容:
|
||||
// 错误内容:{ status: 1, msg: '非法参数' }
|
||||
// 正确内容:{ status: 0, response: { } }
|
||||
// 则以下代码片断可直接适用
|
||||
// if (ev instanceof HttpResponse) {
|
||||
// const body = ev.body;
|
||||
// if (body && body.status !== 0) {
|
||||
// this.injector.get(NzMessageService).error(body.msg);
|
||||
// // 注意:这里如果继续抛出错误会被行254的 catchError 二次拦截,导致外部实现的 Pipe、subscribe 操作被中断,例如:this.http.get('/').subscribe() 不会触发
|
||||
// // 如果你希望外部实现,需要手动移除行254
|
||||
// return throwError({});
|
||||
// } else {
|
||||
// // 忽略 Blob 文件体
|
||||
// if (ev.body instanceof Blob) {
|
||||
// return of(ev);
|
||||
// }
|
||||
// // 重新修改 `body` 内容为 `response` 内容,对于绝大多数场景已经无须再关心业务状态码
|
||||
// return of(new HttpResponse(Object.assign(ev, { body: body.response })));
|
||||
// // 或者依然保持完整的格式
|
||||
// return of(ev);
|
||||
// }
|
||||
// }
|
||||
break;
|
||||
case 401:
|
||||
if (this.refreshTokenEnabled && this.refreshTokenType === 're-request') {
|
||||
return this.tryRefreshToken(ev, req, next);
|
||||
}
|
||||
this.toLogin();
|
||||
break;
|
||||
case 403:
|
||||
case 404:
|
||||
case 500:
|
||||
// this.goTo(`/exception/${ev.status}?url=${req.urlWithParams}`);
|
||||
break;
|
||||
default:
|
||||
if (ev instanceof HttpErrorResponse) {
|
||||
console.warn(
|
||||
'未可知错误,大部分是由于后端不支持跨域CORS或无效配置引起,请参考 https://ng-alain.com/docs/server 解决跨域问题',
|
||||
ev
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (ev instanceof HttpErrorResponse) {
|
||||
return throwError(ev);
|
||||
} else {
|
||||
return of(ev);
|
||||
}
|
||||
}
|
||||
|
||||
private getAdditionalHeaders(headers?: HttpHeaders): { [name: string]: string } {
|
||||
const res: { [name: string]: string } = {};
|
||||
const lang = this.injector.get(ALAIN_I18N_TOKEN).currentLang;
|
||||
if (!headers?.has('Accept-Language') && lang) {
|
||||
res['Accept-Language'] = lang;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
// 统一加上服务端前缀
|
||||
let url = req.url;
|
||||
if (!url.startsWith('https://') && !url.startsWith('http://')) {
|
||||
const { baseUrl } = environment.api;
|
||||
url = baseUrl + (baseUrl.endsWith('/') && url.startsWith('/') ? url.substring(1) : url);
|
||||
}
|
||||
|
||||
const newReq = req.clone({ url, setHeaders: this.getAdditionalHeaders(req.headers) });
|
||||
return next.handle(newReq).pipe(
|
||||
mergeMap(ev => {
|
||||
// 允许统一对请求错误处理
|
||||
if (ev instanceof HttpResponseBase) {
|
||||
return this.handleData(ev, newReq, next);
|
||||
}
|
||||
// 若一切都正常,则后续操作
|
||||
return of(ev);
|
||||
}),
|
||||
catchError((err: HttpErrorResponse) => this.handleData(err, newReq, next))
|
||||
);
|
||||
}
|
||||
}
|
||||
127
web-app/src/app/core/startup/startup.service.ts
Normal file
127
web-app/src/app/core/startup/startup.service.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { Injectable, Inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
|
||||
import { ALAIN_I18N_TOKEN, MenuService, SettingsService, TitleService } from '@delon/theme';
|
||||
import { ACLService } from '@delon/acl';
|
||||
import { I18NService } from '../i18n/i18n.service';
|
||||
import { Observable, zip, of } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import type { NzSafeAny } from 'ng-zorro-antd/core/types';
|
||||
import { NzIconService } from 'ng-zorro-antd/icon';
|
||||
|
||||
import { ICONS } from '../../../style-icons';
|
||||
import { ICONS_AUTO } from '../../../style-icons-auto';
|
||||
|
||||
/**
|
||||
* Used for application startup
|
||||
* Generally used to get the basic data of the application, like: Menu Data, User Data, etc.
|
||||
*/
|
||||
@Injectable()
|
||||
export class StartupService {
|
||||
constructor(
|
||||
iconSrv: NzIconService,
|
||||
private menuService: MenuService,
|
||||
@Inject(ALAIN_I18N_TOKEN) private i18n: I18NService,
|
||||
private settingService: SettingsService,
|
||||
private aclService: ACLService,
|
||||
private titleService: TitleService,
|
||||
@Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService,
|
||||
private httpClient: HttpClient,
|
||||
private router: Router
|
||||
) {
|
||||
iconSrv.addIcon(...ICONS_AUTO, ...ICONS);
|
||||
}
|
||||
|
||||
|
||||
private viaHttp(): Observable<void> {
|
||||
const defaultLang = this.i18n.defaultLang;
|
||||
return zip(this.i18n.loadLangData(defaultLang), this.httpClient.get('assets/tmp/app-data.json')).pipe(
|
||||
catchError((res: NzSafeAny) => {
|
||||
console.warn(`StartupService.load: Network request failed`, res);
|
||||
setTimeout(() => this.router.navigateByUrl(`/exception/500`));
|
||||
return [];
|
||||
}),
|
||||
map(([langData, appData]: [Record<string, string>, NzSafeAny]) => {
|
||||
// setting language data
|
||||
this.i18n.use(defaultLang, langData);
|
||||
|
||||
// Application data
|
||||
// Application information: including site name, description, year
|
||||
this.settingService.setApp(appData.app);
|
||||
// User information: including name, avatar, email address
|
||||
this.settingService.setUser(appData.user);
|
||||
// ACL: Set the permissions to full, https://ng-alain.com/acl/getting-started
|
||||
this.aclService.setFull(true);
|
||||
// Menu data, https://ng-alain.com/theme/menu
|
||||
this.menuService.add(appData.menu);
|
||||
// Can be set page suffix title, https://ng-alain.com/theme/title
|
||||
this.titleService.suffix = appData.app.name;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private viaMockI18n(): Observable<void> {
|
||||
const defaultLang = this.i18n.defaultLang;
|
||||
return this.i18n.loadLangData(defaultLang).pipe(
|
||||
map((langData: NzSafeAny) => {
|
||||
this.i18n.use(defaultLang, langData);
|
||||
|
||||
this.viaMock();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private viaMock(): Observable<void> {
|
||||
// const tokenData = this.tokenService.get();
|
||||
// if (!tokenData.token) {
|
||||
// this.router.navigateByUrl(this.tokenService.login_url!);
|
||||
// return;
|
||||
// }
|
||||
// mock
|
||||
const app: any = {
|
||||
name: `HertzBeat`,
|
||||
description: `面向开发者,易用友好的高性能监控云服务`
|
||||
};
|
||||
const user: any = {
|
||||
name: 'Admin',
|
||||
avatar: './assets/tmp/img/avatar.svg',
|
||||
email: 'tomsun28@outlook.com',
|
||||
token: '123456789'
|
||||
};
|
||||
// Application information: including site name, description, year
|
||||
this.settingService.setApp(app);
|
||||
// User information: including name, avatar, email address
|
||||
this.settingService.setUser(user);
|
||||
// ACL: Set the permissions to full, https://ng-alain.com/acl/getting-started
|
||||
this.aclService.setFull(true);
|
||||
// Menu data, https://ng-alain.com/theme/menu
|
||||
this.menuService.add([
|
||||
{
|
||||
text: 'Main',
|
||||
group: true,
|
||||
children: [
|
||||
{
|
||||
text: 'Dashboard',
|
||||
link: '/dashboard',
|
||||
icon: { type: 'icon', value: 'appstore' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
// Can be set page suffix title, https://ng-alain.com/theme/title
|
||||
this.titleService.suffix = app.name;
|
||||
|
||||
return of();
|
||||
}
|
||||
|
||||
load(): Observable<void> {
|
||||
// http
|
||||
// return this.viaHttp();
|
||||
// mock: Don’t use it in a production environment. ViaMock is just to simulate some data to make the scaffolding work normally
|
||||
// mock:请勿在生产环境中这么使用,viaMock 单纯只是为了模拟一些数据使脚手架一开始能正常运行
|
||||
return this.viaMockI18n();
|
||||
}
|
||||
}
|
||||
77
web-app/src/app/global-config.module.ts
Normal file
77
web-app/src/app/global-config.module.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/* eslint-disable import/order */
|
||||
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
|
||||
import { DelonACLModule } from '@delon/acl';
|
||||
import { AlainThemeModule } from '@delon/theme';
|
||||
import { AlainConfig, ALAIN_CONFIG } from '@delon/util/config';
|
||||
|
||||
import { throwIfAlreadyLoaded } from '@core';
|
||||
|
||||
import { environment } from '@env/environment';
|
||||
|
||||
// Please refer to: https://ng-alain.com/docs/global-config
|
||||
// #region NG-ALAIN Config
|
||||
|
||||
const alainConfig: AlainConfig = {
|
||||
st: { modal: { size: 'lg' } },
|
||||
pageHeader: { homeI18n: 'home' },
|
||||
lodop: {
|
||||
license: `A59B099A586B3851E0F0D7FDBF37B603`,
|
||||
licenseA: `C94CEE276DB2187AE6B65D56B3FC2848`
|
||||
},
|
||||
auth: { login_url: '/passport/login' }
|
||||
};
|
||||
|
||||
const alainModules: any[] = [AlainThemeModule.forRoot(), DelonACLModule.forRoot()];
|
||||
const alainProvides = [{ provide: ALAIN_CONFIG, useValue: alainConfig }];
|
||||
|
||||
// #region reuse-tab
|
||||
/**
|
||||
* 若需要[路由复用](https://ng-alain.com/components/reuse-tab)需要:
|
||||
* 1、在 `shared-delon.module.ts` 导入 `ReuseTabModule` 模块
|
||||
* 2、注册 `RouteReuseStrategy`
|
||||
* 3、在 `src/app/layout/default/default.component.html` 修改:
|
||||
* ```html
|
||||
* <section class="alain-default__content">
|
||||
* <reuse-tab #reuseTab></reuse-tab>
|
||||
* <router-outlet (activate)="reuseTab.activate($event)"></router-outlet>
|
||||
* </section>
|
||||
* ```
|
||||
*/
|
||||
// import { RouteReuseStrategy } from '@angular/router';
|
||||
// import { ReuseTabService, ReuseTabStrategy } from '@delon/abc/reuse-tab';
|
||||
// alainProvides.push({
|
||||
// provide: RouteReuseStrategy,
|
||||
// useClass: ReuseTabStrategy,
|
||||
// deps: [ReuseTabService],
|
||||
// } as any);
|
||||
|
||||
// #endregion
|
||||
|
||||
// #endregion
|
||||
|
||||
// Please refer to: https://ng.ant.design/docs/global-config/en#how-to-use
|
||||
// #region NG-ZORRO Config
|
||||
|
||||
import { NzConfig, NZ_CONFIG } from 'ng-zorro-antd/core/config';
|
||||
|
||||
const ngZorroConfig: NzConfig = {};
|
||||
|
||||
const zorroProvides = [{ provide: NZ_CONFIG, useValue: ngZorroConfig }];
|
||||
|
||||
// #endregion
|
||||
|
||||
@NgModule({
|
||||
imports: [...alainModules, ...(environment.modules || [])]
|
||||
})
|
||||
export class GlobalConfigModule {
|
||||
constructor(@Optional() @SkipSelf() parentModule: GlobalConfigModule) {
|
||||
throwIfAlreadyLoaded(parentModule, 'GlobalConfigModule');
|
||||
}
|
||||
|
||||
static forRoot(): ModuleWithProviders<GlobalConfigModule> {
|
||||
return {
|
||||
ngModule: GlobalConfigModule,
|
||||
providers: [...alainProvides, ...zorroProvides]
|
||||
};
|
||||
}
|
||||
}
|
||||
3
web-app/src/app/layout/README.md
Normal file
3
web-app/src/app/layout/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### layout 页面整体布局
|
||||
|
||||
**作用:** 规范统一类似功能页面的统一样式布局。
|
||||
1
web-app/src/app/layout/basic/README.md
Normal file
1
web-app/src/app/layout/basic/README.md
Normal file
@@ -0,0 +1 @@
|
||||
[Document](https://ng-alain.com/theme/default)
|
||||
85
web-app/src/app/layout/basic/basic.component.ts
Normal file
85
web-app/src/app/layout/basic/basic.component.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { SettingsService, User } from '@delon/theme';
|
||||
import { LayoutDefaultOptions } from '@delon/theme/layout-default';
|
||||
import { environment } from '@env/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'layout-basic',
|
||||
template: `
|
||||
<layout-default [options]="options" [asideUser]="asideUserTpl" [content]="contentTpl" [customError]="null">
|
||||
<layout-default-header-item direction="left">
|
||||
<a layout-default-header-item-trigger href="//github.com/usthe" target="_blank">
|
||||
<i nz-icon nzType="github"></i>
|
||||
</a>
|
||||
</layout-default-header-item>
|
||||
<layout-default-header-item direction="left" hidden="mobile">
|
||||
<a layout-default-header-item-trigger routerLink="/passport/lock">
|
||||
<i nz-icon nzType="lock"></i>
|
||||
</a>
|
||||
</layout-default-header-item>
|
||||
<layout-default-header-item direction="left" hidden="pc">
|
||||
<div layout-default-header-item-trigger (click)="searchToggleStatus = !searchToggleStatus">
|
||||
<i nz-icon nzType="search"></i>
|
||||
</div>
|
||||
</layout-default-header-item>
|
||||
<layout-default-header-item direction="middle">
|
||||
<header-search class="alain-default__search" [toggleChange]="searchToggleStatus"></header-search>
|
||||
</layout-default-header-item>
|
||||
<layout-default-header-item direction="right" hidden="mobile">
|
||||
<div layout-default-header-item-trigger nz-dropdown [nzDropdownMenu]="settingsMenu" nzTrigger="click" nzPlacement="bottomRight">
|
||||
<i nz-icon nzType="setting"></i>
|
||||
</div>
|
||||
<nz-dropdown-menu #settingsMenu="nzDropdownMenu">
|
||||
<div nz-menu style="width: 200px;">
|
||||
<div nz-menu-item>
|
||||
<header-fullscreen></header-fullscreen>
|
||||
</div>
|
||||
<div nz-menu-item>
|
||||
<header-clear-storage></header-clear-storage>
|
||||
</div>
|
||||
<div nz-menu-item>
|
||||
<header-i18n></header-i18n>
|
||||
</div>
|
||||
</div>
|
||||
</nz-dropdown-menu>
|
||||
</layout-default-header-item>
|
||||
<layout-default-header-item direction="right">
|
||||
<header-user></header-user>
|
||||
</layout-default-header-item>
|
||||
<ng-template #asideUserTpl>
|
||||
<div nz-dropdown nzTrigger="click" [nzDropdownMenu]="userMenu" class="alain-default__aside-user">
|
||||
<nz-avatar class="alain-default__aside-user-avatar" [nzSrc]="user.avatar"></nz-avatar>
|
||||
<div class="alain-default__aside-user-info">
|
||||
<strong>{{ user.name }}</strong>
|
||||
<p class="mb0">{{ user.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<nz-dropdown-menu #userMenu="nzDropdownMenu">
|
||||
<ul nz-menu>
|
||||
<li nz-menu-item routerLink="/pro/account/center">{{ 'menu.account.center' | i18n }}</li>
|
||||
<li nz-menu-item routerLink="/pro/account/settings">{{ 'menu.account.settings' | i18n }}</li>
|
||||
</ul>
|
||||
</nz-dropdown-menu>
|
||||
</ng-template>
|
||||
<ng-template #contentTpl>
|
||||
<router-outlet></router-outlet>
|
||||
</ng-template>
|
||||
</layout-default>
|
||||
|
||||
<setting-drawer *ngIf="showSettingDrawer"></setting-drawer>
|
||||
<theme-btn></theme-btn>
|
||||
`,
|
||||
})
|
||||
export class LayoutBasicComponent {
|
||||
options: LayoutDefaultOptions = {
|
||||
logoExpanded: `./assets/logo-color.svg`,
|
||||
logoCollapsed: `./assets/logo.svg`,
|
||||
};
|
||||
searchToggleStatus = false;
|
||||
showSettingDrawer = !environment.production;
|
||||
get user(): User {
|
||||
return this.settings.user;
|
||||
}
|
||||
|
||||
constructor(private settings: SettingsService) {}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ChangeDetectionStrategy, Component, HostListener } from '@angular/core';
|
||||
import { NzMessageService } from 'ng-zorro-antd/message';
|
||||
import { NzModalService } from 'ng-zorro-antd/modal';
|
||||
|
||||
@Component({
|
||||
selector: 'header-clear-storage',
|
||||
template: `
|
||||
<i nz-icon nzType="tool"></i>
|
||||
{{ 'menu.clear.local.storage' | i18n }}
|
||||
`,
|
||||
host: {
|
||||
'[class.d-block]': 'true'
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class HeaderClearStorageComponent {
|
||||
constructor(private modalSrv: NzModalService, private messageSrv: NzMessageService) {}
|
||||
|
||||
@HostListener('click')
|
||||
_click(): void {
|
||||
this.modalSrv.confirm({
|
||||
nzTitle: 'Make sure clear all local storage?',
|
||||
nzOnOk: () => {
|
||||
localStorage.clear();
|
||||
this.messageSrv.success('Clear Finished!');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
32
web-app/src/app/layout/basic/widgets/fullscreen.component.ts
Normal file
32
web-app/src/app/layout/basic/widgets/fullscreen.component.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ChangeDetectionStrategy, Component, HostListener } from '@angular/core';
|
||||
import * as screenfull from 'screenfull';
|
||||
|
||||
@Component({
|
||||
selector: 'header-fullscreen',
|
||||
template: `
|
||||
<i nz-icon [nzType]="status ? 'fullscreen-exit' : 'fullscreen'"></i>
|
||||
{{ (status ? 'menu.fullscreen.exit' : 'menu.fullscreen') | i18n }}
|
||||
`,
|
||||
host: {
|
||||
'[class.d-block]': 'true'
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class HeaderFullScreenComponent {
|
||||
status = false;
|
||||
private get sf(): screenfull.Screenfull {
|
||||
return screenfull as screenfull.Screenfull;
|
||||
}
|
||||
|
||||
@HostListener('window:resize')
|
||||
_resize(): void {
|
||||
this.status = this.sf.isFullscreen;
|
||||
}
|
||||
|
||||
@HostListener('click')
|
||||
_click(): void {
|
||||
if (this.sf.isEnabled) {
|
||||
this.sf.toggle();
|
||||
}
|
||||
}
|
||||
}
|
||||
54
web-app/src/app/layout/basic/widgets/i18n.component.ts
Normal file
54
web-app/src/app/layout/basic/widgets/i18n.component.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input } from '@angular/core';
|
||||
import { I18NService } from '@core';
|
||||
import { ALAIN_I18N_TOKEN, SettingsService } from '@delon/theme';
|
||||
import { BooleanInput, InputBoolean } from '@delon/util/decorator';
|
||||
|
||||
@Component({
|
||||
selector: 'header-i18n',
|
||||
template: `
|
||||
<div *ngIf="showLangText" nz-dropdown [nzDropdownMenu]="langMenu" nzPlacement="bottomRight">
|
||||
<i nz-icon nzType="global"></i>
|
||||
{{ 'menu.lang' | i18n }}
|
||||
<i nz-icon nzType="down"></i>
|
||||
</div>
|
||||
<i *ngIf="!showLangText" nz-dropdown [nzDropdownMenu]="langMenu" nzPlacement="bottomRight" nz-icon nzType="global"></i>
|
||||
<nz-dropdown-menu #langMenu="nzDropdownMenu">
|
||||
<ul nz-menu>
|
||||
<li nz-menu-item *ngFor="let item of langs" [nzSelected]="item.code === curLangCode" (click)="change(item.code)">
|
||||
<span role="img" [attr.aria-label]="item.text" class="pr-xs">{{ item.abbr }}</span>
|
||||
{{ item.text }}
|
||||
</li>
|
||||
</ul>
|
||||
</nz-dropdown-menu>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class HeaderI18nComponent {
|
||||
static ngAcceptInputType_showLangText: BooleanInput;
|
||||
/** Whether to display language text */
|
||||
@Input() @InputBoolean() showLangText = true;
|
||||
|
||||
get langs(): Array<{ code: string; text: string; abbr: string }> {
|
||||
return this.i18n.getLangs();
|
||||
}
|
||||
|
||||
get curLangCode(): string {
|
||||
return this.settings.layout.lang;
|
||||
}
|
||||
|
||||
constructor(private settings: SettingsService, @Inject(ALAIN_I18N_TOKEN) private i18n: I18NService, @Inject(DOCUMENT) private doc: any) {}
|
||||
|
||||
change(lang: string): void {
|
||||
const spinEl = this.doc.createElement('div');
|
||||
spinEl.setAttribute('class', `page-loading ant-spin ant-spin-lg ant-spin-spinning`);
|
||||
spinEl.innerHTML = `<span class="ant-spin-dot ant-spin-dot-spin"><i></i><i></i><i></i><i></i></span>`;
|
||||
this.doc.body.appendChild(spinEl);
|
||||
|
||||
this.i18n.loadLangData(lang).subscribe(res => {
|
||||
this.i18n.use(lang, res);
|
||||
this.settings.setLayout('lang', lang);
|
||||
setTimeout(() => this.doc.location.reload());
|
||||
});
|
||||
}
|
||||
}
|
||||
108
web-app/src/app/layout/basic/widgets/search.component.ts
Normal file
108
web-app/src/app/layout/basic/widgets/search.component.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostBinding,
|
||||
Input,
|
||||
OnDestroy,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'header-search',
|
||||
template: `
|
||||
<nz-input-group [nzPrefix]="iconTpl" [nzSuffix]="loadingTpl">
|
||||
<ng-template #iconTpl>
|
||||
<i nz-icon [nzType]="focus ? 'arrow-down' : 'search'"></i>
|
||||
</ng-template>
|
||||
<ng-template #loadingTpl>
|
||||
<i *ngIf="loading" nz-icon nzType="loading"></i>
|
||||
</ng-template>
|
||||
<input
|
||||
type="text"
|
||||
nz-input
|
||||
[(ngModel)]="q"
|
||||
[nzAutocomplete]="auto"
|
||||
(input)="search($event)"
|
||||
(focus)="qFocus()"
|
||||
(blur)="qBlur()"
|
||||
[attr.placeholder]="'menu.search.placeholder' | i18n"
|
||||
/>
|
||||
</nz-input-group>
|
||||
<nz-autocomplete nzBackfill #auto>
|
||||
<nz-auto-option *ngFor="let i of options" [nzValue]="i">{{ i }}</nz-auto-option>
|
||||
</nz-autocomplete>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class HeaderSearchComponent implements AfterViewInit, OnDestroy {
|
||||
q = '';
|
||||
qIpt: HTMLInputElement | null = null;
|
||||
options: string[] = [];
|
||||
search$ = new BehaviorSubject('');
|
||||
loading = false;
|
||||
|
||||
@HostBinding('class.alain-default__search-focus')
|
||||
focus = false;
|
||||
@HostBinding('class.alain-default__search-toggled')
|
||||
searchToggled = false;
|
||||
|
||||
@Input()
|
||||
set toggleChange(value: boolean) {
|
||||
if (typeof value === 'undefined') {
|
||||
return;
|
||||
}
|
||||
this.searchToggled = value;
|
||||
this.focus = value;
|
||||
if (value) {
|
||||
setTimeout(() => this.qIpt!.focus());
|
||||
}
|
||||
}
|
||||
@Output() readonly toggleChangeChange = new EventEmitter<boolean>();
|
||||
|
||||
constructor(private el: ElementRef<HTMLElement>, private cdr: ChangeDetectorRef) {}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.qIpt = this.el.nativeElement.querySelector('.ant-input') as HTMLInputElement;
|
||||
this.search$
|
||||
.pipe(
|
||||
debounceTime(500),
|
||||
distinctUntilChanged(),
|
||||
tap({
|
||||
complete: () => {
|
||||
this.loading = true;
|
||||
}
|
||||
})
|
||||
)
|
||||
.subscribe(value => {
|
||||
this.options = value ? [value, value + value, value + value + value] : [];
|
||||
this.loading = false;
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
qFocus(): void {
|
||||
this.focus = true;
|
||||
}
|
||||
|
||||
qBlur(): void {
|
||||
this.focus = false;
|
||||
this.searchToggled = false;
|
||||
this.options.length = 0;
|
||||
this.toggleChangeChange.emit(false);
|
||||
}
|
||||
|
||||
search(ev: Event): void {
|
||||
this.search$.next((ev.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.search$.complete();
|
||||
this.search$.unsubscribe();
|
||||
}
|
||||
}
|
||||
48
web-app/src/app/layout/basic/widgets/user.component.ts
Normal file
48
web-app/src/app/layout/basic/widgets/user.component.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
|
||||
import { SettingsService, User } from '@delon/theme';
|
||||
|
||||
@Component({
|
||||
selector: 'header-user',
|
||||
template: `
|
||||
<div class="alain-default__nav-item d-flex align-items-center px-sm" nz-dropdown nzPlacement="bottomRight" [nzDropdownMenu]="userMenu">
|
||||
<nz-avatar [nzSrc]="user.avatar" nzSize="small" class="mr-sm"></nz-avatar>
|
||||
{{ user.name }}
|
||||
</div>
|
||||
<nz-dropdown-menu #userMenu="nzDropdownMenu">
|
||||
<div nz-menu class="width-sm">
|
||||
<div nz-menu-item routerLink="/pro/account/center">
|
||||
<i nz-icon nzType="user" class="mr-sm"></i>
|
||||
{{ 'menu.account.center' | i18n }}
|
||||
</div>
|
||||
<div nz-menu-item routerLink="/pro/account/settings">
|
||||
<i nz-icon nzType="setting" class="mr-sm"></i>
|
||||
{{ 'menu.account.settings' | i18n }}
|
||||
</div>
|
||||
<div nz-menu-item routerLink="/exception/trigger">
|
||||
<i nz-icon nzType="close-circle" class="mr-sm"></i>
|
||||
{{ 'menu.account.trigger' | i18n }}
|
||||
</div>
|
||||
<li nz-menu-divider></li>
|
||||
<div nz-menu-item (click)="logout()">
|
||||
<i nz-icon nzType="logout" class="mr-sm"></i>
|
||||
{{ 'menu.account.logout' | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
</nz-dropdown-menu>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class HeaderUserComponent {
|
||||
get user(): User {
|
||||
return this.settings.user;
|
||||
}
|
||||
|
||||
constructor(private settings: SettingsService, private router: Router, @Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService) {}
|
||||
|
||||
logout(): void {
|
||||
this.tokenService.clear();
|
||||
this.router.navigateByUrl(this.tokenService.login_url!);
|
||||
}
|
||||
}
|
||||
1
web-app/src/app/layout/blank/README.md
Normal file
1
web-app/src/app/layout/blank/README.md
Normal file
@@ -0,0 +1 @@
|
||||
[Document](https://ng-alain.com/theme/blank)
|
||||
10
web-app/src/app/layout/blank/blank.component.ts
Normal file
10
web-app/src/app/layout/blank/blank.component.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'layout-blank',
|
||||
template: `<router-outlet></router-outlet> `,
|
||||
host: {
|
||||
'[class.alain-blank]': 'true'
|
||||
}
|
||||
})
|
||||
export class LayoutBlankComponent {}
|
||||
70
web-app/src/app/layout/layout.module.ts
Normal file
70
web-app/src/app/layout/layout.module.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/* eslint-disable import/order */
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GlobalFooterModule } from '@delon/abc/global-footer';
|
||||
import { NoticeIconModule } from '@delon/abc/notice-icon';
|
||||
import { AlainThemeModule } from '@delon/theme';
|
||||
import { LayoutDefaultModule } from '@delon/theme/layout-default';
|
||||
import { SettingDrawerModule } from '@delon/theme/setting-drawer';
|
||||
import { ThemeBtnModule } from '@delon/theme/theme-btn';
|
||||
import { NzAutocompleteModule } from 'ng-zorro-antd/auto-complete';
|
||||
import { NzAvatarModule } from 'ng-zorro-antd/avatar';
|
||||
import { NzBadgeModule } from 'ng-zorro-antd/badge';
|
||||
import { NzDropDownModule } from 'ng-zorro-antd/dropdown';
|
||||
import { NzFormModule } from 'ng-zorro-antd/form';
|
||||
import { NzGridModule } from 'ng-zorro-antd/grid';
|
||||
import { NzIconModule } from 'ng-zorro-antd/icon';
|
||||
import { NzInputModule } from 'ng-zorro-antd/input';
|
||||
import { NzSpinModule } from 'ng-zorro-antd/spin';
|
||||
|
||||
import { LayoutBasicComponent } from './basic/basic.component';
|
||||
import { HeaderClearStorageComponent } from './basic/widgets/clear-storage.component';
|
||||
import { HeaderFullScreenComponent } from './basic/widgets/fullscreen.component';
|
||||
import { HeaderI18nComponent } from './basic/widgets/i18n.component';
|
||||
import { HeaderSearchComponent } from './basic/widgets/search.component';
|
||||
import { HeaderUserComponent } from './basic/widgets/user.component';
|
||||
import { LayoutBlankComponent } from './blank/blank.component';
|
||||
|
||||
const COMPONENTS = [LayoutBasicComponent, LayoutBlankComponent];
|
||||
|
||||
const HEADER_COMPONENTS = [
|
||||
HeaderSearchComponent,
|
||||
HeaderFullScreenComponent,
|
||||
HeaderI18nComponent,
|
||||
HeaderClearStorageComponent,
|
||||
HeaderUserComponent,
|
||||
];
|
||||
|
||||
// passport
|
||||
import { LayoutPassportComponent } from './passport/passport.component';
|
||||
const PASSPORT = [
|
||||
LayoutPassportComponent
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
RouterModule,
|
||||
AlainThemeModule.forChild(),
|
||||
ThemeBtnModule,
|
||||
SettingDrawerModule,
|
||||
LayoutDefaultModule,
|
||||
NoticeIconModule,
|
||||
GlobalFooterModule,
|
||||
NzDropDownModule,
|
||||
NzInputModule,
|
||||
NzAutocompleteModule,
|
||||
NzGridModule,
|
||||
NzFormModule,
|
||||
NzSpinModule,
|
||||
NzBadgeModule,
|
||||
NzAvatarModule,
|
||||
NzIconModule,
|
||||
],
|
||||
declarations: [...COMPONENTS, ...HEADER_COMPONENTS, ...PASSPORT],
|
||||
exports: [...COMPONENTS, ...PASSPORT],
|
||||
})
|
||||
export class LayoutModule { }
|
||||
19
web-app/src/app/layout/passport/passport.component.html
Normal file
19
web-app/src/app/layout/passport/passport.component.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<div class="container">
|
||||
<header-i18n showLangText="false" class="langs"></header-i18n>
|
||||
<div class="wrap">
|
||||
<div class="top">
|
||||
<div class="head">
|
||||
<img class="logo" src="./assets/logo-color.svg">
|
||||
<span class="title">HertzBeat</span>
|
||||
</div>
|
||||
<div class="desc">面向开发者,易用友好的高性能监控云服务</div>
|
||||
</div>
|
||||
<router-outlet></router-outlet>
|
||||
<global-footer [links]="links">
|
||||
Copyright
|
||||
<i class="anticon anticon-copyright"></i> 2021
|
||||
<a href="https://tancloud.cn" target="_blank">探云 tancloud.cn | </a>
|
||||
<a href="https://tancloud.cn" target="_blank">赫兹跳动 hertzbeat.com</a>
|
||||
</global-footer>
|
||||
</div>
|
||||
</div>
|
||||
98
web-app/src/app/layout/passport/passport.component.less
Normal file
98
web-app/src/app/layout/passport/passport.component.less
Normal file
@@ -0,0 +1,98 @@
|
||||
@import '~@delon/theme/index';
|
||||
:host ::ng-deep {
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
.langs {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
line-height: 44px;
|
||||
text-align: right;
|
||||
.anticon {
|
||||
margin-top: 24px;
|
||||
margin-right: 24px;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.wrap {
|
||||
flex: 1;
|
||||
padding: 32px 0;
|
||||
}
|
||||
.ant-form-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@media (min-width: @screen-md-min) {
|
||||
.container {
|
||||
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center 110px;
|
||||
background-size: 100%;
|
||||
}
|
||||
.wrap {
|
||||
padding: 32px 0 24px;
|
||||
}
|
||||
}
|
||||
.top {
|
||||
text-align: center;
|
||||
}
|
||||
.header {
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
.logo {
|
||||
height: 44px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
.title {
|
||||
position: relative;
|
||||
color: @heading-color;
|
||||
font-weight: 600;
|
||||
font-size: 33px;
|
||||
font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.desc {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 40px;
|
||||
color: @text-color-secondary;
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
:host ::ng-deep {
|
||||
.container {
|
||||
background: #141414;
|
||||
}
|
||||
.title {
|
||||
color: fade(@white, 85%);
|
||||
}
|
||||
.desc {
|
||||
color: fade(@white, 45%);
|
||||
}
|
||||
@media (min-width: @screen-md-min) {
|
||||
.container {
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme='compact'] {
|
||||
:host ::ng-deep {
|
||||
.ant-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
30
web-app/src/app/layout/passport/passport.component.ts
Normal file
30
web-app/src/app/layout/passport/passport.component.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
|
||||
|
||||
@Component({
|
||||
selector: 'layout-passport',
|
||||
templateUrl: './passport.component.html',
|
||||
styleUrls: ['./passport.component.less']
|
||||
})
|
||||
export class LayoutPassportComponent implements OnInit {
|
||||
links = [
|
||||
{
|
||||
title: '帮助',
|
||||
href: ''
|
||||
},
|
||||
{
|
||||
title: '隐私',
|
||||
href: ''
|
||||
},
|
||||
{
|
||||
title: '条款',
|
||||
href: ''
|
||||
}
|
||||
];
|
||||
|
||||
constructor(@Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.tokenService.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { WelcomeComponent } from './welcome.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: WelcomeComponent },
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class WelcomeRoutingModule { }
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-welcome',
|
||||
templateUrl: './welcome.component.html',
|
||||
styleUrls: ['./welcome.component.less']
|
||||
})
|
||||
export class WelcomeComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { WelcomeRoutingModule } from './welcome-routing.module';
|
||||
|
||||
import { WelcomeComponent } from './welcome.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [WelcomeRoutingModule],
|
||||
declarations: [WelcomeComponent],
|
||||
exports: [WelcomeComponent]
|
||||
})
|
||||
export class WelcomeModule { }
|
||||
@@ -0,0 +1 @@
|
||||
<page-header></page-header>
|
||||
8
web-app/src/app/routes/dashboard/dashboard.component.ts
Normal file
8
web-app/src/app/routes/dashboard/dashboard.component.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
templateUrl: './dashboard.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class DashboardComponent {}
|
||||
18
web-app/src/app/routes/exception/exception-routing.module.ts
Normal file
18
web-app/src/app/routes/exception/exception-routing.module.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { ExceptionComponent } from './exception.component';
|
||||
import { ExceptionTriggerComponent } from './trigger.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '403', component: ExceptionComponent, data: { type: 403 } },
|
||||
{ path: '404', component: ExceptionComponent, data: { type: 404 } },
|
||||
{ path: '500', component: ExceptionComponent, data: { type: 500 } },
|
||||
{ path: 'trigger', component: ExceptionTriggerComponent }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class ExceptionRoutingModule {}
|
||||
16
web-app/src/app/routes/exception/exception.component.ts
Normal file
16
web-app/src/app/routes/exception/exception.component.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ExceptionType } from '@delon/abc/exception';
|
||||
|
||||
@Component({
|
||||
selector: 'app-exception',
|
||||
template: ` <exception [type]="type" style="min-height: 500px; height: 80%;"> </exception> `,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ExceptionComponent {
|
||||
get type(): ExceptionType {
|
||||
return this.route.snapshot.data.type;
|
||||
}
|
||||
|
||||
constructor(private route: ActivatedRoute) {}
|
||||
}
|
||||
15
web-app/src/app/routes/exception/exception.module.ts
Normal file
15
web-app/src/app/routes/exception/exception.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ExceptionModule as DelonExceptionModule } from '@delon/abc/exception';
|
||||
import { NzButtonModule } from 'ng-zorro-antd/button';
|
||||
import { NzCardModule } from 'ng-zorro-antd/card';
|
||||
|
||||
import { ExceptionRoutingModule } from './exception-routing.module';
|
||||
import { ExceptionComponent } from './exception.component';
|
||||
import { ExceptionTriggerComponent } from './trigger.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, DelonExceptionModule, NzButtonModule, NzCardModule, ExceptionRoutingModule],
|
||||
declarations: [ExceptionComponent, ExceptionTriggerComponent]
|
||||
})
|
||||
export class ExceptionModule {}
|
||||
35
web-app/src/app/routes/exception/trigger.component.ts
Normal file
35
web-app/src/app/routes/exception/trigger.component.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
|
||||
import { _HttpClient } from '@delon/theme';
|
||||
|
||||
@Component({
|
||||
selector: 'exception-trigger',
|
||||
template: `
|
||||
<div class="pt-lg">
|
||||
<nz-card>
|
||||
<button *ngFor="let t of types" (click)="go(t)" nz-button nzDanger>触发{{ t }}</button>
|
||||
<button nz-button nzType="link" (click)="refresh()">触发刷新Token</button>
|
||||
</nz-card>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class ExceptionTriggerComponent {
|
||||
types = [401, 403, 404, 500];
|
||||
|
||||
constructor(private http: _HttpClient, @Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService) {}
|
||||
|
||||
go(type: number): void {
|
||||
this.http.get(`/api/${type}`).subscribe();
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this.tokenService.set({ token: 'invalid-token' });
|
||||
// 必须提供一个后端地址,无法通过 Mock 来模拟
|
||||
this.http.post(`https://localhost:5001/auth`).subscribe(
|
||||
res => console.warn('成功', res),
|
||||
err => {
|
||||
console.log('最后结果失败', err);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
35
web-app/src/app/routes/passport/callback.component.ts
Normal file
35
web-app/src/app/routes/passport/callback.component.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { SocialService } from '@delon/auth';
|
||||
import { SettingsService } from '@delon/theme';
|
||||
|
||||
@Component({
|
||||
selector: 'app-callback',
|
||||
template: ``,
|
||||
providers: [SocialService]
|
||||
})
|
||||
export class CallbackComponent implements OnInit {
|
||||
type = '';
|
||||
|
||||
constructor(private socialService: SocialService, private settingsSrv: SettingsService, private route: ActivatedRoute) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.type = this.route.snapshot.params.type;
|
||||
this.mockModel();
|
||||
}
|
||||
|
||||
private mockModel(): void {
|
||||
const info = {
|
||||
token: '123456789',
|
||||
name: 'cipchk',
|
||||
email: `${this.type}@${this.type}.com`,
|
||||
id: 10000,
|
||||
time: +new Date()
|
||||
};
|
||||
this.settingsSrv.setUser({
|
||||
...this.settingsSrv.user,
|
||||
...info
|
||||
});
|
||||
this.socialService.callback(info);
|
||||
}
|
||||
}
|
||||
21
web-app/src/app/routes/passport/lock/lock.component.html
Normal file
21
web-app/src/app/routes/passport/lock/lock.component.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<div class="ant-card width-lg" style="margin: 0 auto">
|
||||
<div class="ant-card-body">
|
||||
<div class="avatar">
|
||||
<nz-avatar [nzSrc]="user.avatar" nzIcon="user" nzSize="large"></nz-avatar>
|
||||
</div>
|
||||
<form nz-form [formGroup]="f" (ngSubmit)="submit()" role="form" class="mt-md">
|
||||
<nz-form-item>
|
||||
<nz-form-control [nzErrorTip]="'validation.password.required' | i18n">
|
||||
<nz-input-group nzSuffixIcon="lock">
|
||||
<input type="password" nz-input formControlName="password" />
|
||||
</nz-input-group>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
<nz-row nzType="flex" nzAlign="middle">
|
||||
<nz-col [nzOffset]="12" [nzSpan]="12" style="text-align: right">
|
||||
<button nz-button [disabled]="!f.valid" nzType="primary">{{ 'app.lock' | i18n }}</button>
|
||||
</nz-col>
|
||||
</nz-row>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
12
web-app/src/app/routes/passport/lock/lock.component.less
Normal file
12
web-app/src/app/routes/passport/lock/lock.component.less
Normal file
@@ -0,0 +1,12 @@
|
||||
:host ::ng-deep {
|
||||
.ant-card-body {
|
||||
position: relative;
|
||||
margin-top: 80px;
|
||||
}
|
||||
.avatar {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
left: 50%;
|
||||
margin-left: -20px;
|
||||
}
|
||||
}
|
||||
44
web-app/src/app/routes/passport/lock/lock.component.ts
Normal file
44
web-app/src/app/routes/passport/lock/lock.component.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
|
||||
import { SettingsService, User } from '@delon/theme';
|
||||
|
||||
@Component({
|
||||
selector: 'passport-lock',
|
||||
templateUrl: './lock.component.html',
|
||||
styleUrls: ['./lock.component.less']
|
||||
})
|
||||
export class UserLockComponent {
|
||||
f: FormGroup;
|
||||
|
||||
get user(): User {
|
||||
return this.settings.user;
|
||||
}
|
||||
|
||||
constructor(
|
||||
fb: FormBuilder,
|
||||
@Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService,
|
||||
private settings: SettingsService,
|
||||
private router: Router
|
||||
) {
|
||||
this.f = fb.group({
|
||||
password: [null, Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
submit(): void {
|
||||
for (const i in this.f.controls) {
|
||||
this.f.controls[i].markAsDirty();
|
||||
this.f.controls[i].updateValueAndValidity();
|
||||
}
|
||||
if (this.f.valid) {
|
||||
console.log('Valid!');
|
||||
console.log(this.f.value);
|
||||
this.tokenService.set({
|
||||
token: '123'
|
||||
});
|
||||
this.router.navigate(['dashboard']);
|
||||
}
|
||||
}
|
||||
}
|
||||
74
web-app/src/app/routes/passport/login/login.component.html
Normal file
74
web-app/src/app/routes/passport/login/login.component.html
Normal file
@@ -0,0 +1,74 @@
|
||||
<form nz-form [formGroup]="form" (ngSubmit)="submit()" role="form">
|
||||
<nz-tabset [nzAnimated]="false" class="tabs" (nzSelectChange)="switch($event)">
|
||||
<nz-tab [nzTitle]="'app.login.tab-login-credentials' | i18n">
|
||||
<nz-alert *ngIf="error" [nzType]="'error'" [nzMessage]="error" [nzShowIcon]="true" class="mb-lg"></nz-alert>
|
||||
<nz-form-item>
|
||||
<nz-form-control nzErrorTip="Please enter mobile number, muse be: admin or user">
|
||||
<nz-input-group nzSize="large" nzPrefixIcon="user">
|
||||
<input nz-input formControlName="userName" placeholder="username: admin or user" />
|
||||
</nz-input-group>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
<nz-form-item>
|
||||
<nz-form-control nzErrorTip="Please enter password">
|
||||
<nz-input-group nzSize="large" nzPrefixIcon="lock">
|
||||
<input nz-input type="password" formControlName="password" placeholder="password: admin@123" />
|
||||
</nz-input-group>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
</nz-tab>
|
||||
<nz-tab [nzTitle]="'app.login.tab-login-mobile' | i18n">
|
||||
<nz-form-item>
|
||||
<nz-form-control [nzErrorTip]="mobileErrorTip">
|
||||
<nz-input-group nzSize="large" nzPrefixIcon="user">
|
||||
<input nz-input formControlName="mobile" placeholder="mobile number" />
|
||||
</nz-input-group>
|
||||
<ng-template #mobileErrorTip let-i>
|
||||
<ng-container *ngIf="i.errors.required">
|
||||
{{ 'validation.phone-number.required' | i18n }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="i.errors.pattern">
|
||||
{{ 'validation.phone-number.wrong-format' | i18n }}
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
<nz-form-item>
|
||||
<nz-form-control [nzErrorTip]="'validation.verification-code.required' | i18n">
|
||||
<nz-row [nzGutter]="8">
|
||||
<nz-col [nzSpan]="16">
|
||||
<nz-input-group nzSize="large" nzPrefixIcon="mail">
|
||||
<input nz-input formControlName="captcha" placeholder="captcha" />
|
||||
</nz-input-group>
|
||||
</nz-col>
|
||||
<nz-col [nzSpan]="8">
|
||||
<button type="button" nz-button nzSize="large" (click)="getCaptcha()" [disabled]="count >= 0" nzBlock [nzLoading]="loading">
|
||||
{{ count ? count + 's' : ('app.register.get-verification-code' | i18n) }}
|
||||
</button>
|
||||
</nz-col>
|
||||
</nz-row>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
</nz-tab>
|
||||
</nz-tabset>
|
||||
<nz-form-item>
|
||||
<nz-col [nzSpan]="12">
|
||||
<label nz-checkbox formControlName="remember">{{ 'app.login.remember-me' | i18n }}</label>
|
||||
</nz-col>
|
||||
<nz-col [nzSpan]="12" class="text-right">
|
||||
<a class="forgot" routerLink="/passport/register">{{ 'app.login.forgot-password' | i18n }}</a>
|
||||
</nz-col>
|
||||
</nz-form-item>
|
||||
<nz-form-item>
|
||||
<button nz-button type="submit" nzType="primary" nzSize="large" [nzLoading]="loading" nzBlock>
|
||||
{{ 'app.login.login' | i18n }}
|
||||
</button>
|
||||
</nz-form-item>
|
||||
</form>
|
||||
<div class="other">
|
||||
{{ 'app.login.sign-in-with' | i18n }}
|
||||
<i nz-tooltip nzTooltipTitle="in fact Auth0 via window" (click)="open('auth0', 'window')" nz-icon nzType="alipay-circle" class="icon"></i>
|
||||
<i nz-tooltip nzTooltipTitle="in fact Github via redirect" (click)="open('github')" nz-icon nzType="taobao-circle" class="icon"></i>
|
||||
<i (click)="open('weibo', 'window')" nz-icon nzType="weibo-circle" class="icon"></i>
|
||||
<a class="register" routerLink="/passport/register">{{ 'app.login.signup' | i18n }}</a>
|
||||
</div>
|
||||
53
web-app/src/app/routes/passport/login/login.component.less
Normal file
53
web-app/src/app/routes/passport/login/login.component.less
Normal file
@@ -0,0 +1,53 @@
|
||||
@import '~@delon/theme/index';
|
||||
:host {
|
||||
display: block;
|
||||
width: 368px;
|
||||
margin: 0 auto;
|
||||
::ng-deep {
|
||||
.ant-tabs .ant-tabs-bar {
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
border-bottom: 0;
|
||||
}
|
||||
.ant-tabs-tab {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
.ant-input-affix-wrapper .ant-input:not(:first-child) {
|
||||
padding-left: 4px;
|
||||
}
|
||||
.icon {
|
||||
margin-left: 16px;
|
||||
color: rgba(0, 0, 0, 0.2);
|
||||
font-size: 24px;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
.other {
|
||||
margin-top: 24px;
|
||||
line-height: 22px;
|
||||
text-align: left;
|
||||
nz-tooltip {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.register {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
:host ::ng-deep {
|
||||
.icon {
|
||||
color: rgba(255, 255, 255, 0.2);
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
196
web-app/src/app/routes/passport/login/login.component.ts
Normal file
196
web-app/src/app/routes/passport/login/login.component.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnDestroy, Optional } from '@angular/core';
|
||||
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { StartupService } from '@core';
|
||||
import { ReuseTabService } from '@delon/abc/reuse-tab';
|
||||
import { DA_SERVICE_TOKEN, ITokenService, SocialOpenType, SocialService } from '@delon/auth';
|
||||
import { SettingsService, _HttpClient } from '@delon/theme';
|
||||
import { environment } from '@env/environment';
|
||||
import { NzTabChangeEvent } from 'ng-zorro-antd/tabs';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'passport-login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.less'],
|
||||
providers: [SocialService],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class UserLoginComponent implements OnDestroy {
|
||||
constructor(
|
||||
fb: FormBuilder,
|
||||
private router: Router,
|
||||
private settingsService: SettingsService,
|
||||
private socialService: SocialService,
|
||||
@Optional()
|
||||
@Inject(ReuseTabService)
|
||||
private reuseTabService: ReuseTabService,
|
||||
@Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService,
|
||||
private startupSrv: StartupService,
|
||||
private http: _HttpClient,
|
||||
private cdr: ChangeDetectorRef
|
||||
) {
|
||||
this.form = fb.group({
|
||||
userName: [null, [Validators.required, Validators.pattern(/^(admin|user)$/)]],
|
||||
password: [null, [Validators.required, Validators.pattern(/^(admin@123)$/)]],
|
||||
mobile: [null, [Validators.required, Validators.pattern(/^1\d{10}$/)]],
|
||||
captcha: [null, [Validators.required]],
|
||||
remember: [true]
|
||||
});
|
||||
}
|
||||
|
||||
// #region fields
|
||||
|
||||
get userName(): AbstractControl {
|
||||
return this.form.controls.userName;
|
||||
}
|
||||
get password(): AbstractControl {
|
||||
return this.form.controls.password;
|
||||
}
|
||||
get mobile(): AbstractControl {
|
||||
return this.form.controls.mobile;
|
||||
}
|
||||
get captcha(): AbstractControl {
|
||||
return this.form.controls.captcha;
|
||||
}
|
||||
form: FormGroup;
|
||||
error = '';
|
||||
type = 0;
|
||||
loading = false;
|
||||
|
||||
// #region get captcha
|
||||
|
||||
count = 0;
|
||||
interval$: any;
|
||||
|
||||
// #endregion
|
||||
|
||||
switch({ index }: NzTabChangeEvent): void {
|
||||
this.type = index!;
|
||||
}
|
||||
|
||||
getCaptcha(): void {
|
||||
if (this.mobile.invalid) {
|
||||
this.mobile.markAsDirty({ onlySelf: true });
|
||||
this.mobile.updateValueAndValidity({ onlySelf: true });
|
||||
return;
|
||||
}
|
||||
this.count = 59;
|
||||
this.interval$ = setInterval(() => {
|
||||
this.count -= 1;
|
||||
if (this.count <= 0) {
|
||||
clearInterval(this.interval$);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
submit(): void {
|
||||
this.error = '';
|
||||
if (this.type === 0) {
|
||||
this.userName.markAsDirty();
|
||||
this.userName.updateValueAndValidity();
|
||||
this.password.markAsDirty();
|
||||
this.password.updateValueAndValidity();
|
||||
if (this.userName.invalid || this.password.invalid) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.mobile.markAsDirty();
|
||||
this.mobile.updateValueAndValidity();
|
||||
this.captcha.markAsDirty();
|
||||
this.captcha.updateValueAndValidity();
|
||||
if (this.mobile.invalid || this.captcha.invalid) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 默认配置中对所有HTTP请求都会强制 [校验](https://ng-alain.com/auth/getting-started) 用户 Token
|
||||
// 然一般来说登录请求不需要校验,因此可以在请求URL加上:`/login?_allow_anonymous=true` 表示不触发用户 Token 校验
|
||||
this.loading = true;
|
||||
this.cdr.detectChanges();
|
||||
this.http
|
||||
.post('/login/account?_allow_anonymous=true', {
|
||||
type: this.type,
|
||||
userName: this.userName.value,
|
||||
password: this.password.value
|
||||
})
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
this.loading = true;
|
||||
this.cdr.detectChanges();
|
||||
})
|
||||
)
|
||||
.subscribe(res => {
|
||||
if (res.msg !== 'ok') {
|
||||
this.error = res.msg;
|
||||
this.cdr.detectChanges();
|
||||
return;
|
||||
}
|
||||
// 清空路由复用信息
|
||||
this.reuseTabService.clear();
|
||||
// 设置用户Token信息
|
||||
// TODO: Mock expired value
|
||||
res.user.expired = +new Date() + 1000 * 60 * 5;
|
||||
this.tokenService.set(res.user);
|
||||
// 重新获取 StartupService 内容,我们始终认为应用信息一般都会受当前用户授权范围而影响
|
||||
this.startupSrv.load().subscribe(() => {
|
||||
let url = this.tokenService.referrer!.url || '/';
|
||||
if (url.includes('/passport')) {
|
||||
url = '/';
|
||||
}
|
||||
this.router.navigateByUrl(url);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// #region social
|
||||
|
||||
open(type: string, openType: SocialOpenType = 'href'): void {
|
||||
let url = ``;
|
||||
let callback = ``;
|
||||
if (environment.production) {
|
||||
callback = `https://ng-alain.github.io/ng-alain/#/passport/callback/${type}`;
|
||||
} else {
|
||||
callback = `http://localhost:4200/#/passport/callback/${type}`;
|
||||
}
|
||||
switch (type) {
|
||||
case 'auth0':
|
||||
url = `//cipchk.auth0.com/login?client=8gcNydIDzGBYxzqV0Vm1CX_RXH-wsWo5&redirect_uri=${decodeURIComponent(callback)}`;
|
||||
break;
|
||||
case 'github':
|
||||
url = `//github.com/login/oauth/authorize?client_id=9d6baae4b04a23fcafa2&response_type=code&redirect_uri=${decodeURIComponent(
|
||||
callback
|
||||
)}`;
|
||||
break;
|
||||
case 'weibo':
|
||||
url = `https://api.weibo.com/oauth2/authorize?client_id=1239507802&response_type=code&redirect_uri=${decodeURIComponent(callback)}`;
|
||||
break;
|
||||
}
|
||||
if (openType === 'window') {
|
||||
this.socialService
|
||||
.login(url, '/', {
|
||||
type: 'window'
|
||||
})
|
||||
.subscribe(res => {
|
||||
if (res) {
|
||||
this.settingsService.setUser(res);
|
||||
this.router.navigateByUrl('/');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.socialService.login(url, '/', {
|
||||
type: 'href'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.interval$) {
|
||||
clearInterval(this.interval$);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<result type="success" [title]="title" description="{{ 'app.register-result.activation-email' | i18n }}">
|
||||
<ng-template #title>
|
||||
<div class="title" style="font-size: 20px">
|
||||
{{ 'app.register-result.msg' | i18n: params }}
|
||||
</div>
|
||||
</ng-template>
|
||||
<button (click)="msg.success('email')" nz-button nzSize="large" [nzType]="'primary'">
|
||||
{{ 'app.register-result.view-mailbox' | i18n }}
|
||||
</button>
|
||||
<button routerLink="/" nz-button nzSize="large">
|
||||
{{ 'app.register-result.back-home' | i18n }}
|
||||
</button>
|
||||
</result>
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { NzMessageService } from 'ng-zorro-antd/message';
|
||||
|
||||
@Component({
|
||||
selector: 'passport-register-result',
|
||||
templateUrl: './register-result.component.html'
|
||||
})
|
||||
export class UserRegisterResultComponent {
|
||||
params = { email: '' };
|
||||
email = '';
|
||||
constructor(route: ActivatedRoute, public msg: NzMessageService) {
|
||||
this.params.email = this.email = route.snapshot.queryParams.email || 'ng-alain@example.com';
|
||||
}
|
||||
}
|
||||
100
web-app/src/app/routes/passport/register/register.component.html
Normal file
100
web-app/src/app/routes/passport/register/register.component.html
Normal file
@@ -0,0 +1,100 @@
|
||||
<h3>{{ 'app.register.register' | i18n }}</h3>
|
||||
<form nz-form [formGroup]="form" (ngSubmit)="submit()" role="form">
|
||||
<nz-alert *ngIf="error" [nzType]="'error'" [nzMessage]="error" [nzShowIcon]="true" class="mb-lg"></nz-alert>
|
||||
<nz-form-item>
|
||||
<nz-form-control [nzErrorTip]="mailErrorTip">
|
||||
<nz-input-group nzSize="large" nzAddonBeforeIcon="user">
|
||||
<input nz-input formControlName="mail" placeholder="Email" />
|
||||
</nz-input-group>
|
||||
<ng-template #mailErrorTip let-i>
|
||||
<ng-container *ngIf="i.errors?.required">{{ 'validation.email.required' | i18n }}</ng-container>
|
||||
<ng-container *ngIf="i.errors?.email">{{ 'validation.email.wrong-format' | i18n }}</ng-container>
|
||||
</ng-template>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
<nz-form-item>
|
||||
<nz-form-control [nzErrorTip]="'validation.password.required' | i18n">
|
||||
<nz-input-group
|
||||
nzSize="large"
|
||||
nzAddonBeforeIcon="lock"
|
||||
nz-popover
|
||||
nzPopoverPlacement="right"
|
||||
nzPopoverTrigger="focus"
|
||||
[(nzPopoverVisible)]="visible"
|
||||
nzPopoverOverlayClassName="register-password-cdk"
|
||||
[nzPopoverOverlayStyle]="{ 'width.px': 240 }"
|
||||
[nzPopoverContent]="pwdCdkTpl"
|
||||
>
|
||||
<input nz-input type="password" formControlName="password" placeholder="Password" />
|
||||
</nz-input-group>
|
||||
<ng-template #pwdCdkTpl>
|
||||
<div style="padding: 4px 0">
|
||||
<ng-container [ngSwitch]="status">
|
||||
<div *ngSwitchCase="'ok'" class="success">{{ 'validation.password.strength.strong' | i18n }}</div>
|
||||
<div *ngSwitchCase="'pass'" class="warning">{{ 'validation.password.strength.medium' | i18n }}</div>
|
||||
<div *ngSwitchDefault class="error">{{ 'validation.password.strength.short' | i18n }}</div>
|
||||
</ng-container>
|
||||
<div class="progress-{{ status }}">
|
||||
<nz-progress
|
||||
[nzPercent]="progress"
|
||||
[nzStatus]="passwordProgressMap[status]"
|
||||
[nzStrokeWidth]="6"
|
||||
[nzShowInfo]="false"
|
||||
></nz-progress>
|
||||
</div>
|
||||
<p class="mt-sm">{{ 'validation.password.strength.msg' | i18n }}</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
<nz-form-item>
|
||||
<nz-form-control [nzErrorTip]="confirmErrorTip">
|
||||
<nz-input-group nzSize="large" nzAddonBeforeIcon="lock">
|
||||
<input nz-input type="password" formControlName="confirm" placeholder="Confirm Password" />
|
||||
</nz-input-group>
|
||||
<ng-template #confirmErrorTip let-i>
|
||||
<ng-container *ngIf="i.errors?.required">{{ 'validation.confirm-password.required' | i18n }}</ng-container>
|
||||
<ng-container *ngIf="i.errors?.matchControl">{{ 'validation.password.twice' | i18n }}</ng-container>
|
||||
</ng-template>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
<nz-form-item>
|
||||
<nz-form-control [nzErrorTip]="mobileErrorTip">
|
||||
<nz-input-group nzSize="large" [nzAddOnBefore]="addOnBeforeTemplate">
|
||||
<ng-template #addOnBeforeTemplate>
|
||||
<nz-select formControlName="mobilePrefix" style="width: 100px">
|
||||
<nz-option [nzLabel]="'+86'" [nzValue]="'+86'"></nz-option>
|
||||
<nz-option [nzLabel]="'+87'" [nzValue]="'+87'"></nz-option>
|
||||
</nz-select>
|
||||
</ng-template>
|
||||
<input formControlName="mobile" nz-input placeholder="Phone number" />
|
||||
</nz-input-group>
|
||||
<ng-template #mobileErrorTip let-i>
|
||||
<ng-container *ngIf="i.errors?.required">{{ 'validation.phone-number.required' | i18n }}</ng-container>
|
||||
<ng-container *ngIf="i.errors?.pattern">{{ 'validation.phone-number.wrong-format' | i18n }}</ng-container>
|
||||
</ng-template>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
<nz-form-item>
|
||||
<nz-form-control [nzErrorTip]="'validation.verification-code.required' | i18n">
|
||||
<nz-row [nzGutter]="8">
|
||||
<nz-col [nzSpan]="16">
|
||||
<nz-input-group nzSize="large" nzAddonBeforeIcon="mail">
|
||||
<input nz-input formControlName="captcha" placeholder="Captcha" />
|
||||
</nz-input-group>
|
||||
</nz-col>
|
||||
<nz-col [nzSpan]="8">
|
||||
<button type="button" nz-button nzSize="large" (click)="getCaptcha()" [disabled]="count > 0" nzBlock [nzLoading]="loading">
|
||||
{{ count ? count + 's' : ('app.register.get-verification-code' | i18n) }}
|
||||
</button>
|
||||
</nz-col>
|
||||
</nz-row>
|
||||
</nz-form-control>
|
||||
</nz-form-item>
|
||||
<nz-form-item>
|
||||
<button nz-button nzType="primary" nzSize="large" type="submit" [nzLoading]="loading" class="submit">
|
||||
{{ 'app.register.register' | i18n }}
|
||||
</button>
|
||||
<a class="login" routerLink="/passport/login">{{ 'app.register.sign-in' | i18n }}</a>
|
||||
</nz-form-item>
|
||||
</form>
|
||||
@@ -0,0 +1,42 @@
|
||||
@import '~@delon/theme/index';
|
||||
:host {
|
||||
display: block;
|
||||
width: 368px;
|
||||
margin: 0 auto;
|
||||
::ng-deep {
|
||||
h3 {
|
||||
margin-bottom: 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.submit {
|
||||
width: 50%;
|
||||
}
|
||||
.login {
|
||||
float: right;
|
||||
line-height: @btn-height-lg;
|
||||
}
|
||||
}
|
||||
}
|
||||
::ng-deep {
|
||||
.register-password-cdk {
|
||||
.success,
|
||||
.warning,
|
||||
.error {
|
||||
transition: color 0.3s;
|
||||
}
|
||||
.success {
|
||||
color: @success-color;
|
||||
}
|
||||
.warning {
|
||||
color: @warning-color;
|
||||
}
|
||||
.error {
|
||||
color: @error-color;
|
||||
}
|
||||
.progress-pass > .progress {
|
||||
.ant-progress-bg {
|
||||
background-color: @warning-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
139
web-app/src/app/routes/passport/register/register.component.ts
Normal file
139
web-app/src/app/routes/passport/register/register.component.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
|
||||
import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { _HttpClient } from '@delon/theme';
|
||||
import { MatchControl } from '@delon/util/form';
|
||||
import { NzSafeAny } from 'ng-zorro-antd/core/types';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'passport-register',
|
||||
templateUrl: './register.component.html',
|
||||
styleUrls: ['./register.component.less'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class UserRegisterComponent implements OnDestroy {
|
||||
constructor(fb: FormBuilder, private router: Router, private http: _HttpClient, private cdr: ChangeDetectorRef) {
|
||||
this.form = fb.group(
|
||||
{
|
||||
mail: [null, [Validators.required, Validators.email]],
|
||||
password: [null, [Validators.required, Validators.minLength(6), UserRegisterComponent.checkPassword.bind(this)]],
|
||||
confirm: [null, [Validators.required, Validators.minLength(6)]],
|
||||
mobilePrefix: ['+86'],
|
||||
mobile: [null, [Validators.required, Validators.pattern(/^1\d{10}$/)]],
|
||||
captcha: [null, [Validators.required]]
|
||||
},
|
||||
{
|
||||
validators: MatchControl('password', 'confirm')
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// #region fields
|
||||
|
||||
get mail(): AbstractControl {
|
||||
return this.form.controls.mail;
|
||||
}
|
||||
get password(): AbstractControl {
|
||||
return this.form.controls.password;
|
||||
}
|
||||
get confirm(): AbstractControl {
|
||||
return this.form.controls.confirm;
|
||||
}
|
||||
get mobile(): AbstractControl {
|
||||
return this.form.controls.mobile;
|
||||
}
|
||||
get captcha(): AbstractControl {
|
||||
return this.form.controls.captcha;
|
||||
}
|
||||
form: FormGroup;
|
||||
error = '';
|
||||
type = 0;
|
||||
loading = false;
|
||||
visible = false;
|
||||
status = 'pool';
|
||||
progress = 0;
|
||||
passwordProgressMap: { [key: string]: 'success' | 'normal' | 'exception' } = {
|
||||
ok: 'success',
|
||||
pass: 'normal',
|
||||
pool: 'exception'
|
||||
};
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region get captcha
|
||||
|
||||
count = 0;
|
||||
interval$: any;
|
||||
|
||||
static checkPassword(control: FormControl): NzSafeAny {
|
||||
if (!control) {
|
||||
return null;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const self: any = this;
|
||||
self.visible = !!control.value;
|
||||
if (control.value && control.value.length > 9) {
|
||||
self.status = 'ok';
|
||||
} else if (control.value && control.value.length > 5) {
|
||||
self.status = 'pass';
|
||||
} else {
|
||||
self.status = 'pool';
|
||||
}
|
||||
|
||||
if (self.visible) {
|
||||
self.progress = control.value.length * 10 > 100 ? 100 : control.value.length * 10;
|
||||
}
|
||||
}
|
||||
|
||||
getCaptcha(): void {
|
||||
if (this.mobile.invalid) {
|
||||
this.mobile.markAsDirty({ onlySelf: true });
|
||||
this.mobile.updateValueAndValidity({ onlySelf: true });
|
||||
return;
|
||||
}
|
||||
this.count = 59;
|
||||
this.cdr.detectChanges();
|
||||
this.interval$ = setInterval(() => {
|
||||
this.count -= 1;
|
||||
this.cdr.detectChanges();
|
||||
if (this.count <= 0) {
|
||||
clearInterval(this.interval$);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
submit(): void {
|
||||
this.error = '';
|
||||
Object.keys(this.form.controls).forEach(key => {
|
||||
this.form.controls[key].markAsDirty();
|
||||
this.form.controls[key].updateValueAndValidity();
|
||||
});
|
||||
if (this.form.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = this.form.value;
|
||||
this.loading = true;
|
||||
this.cdr.detectChanges();
|
||||
this.http
|
||||
.post('/register?_allow_anonymous=true', data)
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
this.loading = false;
|
||||
this.cdr.detectChanges();
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.router.navigate(['passport', 'register-result'], { queryParams: { email: data.mail } });
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.interval$) {
|
||||
clearInterval(this.interval$);
|
||||
}
|
||||
}
|
||||
}
|
||||
66
web-app/src/app/routes/routes-routing.module.ts
Normal file
66
web-app/src/app/routes/routes-routing.module.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { SimpleGuard } from '@delon/auth';
|
||||
import { environment } from '@env/environment';
|
||||
// layout
|
||||
import { LayoutBasicComponent } from '../layout/basic/basic.component';
|
||||
import { LayoutPassportComponent } from '../layout/passport/passport.component';
|
||||
// dashboard pages
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
// single pages
|
||||
import { CallbackComponent } from './passport/callback.component';
|
||||
import { UserLockComponent } from './passport/lock/lock.component';
|
||||
// passport pages
|
||||
import { UserLoginComponent } from './passport/login/login.component';
|
||||
import { UserRegisterResultComponent } from './passport/register-result/register-result.component';
|
||||
import { UserRegisterComponent } from './passport/register/register.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: LayoutBasicComponent,
|
||||
canActivate: [SimpleGuard],
|
||||
children: [
|
||||
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||
{ path: 'dashboard', component: DashboardComponent, data: { title: '仪表盘' } },
|
||||
{ path: 'exception', loadChildren: () => import('./exception/exception.module').then(m => m.ExceptionModule) },
|
||||
// 业务子模块
|
||||
// { path: 'widgets', loadChildren: () => import('./widgets/widgets.module').then(m => m.WidgetsModule) },
|
||||
]
|
||||
},
|
||||
// 空白布局
|
||||
// {
|
||||
// path: 'blank',
|
||||
// component: LayoutBlankComponent,
|
||||
// children: [
|
||||
// ]
|
||||
// },
|
||||
// passport
|
||||
{
|
||||
path: 'passport',
|
||||
component: LayoutPassportComponent,
|
||||
children: [
|
||||
{ path: 'login', component: UserLoginComponent, data: { title: '登录' } },
|
||||
{ path: 'register', component: UserRegisterComponent, data: { title: '注册' } },
|
||||
{ path: 'register-result', component: UserRegisterResultComponent, data: { title: '注册结果' } },
|
||||
{ path: 'lock', component: UserLockComponent, data: { title: '锁屏' } },
|
||||
]
|
||||
},
|
||||
// 单页不包裹Layout
|
||||
{ path: 'passport/callback/:type', component: CallbackComponent },
|
||||
{ path: '**', redirectTo: 'exception/404' },
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(
|
||||
routes, {
|
||||
useHash: environment.useHash,
|
||||
// NOTICE: If you use `reuse-tab` component and turn on keepingScroll you can set to `disabled`
|
||||
// Pls refer to https://ng-alain.com/components/reuse-tab
|
||||
scrollPositionRestoration: 'top',
|
||||
}
|
||||
)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class RouteRoutingModule { }
|
||||
29
web-app/src/app/routes/routes.module.ts
Normal file
29
web-app/src/app/routes/routes.module.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { NgModule, Type } from '@angular/core';
|
||||
import { SharedModule } from '@shared';
|
||||
// dashboard pages
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
// single pages
|
||||
import { CallbackComponent } from './passport/callback.component';
|
||||
import { UserLockComponent } from './passport/lock/lock.component';
|
||||
// passport pages
|
||||
import { UserLoginComponent } from './passport/login/login.component';
|
||||
import { UserRegisterResultComponent } from './passport/register-result/register-result.component';
|
||||
import { UserRegisterComponent } from './passport/register/register.component';
|
||||
import { RouteRoutingModule } from './routes-routing.module';
|
||||
|
||||
const COMPONENTS: Array<Type<void>> = [
|
||||
DashboardComponent,
|
||||
// passport pages
|
||||
UserLoginComponent,
|
||||
UserRegisterComponent,
|
||||
UserRegisterResultComponent,
|
||||
// single pages
|
||||
CallbackComponent,
|
||||
UserLockComponent,
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, RouteRoutingModule],
|
||||
declarations: COMPONENTS,
|
||||
})
|
||||
export class RoutesModule {}
|
||||
2
web-app/src/app/shared/components/README.md
Normal file
2
web-app/src/app/shared/components/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
### 公共通用小组件
|
||||
|
||||
8
web-app/src/app/shared/index.ts
Normal file
8
web-app/src/app/shared/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// Components
|
||||
|
||||
// Utils
|
||||
export * from './utils/yuan';
|
||||
|
||||
// Module
|
||||
export * from './shared.module';
|
||||
export * from './json-schema/json-schema.module';
|
||||
3
web-app/src/app/shared/json-schema/README.md
Normal file
3
web-app/src/app/shared/json-schema/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 建议统一在 `widgets` 目录下自定义小部件
|
||||
|
||||
> 注:@delon/form 本身提供 nz-zorro-antd 数据录入组件的全部实现,以及若干第三方组件的代码,可从[widgets-third](https://github.com/ng-alain/delon/tree/master/packages/form/widgets-third)中获取并放置 `widgets` 目录下注册即可。
|
||||
18
web-app/src/app/shared/json-schema/json-schema.module.ts
Normal file
18
web-app/src/app/shared/json-schema/json-schema.module.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { DelonFormModule, WidgetRegistry } from '@delon/form';
|
||||
|
||||
import { SharedModule } from '../shared.module';
|
||||
import { TestWidget } from './test/test.widget';
|
||||
|
||||
export const SCHEMA_THIRDS_COMPONENTS = [TestWidget];
|
||||
|
||||
@NgModule({
|
||||
declarations: SCHEMA_THIRDS_COMPONENTS,
|
||||
imports: [SharedModule, DelonFormModule.forRoot()],
|
||||
exports: SCHEMA_THIRDS_COMPONENTS
|
||||
})
|
||||
export class JsonSchemaModule {
|
||||
constructor(widgetRegistry: WidgetRegistry) {
|
||||
widgetRegistry.register(TestWidget.KEY, TestWidget);
|
||||
}
|
||||
}
|
||||
20
web-app/src/app/shared/json-schema/test/test.widget.ts
Normal file
20
web-app/src/app/shared/json-schema/test/test.widget.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ControlWidget } from '@delon/form';
|
||||
|
||||
@Component({
|
||||
selector: 'test',
|
||||
template: `
|
||||
<sf-item-wrap [id]="id" [schema]="schema" [ui]="ui" [showError]="showError" [error]="error" [showTitle]="schema.title">
|
||||
test widget
|
||||
</sf-item-wrap>
|
||||
`,
|
||||
preserveWhitespaces: false,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class TestWidget extends ControlWidget implements OnInit {
|
||||
static readonly KEY = 'test';
|
||||
|
||||
ngOnInit(): void {
|
||||
console.warn('init test widget');
|
||||
}
|
||||
}
|
||||
7
web-app/src/app/shared/shared-delon.module.ts
Normal file
7
web-app/src/app/shared/shared-delon.module.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { PageHeaderModule } from '@delon/abc/page-header';
|
||||
import { ResultModule } from '@delon/abc/result';
|
||||
import { SEModule } from '@delon/abc/se';
|
||||
import { STModule } from '@delon/abc/st';
|
||||
import { SVModule } from '@delon/abc/sv';
|
||||
|
||||
export const SHARED_DELON_MODULES = [PageHeaderModule, STModule, SEModule, SVModule, ResultModule];
|
||||
45
web-app/src/app/shared/shared-zorro.module.ts
Normal file
45
web-app/src/app/shared/shared-zorro.module.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { NzAlertModule } from 'ng-zorro-antd/alert';
|
||||
import { NzAvatarModule } from 'ng-zorro-antd/avatar';
|
||||
import { NzButtonModule } from 'ng-zorro-antd/button';
|
||||
import { NzCardModule } from 'ng-zorro-antd/card';
|
||||
import { NzCheckboxModule } from 'ng-zorro-antd/checkbox';
|
||||
import { NzDrawerModule } from 'ng-zorro-antd/drawer';
|
||||
import { NzDropDownModule } from 'ng-zorro-antd/dropdown';
|
||||
import { NzFormModule } from 'ng-zorro-antd/form';
|
||||
import { NzGridModule } from 'ng-zorro-antd/grid';
|
||||
import { NzIconModule } from 'ng-zorro-antd/icon';
|
||||
import { NzInputModule } from 'ng-zorro-antd/input';
|
||||
import { NzInputNumberModule } from 'ng-zorro-antd/input-number';
|
||||
import { NzModalModule } from 'ng-zorro-antd/modal';
|
||||
import { NzPopconfirmModule } from 'ng-zorro-antd/popconfirm';
|
||||
import { NzPopoverModule } from 'ng-zorro-antd/popover';
|
||||
import { NzProgressModule } from 'ng-zorro-antd/progress';
|
||||
import { NzSelectModule } from 'ng-zorro-antd/select';
|
||||
import { NzSpinModule } from 'ng-zorro-antd/spin';
|
||||
import { NzTableModule } from 'ng-zorro-antd/table';
|
||||
import { NzTabsModule } from 'ng-zorro-antd/tabs';
|
||||
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
|
||||
|
||||
export const SHARED_ZORRO_MODULES = [
|
||||
NzFormModule,
|
||||
NzGridModule,
|
||||
NzButtonModule,
|
||||
NzInputModule,
|
||||
NzInputNumberModule,
|
||||
NzAlertModule,
|
||||
NzProgressModule,
|
||||
NzSelectModule,
|
||||
NzAvatarModule,
|
||||
NzCardModule,
|
||||
NzDropDownModule,
|
||||
NzPopconfirmModule,
|
||||
NzTableModule,
|
||||
NzPopoverModule,
|
||||
NzDrawerModule,
|
||||
NzModalModule,
|
||||
NzTabsModule,
|
||||
NzToolTipModule,
|
||||
NzIconModule,
|
||||
NzCheckboxModule,
|
||||
NzSpinModule,
|
||||
];
|
||||
61
web-app/src/app/shared/shared.module.ts
Normal file
61
web-app/src/app/shared/shared.module.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { NgModule, Type } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { AlainThemeModule } from '@delon/theme';
|
||||
import { DelonACLModule } from '@delon/acl';
|
||||
import { DelonFormModule } from '@delon/form';
|
||||
|
||||
import { SHARED_DELON_MODULES } from './shared-delon.module';
|
||||
import { SHARED_ZORRO_MODULES } from './shared-zorro.module';
|
||||
|
||||
// #region third libs
|
||||
|
||||
const THIRDMODULES: Array<Type<void>> = [];
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region your components & directives
|
||||
|
||||
const COMPONENTS: Array<Type<void>> = [];
|
||||
const DIRECTIVES: Array<Type<void>> = [];
|
||||
|
||||
// #endregion
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
RouterModule,
|
||||
ReactiveFormsModule,
|
||||
AlainThemeModule.forChild(),
|
||||
DelonACLModule,
|
||||
DelonFormModule,
|
||||
...SHARED_DELON_MODULES,
|
||||
...SHARED_ZORRO_MODULES,
|
||||
// third libs
|
||||
...THIRDMODULES
|
||||
],
|
||||
declarations: [
|
||||
// your components
|
||||
...COMPONENTS,
|
||||
...DIRECTIVES
|
||||
],
|
||||
exports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule,
|
||||
AlainThemeModule,
|
||||
DelonACLModule,
|
||||
DelonFormModule,
|
||||
...SHARED_DELON_MODULES,
|
||||
...SHARED_ZORRO_MODULES,
|
||||
// third libs
|
||||
...THIRDMODULES,
|
||||
// your components
|
||||
...COMPONENTS,
|
||||
...DIRECTIVES
|
||||
]
|
||||
})
|
||||
export class SharedModule { }
|
||||
17
web-app/src/app/shared/st-widget/st-widget.module.ts
Normal file
17
web-app/src/app/shared/st-widget/st-widget.module.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
// import { STWidgetRegistry } from '@delon/abc/st';
|
||||
import { SharedModule } from '../shared.module';
|
||||
|
||||
export const STWIDGET_COMPONENTS = [];
|
||||
|
||||
@NgModule({
|
||||
declarations: STWIDGET_COMPONENTS,
|
||||
imports: [SharedModule],
|
||||
exports: [...STWIDGET_COMPONENTS]
|
||||
})
|
||||
export class STWidgetModule {
|
||||
// constructor(widgetRegistry: STWidgetRegistry) {
|
||||
// widgetRegistry.register(STImgWidget.KEY, STImgWidget);
|
||||
// }
|
||||
}
|
||||
11
web-app/src/app/shared/utils/yuan.ts
Normal file
11
web-app/src/app/shared/utils/yuan.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 转化成RMB元字符串
|
||||
*
|
||||
* @param digits 当数字类型时,允许指定小数点后数字的个数,默认2位小数
|
||||
*/
|
||||
export function yuan(value: number | string, digits: number = 2): string {
|
||||
if (typeof value === 'number') {
|
||||
value = value.toFixed(digits);
|
||||
}
|
||||
return `¥ ${value}`;
|
||||
}
|
||||
Reference in New Issue
Block a user