From f1c1b27a07bc4103b3e4763e994ba317bbeaa9bc Mon Sep 17 00:00:00 2001 From: AlexBob Date: Fri, 20 Sep 2024 11:58:30 +0800 Subject: [PATCH] =?UTF-8?q?```markdown=20=E2=9C=A8=20feat(`environments`,?= =?UTF-8?q?=20`angular.json`,=20`package.json`,=20`services`,=20`styles.sc?= =?UTF-8?q?ss`,=20`app.config.ts`,=20`proxy.conf.json`,=20`.prettierrc.jso?= =?UTF-8?q?n`,=20`.vscode/settings.json`):=20Implement=20application=20env?= =?UTF-8?q?ironment=20setup,=20add=20services,=20configure=20routing,=20an?= =?UTF-8?q?d=20improve=20project=20configurations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds development and production environment configurations - Updates Angular JSON to include Bootstrap CSS and JS, removes unnecessary polyfills, sets up proxy configuration, and adjusts build targets - Installs and configures Bootstrap and DayJS dependencies - Introduces BrowserStorage and SessionStorage services for local and session storage management - Implements HTTP Interceptors for default request handling and authentication token management - Adds a page title strategy service for dynamic page titles - Adjusts global SCSS styles and includes them in the project - Enhances app routing with custom title strategy and HTTP client configurations - Initializes a proxy configuration for backend API and RSocket communication - Sets up Prettier and VSCode settings for code formatting and linting ``` --- ng-web/.prettierignore | 42 ++++++++ ng-web/.prettierrc.json | 12 +++ ng-web/.vscode/settings.json | 24 ++++- ng-web/angular.json | 17 ++-- ng-web/package-lock.json | 44 ++++++++- ng-web/package.json | 5 +- ng-web/proxy.conf.json | 18 ++++ ng-web/src/app/app.config.ts | 42 +++++++- .../environments/environment.development.ts | 4 + ng-web/src/environments/environment.ts | 4 + ng-web/src/service/auth.service.ts | 97 +++++++++++++++++++ ng-web/src/service/browser-storage.service.ts | 35 +++++++ ng-web/src/service/http.Interceptor.ts | 61 ++++++++++++ ng-web/src/service/loading.service.ts | 21 ++++ .../service/page-title-strategy.service.ts | 17 ++++ ng-web/src/service/session-storage.service.ts | 30 ++++++ ng-web/src/styles.scss | 17 ++++ 17 files changed, 471 insertions(+), 19 deletions(-) create mode 100644 ng-web/.prettierignore create mode 100644 ng-web/.prettierrc.json create mode 100644 ng-web/proxy.conf.json create mode 100644 ng-web/src/environments/environment.development.ts create mode 100644 ng-web/src/environments/environment.ts create mode 100644 ng-web/src/service/auth.service.ts create mode 100644 ng-web/src/service/browser-storage.service.ts create mode 100644 ng-web/src/service/http.Interceptor.ts create mode 100644 ng-web/src/service/loading.service.ts create mode 100644 ng-web/src/service/page-title-strategy.service.ts create mode 100644 ng-web/src/service/session-storage.service.ts diff --git a/ng-web/.prettierignore b/ng-web/.prettierignore new file mode 100644 index 00000000..1b062701 --- /dev/null +++ b/ng-web/.prettierignore @@ -0,0 +1,42 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/ng-web/.prettierrc.json b/ng-web/.prettierrc.json new file mode 100644 index 00000000..3d3924f8 --- /dev/null +++ b/ng-web/.prettierrc.json @@ -0,0 +1,12 @@ +{ + "tabWidth": 2, + "useTabs": false, + "singleQuote": true, + "semi": true, + "bracketSpacing": true, + "arrowParens": "avoid", + "trailingComma": "es5", + "bracketSameLine": true, + "printWidth": 120, + "endOfLine": "auto" +} diff --git a/ng-web/.vscode/settings.json b/ng-web/.vscode/settings.json index b8479d22..2fbafb02 100644 --- a/ng-web/.vscode/settings.json +++ b/ng-web/.vscode/settings.json @@ -1,3 +1,25 @@ { + "[css]": { + "editor.foldingStrategy": "indentation" + }, + "files.autoSave": "afterDelay", + "angular.forceStrictTemplates": true, + "editor.suggest.snippetsPreventQuickSuggestions": false, + "editor.inlineSuggest.enabled": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "angular.view-engine": false, + "Codegeex.CompletionModel": "CodeGeeX Pro[Beta]", + "Codegeex.Privacy": true, + "merge-conflict.autoNavigateNextConflict.enabled": true, + "css.styleSheets": [], + "[html]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "prettier.bracketSameLine": true, + "Codegeex.CommitMessageStyle": "ConventionalCommits", "Codegeex.RepoIndex": true -} +} \ No newline at end of file diff --git a/ng-web/angular.json b/ng-web/angular.json index 71c329ee..da8da540 100644 --- a/ng-web/angular.json +++ b/ng-web/angular.json @@ -20,9 +20,7 @@ "outputPath": "dist/ng-web", "index": "src/index.html", "browser": "src/main.ts", - "polyfills": [ - "zone.js" - ], + "polyfills": [], "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ @@ -32,9 +30,12 @@ } ], "styles": [ + "bootstrap/dist/css/bootstrap.css", "src/styles.scss" ], - "scripts": [] + "scripts": [ + "bootstrap/dist/js/bootstrap.bundle.js" + ] }, "configurations": { "production": { @@ -67,7 +68,8 @@ "buildTarget": "ng-web:build:production" }, "development": { - "buildTarget": "ng-web:build:development" + "buildTarget": "ng-web:build:development", + "proxyConfig": "proxy.conf.json" } }, "defaultConfiguration": "development" @@ -78,10 +80,7 @@ "test": { "builder": "@angular-devkit/build-angular:karma", "options": { - "polyfills": [ - "zone.js", - "zone.js/testing" - ], + "polyfills": [], "tsConfig": "tsconfig.spec.json", "inlineStyleLanguage": "scss", "assets": [ diff --git a/ng-web/package-lock.json b/ng-web/package-lock.json index e48e2a59..bcc51a5b 100644 --- a/ng-web/package-lock.json +++ b/ng-web/package-lock.json @@ -16,9 +16,10 @@ "@angular/platform-browser": "^18.2.0", "@angular/platform-browser-dynamic": "^18.2.0", "@angular/router": "^18.2.0", + "bootstrap": "^5.3.3", + "dayjs": "^1.11.13", "rxjs": "~7.8.0", - "tslib": "^2.3.0", - "zone.js": "~0.14.10" + "tslib": "^2.3.0" }, "devDependencies": { "@angular-devkit/build-angular": "^18.2.5", @@ -3852,6 +3853,17 @@ "node": ">=14" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.20.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz", @@ -5181,6 +5193,25 @@ "dev": true, "license": "ISC" }, + "node_modules/bootstrap": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", + "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -6217,6 +6248,12 @@ "node": ">=4.0" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -14438,7 +14475,8 @@ "version": "0.14.10", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.10.tgz", "integrity": "sha512-YGAhaO7J5ywOXW6InXNlLmfU194F8lVgu7bRntUF3TiG8Y3nBK0x1UJJuHUP/e8IyihkjCYqhCScpSwnlaSRkQ==", - "license": "MIT" + "license": "MIT", + "peer": true } } } diff --git a/ng-web/package.json b/ng-web/package.json index d19a7179..afa4c199 100644 --- a/ng-web/package.json +++ b/ng-web/package.json @@ -18,9 +18,10 @@ "@angular/platform-browser": "^18.2.0", "@angular/platform-browser-dynamic": "^18.2.0", "@angular/router": "^18.2.0", + "bootstrap": "^5.3.3", + "dayjs": "^1.11.13", "rxjs": "~7.8.0", - "tslib": "^2.3.0", - "zone.js": "~0.14.10" + "tslib": "^2.3.0" }, "devDependencies": { "@angular-devkit/build-angular": "^18.2.5", diff --git a/ng-web/proxy.conf.json b/ng-web/proxy.conf.json new file mode 100644 index 00000000..3a8082a0 --- /dev/null +++ b/ng-web/proxy.conf.json @@ -0,0 +1,18 @@ +{ + "/api": { + "target": "http://localhost:9001", + "secure": false, + "pathRewrite": { + "^/api": "" + }, + "changeOrigin": true, + "logLevel": "debug" + }, + "/rsocket": { + "target": "ws://localhost:9001/", + "secure": false, + "ws": true, + "changeOrigin": true, + "logLevel": "debug" + } +} diff --git a/ng-web/src/app/app.config.ts b/ng-web/src/app/app.config.ts index 7afc797f..d80b37b1 100644 --- a/ng-web/src/app/app.config.ts +++ b/ng-web/src/app/app.config.ts @@ -1,8 +1,42 @@ -import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; -import { provideRouter } from '@angular/router'; - +import { ApplicationConfig, importProvidersFrom, provideExperimentalZonelessChangeDetection } from '@angular/core'; +import { provideRouter, TitleStrategy, withComponentInputBinding } from '@angular/router'; +import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { routes } from './app.routes'; +import { + provideHttpClient, + withFetch, + withInterceptors, + withInterceptorsFromDi, + withXsrfConfiguration, +} from '@angular/common/http'; + +import dayjs from 'dayjs'; +import isLeapYear from 'dayjs/plugin/isLeapYear'; +import 'dayjs/locale/zh-cn'; +import { authTokenInterceptor, defaultInterceptor } from '../service/http.Interceptor'; +import { PageTitleStrategy } from '../service/page-title-strategy.service'; + +dayjs.extend(isLeapYear); +dayjs.locale('zh-cn'); + export const appConfig: ApplicationConfig = { - providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)], + providers: [ + provideExperimentalZonelessChangeDetection(), + provideRouter(routes, withComponentInputBinding()), + importProvidersFrom(BrowserAnimationsModule), + provideAnimationsAsync(), + provideRouter(routes), + provideHttpClient( + withFetch(), + withInterceptorsFromDi(), + withInterceptors([defaultInterceptor, authTokenInterceptor]), + withXsrfConfiguration({ + cookieName: 'XSRF-TOKEN', + headerName: 'X-XSRF-TOKEN', + }), + ), + { provide: TitleStrategy, useClass: PageTitleStrategy }, + ], }; diff --git a/ng-web/src/environments/environment.development.ts b/ng-web/src/environments/environment.development.ts new file mode 100644 index 00000000..a86b8e4b --- /dev/null +++ b/ng-web/src/environments/environment.development.ts @@ -0,0 +1,4 @@ +export const environment = { + production: false, + host: '/api', +}; diff --git a/ng-web/src/environments/environment.ts b/ng-web/src/environments/environment.ts new file mode 100644 index 00000000..344b0e35 --- /dev/null +++ b/ng-web/src/environments/environment.ts @@ -0,0 +1,4 @@ +export const environment = { + production: true, + host: '', +}; diff --git a/ng-web/src/service/auth.service.ts b/ng-web/src/service/auth.service.ts new file mode 100644 index 00000000..304435cb --- /dev/null +++ b/ng-web/src/service/auth.service.ts @@ -0,0 +1,97 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { inject, Injectable, signal } from '@angular/core'; +import { CanActivateChildFn, CanActivateFn, CanMatchFn, Router } from '@angular/router'; +import dayjs from 'dayjs'; +import { SessionStorageService } from './session-storage.service'; + +// 定义一个接口,用于存储用户的认证信息 +export interface Authentication { + token: string; + expires: number; + lastAccessTime: number; + details: any; +} + +// 定义一个函数,用于判断用户是否已登录 +export const authGuard: CanMatchFn | CanActivateFn | CanActivateChildFn = () => { + const auth = inject(AuthService); + const router = inject(Router); + if (auth.isLogged()) { + return true; + } + return router.parseUrl(auth.loginUrl); +}; + +@Injectable({ providedIn: 'root' }) +export class AuthService { + readonly loginUrl = '/auth/login'; + private _storage = inject(SessionStorageService); + private readonly authenticationKey = 'authentication'; + private isLoggedIn = signal(false); + private authentication = signal({} as Authentication); + + authenticationToken() { + if (this.isLoggedIn()) { + return this.authentication(); + } + const authentication = this.authenticationLoadStorage(); + if (authentication) { + authentication.lastAccessTime = dayjs().unix(); + this.login(authentication); + return authentication; + } + throw new HttpErrorResponse({ + error: 'Authenticate is incorrectness,please login again.', + status: 401, + }); + } + + isLogged(): boolean { + if (this.isLoggedIn()) { + return true; + } + return false; + } + + authToken(): string { + if (this.isLoggedIn()) { + return this.authentication().token; + } + const authentication = this.authenticationLoadStorage(); + if (authentication) { + authentication.lastAccessTime = dayjs().unix(); + this.login(authentication); + return authentication.token; + } + throw new HttpErrorResponse({ + error: 'Authenticate is incorrectness,please login again.', + status: 401, + }); + } + + login(authentication: Authentication): void { + this.isLoggedIn.set(true); + this.authentication.set(authentication); + this._storage.set(this.authenticationKey, JSON.stringify(authentication)); + } + + logout(): void { + this.isLoggedIn.set(false); + this.authentication.set({} as Authentication); + this._storage.remove(this.authenticationKey); + } + + private authenticationLoadStorage(): Authentication | null { + const authenticationJsonStr = this._storage.get(this.authenticationKey); + if (authenticationJsonStr) { + const authentication: Authentication = JSON.parse(authenticationJsonStr); + const lastAccessTime = dayjs.unix(authentication.lastAccessTime); + const diffSec = dayjs().diff(lastAccessTime, 'second'); + if (diffSec < authentication.expires) { + return authentication; + } + this._storage.remove(this.authenticationKey); + } + return null; + } +} diff --git a/ng-web/src/service/browser-storage.service.ts b/ng-web/src/service/browser-storage.service.ts new file mode 100644 index 00000000..2ea14ae1 --- /dev/null +++ b/ng-web/src/service/browser-storage.service.ts @@ -0,0 +1,35 @@ +import { Inject, Injectable, InjectionToken } from '@angular/core'; + +export const BROWSER_STORAGE = new InjectionToken('Browser Storage', { + providedIn: 'root', + factory: () => localStorage, +}); + +@Injectable({ + providedIn: 'root', +}) +export class BrowserStorageService { + constructor(@Inject(BROWSER_STORAGE) public storage: Storage) { + } + + get(key: string) { + const itemStr = this.storage.getItem(key); + if (itemStr) { + return atob(itemStr); + } + return null; + } + + set(key: string, value: string) { + const btoaStr = btoa(value); + this.storage.setItem(key, btoaStr); + } + + remove(key: string) { + this.storage.removeItem(key); + } + + clear() { + this.storage.clear(); + } +} diff --git a/ng-web/src/service/http.Interceptor.ts b/ng-web/src/service/http.Interceptor.ts new file mode 100644 index 00000000..ae4f4da3 --- /dev/null +++ b/ng-web/src/service/http.Interceptor.ts @@ -0,0 +1,61 @@ +import { inject } from '@angular/core'; +import { HttpEvent, HttpHandlerFn, HttpRequest } from '@angular/common/http'; +import { catchError, finalize, Observable, throwError, timeout } from 'rxjs'; +import { Router } from '@angular/router'; +import { environment } from '../environments/environment'; +import { LoadingService } from './loading.service'; +import { AuthService } from './auth.service'; + +export function defaultInterceptor(req: HttpRequest, next: HttpHandlerFn): Observable> { + const _loading = inject(LoadingService); + + _loading.show(); + if (req.url.indexOf('assets/') > -1) { + return next(req); + } + const originalUrl = req.url.indexOf('http') > -1 ? req.url : environment.host + req.url; + const xRequestedReq = req.clone({ + headers: req.headers.append('X-Requested-With', 'XMLHttpRequest'), + url: originalUrl, + }); + return next(xRequestedReq).pipe( + timeout({ first: 5_000, each: 10_000 }), + catchError(errorResponse => { + let alertMessage = ''; + const status = errorResponse.status; + if (status > 0) { + if (errorResponse.error) { + alertMessage = errorResponse.error.message; + } else { + alertMessage = '服务器无响应,请稍后重试!'; + } + } else { + alertMessage = errorResponse.message; + } + return throwError(() => errorResponse); + }), + finalize(() => _loading.hide()), + ); +} + +export function authTokenInterceptor(req: HttpRequest, next: HttpHandlerFn): Observable> { + const _auth = inject(AuthService); + const _route = inject(Router); + + if (!_auth.isLogged()) { + return next(req); + } + const authReq = req.clone({ + headers: req.headers.set('Authorization', `Bearer ${_auth.authToken()}`), + }); + + return next(authReq).pipe( + catchError(errorResponse => { + if (errorResponse.status === 401) { + _auth.logout(); + _route.navigate([_auth.loginUrl]).then(); + } + return throwError(() => errorResponse); + }), + ); +} diff --git a/ng-web/src/service/loading.service.ts b/ng-web/src/service/loading.service.ts new file mode 100644 index 00000000..f32795e1 --- /dev/null +++ b/ng-web/src/service/loading.service.ts @@ -0,0 +1,21 @@ +import { Injectable, signal } from '@angular/core'; +import { toObservable } from '@angular/core/rxjs-interop'; +import { debounceTime, distinctUntilChanged, Observable, tap } from 'rxjs'; + +@Injectable({ providedIn: 'root' }) +export class LoadingService { + private progressSource = signal(false); + progress$: Observable = toObservable(this.progressSource).pipe( + debounceTime(100), + distinctUntilChanged(), + tap(res => console.log(`Loading Spin show is: ${res}`)), + ); + + show(): void { + this.progressSource.set(true); + } + + hide(): void { + this.progressSource.set(false); + } +} diff --git a/ng-web/src/service/page-title-strategy.service.ts b/ng-web/src/service/page-title-strategy.service.ts new file mode 100644 index 00000000..206980ce --- /dev/null +++ b/ng-web/src/service/page-title-strategy.service.ts @@ -0,0 +1,17 @@ +import { RouterStateSnapshot, TitleStrategy } from '@angular/router'; +import { Title } from '@angular/platform-browser'; +import { Injectable } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class PageTitleStrategy extends TitleStrategy { + constructor(private readonly title: Title) { + super(); + } + + override updateTitle(routerState: RouterStateSnapshot) { + const title = this.buildTitle(routerState); + if (title !== undefined) { + this.title.setTitle(`盘子管理平台 | ${title}`); + } + } +} diff --git a/ng-web/src/service/session-storage.service.ts b/ng-web/src/service/session-storage.service.ts new file mode 100644 index 00000000..34fe004c --- /dev/null +++ b/ng-web/src/service/session-storage.service.ts @@ -0,0 +1,30 @@ +import { Inject, Injectable, InjectionToken } from '@angular/core'; + +export const SESSION_STORAGE = new InjectionToken('Session Storage', { + providedIn: 'root', + factory: () => sessionStorage, +}); + +@Injectable({ + providedIn: 'root', +}) +export class SessionStorageService { + constructor(@Inject(SESSION_STORAGE) public storage: Storage) { + } + + get(key: string) { + return this.storage.getItem(key); + } + + set(key: string, value: string) { + this.storage.setItem(key, value); + } + + remove(key: string) { + this.storage.removeItem(key); + } + + clear() { + this.storage.clear(); + } +} diff --git a/ng-web/src/styles.scss b/ng-web/src/styles.scss index 90d4ee00..1c8ea332 100644 --- a/ng-web/src/styles.scss +++ b/ng-web/src/styles.scss @@ -1 +1,18 @@ /* You can add global styles to this file, and also import other style files */ +html, +body { + min-height: 100%; + min-width: 100%; + font-size: 16px; + background-color: #f5f9f2; +} + +body { + margin: 0; + font-family: Roboto, 'Helvetica Neue', sans-serif; +} + +.ant-menu-submenu-title, +.ant-menu-item { + font-size: 14px; +}