沈斌
2017-12-14 8dd2337c2d65e29034950940a239d0c73540f92a
updates
2 files deleted
36 files added
10 files modified
5043 ■■■■■ changed files
package-lock.json 3162 ●●●●● patch | view | raw | blame | history
package.json 67 ●●●● patch | view | raw | blame | history
src/app/app.component.css patch | view | raw | blame | history
src/app/app.component.html 20 ●●●●● patch | view | raw | blame | history
src/app/app.component.spec.ts 43 ●●●●● patch | view | raw | blame | history
src/app/app.component.ts 32 ●●●● patch | view | raw | blame | history
src/app/app.module.ts 63 ●●●●● patch | view | raw | blame | history
src/app/core/core.module.ts 19 ●●●●● patch | view | raw | blame | history
src/app/core/i18n/i18n.service.spec.ts 31 ●●●●● patch | view | raw | blame | history
src/app/core/i18n/i18n.service.ts 47 ●●●●● patch | view | raw | blame | history
src/app/core/module-import-guard.ts 6 ●●●●● patch | view | raw | blame | history
src/app/core/net/default.interceptor.ts 70 ●●●●● patch | view | raw | blame | history
src/app/core/services/startup.service.ts 48 ●●●●● patch | view | raw | blame | history
src/app/layout/fullscreen/fullscreen.component.html 1 ●●●● patch | view | raw | blame | history
src/app/layout/fullscreen/fullscreen.component.ts 9 ●●●●● patch | view | raw | blame | history
src/app/layout/header/components/fullscreen.component.ts 22 ●●●●● patch | view | raw | blame | history
src/app/layout/header/components/icon.component.ts 59 ●●●●● patch | view | raw | blame | history
src/app/layout/header/components/langs.component.ts 40 ●●●●● patch | view | raw | blame | history
src/app/layout/header/components/notify.component.ts 163 ●●●●● patch | view | raw | blame | history
src/app/layout/header/components/search.component.ts 49 ●●●●● patch | view | raw | blame | history
src/app/layout/header/components/storage.component.ts 28 ●●●●● patch | view | raw | blame | history
src/app/layout/header/components/task.component.ts 80 ●●●●● patch | view | raw | blame | history
src/app/layout/header/components/theme.component.ts 44 ●●●●● patch | view | raw | blame | history
src/app/layout/header/components/user.component.ts 46 ●●●●● patch | view | raw | blame | history
src/app/layout/header/header.component.html 74 ●●●●● patch | view | raw | blame | history
src/app/layout/header/header.component.spec.ts 17 ●●●●● patch | view | raw | blame | history
src/app/layout/header/header.component.ts 21 ●●●●● patch | view | raw | blame | history
src/app/layout/header/index.md 20 ●●●●● patch | view | raw | blame | history
src/app/layout/layout.component.html 6 ●●●●● patch | view | raw | blame | history
src/app/layout/layout.component.spec.ts 17 ●●●●● patch | view | raw | blame | history
src/app/layout/layout.component.ts 38 ●●●●● patch | view | raw | blame | history
src/app/layout/layout.module.ts 56 ●●●●● patch | view | raw | blame | history
src/app/layout/pro/user/user.component.html 17 ●●●●● patch | view | raw | blame | history
src/app/layout/pro/user/user.component.less 58 ●●●●● patch | view | raw | blame | history
src/app/layout/pro/user/user.component.ts 23 ●●●●● patch | view | raw | blame | history
src/app/layout/sidebar/sidebar.component.html 17 ●●●●● patch | view | raw | blame | history
src/app/layout/sidebar/sidebar.component.spec.ts 16 ●●●●● patch | view | raw | blame | history
src/app/layout/sidebar/sidebar.component.ts 12 ●●●●● patch | view | raw | blame | history
src/app/routes/routes.module.ts 10 ●●●●● patch | view | raw | blame | history
src/app/shared/shared.module.ts 211 ●●●●● patch | view | raw | blame | history
src/assets/i18n/en.json 94 ●●●●● patch | view | raw | blame | history
src/assets/i18n/zh-CN.json 94 ●●●●● patch | view | raw | blame | history
src/environments/environment.prod.ts 1 ●●●● patch | view | raw | blame | history
src/environments/environment.ts 1 ●●●● patch | view | raw | blame | history
src/testing/common.spec.ts 66 ●●●●● patch | view | raw | blame | history
src/tsconfig.app.json 8 ●●●● patch | view | raw | blame | history
src/tsconfig.spec.json 8 ●●●● patch | view | raw | blame | history
tsconfig.json 9 ●●●● patch | view | raw | blame | history
package-lock.json
Diff too large
package.json
@@ -21,29 +21,70 @@
    "@angular/platform-browser": "^5.0.0",
    "@angular/platform-browser-dynamic": "^5.0.0",
    "@angular/router": "^5.0.0",
    "core-js": "^2.4.1",
    "rxjs": "^5.5.2",
    "zone.js": "^0.8.14"
    "@antv/data-set": "^0.7.0",
    "@antv/g2": "^3.0.1",
    "@antv/g2-plugin-slider": "^2.0.0",
    "@delon/abc": "^0.3.0-rc.1",
    "@delon/acl": "^0.3.0-rc.1",
    "@delon/theme": "^0.3.0-rc.1",
    "@delon/auth": "^0.3.0-rc.1",
    "@ngx-translate/core": "^9.0.0",
    "@ngx-translate/http-loader": "^2.0.0",
    "angular-baidu-maps": "^1.0.1",
    "angular-qq-maps": "^1.0.1",
    "angular-tree-component": "^6.1.0",
    "core-js": "^2.5.1",
    "file-saver": "^1.3.3",
    "font-awesome": "^4.7.0",
    "moment": "^2.19.3",
    "ng-tree-antd": "^2.0.0",
    "ng-zorro-antd": "^0.6.5",
    "ng-zorro-antd-extra": "^1.1.3",
    "ng2-dnd": "^5.0.0",
    "ng2-file-upload": "^1.2.1",
    "ng2-img-cropper": "^0.9.0",
    "ngx-color-picker": "^5.0.0",
    "ngx-countdown": "^2.0.0",
    "rxjs": "^5.5.5",
    "screenfull": "^3.3.1",
    "simple-line-icons": "^2.4.1",
    "sweetalert2": "^7.0.0",
    "weather-icons": "^1.3.2",
    "zone.js": "^0.8.18"
  },
  "devDependencies": {
    "@angular/cli": "1.6.0",
    "@angular/cli": "^1.5.2",
    "@angular/compiler-cli": "^5.0.0",
    "@angular/language-service": "^5.0.0",
    "@types/jasmine": "~2.5.53",
    "@types/jasminewd2": "~2.0.2",
    "@angularclass/hmr": "^2.1.3",
    "@angularclass/hmr-loader": "^3.0.4",
    "@types/jasmine": "~2.6.0",
    "@types/jasminewd2": "~2.0.3",
    "@types/node": "~6.0.60",
    "codelyzer": "^4.0.1",
    "jasmine-core": "~2.6.2",
    "jasmine-spec-reporter": "~4.1.0",
    "karma": "~1.7.0",
    "karma-chrome-launcher": "~2.1.1",
    "codecov": "^3.0.0",
    "codelyzer": "~4.0.1",
    "jasmine-core": "~2.8.0",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~1.7.1",
    "karma-chrome-launcher": "~2.2.0",
    "karma-cli": "~1.0.1",
    "karma-coverage-istanbul-reporter": "^1.2.1",
    "karma-coverage-istanbul-reporter": "^1.3.0",
    "karma-jasmine": "~1.1.0",
    "karma-jasmine-html-reporter": "^0.2.2",
    "karma-remap-istanbul": "^0.6.0",
    "karma-sauce-launcher": "^1.2.0",
    "lint-staged": "^5.0.0",
    "npm-run-all": "^4.1.1",
    "protractor": "~5.1.2",
    "stylelint": "^8.2.0",
    "stylelint-config-standard": "^17.0.0",
    "ts-node": "~3.2.0",
    "tslint": "~5.7.0",
    "typescript": "~2.4.2"
    "typescript": "~2.5.0",
    "webpack-bundle-analyzer": "^2.9.0"
  },
  "lint-staged": {
    "src/**/*.ts": "lint:ts",
    "src/**/*.less": "lint:style"
  }
}
src/app/app.component.css
src/app/app.component.html
File was deleted
src/app/app.component.spec.ts
@@ -1,27 +1,22 @@
import { TestBed, async } from '@angular/core/testing';
import { TestBed, TestModuleMetadata } from '@angular/core/testing';
import { setUpTestBed } from '../testing/common.spec';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
    }).compileComponents();
  }));
  it('should create the app', async(() => {
import { APP_BASE_HREF } from '@angular/common';
import { ThemesService, TitleService } from '@delon/theme';
describe('Component: App', () => {
    setUpTestBed(<TestModuleMetadata>{
        declarations: [ AppComponent ],
        providers: [
            ThemesService, TitleService,
            { provide: APP_BASE_HREF, useValue: '/' }
        ]
    });
    it('should create the app', () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  }));
  it(`should have as title 'app'`, async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app.title).toEqual('app');
  }));
  it('should render title in a h1 tag', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
  }));
        const comp = fixture.debugElement.componentInstance;
        expect(comp).toBeTruthy();
    });
});
src/app/app.component.ts
@@ -1,10 +1,32 @@
import { Component } from '@angular/core';
import { Component, HostBinding, OnInit } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { ThemesService, SettingsService, TitleService } from '@delon/theme';
import { filter, map } from 'rxjs/operators';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
  template: `<router-outlet></router-outlet>`
})
export class AppComponent {
  title = 'app';
export class AppComponent implements OnInit {
  @HostBinding('class.layout-fixed') get isFixed() { return this.settings.layout.fixed; }
  @HostBinding('class.layout-boxed') get isBoxed() { return this.settings.layout.boxed; }
  @HostBinding('class.aside-collapsed') get isCollapsed() { return this.settings.layout.collapsed; }
  constructor(
    private theme: ThemesService,
    private settings: SettingsService,
    private router: Router,
    private titleSrv: TitleService) {
  }
  ngOnInit() {
    this.router.events.pipe(
            filter(evt => evt instanceof NavigationEnd),
            map(() => this.router.url)
        )
        .subscribe(url => {
            this.titleSrv.setTitleByUrl(url);
        });
  }
}
src/app/app.module.ts
@@ -1,18 +1,73 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { NgModule, LOCALE_ID, APP_INITIALIZER, Injector } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule, TranslateLoader, TranslateService } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { HttpClient, HTTP_INTERCEPTORS } from '@angular/common/http';
import { CoreModule } from './core/core.module';
import { SharedModule } from './shared/shared.module';
import { AppComponent } from './app.component';
import { RoutesModule } from './routes/routes.module';
import { LayoutModule } from './layout/layout.module';
import { StartupService } from './core/services/startup.service';
import { DefaultInterceptor } from '@core/net/default.interceptor';
import { AlainAuthModule, SimpleInterceptor } from '@delon/auth';
// i18n
import { I18NService } from './core/i18n/i18n.service';
import { ALAIN_I18N_TOKEN } from '@delon/theme';
import { registerLocaleData } from '@angular/common';
import localeZhHans from '@angular/common/locales/zh-Hans';
registerLocaleData(localeZhHans);
// AoT requires an exported function for factories
export function HttpLoaderFactory(http: HttpClient) {
    return new TranslateHttpLoader(http, `assets/i18n/`, '.json');
}
export function StartupServiceFactory(startupService: StartupService): Function {
    return () => startupService.load();
}
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
        BrowserModule,
        BrowserAnimationsModule,
        SharedModule.forRoot(),
        CoreModule,
        LayoutModule,
        RoutesModule,
        // auth
        AlainAuthModule.forRoot({
            login_url: `/pro/user/login`
        }),
        // i18n
        TranslateModule.forRoot({
            loader: {
                provide: TranslateLoader,
                useFactory: HttpLoaderFactory,
                deps: [HttpClient]
            }
        })
  ],
  providers: [],
    providers: [
        { provide: LOCALE_ID, useValue: 'zh-Hans' },
        { provide: HTTP_INTERCEPTORS, useClass: SimpleInterceptor, multi: true},
        { provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true},
        { provide: ALAIN_I18N_TOKEN, useClass: I18NService, multi: false },
        StartupService,
        {
            provide: APP_INITIALIZER,
            useFactory: StartupServiceFactory,
            deps: [StartupService],
            multi: true
        }
    ],
  bootstrap: [AppComponent]
})
export class AppModule { }
src/app/core/core.module.ts
New file
@@ -0,0 +1,19 @@
import { NgModule, Optional, SkipSelf } from '@angular/core';
import { throwIfAlreadyLoaded } from './module-import-guard';
import { AlainThemeModule } from '@delon/theme';
import { I18NService } from './i18n/i18n.service';
@NgModule({
    imports: [
       AlainThemeModule.forRoot()
    ],
    providers: [
        I18NService
    ]
})
export class CoreModule {
  constructor( @Optional() @SkipSelf() parentModule: CoreModule) {
    throwIfAlreadyLoaded(parentModule, 'CoreModule');
  }
}
src/app/core/i18n/i18n.service.spec.ts
New file
@@ -0,0 +1,31 @@
import { SharedModule } from '@shared/shared.module';
import { TestBed, async, inject } from '@angular/core/testing';
import { TranslateService, TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { I18NService } from './i18n.service';
import { HttpLoaderFactory } from '../../app.module';
import { SettingsService } from '@delon/theme';
describe('Service: I18n', () => {
    beforeEach(() => {
        TestBed.configureTestingModule({
            imports: [
                HttpClientModule,
                SharedModule.forRoot(),
                TranslateModule.forRoot({
                    loader: {
                        provide: TranslateLoader,
                        useFactory: (HttpLoaderFactory),
                        deps: [HttpClient]
                    }
                })
            ],
            providers: [I18NService, SettingsService]
        });
    });
    it('should create an instance', inject([I18NService], (service: I18NService) => {
        expect(service).toBeTruthy();
    }));
});
src/app/core/i18n/i18n.service.ts
New file
@@ -0,0 +1,47 @@
import { Injectable, Inject, Injector } from '@angular/core';
import { Router } from '@angular/router';
import { zhCN, enUS, NzLocaleService } from 'ng-zorro-antd';
import { TranslateService } from '@ngx-translate/core';
import { SettingsService, AlainI18NService } from '@delon/theme';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class I18NService implements AlainI18NService {
    private _default = 'en';
    private _langs = [
        { code: 'en', text: 'English' },
        { code: 'zh-CN', text: '中文' }
    ];
    constructor(settings: SettingsService,
        private nzLocalService: NzLocaleService,
        private translate: TranslateService,
        private injector: Injector) {
        this._default = settings.layout.lang || translate.getBrowserLang();
        const lans = this._langs.map(item => item.code);
        if (!lans.includes(this._default)) {
            this._default = lans[0];
        }
        translate.addLangs(lans);
        translate.setDefaultLang(this._default);
    }
    use(lang: string = null, firstLoad = true): Observable<any> {
        lang = lang || this.translate.getDefaultLang();
        this.nzLocalService.setLocale(lang === 'en' ? enUS : zhCN);
        // need reload router because of ng-zorro-antd local system
        if (!firstLoad) this.injector.get(Router).navigate([ '/' ]);
        return this.translate.use(lang);
    }
    getLangs() {
        return this._langs;
    }
    fanyi(key: string) {
        return this.translate.instant(key);
    }
}
src/app/core/module-import-guard.ts
New file
@@ -0,0 +1,6 @@
// https://angular.io/guide/styleguide#style-04-12
export function throwIfAlreadyLoaded(parentModule: any, moduleName: string) {
  if (parentModule) {
    throw new Error(`${moduleName} has already been loaded. Import Core modules in the AppModule only.`);
  }
}
src/app/core/net/default.interceptor.ts
New file
@@ -0,0 +1,70 @@
import { Injectable, Injector } from '@angular/core';
import { Router } from '@angular/router';
import { HttpInterceptor, HttpRequest, HttpHandler,
         HttpSentEvent, HttpHeaderResponse, HttpProgressEvent, HttpResponse, HttpUserEvent,
       } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
import { catchError } from 'rxjs/operators';
import { map, mergeMap } from 'rxjs/operators';
import { environment } from '../../../environments/environment';
/**
 * 默认HTTP拦截器,其注册细节见 `app.module.ts`
 */
@Injectable()
export class DefaultInterceptor implements HttpInterceptor {
    constructor(private injector: Injector) {}
    private goLogin() {
        const router = this.injector.get(Router);
        this.injector.get(Router).navigate([ '/login' ]);
    }
    intercept(req: HttpRequest<any>, next: HttpHandler):
        Observable<HttpSentEvent | HttpHeaderResponse | HttpProgressEvent | HttpResponse<any> | HttpUserEvent<any>> {
        // TIPS:原TOKEN信息已交由 `@delon/auth` 处理
        // Document: http://ng-alain.com/docs/auth
        // 统一加上服务端前缀
        let url = req.url;
        if (!url.startsWith('https://') && !url.startsWith('http://')) {
            url = environment.SERVER_URL + url;
        }
        const newReq = req.clone({
            url: url
        });
        return next.handle(newReq).pipe(
                    mergeMap((event: any) => {
                        // 允许统一对请求错误处理,这是因为一个请求若是业务上错误的情况下其HTTP请求的状态是200的情况下需要
                        if (event instanceof HttpResponse && event.status !== 200) {
                            // 业务处理:observer.error 会跳转至后面的 `catch`
                            // return ErrorObservable.create(event);
                        }
                        // 若一切都正常,则后续操作
                        return Observable.create(observer => observer.next(event));
                    }),
                    catchError((res: HttpResponse<any>) => {
                        // 业务处理:一些通用操作
                        switch (res.status) {
                            case 401: // 未登录状态码
                                this.goLogin();
                                break;
                            case 200:
                                // 业务层级错误处理
                                console.log('业务错误');
                                break;
                            case 404:
                                // 404
                                break;
                        }
                        // 以错误的形式结束本次请求
                        return ErrorObservable.create(event);
                    })
                );
    }
}
src/app/core/services/startup.service.ts
New file
@@ -0,0 +1,48 @@
import { Router } from '@angular/router';
import { Injectable, Injector } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { MenuService, SettingsService, TitleService } from '@delon/theme';
import { ACLService } from '@delon/acl';
import { I18NService } from '../i18n/i18n.service';
/**
 * 用于应用启动时
 * 一般用来获取应用所需要的基础数据等
 */
@Injectable()
export class StartupService {
    constructor(
        private menuService: MenuService,
        private i18n: I18NService,
        private settingService: SettingsService,
        private aclService: ACLService,
        private titleService: TitleService,
        private httpClient: HttpClient,
        private injector: Injector) { }
    load(): Promise<any> {
        // only works with promises
        // https://github.com/angular/angular/issues/15088
        return new Promise((resolve, reject) => {
            this.httpClient.get('assets/app-data.json')
                           .subscribe((res: any) => {
                               // 应用信息:包括站点名、描述、年份
                                this.settingService.setApp(res.app);
                                // 用户信息:包括姓名、头像、邮箱地址
                                this.settingService.setUser(res.user);
                                // ACL:设置权限为全量
                                this.aclService.setFull(true);
                                // 初始化菜单
                                this.menuService.add(res.menu);
                                // i18n:设置默认语言
                                this.i18n.use(this.settingService.layout.lang);
                                // 设置页面标题的后缀
                                this.titleService.suffix = res.app.name;
                                resolve(res);
                            }, (err: HttpErrorResponse) => {
                                resolve(null);
                            });
        });
    }
}
src/app/layout/fullscreen/fullscreen.component.html
New file
@@ -0,0 +1 @@
<router-outlet></router-outlet>
src/app/layout/fullscreen/fullscreen.component.ts
New file
@@ -0,0 +1,9 @@
import { Component } from '@angular/core';
@Component({
    selector: 'app-layout-fullscreen',
    templateUrl: './fullscreen.component.html'
})
export class LayoutFullScreenComponent {
}
src/app/layout/header/components/fullscreen.component.ts
New file
@@ -0,0 +1,22 @@
import { Component, HostListener } from '@angular/core';
import * as screenfull from 'screenfull';
@Component({
    selector: 'header-fullscreen',
    template: `
    <i class="anticon anticon-{{status ? 'shrink' : 'arrows-alt'}}"></i>
    {{status ? 'fullscreen-exit' : 'fullscreen' | translate }}
    `
})
export class HeaderFullScreenComponent {
    status = false;
    @HostListener('click')
    _click() {
        if (screenfull.enabled) {
            screenfull.toggle();
        }
        this.status = !screenfull.isFullscreen;
    }
}
src/app/layout/header/components/icon.component.ts
New file
@@ -0,0 +1,59 @@
import { Component } from '@angular/core';
@Component({
    selector: 'header-icon',
    template: `
    <nz-dropdown nzTrigger="click" nzPlacement="bottomRight" (nzVisibleChange)="change()">
        <div class="item" nz-dropdown>
            <i class="anticon anticon-appstore-o"></i>
        </div>
        <div nz-menu class="wd-xl animated jello">
            <nz-spin [nzSpinning]="loading" [nzTip]="'正在读取数据...'">
                <div nz-row [nzType]="'flex'" [nzJustify]="'center'" [nzAlign]="'middle'" class="app-icons">
                    <div nz-col [nzSpan]="6">
                        <i class="anticon anticon-calendar bg-error text-white"></i>
                        <small>Calendar</small>
                    </div>
                    <div nz-col [nzSpan]="6">
                        <i class="anticon anticon-file bg-teal text-white"></i>
                        <small>Files</small>
                    </div>
                    <div nz-col [nzSpan]="6">
                        <i class="anticon anticon-cloud bg-success text-white"></i>
                        <small>Cloud</small>
                    </div>
                    <div nz-col [nzSpan]="6">
                        <i class="anticon anticon-star-o bg-pink text-white"></i>
                        <small>Star</small>
                    </div>
                    <div nz-col [nzSpan]="6">
                        <i class="anticon anticon-team bg-purple text-white"></i>
                        <small>Team</small>
                    </div>
                    <div nz-col [nzSpan]="6">
                        <i class="anticon anticon-scan bg-warning text-white"></i>
                        <small>QR</small>
                    </div>
                    <div nz-col [nzSpan]="6">
                        <i class="anticon anticon-pay-circle-o bg-cyan text-white"></i>
                        <small>Pay</small>
                    </div>
                    <div nz-col [nzSpan]="6">
                        <i class="anticon anticon-printer bg-grey text-white"></i>
                        <small>Print</small>
                    </div>
                </div>
            </nz-spin>
        </div>
    </nz-dropdown>
    `
})
export class HeaderIconComponent {
    loading = true;
    change() {
        setTimeout(() => this.loading = false, 500);
    }
}
src/app/layout/header/components/langs.component.ts
New file
@@ -0,0 +1,40 @@
import { Component } from '@angular/core';
import { SettingsService, MenuService } from '@delon/theme';
import { I18NService } from '@core/i18n/i18n.service';
@Component({
    selector: 'header-langs',
    template: `
    <nz-dropdown>
        <div nz-dropdown>
            <i class="anticon anticon-edit"></i>
            {{ 'language' | translate}}
            <i class="anticon anticon-down"></i>
        </div>
        <ul nz-menu>
            <li nz-menu-item *ngFor="let item of langs"
            [nzSelected]="item.code === settings.layout.lang"
                (click)="change(item.code)">{{item.text}}</li>
        </ul>
    </nz-dropdown>
    `
})
export class HeaderLangsComponent {
    langs: any[];
    constructor(
        private menuService: MenuService,
        public settings: SettingsService,
        public tsServ: I18NService
    ) {
        this.langs = this.tsServ.getLangs();
    }
    change(lang: string) {
        this.tsServ.use(lang, false).subscribe(() => {
            this.menuService.resume();
        });
        this.settings.setLayout('lang', lang);
    }
}
src/app/layout/header/components/notify.component.ts
New file
@@ -0,0 +1,163 @@
import { NzMessageService } from 'ng-zorro-antd';
import { Component, OnInit, Input } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { ArrayObservable } from 'rxjs/observable/ArrayObservable';
import { map, groupBy, concatMap, mergeMap, flatMap, delay, tap, toArray } from 'rxjs/operators';
import * as moment from 'moment';
import { NoticeItem } from '@delon/abc';
import { SettingsService } from '@delon/theme';
/**
 * 菜单通知
 */
@Component({
    selector: 'header-notify',
    template: `
    <notice-icon
        [data]="data"
        [count]="count"
        [loading]="loading"
        (select)="select($event)"
        (clear)="clear($event)"
        (popupVisibleChange)="loadData($event)"></notice-icon>
    `
})
export class HeaderNotifyComponent implements OnInit {
    data: NoticeItem[] = [
        { title: '通知', list: [], emptyText: '你已查看所有通知', emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg' },
        { title: '消息', list: [], emptyText: '您已读完所有消息', emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg' },
        { title: '待办', list: [], emptyText: '你已完成所有待办', emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/HsIsxMZiWKrNUavQUXqx.svg' }
    ];
    count = 0;
    loading = false;
    constructor(private msg: NzMessageService, private settings: SettingsService) {}
    ngOnInit() {
        // mock data
        this.count = this.settings.user.notifyCount || 12;
    }
    private parseGroup(data: Observable<any[]>) {
        console.log('parseGroup');
        data.pipe(
                concatMap((i: any) => i),
                map((i: any) => {
                    if (i.datetime) i.datetime = moment(i.datetime).fromNow();
                    // change to color
                    if (i.status) {
                        i.color = ({
                            todo: '',
                            processing: 'blue',
                            urgent: 'red',
                            doing: 'gold',
                        })[i.status];
                    }
                    return i;
                }),
                groupBy((x: any) => x.type),
                mergeMap(g => g.pipe(toArray())),
                tap((ls: any) => {
                    this.data.find(w => w.title === ls[0].type).list = ls;
                })
            ).subscribe(res => this.loading = false);
    }
    loadData(res) {
        if (!res || this.loading) return;
        this.loading = true;
        // region: mock http request
        this.parseGroup(ArrayObservable.of([{
            id: '000000001',
            avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
            title: '你收到了 14 份新周报',
            datetime: '2017-08-09',
            type: '通知',
          }, {
            id: '000000002',
            avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
            title: '你推荐的 曲妮妮 已通过第三轮面试',
            datetime: '2017-08-08',
            type: '通知',
          }, {
            id: '000000003',
            avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
            title: '这种模板可以区分多种通知类型',
            datetime: '2017-08-07',
            read: true,
            type: '通知',
          }, {
            id: '000000004',
            avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
            title: '左侧图标用于区分不同的类型',
            datetime: '2017-08-07',
            type: '通知',
          }, {
            id: '000000005',
            avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
            title: '内容不要超过两行字,超出时自动截断',
            datetime: '2017-08-07',
            type: '通知',
          }, {
            id: '000000006',
            avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
            title: '曲丽丽 评论了你',
            description: '描述信息描述信息描述信息',
            datetime: '2017-08-07',
            type: '消息',
          }, {
            id: '000000007',
            avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
            title: '朱偏右 回复了你',
            description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
            datetime: '2017-08-07',
            type: '消息',
          }, {
            id: '000000008',
            avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
            title: '标题',
            description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
            datetime: '2017-08-07',
            type: '消息',
          }, {
            id: '000000009',
            title: '任务名称',
            description: '任务需要在 2017-01-12 20:00 前启动',
            extra: '未开始',
            status: 'todo',
            type: '待办',
          }, {
            id: '000000010',
            title: '第三方紧急代码变更',
            description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
            extra: '马上到期',
            status: 'urgent',
            type: '待办',
          }, {
            id: '000000011',
            title: '信息安全考试',
            description: '指派竹尔于 2017-01-09 前完成更新并发布',
            extra: '已耗时 8 天',
            status: 'doing',
            type: '待办',
          }, {
            id: '000000012',
            title: 'ABCD 版本发布',
            description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
            extra: '进行中',
            status: 'processing',
            type: '待办',
          }
        ]).pipe(delay(1000)));
        // endregion
    }
    clear(type: string) {
        this.msg.success(`清空了 ${type}`);
    }
    select(res: any) {
        this.msg.success(`点击了 ${res.title} 的 ${res.item.title}`);
    }
}
src/app/layout/header/components/search.component.ts
New file
@@ -0,0 +1,49 @@
import { Component, HostBinding, ViewChild, Input, OnInit, ElementRef, AfterViewInit } from '@angular/core';
@Component({
    selector: 'header-search',
    template: `
    <nz-input nzPlaceHolder='{{ "top-search-ph" | translate}}' [(ngModel)]="q"
        (nzFocus)="qFocus()" (nzBlur)="qBlur()">
        <ng-template #prefix>
            <i class="anticon anticon-search"></i>
        </ng-template>
    </nz-input>
    `
})
export class HeaderSearchComponent implements AfterViewInit {
    q: string;
    qIpt: HTMLInputElement;
    @HostBinding('class.header-search__focus')
    focus = false;
    @HostBinding('class.header-search__toggled')
    searchToggled = false;
    @Input()
    set toggleChange(value: boolean) {
        if (typeof value === 'undefined') return;
        console.log('toggleChange', value);
        this.searchToggled = true;
        this.focus = true;
        setTimeout(() => this.qIpt.focus(), 300);
    }
    constructor(private el: ElementRef) {}
    ngAfterViewInit() {
        this.qIpt = (this.el.nativeElement as HTMLElement).querySelector('.ant-input') as HTMLInputElement;
    }
    qFocus() {
        this.focus = true;
    }
    qBlur() {
        this.focus = false;
        this.searchToggled = false;
    }
}
src/app/layout/header/components/storage.component.ts
New file
@@ -0,0 +1,28 @@
import { Component, HostListener } from '@angular/core';
import { NzModalService, NzMessageService } from 'ng-zorro-antd';
@Component({
    selector: 'header-storage',
    template: `
    <i class="anticon anticon-tool"></i>
    {{ 'clear-local-storage' | translate}}`
})
export class HeaderStorageComponent {
    constructor(
        private confirmServ: NzModalService,
        private messageServ: NzMessageService
    ) {
    }
    @HostListener('click')
    _click() {
        this.confirmServ.confirm({
            title: 'Make sure clear all local storage?',
            onOk: () => {
                localStorage.clear();
                this.messageServ.success('Clear Finished!');
            }
        });
    }
}
src/app/layout/header/components/task.component.ts
New file
@@ -0,0 +1,80 @@
import { Component } from '@angular/core';
@Component({
    selector: 'header-task',
    template: `
    <nz-dropdown nzTrigger="click" nzPlacement="bottomRight" (nzVisibleChange)="change()">
        <div class="item" nz-dropdown>
            <nz-badge [nzDot]="true">
                <ng-template #content>
                    <i class="anticon anticon-bell"></i>
                </ng-template>
            </nz-badge>
        </div>
        <div nz-menu class="wd-lg">
            <nz-card nzTitle="Notifications" [nzLoading]="loading" class="ant-card__body-nopadding">
                <ng-template #extra><i class="anticon anticon-plus"></i></ng-template>
                <div nz-row [nzType]="'flex'" [nzJustify]="'center'" [nzAlign]="'middle'" class="py-sm bg-grey-lighter-h point">
                    <div nz-col [nzSpan]="4" class="text-center">
                        <nz-avatar [nzSrc]="'./assets/img/1.png'" [nzSize]="'large'"></nz-avatar>
                    </div>
                    <div nz-col [nzSpan]="20">
                        <strong>cipchk</strong>
                        <p>Please tell me what happened in a few words, don't go into details.</p>
                    </div>
                </div>
                <div nz-row [nzType]="'flex'" [nzJustify]="'center'" [nzAlign]="'middle'" class="py-sm bg-grey-lighter-h point">
                    <div nz-col [nzSpan]="4" class="text-center">
                        <nz-avatar [nzSrc]="'./assets/img/2.png'" [nzSize]="'large'"></nz-avatar>
                    </div>
                    <div nz-col [nzSpan]="20">
                        <strong>はなさき</strong>
                        <p>ハルカソラトキヘダツヒカリ </p>
                    </div>
                </div>
                <div nz-row [nzType]="'flex'" [nzJustify]="'center'" [nzAlign]="'middle'" class="py-sm bg-grey-lighter-h point">
                    <div nz-col [nzSpan]="4" class="text-center">
                        <nz-avatar [nzSrc]="'./assets/img/3.png'" [nzSize]="'large'"></nz-avatar>
                    </div>
                    <div nz-col [nzSpan]="20">
                        <strong>苏先生</strong>
                        <p>请告诉我,我应该说点什么好?</p>
                    </div>
                </div>
                <div nz-row [nzType]="'flex'" [nzJustify]="'center'" [nzAlign]="'middle'" class="py-sm bg-grey-lighter-h point">
                    <div nz-col [nzSpan]="4" class="text-center">
                        <nz-avatar [nzSrc]="'./assets/img/4.png'" [nzSize]="'large'"></nz-avatar>
                    </div>
                    <div nz-col [nzSpan]="20">
                        <strong>Kent</strong>
                        <p>Please tell me what happened in a few words, don't go into details.</p>
                    </div>
                </div>
                <div nz-row [nzType]="'flex'" [nzJustify]="'center'" [nzAlign]="'middle'" class="py-sm bg-grey-lighter-h point">
                    <div nz-col [nzSpan]="4" class="text-center">
                        <nz-avatar [nzSrc]="'./assets/img/5.png'" [nzSize]="'large'"></nz-avatar>
                    </div>
                    <div nz-col [nzSpan]="20">
                        <strong>Jefferson</strong>
                        <p>Please tell me what happened in a few words, don't go into details.</p>
                    </div>
                </div>
                <div nz-row class="pt-lg pb-lg">
                    <div nz-col [nzSpan]="24" class="text-center text-grey point">
                        See All
                    </div>
                </div>
            </nz-card>
        </div>
    </nz-dropdown>
    `
})
export class HeaderTaskComponent {
    loading = true;
    change() {
        setTimeout(() => this.loading = false, 500);
    }
}
src/app/layout/header/components/theme.component.ts
New file
@@ -0,0 +1,44 @@
import { Component } from '@angular/core';
import { SettingsService, ThemesService, ThemeType } from '@delon/theme';
@Component({
    selector: 'header-theme',
    template: `
    <strong>{{ 'theme-switch' | translate}}</strong>
    <div class="theme-icons">
        <label *ngFor="let item of themes" (click)="changeTheme(item.l)" [style.background]="item.bg">
            <i class="anticon anticon-check" *ngIf="item.l == settings.layout.theme"></i>
            <div class="areas">
                <span class="nav" [style.background]="item.nav"></span>
                <span class="con" [style.background]="item.con"></span>
            </div>
        </label>
    </div>
    `
})
export class HeaderThemeComponent {
    themes: { l: ThemeType, bg: string, nav: string, con: string }[] = [
        { l: 'A', bg: '#108ee9', nav: '#fff', con: '#f5f7fa' },
        { l: 'B', bg: '#00a2ae', nav: '#fff', con: '#f5f7fa' },
        { l: 'C', bg: '#00a854', nav: '#fff', con: '#f5f7fa' },
        { l: 'D', bg: '#f04134', nav: '#fff', con: '#f5f7fa' },
        { l: 'E', bg: '#373d41', nav: '#fff', con: '#f5f7fa' },
        { l: 'F', bg: '#108ee9', nav: '#404040', con: '#f5f7fa' },
        { l: 'G', bg: '#00a2ae', nav: '#404040', con: '#f5f7fa' },
        { l: 'H', bg: '#00a854', nav: '#404040', con: '#f5f7fa' },
        { l: 'I', bg: '#f04134', nav: '#404040', con: '#f5f7fa' },
        { l: 'J', bg: '#373d41', nav: '#404040', con: '#f5f7fa' }
    ];
    constructor(
        public settings: SettingsService,
        private themeServ: ThemesService
    ) {
    }
    changeTheme(theme: ThemeType) {
        this.themeServ.setTheme(theme);
        this.settings.setLayout('theme', theme);
    }
}
src/app/layout/header/components/user.component.ts
New file
@@ -0,0 +1,46 @@
import { Component, OnInit, Inject } from '@angular/core';
import { Router } from '@angular/router';
import { SettingsService } from '@delon/theme';
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
@Component({
    selector: 'header-user',
    template: `
    <nz-dropdown nzPlacement="bottomRight">
        <div class="item d-flex align-items-center px-sm" nz-dropdown>
            <nz-avatar [nzSrc]="settings.user.avatar" nzSize="small" class="mr-sm"></nz-avatar>
            {{settings.user.name}}
        </div>
        <div nz-menu class="width-sm">
            <div nz-menu-item [nzDisable]="true"><i class="anticon anticon-user mr-sm"></i>个人中心</div>
            <div nz-menu-item [nzDisable]="true"><i class="anticon anticon-setting mr-sm"></i>设置</div>
            <li nz-menu-divider></li>
            <div nz-menu-item (click)="logout()"><i class="anticon anticon-setting mr-sm"></i>退出登录</div>
        </div>
    </nz-dropdown>
    `
})
export class HeaderUserComponent implements OnInit {
    constructor(
        public settings: SettingsService,
        private router: Router,
        @Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService) {}
    ngOnInit(): void {
        this.tokenService.change().subscribe((res: any) => {
            this.settings.setUser(res);
        });
        const token = this.tokenService.get() || {
            token: 'nothing',
            name: 'Admin',
            avatar: './assets/img/zorro.svg',
            email: 'cipchk@qq.com'
        };
        this.tokenService.set(token);
    }
    logout() {
        this.tokenService.clear();
        this.router.navigateByUrl('/pro/user/login');
    }
}
src/app/layout/header/header.component.html
New file
@@ -0,0 +1,74 @@
<div class="logo">
    <a [routerLink]="['/']">
        <img class="expanded" src="./assets/img/logo-full.svg" alt="{{settings.app.name}}" style="max-height:40px;" />
        <img class="collapsed" src="./assets/img/logo.svg" alt="{{settings.app.name}}" style="max-height:30px;" />
    </a>
</div>
<div class="top-nav-wrap">
    <ul class="top-nav">
        <!-- Menu -->
        <li>
            <div class="item" (click)="toggleCollapsedSideabar()">
                <i class="anticon anticon-menu-{{settings.layout.collapsed ? 'unfold' : 'fold'}}"></i>
            </div>
        </li>
        <!-- Github Page -->
        <li>
            <a class="item" href="//github.com/cipchk/ng-alain" target="_blank">
                <i class="anticon anticon-github"></i>
            </a>
        </li>
        <!-- Lock Page -->
        <li class="hidden-xs">
            <div class="item" [routerLink]="['/lock']">
                <i class="anticon anticon-lock"></i>
            </div>
        </li>
        <!-- Search Button -->
        <li class="header-search__btn" (click)="searchToggleChange()">
            <div class="item">
                <i class="anticon anticon-search"></i>
            </div>
        </li>
    </ul>
    <header-search class="header-search" [toggleChange]="searchToggleStatus"></header-search>
    <ul class="top-nav">
        <!-- Notify -->
        <li>
            <header-notify></header-notify>
        </li>
        <!-- Task -->
        <li class="hidden-xs">
            <header-task></header-task>
        </li>
        <!-- App Icons -->
        <li class="hidden-xs">
            <header-icon></header-icon>
        </li>
        <!-- Settings -->
        <li class="hidden-xs">
            <nz-dropdown nzTrigger="click" nzPlacement="bottomRight">
                <div class="item" nz-dropdown>
                    <i class="anticon anticon-setting"></i>
                </div>
                <div nz-menu style="width:200px">
                    <div nz-menu-item class="theme-switch">
                        <header-theme></header-theme>
                    </div>
                    <div nz-menu-item>
                        <header-fullscreen></header-fullscreen>
                    </div>
                    <div nz-menu-item>
                        <header-storage></header-storage>
                    </div>
                    <div nz-menu-item>
                        <header-langs></header-langs>
                    </div>
                </div>
            </nz-dropdown>
        </li>
        <li class="hidden-xs">
            <header-user></header-user>
        </li>
    </ul>
</div>
src/app/layout/header/header.component.spec.ts
New file
@@ -0,0 +1,17 @@
import { TestBed, TestModuleMetadata } from '@angular/core/testing';
import { setUpTestBed } from 'testing/common.spec';
import { HeaderComponent } from './header.component';
describe('Layout: Header', () => {
    setUpTestBed(<TestModuleMetadata>{
        declarations: [ HeaderComponent ]
    });
    it('should create an instance', () => {
        const fixture = TestBed.createComponent(HeaderComponent);
        const comp = fixture.debugElement.componentInstance;
        expect(comp).toBeTruthy();
    });
});
src/app/layout/header/header.component.ts
New file
@@ -0,0 +1,21 @@
import { Component, ViewChild } from '@angular/core';
import { SettingsService } from '@delon/theme';
@Component({
    selector: 'app-header',
    templateUrl: './header.component.html'
})
export class HeaderComponent {
    searchToggleStatus: boolean;
    constructor(public settings: SettingsService) { }
    toggleCollapsedSideabar() {
        this.settings.setLayout('collapsed', !this.settings.layout.collapsed);
    }
    searchToggleChange() {
        this.searchToggleStatus = !this.searchToggleStatus;
    }
}
src/app/layout/header/index.md
New file
@@ -0,0 +1,20 @@
---
component: app-header
title: 顶部菜单
---
顶部菜单组件允许通过 `components` 目录下的组件进行按需组装。
## 组件列表
组件名 | 说明
----|------
`header-fullscreen` | 全屏切换
`header-icon` | 应用图标
`header-langs` | 语言切换
`header-notify` | 菜单通知
`header-search` | 搜索框
`header-storage` | 清除 LocalStorage 缓存
`header-task` | 任务通知
`header-theme` | 主题切换
`header-user` | 用户菜单
src/app/layout/layout.component.html
New file
@@ -0,0 +1,6 @@
<div class="wrapper">
    <div class="router-progress-bar" *ngIf="isFetching"></div>
    <app-header class="header"></app-header>
    <app-sidebar class="aside"></app-sidebar>
    <section class="content"><router-outlet></router-outlet></section>
</div>
src/app/layout/layout.component.spec.ts
New file
@@ -0,0 +1,17 @@
import { TestBed, TestModuleMetadata } from '@angular/core/testing';
import { setUpTestBed } from 'testing/common.spec';
import { LayoutComponent } from './layout.component';
describe('Layout', () => {
    setUpTestBed(<TestModuleMetadata>{
        declarations: [LayoutComponent]
    });
    it('should create an instance', () => {
        const fixture = TestBed.createComponent(LayoutComponent);
        const comp = fixture.debugElement.componentInstance;
        expect(comp).toBeTruthy();
    });
});
src/app/layout/layout.component.ts
New file
@@ -0,0 +1,38 @@
import { Component } from '@angular/core';
import { Router, NavigationEnd, RouteConfigLoadStart, NavigationError } from '@angular/router';
import { NzMessageService } from 'ng-zorro-antd';
import { ScrollService, MenuService, SettingsService } from '@delon/theme';
@Component({
    selector: 'app-layout',
    templateUrl: './layout.component.html'
})
export class LayoutComponent {
    isFetching = false;
    constructor(
        router: Router,
        scroll: ScrollService,
        private _message: NzMessageService,
        public menuSrv: MenuService,
        public settings: SettingsService) {
        // scroll to top in change page
        router.events.subscribe(evt => {
            if (!this.isFetching && evt instanceof RouteConfigLoadStart) {
                this.isFetching = true;
            }
            if (evt instanceof NavigationError) {
                this.isFetching = false;
                _message.error(`无法加载${evt.url}路由`, { nzDuration: 1000 * 3 });
                return;
            }
            if (!(evt instanceof NavigationEnd)) {
                return;
            }
            setTimeout(() => {
                scroll.scrollToTop();
                this.isFetching = false;
            }, 100);
        });
    }
}
src/app/layout/layout.module.ts
New file
@@ -0,0 +1,56 @@
import { NgModule } from '@angular/core';
import { SharedModule } from '@shared/shared.module';
import { LayoutComponent } from './layout.component';
import { LayoutFullScreenComponent } from './fullscreen/fullscreen.component';
import { HeaderComponent } from './header/header.component';
import { SidebarComponent } from './sidebar/sidebar.component';
import { HeaderSearchComponent } from './header/components/search.component';
import { HeaderThemeComponent } from './header/components/theme.component';
import { HeaderNotifyComponent } from './header/components/notify.component';
import { HeaderTaskComponent } from './header/components/task.component';
import { HeaderIconComponent } from './header/components/icon.component';
import { HeaderFullScreenComponent } from './header/components/fullscreen.component';
import { HeaderLangsComponent } from './header/components/langs.component';
import { HeaderStorageComponent } from './header/components/storage.component';
import { HeaderUserComponent } from './header/components/user.component';
const COMPONENTS = [
    LayoutComponent,
    LayoutFullScreenComponent,
    HeaderComponent,
    SidebarComponent
];
const HEADERCOMPONENTS = [
    HeaderSearchComponent,
    HeaderNotifyComponent,
    HeaderTaskComponent,
    HeaderIconComponent,
    HeaderFullScreenComponent,
    HeaderThemeComponent,
    HeaderLangsComponent,
    HeaderStorageComponent,
    HeaderUserComponent
];
// pro
import { ProUserLayoutComponent } from './pro/user/user.component';
const PRO = [
    ProUserLayoutComponent
];
@NgModule({
    imports: [SharedModule],
    providers: [],
    declarations: [
        ...COMPONENTS,
        ...HEADERCOMPONENTS,
        ...PRO
    ],
    exports: [
        ...COMPONENTS,
        ...PRO
    ]
})
export class LayoutModule { }
src/app/layout/pro/user/user.component.html
New file
@@ -0,0 +1,17 @@
<div class="container">
    <div class="top">
        <div class="head">
            <a [routerLink]="['/']">
                <img class="logo" src="./assets/img/logo-color.svg">
                <span class="title">ng-alain</span>
            </a>
        </div>
        <p class="desc">武林中最有影响力的《葵花宝典》;欲练神功,挥刀自宫</p>
    </div>
    <router-outlet></router-outlet>
    <global-footer [links]="links">
        <ng-template #copyright>
            Copyright <nz-icon nzType="copyright"></nz-icon> 2017 <a href="//github.com/cipchk" target="_blank">卡色</a>出品
        </ng-template>
    </global-footer>
</div>
src/app/layout/pro/user/user.component.less
New file
@@ -0,0 +1,58 @@
@import '~@delon/theme/styles/antd/themes/default.less';
:host {
    ::ng-deep {
        .container {
            background: #f0f2f5;
            background-image: url("https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg");
            width: 100%;
            min-height: 100%;
            background-repeat: no-repeat;
            background-position: center;
            background-size: 100%;
            padding: 110px 0 144px;
            position: relative;
        }
        .top {
            text-align: center;
        }
        .head {
            height: 44px;
            line-height: 44px;
            a {
                text-decoration: none;
            }
        }
        .logo {
            height: 44px;
            vertical-align: top;
            margin-right: 16px;
        }
        .title {
            font-size: 33px;
            color: @heading-color;
            font-family: "Myriad Pro", "Helvetica Neue", Arial, Helvetica, sans-serif;
            font-weight: 600;
            position: relative;
            top: 2px;
        }
        .desc {
            font-size: @font-size-base;
            color: @text-color-secondary;
            margin-top: 12px;
            margin-bottom: 40px;
        }
        .footer {
            position: absolute;
            width: 100%;
            bottom: 0;
        }
    }
}
src/app/layout/pro/user/user.component.ts
New file
@@ -0,0 +1,23 @@
import { Component } from '@angular/core';
@Component({
    selector: 'pro-user-layout',
    templateUrl: './user.component.html',
    styleUrls: ['./user.component.less']
})
export class ProUserLayoutComponent {
    links = [
        {
            title: '帮助',
            href: ''
        },
        {
            title: '隐私',
            href: ''
        },
        {
            title: '条款',
            href: ''
        }
    ];
}
src/app/layout/sidebar/sidebar.component.html
New file
@@ -0,0 +1,17 @@
<div class="aside-inner">
    <nz-dropdown nzTrigger="click" class="user-block clearfix">
        <div nz-dropdown class="user-block-dropdown">
            <nz-avatar class="avatar" [nzIcon]="'user'" [nzSize]="'large'"></nz-avatar>
            <div class="info">
                <strong>{{settings.user.name}}</strong>
                <p class="text-truncate">{{settings.user.email}}</p>
            </div>
        </div>
        <ul nz-menu>
            <li nz-menu-item (click)="msgSrv.success('profile')">{{ 'profile' | translate }}</li>
            <li nz-menu-item (click)="msgSrv.success('settings')">{{ 'settings' | translate }}</li>
            <li nz-menu-item (click)="msgSrv.success('logout')">{{ 'logout' | translate }}</li>
        </ul>
    </nz-dropdown>
    <sidebar-nav class="d-block py-lg"></sidebar-nav>
</div>
src/app/layout/sidebar/sidebar.component.spec.ts
New file
@@ -0,0 +1,16 @@
import { TestBed, TestModuleMetadata } from '@angular/core/testing';
import { setUpTestBed } from '../../../testing/common.spec';
import { SidebarComponent } from './sidebar.component';
describe('Layout: Sidebar', () => {
    setUpTestBed(<TestModuleMetadata>{
        declarations: [ SidebarComponent ]
    });
    it('should create an instance', () => {
        const fixture = TestBed.createComponent(SidebarComponent);
        const comp = fixture.debugElement.componentInstance;
        expect(comp).toBeTruthy();
    });
});
src/app/layout/sidebar/sidebar.component.ts
New file
@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
import { NzMessageService } from 'ng-zorro-antd';
import { SettingsService } from '@delon/theme';
@Component({
  selector   : 'app-sidebar',
  templateUrl: './sidebar.component.html'
})
export class SidebarComponent {
    constructor(public settings: SettingsService, public msgSrv: NzMessageService) {
    }
}
src/app/routes/routes.module.ts
New file
@@ -0,0 +1,10 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@NgModule({
  imports: [
    CommonModule
  ],
  declarations: []
})
export class RoutesModule { }
src/app/shared/shared.module.ts
New file
@@ -0,0 +1,211 @@
import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { RouterModule } from '@angular/router';
import { NgZorroAntdExtraModule } from 'ng-zorro-antd-extra';
import { AlainThemeModule } from '@delon/theme';
import { AlainABCModule } from '@delon/abc';
import { AlainACLModule } from '@delon/acl';
// third libs
import { CountdownModule } from 'ngx-countdown';
// i18n
import { TranslateModule } from '@ngx-translate/core';
import { I18NService } from '@core/i18n/i18n.service';
// region: zorro modules
import {
    // LoggerModule,
    // NzLocaleModule,
    NzButtonModule,
    NzAlertModule,
    NzBadgeModule,
    // NzCalendarModule,
    NzCascaderModule,
    NzCheckboxModule,
    NzDatePickerModule,
    NzFormModule,
    NzInputModule,
    NzInputNumberModule,
    NzGridModule,
    NzMessageModule,
    NzModalModule,
    NzNotificationModule,
    NzPaginationModule,
    NzPopconfirmModule,
    NzPopoverModule,
    NzRadioModule,
    NzRateModule,
    NzSelectModule,
    NzSpinModule,
    NzSliderModule,
    NzSwitchModule,
    NzProgressModule,
    NzTableModule,
    NzTabsModule,
    NzTagModule,
    NzTimePickerModule,
    NzUtilModule,
    NzStepsModule,
    NzDropDownModule,
    NzMenuModule,
    NzBreadCrumbModule,
    NzLayoutModule,
    NzRootModule,
    NzCarouselModule,
    // NzCardModule,
    NzCollapseModule,
    NzTimelineModule,
    NzToolTipModule,
    // NzBackTopModule,
    // NzAffixModule,
    // NzAnchorModule,
    NzAvatarModule,
    // SERVICES
    NzNotificationService,
    NzMessageService
} from 'ng-zorro-antd';
const ZORROMODULES = [
    // LoggerModule,
    // NzLocaleModule,
    NzButtonModule,
    NzAlertModule,
    NzBadgeModule,
    // NzCalendarModule,
    NzCascaderModule,
    NzCheckboxModule,
    NzDatePickerModule,
    NzFormModule,
    NzInputModule,
    NzInputNumberModule,
    NzGridModule,
    NzMessageModule,
    NzModalModule,
    NzNotificationModule,
    NzPaginationModule,
    NzPopconfirmModule,
    NzPopoverModule,
    NzRadioModule,
    NzRateModule,
    NzSelectModule,
    NzSpinModule,
    NzSliderModule,
    NzSwitchModule,
    NzProgressModule,
    NzTableModule,
    NzTabsModule,
    NzTagModule,
    NzTimePickerModule,
    NzUtilModule,
    NzStepsModule,
    NzDropDownModule,
    NzMenuModule,
    NzBreadCrumbModule,
    NzLayoutModule,
    NzRootModule,
    NzCarouselModule,
    // NzCardModule,
    NzCollapseModule,
    NzTimelineModule,
    NzToolTipModule,
    // NzBackTopModule,
    // NzAffixModule,
    // NzAnchorModule,
    NzAvatarModule
];
// endregion
// region: @delon/abc modules
import {
    AdAvatarListModule,
    AdChartsModule,
    AdCountDownModule,
    AdDescListModule,
    AdEllipsisModule,
    AdErrorCollectModule,
    AdExceptionModule,
    AdFooterToolbarModule,
    AdGlobalFooterModule,
    AdNoticeIconModule,
    AdNumberInfoModule,
    AdProHeaderModule,
    AdResultModule,
    AdSidebarNavModule,
    AdStandardFormRowModule,
    AdTagSelectModule,
    AdTrendModule,
    AdDownFileModule,
    AdImageModule,
    AdUtilsModule
} from '@delon/abc';
const ABCMODULES = [
    AdAvatarListModule,
    AdChartsModule,
    AdCountDownModule,
    AdDescListModule,
    AdEllipsisModule,
    AdErrorCollectModule,
    AdExceptionModule,
    AdFooterToolbarModule,
    AdGlobalFooterModule,
    AdNoticeIconModule,
    AdNumberInfoModule,
    AdProHeaderModule,
    AdResultModule,
    AdSidebarNavModule,
    AdStandardFormRowModule,
    AdTagSelectModule,
    AdTrendModule,
    AdDownFileModule,
    AdImageModule,
    AdUtilsModule
];
// endregion
@NgModule({
    imports: [
        CommonModule,
        FormsModule,
        RouterModule,
        ReactiveFormsModule,
        HttpClientModule,
        ...ZORROMODULES,
        NgZorroAntdExtraModule.forRoot(),
        AlainThemeModule.forChild(),
        ...ABCMODULES,
        AlainACLModule.forRoot(),
        // third libs
        CountdownModule
    ],
    exports: [
        CommonModule,
        FormsModule,
        ReactiveFormsModule,
        RouterModule,
        ...ZORROMODULES,
        NgZorroAntdExtraModule,
        AlainThemeModule,
        ...ABCMODULES,
        AlainACLModule,
        // i18n
        TranslateModule,
        // third libs
        CountdownModule
    ]
})
export class SharedModule {
    static forRoot(): ModuleWithProviders {
        return {
            ngModule: SharedModule,
            providers: [
                // ng-zorro-antd Services
                NzNotificationService,
                NzMessageService
            ]
        };
    }
}
src/assets/i18n/en.json
New file
@@ -0,0 +1,94 @@
{
    "home": "Home",
    "settings": "Settings",
    "profile": "Profile",
    "login": "Login",
    "logout": "Logout",
    "more": "More",
    "full": "Full",
    "top-search-ph": "Search for people, file, photos...",
    "theme": "Theme",
    "theme-switch": "Theme Switch",
    "light": "Light",
    "dark": "Dark",
    "fullscreen": "Fullscreen",
    "fullscreen-exit": "Exit Fullscreen",
    "clear-local-storage": "Clear Local Storage",
    "language": "Language",
    "shortcut": "Shortcut",
    "dashboard": "Dashboard",
    "dashboard_v1": "Dashboard",
    "dashboard_analysis": "Analysis",
    "dashboard_monitor": "Monitor",
    "dashboard_workplace": "Workplace",
    "widgets": "Widgets",
    "main_navigation": "Main Navigation",
    "component": "Component",
    "elements": "Elements",
    "buttons": "Buttons",
    "notification": "Notification",
    "modal": "Modal",
    "sweetalert": "SweetAlert",
    "spin": "Loading Spin",
    "dropdown": "Dropdown",
    "tree-antd": "Tree",
    "sortable": "Sortable",
    "grid": "Grid",
    "gridmasonry": "Grid Masonry",
    "typography": "Typography",
    "iconsfont": "Iconsfont",
    "colors": "Colors",
    "forms": "Forms",
    "extended": "Extended",
    "standard": "Standard",
    "validation": "Validation",
    "upload": "Upload",
    "cropper": "Image Crop",
    "charts": "Charts",
    "tables": "Tables",
    "maps": "Maps",
    "qq": "QQ",
    "baidu": "Baidu",
    "logics": "Logics",
    "guard": "Route Guard",
    "acl": "ACL",
    "downfile": "Down File",
    "report": "Report",
    "relation": "Relation",
    "pages": "Pages",
    "m-login": "Login",
    "m-register": "Register",
    "m-forget": "Forget",
    "m-lock": "Lock",
    "m-maintenance": "Maintenance",
    "extras": "Extras",
    "blog": "Blog",
    "list": "List",
    "comment": "Comment",
    "post": "Post",
    "website": "Web Site (external)",
    "helpcenter": "Help Center",
    "poi": "Poi",
    "pro": "Ant Design Pro",
    "form": "Form Page",
    "step-form": "Step Form",
    "advanced-form": "Advanced Form",
    "pro-list": "List Page",
    "pro-table-list": "Table List",
    "pro-basic-list": "Basic List",
    "pro-card-list": "Card List",
    "pro-cover-card-list": "Cover Card List",
    "pro-filter-card-list": "Filter Card List",
    "pro-search": "Search(Article)",
    "pro-profile": "Profile Page",
    "pro-profile-basic": "Basic",
    "pro-profile-advanced": "Advanced",
    "pro-result": "Result Page",
    "pro-result-success": "Success",
    "pro-result-fail": "Fail",
    "pro-exception": "Exception",
    "pro-user": "Account",
    "pro-login": "Login",
    "pro-register": "Register",
    "pro-register-result": "Register Result"
}
src/assets/i18n/zh-CN.json
New file
@@ -0,0 +1,94 @@
{
    "home": "主页",
    "settings": "设置",
    "profile": "个人资料",
    "login": "登录",
    "logout": "登出",
    "more": "更多",
    "full": "完整",
    "top-search-ph": "搜索:员工、文件、照片等",
    "theme": "主题",
    "theme-switch": "切换主题",
    "light": "亮",
    "dark": "暗",
    "fullscreen": "全屏",
    "fullscreen-exit": "退出全屏",
    "clear-local-storage": "清理本地缓存",
    "language": "语言",
    "shortcut": "快捷菜单",
    "dashboard": "仪表盘",
    "dashboard_v1": "仪表盘",
    "dashboard_analysis": "分析页",
    "dashboard_monitor": "监控页",
    "dashboard_workplace": "工作台",
    "widgets": "小部件",
    "main_navigation": "主导航",
    "component": "组件",
    "elements": "基础元素",
    "buttons": "按钮",
    "notification": "通知",
    "modal": "模态框",
    "sweetalert": "SweetAlert",
    "spin": "加载中",
    "dropdown": "下拉菜单",
    "tree-antd": "树形控件",
    "sortable": "拖放",
    "grid": "栅格系统",
    "gridmasonry": "瀑布流",
    "typography": "字体排印",
    "iconsfont": "icon图标",
    "colors": "色彩",
    "forms": "表单",
    "standard": "标准",
    "extended": "扩展",
    "validation": "校验",
    "upload": "上传",
    "cropper": "图片裁剪",
    "charts": "图表",
    "tables": "表格",
    "maps": "地图",
    "qq": "QQ",
    "baidu": "百度",
    "logics": "常用逻辑",
    "guard": "路由守卫",
    "acl": "基于角色访问控制",
    "downfile": "下载文件",
    "report": "报表",
    "relation": "全屏关系图",
    "pages": "页面",
    "m-login": "登录页",
    "m-register": "注册页",
    "m-forget": "忘记密码页",
    "m-lock": "锁屏页",
    "m-maintenance": "维护中",
    "extras": "扩展",
    "blog": "博客",
    "list": "列表",
    "comment": "评论",
    "post": "新建",
    "website": "前台(外链)",
    "helpcenter": "帮助中心",
    "poi": "门店",
    "pro": "Ant Design Pro",
    "form": "表单页",
    "step-form": "分步表单",
    "advanced-form": "高级表单",
    "pro-list": "列表页",
    "pro-table-list": "查询表格",
    "pro-basic-list": "标准列表",
    "pro-card-list": "卡片列表",
    "pro-cover-card-list": "搜索列表(项目)",
    "pro-filter-card-list": "搜索列表(应用)",
    "pro-search": "搜索列表(文章)",
    "pro-profile": "详情页",
    "pro-profile-basic": "基础详情页",
    "pro-profile-advanced": "高级详情页",
    "pro-result": "结果",
    "pro-result-success": "成功",
    "pro-result-fail": "失败",
    "pro-exception": "异常",
    "pro-user": "账户",
    "pro-login": "登录",
    "pro-register": "注册",
    "pro-register-result": "注册结果"
}
src/environments/environment.prod.ts
@@ -1,3 +1,4 @@
export const environment = {
  SERVER_URL: `./`,
  production: true
};
src/environments/environment.ts
@@ -4,5 +4,6 @@
// The list of which env maps to which file can be found in `.angular-cli.json`.
export const environment = {
  SERVER_URL: `./`,
  production: false
};
src/testing/common.spec.ts
New file
@@ -0,0 +1,66 @@
// from: https://github.com/angular/angular/issues/12409
import { TestBed, async, TestModuleMetadata } from '@angular/core/testing';
import { Type, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CoreModule } from '@core/core.module';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { HttpLoaderFactory } from 'app/app.module';
import { HttpClient } from '@angular/common/http';
import { ALAIN_I18N_TOKEN, ColorsService, SettingsService, MenuService, ScrollService, _HttpClient, ALAIN_THEME_OPTIONS } from '@delon/theme';
import { I18NService } from '@core/i18n/i18n.service';
import { RouterTestingModule } from '@angular/router/testing';
import { SharedModule } from '@shared/shared.module';
const resetTestingModule = TestBed.resetTestingModule,
      preventAngularFromResetting = () => TestBed.resetTestingModule = () => TestBed;
const allowAngularToReset = () => TestBed.resetTestingModule = resetTestingModule;
export const setUpTestBed = (moduleDef: TestModuleMetadata) => {
    beforeAll(done => (async () => {
        resetTestingModule();
        preventAngularFromResetting();
        // region: schemas
        if (!moduleDef.schemas) moduleDef.schemas = [];
        moduleDef.schemas.push(CUSTOM_ELEMENTS_SCHEMA);
        // endregion
        // region: imports
        if (!moduleDef.imports) moduleDef.imports = [];
        moduleDef.imports.push(RouterTestingModule);
        moduleDef.imports.push(SharedModule.forRoot());
        moduleDef.imports.push(TranslateModule.forRoot({
            loader: {
                provide: TranslateLoader,
                useFactory: (HttpLoaderFactory),
                deps: [HttpClient]
            }
        }));
        // endregion
        // region: providers
        if (!moduleDef.providers) moduleDef.providers = [];
        moduleDef.providers.push({ provide: ALAIN_THEME_OPTIONS, useValue: {} });
        moduleDef.providers.push({ provide: ALAIN_I18N_TOKEN, useClass: I18NService, multi: false });
        // load full services
        [ SettingsService, MenuService, ScrollService, ColorsService, _HttpClient ].forEach((item: any) => {
            if (moduleDef.providers.includes(item)) return;
            moduleDef.providers.push(item);
        });
        // endregion
        TestBed.configureTestingModule(moduleDef);
        await TestBed.compileComponents();
        // prevent Angular from resetting testing module
        TestBed.resetTestingModule = () => TestBed;
    })().then(done).catch(done.fail));
    afterAll(() => allowAngularToReset());
};
/**
 * get service instance
 */
export const getService = <T>(type: Type<T>): T => <T>TestBed.get(type);
src/tsconfig.app.json
@@ -4,7 +4,13 @@
    "outDir": "../out-tsc/app",
    "baseUrl": "./",
    "module": "es2015",
    "types": []
    "types": [],
    "paths": {
      "@shared": [ "app/shared" ],
      "@shared/*": [ "app/shared/*" ],
      "@core": [ "app/core/" ],
      "@core/*": [ "app/core/*" ]
    }
  },
  "exclude": [
    "test.ts",
src/tsconfig.spec.json
@@ -8,7 +8,13 @@
    "types": [
      "jasmine",
      "node"
    ]
    ],
    "paths": {
      "@shared": [ "app/shared" ],
      "@shared/*": [ "app/shared/*" ],
      "@core": [ "app/core/" ],
      "@core/*": [ "app/core/*" ]
    }
  },
  "files": [
    "test.ts"
tsconfig.json
@@ -14,6 +14,13 @@
    "lib": [
      "es2017",
      "dom"
    ]
    ],
    "baseUrl": "src/",
    "paths": {
      "@shared": [ "app/shared" ],
      "@shared/*": [ "app/shared/*" ],
      "@core": [ "app/core/" ],
      "@core/*": [ "app/core/*" ]
    }
  }
}