项目初始化
This commit is contained in:
5
src/app/core/README.md
Normal file
5
src/app/core/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
### CoreModule
|
||||
|
||||
**应** 仅只留 `providers` 属性。
|
||||
|
||||
**作用:** 一些通用服务,例如:用户消息、HTTP数据访问。
|
||||
12
src/app/core/core.module.ts
Normal file
12
src/app/core/core.module.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
90
src/app/core/core.service.ts
Normal file
90
src/app/core/core.service.ts
Normal 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
6
src/app/core/index.ts
Normal 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';
|
||||
6
src/app/core/module-import-guard.ts
Normal file
6
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
src/app/core/net/default.interceptor.ts
Normal file
261
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 { _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))
|
||||
);
|
||||
}
|
||||
}
|
||||
158
src/app/core/startup/startup.service.ts
Normal file
158
src/app/core/startup/startup.service.ts
Normal 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;
|
||||
// })
|
||||
// );
|
||||
// }
|
||||
}
|
||||
Reference in New Issue
Block a user