项目初始化

This commit is contained in:
Taric Xin
2021-11-26 16:34:35 +08:00
parent 66644bcf0a
commit 5287578452
354 changed files with 45736 additions and 0 deletions

5
src/app/core/README.md Normal file
View File

@ -0,0 +1,5 @@
### CoreModule
**应** 仅只留 `providers` 属性。
**作用:** 一些通用服务例如用户消息、HTTP数据访问。

View File

@ -0,0 +1,12 @@
import { NgModule, Optional, SkipSelf } from '@angular/core';
import { throwIfAlreadyLoaded } from './module-import-guard';
@NgModule({
providers: []
})
export class CoreModule {
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
throwIfAlreadyLoaded(parentModule, 'CoreModule');
}
}

View File

@ -0,0 +1,90 @@
/*
* @Author: Maple
* @Date: 2021-03-22 11:42:26
* @LastEditors: Do not edit
* @LastEditTime: 2021-05-27 14:06:18
* @Description: 全局核心服务
*/
import { Injectable, Injector } from '@angular/core';
import { Router } from '@angular/router';
import { ReuseTabService } from '@delon/abc/reuse-tab';
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
import { CacheService } from '@delon/cache';
import { SettingsService } from '@delon/theme';
import { EnvironmentService } from '@env/environment.service';
import { NzMessageService } from 'ng-zorro-antd/message';
@Injectable({
providedIn: 'root',
})
export class CoreService {
// 获取当前登录用户信息
public $api_get_current_user_info = `/scm/cuc/cuc/user/getUserDetail`;
// 获取当前用户所拥有的菜单
public $api_get_current_user_menus = `/scm/cuc/cuc/functionInfo/getUserHaveFunctionsList`;
constructor(private injector: Injector) {}
// 注入路由
public get router(): Router {
return this.injector.get(Router);
}
// 注入全局设置服务
public get settingSrv(): SettingsService {
return this.injector.get(SettingsService);
}
// 注入缓存服务
public get cacheSrv(): CacheService {
return this.injector.get(CacheService);
}
// 注入令牌服务
public get tokenSrv(): ITokenService {
return this.injector.get(DA_SERVICE_TOKEN);
}
// 注入消息服务
public get msgSrv(): NzMessageService {
return this.injector.get(NzMessageService);
}
// 注入环境服务
public get envSrv(): EnvironmentService {
return this.injector.get(EnvironmentService);
}
// 注入路由复用服务
private get reuseTabService(): ReuseTabService {
return this.injector.get(ReuseTabService);
}
// 登录状态
public get loginStatus(): boolean {
try {
return !!this.tokenSrv.get()?.token;
} catch (error) {
return false;
}
}
// 权限认证凭据TOKEN
public get token(): string {
return this.tokenSrv.get()?.token || '';
}
/**
* 登出系统
* @param showMsg 是否显示登录过期弹窗
*/
logout(showMsg: boolean = false): void {
if (showMsg) {
this.msgSrv.warning('未登录或登录信息已过期,请重新登录!');
}
this.settingSrv.setUser({});
this.tokenSrv.clear();
this.cacheSrv.clear();
this.router.navigate([this.tokenSrv.login_url]);
}
}

6
src/app/core/index.ts Normal file
View File

@ -0,0 +1,6 @@
export * from './module-import-guard';
export * from './net/default.interceptor';
// Services
export * from './core.service';
export * from './startup/startup.service';

View 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.`);
}
}

View 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 { _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}`);
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))
);
}
}

View File

@ -0,0 +1,158 @@
import { HttpClient } from '@angular/common/http';
import { Inject, Injectable } from '@angular/core';
import { ACLService } from '@delon/acl';
import { MenuService, SettingsService, TitleService, _HttpClient } from '@delon/theme';
import { NzSafeAny } from 'ng-zorro-antd/core/types';
import { NzIconService } from 'ng-zorro-antd/icon';
import { Observable, zip } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { ICONS } from '../../../style-icons';
import { ICONS_AUTO } from '../../../style-icons-auto';
import { CoreService } from '../core.service';
/**
* 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,
private settingService: SettingsService,
private aclService: ACLService,
private titleService: TitleService,
private httpClient: _HttpClient,
private coreSrv: CoreService
) {
iconSrv.addIcon(...ICONS_AUTO, ...ICONS);
}
// TODO: 退出登录时需要清理用户信息
load(): Promise<void> {
return new Promise(resolve => {
let data;
if (this.coreSrv.loginStatus) {
// 本地菜单
// data = this.loadMockData();
// 远程菜单
data = this.loadRemoteData();
} else {
data = this.loadMockData();
}
data
.pipe(
catchError(res => {
console.warn(`StartupService.load: Network request failed`, res);
resolve();
return [];
})
)
.subscribe(
([appData, userData, menuData]) => this.initSystem(appData, userData, menuData),
err => {
console.log(err);
},
() => resolve()
);
});
}
/**
* 系统初始化
*
* @param langData 翻译数据
* @param appData App应用数据
* @param userData 用户数据
* @param menuData 菜单数据
*/
private initSystem(appData: NzSafeAny, userData: NzSafeAny, menuData: NzSafeAny): void {
// 应用信息:包括站点名、描述、年份
this.settingService.setApp(appData);
// 用户信息:包括姓名、头像、邮箱地址
this.settingService.setUser(userData);
// ACL设置权限为全量
this.aclService.setFull(true);
// 初始化菜单
this.menuService.add(menuData);
// 设置页面标题的后缀
this.titleService.default = '';
this.titleService.suffix = appData.name;
}
/**
* @description 加载本地模拟数据
* @returns 程序初始化数据
*/
loadMockData(): Observable<[object, object, object]> {
// 登录时调用远程数据, 非登录状态下调用Mock数据
// App数据
const appData = this.httpClient.get(`assets/mocks/app-data.json`).pipe(map((res: any) => res.app));
// 用户数据
const userData = this.coreSrv.loginStatus
? this.httpClient.post(this.coreSrv.$api_get_current_user_info, {}).pipe(map((res: any) => res.data))
: this.httpClient.get('assets/mocks/user-data.json').pipe(map((res: any) => res.user));
// 菜单数据
const menuData = this.httpClient.get('assets/mocks/menu-data.json').pipe(map((res: any) => res.menu));
return zip(appData, userData, menuData);
}
/**
* @description 加载远程数据
* @returns 程序初始化数据
*/
loadRemoteData(): Observable<[object, object, object]> {
// 登录时调用远程数据, 非登录状态下调用Mock数据
// App数据
const appData = this.httpClient.get(`assets/mocks/app-data.json`).pipe(map((res: any) => res.app));
// 用户数据
const userData = this.coreSrv.loginStatus
? this.httpClient.post(this.coreSrv.$api_get_current_user_info, {}).pipe(map((res: any) => res.data))
: this.httpClient.get('assets/mocks/user-data.json').pipe(map((res: any) => res.user));
// 菜单数据
const menuData = this.httpClient
.post(this.coreSrv.$api_get_current_user_menus, {
appId: this.coreSrv.envSrv.getEnvironment().appId
})
.pipe(map((res: any) => res.data));
return zip(appData, userData, menuData);
}
// load(): Observable<void> {
// const defaultLang = this.i18n.defaultLang;
// return zip(this.i18n.loadLangData(defaultLang), this.httpClient.get('assets/tmp/app-data.json')).pipe(
// // 接收其他拦截器后产生的异常消息
// catchError(res => {
// console.warn(`StartupService.load: Network request failed`, res);
// return [];
// }),
// map(([langData, appData]: [Record<string, string>, NzSafeAny]) => {
// // setting language data
// this.i18n.use(defaultLang, langData);
// // 应用信息:包括站点名、描述、年份
// this.settingService.setApp(appData.app);
// // 用户信息:包括姓名、头像、邮箱地址
// this.settingService.setUser(appData.user);
// // ACL设置权限为全量
// this.aclService.setFull(true);
// // 初始化菜单
// this.menuService.add(appData.menu);
// // 设置页面标题的后缀
// this.titleService.default = '';
// this.titleService.suffix = appData.app.name;
// })
// );
// }
}