diff --git a/boot/platform/build.gradle b/boot/platform/build.gradle index 36521efe..590d73d9 100644 --- a/boot/platform/build.gradle +++ b/boot/platform/build.gradle @@ -51,6 +51,7 @@ dependencies { implementation("com.google.guava:guava:33.+") implementation("com.github.f4b6a3:ulid-creator:5.+") + implementation("org.springframework.security:spring-security-rsocket") implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-oauth2-client") diff --git a/boot/platform/src/main/java/com/platform/boot/config/SecurityConfiguration.java b/boot/platform/src/main/java/com/platform/boot/config/SecurityConfiguration.java index 86e01bba..014826ee 100644 --- a/boot/platform/src/main/java/com/platform/boot/config/SecurityConfiguration.java +++ b/boot/platform/src/main/java/com/platform/boot/config/SecurityConfiguration.java @@ -15,10 +15,12 @@ import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; +import org.springframework.security.config.annotation.rsocket.RSocketSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.core.AuthenticationException; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.rsocket.core.PayloadSocketAcceptorInterceptor; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint; import org.springframework.security.web.server.authentication.logout.*; @@ -56,10 +58,17 @@ public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } + @Bean + public PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) { + rsocket.authorizePayload(authorize -> authorize.anyRequest().permitAll().anyExchange().permitAll()) + .simpleAuthentication(Customizer.withDefaults()); + return rsocket.build(); + } + @Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http.authorizeExchange(exchange -> { - exchange.pathMatchers("/captcha/code", "/oauth2/qr/code").permitAll(); + exchange.pathMatchers("/captcha/code", "/oauth2/qr/code","/command/v1/send").permitAll(); exchange.matchers(PathRequest.toStaticResources().atCommonLocations()).permitAll(); exchange.pathMatchers("/tenants/**", "/users/**", "/groups/**") .hasAnyRole("SYSTEM_ADMINISTRATORS", "ADMINISTRATORS"); diff --git a/boot/platform/src/main/java/com/platform/boot/config/WebConfiguration.java b/boot/platform/src/main/java/com/platform/boot/config/WebConfiguration.java index 1d2da331..c9a0b319 100644 --- a/boot/platform/src/main/java/com/platform/boot/config/WebConfiguration.java +++ b/boot/platform/src/main/java/com/platform/boot/config/WebConfiguration.java @@ -1,13 +1,19 @@ package com.platform.boot.config; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.domain.Pageable; import org.springframework.data.web.ReactivePageableHandlerMethodArgumentResolver; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.web.reactive.config.WebFluxConfigurer; import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; +import java.net.URI; + /** * @author Alex bob */ @@ -16,6 +22,16 @@ @EnableAsync public class WebConfiguration implements WebFluxConfigurer { + @Value("${server.port:8080}") + private Integer serverPort; + + @Bean + public RSocketRequester rSocketRequester(RSocketRequester.Builder requesterBuilder, RSocketMessageHandler handler) { + URI url = URI.create("http://localhost:" + serverPort + "/rsocket"); + return requesterBuilder.setupData("CommandClient").setupRoute("connect.setup") + .rsocketConnector(connector -> connector.acceptor(handler.responder())).websocket(url); + } + @Override public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { ReactivePageableHandlerMethodArgumentResolver pageableResolver = diff --git a/boot/platform/src/main/java/com/platform/boot/relational/rsocket/CommandController.java b/boot/platform/src/main/java/com/platform/boot/relational/rsocket/CommandController.java new file mode 100644 index 00000000..ad952957 --- /dev/null +++ b/boot/platform/src/main/java/com/platform/boot/relational/rsocket/CommandController.java @@ -0,0 +1,30 @@ +package com.platform.boot.relational.rsocket; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +/** + * @author Alex Bob + */ +@Log4j2 +@RestController +@RequestMapping("/command/v1") +@RequiredArgsConstructor +public class CommandController { + + private final RSocketRequester rSocketRequester; + + @PostMapping("send") + public Mono send(@RequestBody CommandRequest command) { + var dataFlux = Mono.just(MessageIn.of(command.getType(), command.getCommand(), null)); + return this.rSocketRequester.route("request.sender") + .data(dataFlux).retrieveMono(MessageOut.class); + } + +} diff --git a/boot/platform/src/main/java/com/platform/boot/relational/rsocket/CommandRequest.java b/boot/platform/src/main/java/com/platform/boot/relational/rsocket/CommandRequest.java new file mode 100644 index 00000000..a00e2818 --- /dev/null +++ b/boot/platform/src/main/java/com/platform/boot/relational/rsocket/CommandRequest.java @@ -0,0 +1,17 @@ +package com.platform.boot.relational.rsocket; + +import lombok.Data; + +import java.io.Serializable; + +/** + * @author Alex Bob + */ +@Data +public class CommandRequest implements Serializable { + + private MessageType type; + private String command; + private String content; + +} diff --git a/ng-ui/.prettierignore b/ng-ui/.prettierignore new file mode 100644 index 00000000..1b062701 --- /dev/null +++ b/ng-ui/.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-ui/.prettierrc.json b/ng-ui/.prettierrc.json new file mode 100644 index 00000000..3d3924f8 --- /dev/null +++ b/ng-ui/.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-ui/.vscode/launch.json b/ng-ui/.vscode/launch.json index 925af837..5826f2af 100644 --- a/ng-ui/.vscode/launch.json +++ b/ng-ui/.vscode/launch.json @@ -17,4 +17,4 @@ "url": "http://localhost:9876/debug.html" } ] -} +} \ No newline at end of file diff --git a/ng-ui/.vscode/settings.json b/ng-ui/.vscode/settings.json index dff55bdc..527fe28d 100644 --- a/ng-ui/.vscode/settings.json +++ b/ng-ui/.vscode/settings.json @@ -21,5 +21,5 @@ "source.fixAll.eslint": "explicit" }, "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true -} + "editor.formatOnSave": true, +} \ No newline at end of file diff --git a/ng-ui/angular.json b/ng-ui/angular.json index 2ca55dc5..0b65d628 100644 --- a/ng-ui/angular.json +++ b/ng-ui/angular.json @@ -7,7 +7,7 @@ "projectType": "application", "schematics": { "@schematics/angular:component": { - "style": "sass" + "style": "scss" } }, "root": "projects/web", @@ -20,9 +20,9 @@ "outputPath": "dist/web", "index": "projects/web/src/index.html", "browser": "projects/web/src/main.ts", - "polyfills": [], + "polyfills": ["@angular/localize/init", "zone.js"], "tsConfig": "projects/web/tsconfig.app.json", - "inlineStyleLanguage": "sass", + "inlineStyleLanguage": "scss", "assets": [ { "glob": "**/*", @@ -37,24 +37,20 @@ "styles": [ { "input": "node_modules/bootstrap/dist/css/bootstrap.css", - "inject": false, "bundleName": "bootstrap" }, { "input": "node_modules/bootstrap-icons/font/bootstrap-icons.css", - "inject": false, "bundleName": "icons" }, { - "input": "projects/web/src/styles.sass", - "inject": false, + "input": "projects/web/src/styles.scss", "bundleName": "main" } ], "scripts": [ { "input": "node_modules/bootstrap/dist/js/bootstrap.bundle.js", - "inject": false, "bundleName": "bootstrap" } ], @@ -91,7 +87,13 @@ "development": { "optimization": false, "extractLicenses": false, - "sourceMap": true + "sourceMap": true, + "fileReplacements": [ + { + "replace": "projects/web/environments/environment.ts", + "with": "projects/web/environments/environment.development.ts" + } + ] } }, "defaultConfiguration": "production" @@ -103,7 +105,8 @@ "buildTarget": "web:build:production" }, "development": { - "buildTarget": "web:build:development" + "buildTarget": "web:build:development", + "proxyConfig": "proxy.conf.json" } }, "defaultConfiguration": "development" @@ -114,7 +117,7 @@ "test": { "builder": "@angular-devkit/build-angular:karma", "options": { - "polyfills": [], + "polyfills": ["@angular/localize/init", "zone.js", "zone.js/testing"], "tsConfig": "projects/web/tsconfig.spec.json", "inlineStyleLanguage": "sass", "assets": [ @@ -123,9 +126,7 @@ "input": "projects/web/public" } ], - "styles": [ - "projects/web/src/styles.sass" - ], + "styles": ["projects/web/src/styles.sass"], "scripts": [] } } @@ -156,10 +157,7 @@ "builder": "@angular-devkit/build-angular:karma", "options": { "tsConfig": "projects/commons/tsconfig.spec.json", - "polyfills": [ - "zone.js", - "zone.js/testing" - ] + "polyfills": ["zone.js", "zone.js/testing"] } } } diff --git a/ng-ui/package-lock.json b/ng-ui/package-lock.json index b7597ee7..4cbc38a9 100644 --- a/ng-ui/package-lock.json +++ b/ng-ui/package-lock.json @@ -31,6 +31,7 @@ "@angular-devkit/build-angular": "^18.0.6", "@angular/cli": "^18.0.6", "@angular/compiler-cli": "^18.0.0", + "@angular/localize": "^18.0.5", "@types/express": "^4.17.17", "@types/jasmine": "~5.1.0", "@types/node": "^18.18.0", @@ -696,6 +697,31 @@ "rxjs": "^6.5.3 || ^7.4.0" } }, + "node_modules/@angular/localize": { + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.0.5.tgz", + "integrity": "sha512-QlMu5sXodqyvCuA158JXsrYkyjN8AtnrZhDZDImQvkFclA5Bkc7+7lcCWoRc9g8bzAFbLuwJAzYGqpPTWPnpxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.24.7", + "@types/babel__core": "7.20.5", + "fast-glob": "3.3.2", + "yargs": "^17.2.1" + }, + "bin": { + "localize-extract": "tools/bundles/src/extract/cli.js", + "localize-migrate": "tools/bundles/src/migrate/cli.js", + "localize-translate": "tools/bundles/src/translate/cli.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/compiler": "18.0.5", + "@angular/compiler-cli": "18.0.5" + } + }, "node_modules/@angular/platform-browser": { "version": "18.0.5", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.0.5.tgz", @@ -4447,6 +4473,51 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", diff --git a/ng-ui/package.json b/ng-ui/package.json index 4a219c8a..a1cbcf3a 100644 --- a/ng-ui/package.json +++ b/ng-ui/package.json @@ -21,19 +21,20 @@ "@angular/platform-server": "^18.0.0", "@angular/router": "^18.0.0", "@angular/ssr": "^18.0.6", + "bootstrap": "^5.3.3", + "bootstrap-icons": "^1.11.3", "express": "^4.18.2", - "rxjs": "~7.8.0", - "tslib": "^2.3.0", - "zone.js": "~0.14.3", "ng-zorro-antd": "^18.0.0", "plate-commons": "^0.0.3", - "bootstrap": "^5.3.3", - "bootstrap-icons": "^1.11.3" + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.14.3" }, "devDependencies": { "@angular-devkit/build-angular": "^18.0.6", "@angular/cli": "^18.0.6", "@angular/compiler-cli": "^18.0.0", + "@angular/localize": "^18.0.5", "@types/express": "^4.17.17", "@types/jasmine": "~5.1.0", "@types/node": "^18.18.0", diff --git a/ng-ui/projects/app/public/favicon.ico b/ng-ui/projects/app/public/favicon.ico new file mode 100644 index 00000000..57614f9c Binary files /dev/null and b/ng-ui/projects/app/public/favicon.ico differ diff --git a/ng-ui/projects/app/server.ts b/ng-ui/projects/app/server.ts new file mode 100644 index 00000000..a0a90966 --- /dev/null +++ b/ng-ui/projects/app/server.ts @@ -0,0 +1,57 @@ +import {APP_BASE_HREF} from '@angular/common'; +import {CommonEngine} from '@angular/ssr'; +import express from 'express'; +import {fileURLToPath} from 'node:url'; +import {dirname, join, resolve} from 'node:path'; +import bootstrap from './src/main.server'; + +// The Express app is exported so that it can be used by serverless Functions. +export function app(): express.Express { + const server = express(); + const serverDistFolder = dirname(fileURLToPath(import.meta.url)); + const browserDistFolder = resolve(serverDistFolder, '../browser'); + const indexHtml = join(serverDistFolder, 'index.server.html'); + + const commonEngine = new CommonEngine(); + + server.set('view engine', 'html'); + server.set('views', browserDistFolder); + + // Example Express Rest API endpoints + // server.get('/api/**', (req, res) => { }); + // Serve static files from /browser + server.get('**', express.static(browserDistFolder, { + maxAge: '1y', + index: 'index.html', + })); + + // All regular routes use the Angular engine + server.get('**', (req, res, next) => { + const { protocol, originalUrl, baseUrl, headers } = req; + + commonEngine + .render({ + bootstrap, + documentFilePath: indexHtml, + url: `${protocol}://${headers.host}${originalUrl}`, + publicPath: browserDistFolder, + providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }], + }) + .then((html) => res.send(html)) + .catch((err) => next(err)); + }); + + return server; +} + +function run(): void { + const port = process.env['PORT'] || 4000; + + // Start up the Node server + const server = app(); + server.listen(port, () => { + console.log(`Node Express server listening on http://localhost:${port}`); + }); +} + +run(); diff --git a/ng-ui/projects/web/src/app/app.component.sass b/ng-ui/projects/app/src/app/app.component.css similarity index 100% rename from ng-ui/projects/web/src/app/app.component.sass rename to ng-ui/projects/app/src/app/app.component.css diff --git a/ng-ui/projects/app/src/app/app.component.html b/ng-ui/projects/app/src/app/app.component.html new file mode 100644 index 00000000..8409e93c --- /dev/null +++ b/ng-ui/projects/app/src/app/app.component.html @@ -0,0 +1,336 @@ + + + + + + + + + + + +
+
+
+ +

Hello, {{ title }}

+

Congratulations! Your app is running. 🎉

+
+ +
+
+ @for (item of [ + { title: 'Explore the Docs', link: 'https://angular.dev' }, + { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' }, + { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' }, + { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' }, + { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' }, + ]; track item.title) { + + {{ item.title }} + + + + + } +
+ +
+
+
+ + + + + + + + + + + diff --git a/ng-ui/projects/app/src/app/app.component.spec.ts b/ng-ui/projects/app/src/app/app.component.spec.ts new file mode 100644 index 00000000..5f7792a7 --- /dev/null +++ b/ng-ui/projects/app/src/app/app.component.spec.ts @@ -0,0 +1,29 @@ +import {TestBed} from '@angular/core/testing'; +import {AppComponent} from './app.component'; + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppComponent], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have the 'app' title`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('app'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('h1')?.textContent).toContain('Hello, app'); + }); +}); diff --git a/ng-ui/projects/app/src/app/app.component.ts b/ng-ui/projects/app/src/app/app.component.ts new file mode 100644 index 00000000..dd0f765e --- /dev/null +++ b/ng-ui/projects/app/src/app/app.component.ts @@ -0,0 +1,13 @@ +import {Component} from '@angular/core'; +import {RouterOutlet} from '@angular/router'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [RouterOutlet], + templateUrl: './app.component.html', + styleUrl: './app.component.css' +}) +export class AppComponent { + title = 'app'; +} diff --git a/ng-ui/projects/app/src/app/app.config.server.ts b/ng-ui/projects/app/src/app/app.config.server.ts new file mode 100644 index 00000000..7cf83665 --- /dev/null +++ b/ng-ui/projects/app/src/app/app.config.server.ts @@ -0,0 +1,11 @@ +import {ApplicationConfig, mergeApplicationConfig} from '@angular/core'; +import {provideServerRendering} from '@angular/platform-server'; +import {appConfig} from './app.config'; + +const serverConfig: ApplicationConfig = { + providers: [ + provideServerRendering() + ] +}; + +export const config = mergeApplicationConfig(appConfig, serverConfig); diff --git a/ng-ui/projects/app/src/app/app.config.ts b/ng-ui/projects/app/src/app/app.config.ts new file mode 100644 index 00000000..be17bac7 --- /dev/null +++ b/ng-ui/projects/app/src/app/app.config.ts @@ -0,0 +1,10 @@ +import {ApplicationConfig, provideZoneChangeDetection} from '@angular/core'; +import {provideRouter} from '@angular/router'; + +import {routes} from './app.routes'; +import {provideClientHydration} from '@angular/platform-browser'; + +export const appConfig: ApplicationConfig = { + providers: [] +}; +provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideClientHydration() diff --git a/ng-ui/projects/app/src/app/app.routes.ts b/ng-ui/projects/app/src/app/app.routes.ts new file mode 100644 index 00000000..8b5e5fb2 --- /dev/null +++ b/ng-ui/projects/app/src/app/app.routes.ts @@ -0,0 +1,3 @@ +import {Routes} from '@angular/router'; + +export const routes: Routes = []; diff --git a/ng-ui/projects/app/src/index.html b/ng-ui/projects/app/src/index.html new file mode 100644 index 00000000..0db282fd --- /dev/null +++ b/ng-ui/projects/app/src/index.html @@ -0,0 +1,13 @@ + + + + + App + + + + + + + + diff --git a/ng-ui/projects/app/src/main.server.ts b/ng-ui/projects/app/src/main.server.ts new file mode 100644 index 00000000..e83e990a --- /dev/null +++ b/ng-ui/projects/app/src/main.server.ts @@ -0,0 +1,7 @@ +import {bootstrapApplication} from '@angular/platform-browser'; +import {AppComponent} from './app/app.component'; +import {config} from './app/app.config.server'; + +const bootstrap = () => bootstrapApplication(AppComponent, config); + +export default bootstrap; diff --git a/ng-ui/projects/app/src/main.ts b/ng-ui/projects/app/src/main.ts new file mode 100644 index 00000000..e4260eff --- /dev/null +++ b/ng-ui/projects/app/src/main.ts @@ -0,0 +1,6 @@ +import {bootstrapApplication} from '@angular/platform-browser'; +import {appConfig} from './app/app.config'; +import {AppComponent} from './app/app.component'; + +bootstrapApplication(AppComponent, appConfig) + .catch((err) => console.error(err)); diff --git a/ng-ui/projects/web/src/styles.sass b/ng-ui/projects/app/src/styles.css similarity index 100% rename from ng-ui/projects/web/src/styles.sass rename to ng-ui/projects/app/src/styles.css diff --git a/ng-ui/projects/app/tsconfig.app.json b/ng-ui/projects/app/tsconfig.app.json new file mode 100644 index 00000000..93e66fbe --- /dev/null +++ b/ng-ui/projects/app/tsconfig.app.json @@ -0,0 +1,19 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/app", + "types": [ + "node" + ] + }, + "files": [ + "src/main.ts", + "src/main.server.ts", + "server.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/ng-ui/projects/app/tsconfig.spec.json b/ng-ui/projects/app/tsconfig.spec.json new file mode 100644 index 00000000..0a43b832 --- /dev/null +++ b/ng-ui/projects/app/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/ng-ui/projects/web/environments/environment.development.ts b/ng-ui/projects/web/environments/environment.development.ts new file mode 100644 index 00000000..a86b8e4b --- /dev/null +++ b/ng-ui/projects/web/environments/environment.development.ts @@ -0,0 +1,4 @@ +export const environment = { + production: false, + host: '/api', +}; diff --git a/ng-ui/projects/web/environments/environment.ts b/ng-ui/projects/web/environments/environment.ts new file mode 100644 index 00000000..344b0e35 --- /dev/null +++ b/ng-ui/projects/web/environments/environment.ts @@ -0,0 +1,4 @@ +export const environment = { + production: true, + host: '', +}; diff --git a/ng-ui/projects/web/public/assets/th.jpg b/ng-ui/projects/web/public/assets/th.jpg new file mode 100644 index 00000000..2b8d7c4e Binary files /dev/null and b/ng-ui/projects/web/public/assets/th.jpg differ diff --git a/ng-ui/projects/web/src/app/app.component.html b/ng-ui/projects/web/src/app/app.component.html index 2ce56a87..50322faa 100644 --- a/ng-ui/projects/web/src/app/app.component.html +++ b/ng-ui/projects/web/src/app/app.component.html @@ -1,340 +1,10 @@ - - - - - - - - - - - -
-
-
- -

Hello, {{ title }}

-

Congratulations! Your app is running. 🎉

-
- -
-
- @for (item of [ - {title: 'Explore the Docs', link: 'https://angular.dev'}, - {title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials'}, - {title: 'CLI Docs', link: 'https://angular.dev/tools/cli'}, - {title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service'}, - {title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools'}, - ]; track item.title) { - - {{ item.title }} - - - - - } -
- -
-
-
- - - - - - - - - - - +
+ + + +
+ + +
UP
+
+
diff --git a/ng-ui/projects/web/src/app/app.component.scss b/ng-ui/projects/web/src/app/app.component.scss new file mode 100644 index 00000000..46e6e8b1 --- /dev/null +++ b/ng-ui/projects/web/src/app/app.component.scss @@ -0,0 +1,4 @@ +:host { + min-height: 100%; + min-width: 100%; +} diff --git a/ng-ui/projects/web/src/app/app.component.ts b/ng-ui/projects/web/src/app/app.component.ts index 4a9261f3..3bfc252b 100644 --- a/ng-ui/projects/web/src/app/app.component.ts +++ b/ng-ui/projects/web/src/app/app.component.ts @@ -1,13 +1,33 @@ +import {AsyncPipe} from '@angular/common'; import {Component} from '@angular/core'; import {RouterOutlet} from '@angular/router'; +import {toSignal} from '@angular/core/rxjs-interop'; + +import {NzBackTopModule} from 'ng-zorro-antd/back-top'; +import {NzSpinModule} from 'ng-zorro-antd/spin'; +import {debounceTime, distinctUntilChanged, tap} from 'rxjs'; +import {LoadingService} from '../core/loading.service'; @Component({ selector: 'app-root', standalone: true, - imports: [RouterOutlet], + imports: [RouterOutlet, AsyncPipe, NzBackTopModule, NzSpinModule], templateUrl: './app.component.html', - styleUrl: './app.component.sass' + styleUrl: './app.component.scss', }) export class AppComponent { - title = 'web'; + loadingShow = toSignal( + this.loading.progress$.pipe( + debounceTime(500), + distinctUntilChanged(), + tap(res => console.log(`Loading show is: ${res}`)) + ), + { initialValue: false } + ); + + constructor(private loading: LoadingService) {} + + ngOnInit(): void { + this.loading.show(); + } } diff --git a/ng-ui/projects/web/src/app/app.config.ts b/ng-ui/projects/web/src/app/app.config.ts index 2d733b74..7ca44a31 100644 --- a/ng-ui/projects/web/src/app/app.config.ts +++ b/ng-ui/projects/web/src/app/app.config.ts @@ -1,14 +1,52 @@ -import {ApplicationConfig, provideExperimentalZonelessChangeDetection,} from '@angular/core'; -import {provideRouter} from '@angular/router'; +import {ApplicationConfig, importProvidersFrom, provideZoneChangeDetection} from '@angular/core'; +import {provideRouter, TitleStrategy} from '@angular/router'; import {routes} from './app.routes'; +import {NzConfig, provideNzConfig} from 'ng-zorro-antd/core/config'; +import {provideAnimationsAsync} from '@angular/platform-browser/animations/async'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {PageTitleStrategy} from '../core/title-strategy.service'; +import { + provideHttpClient, + withFetch, + withInterceptors, + withInterceptorsFromDi, + withXsrfConfiguration, +} from '@angular/common/http'; +import {authTokenInterceptor, defaultInterceptor} from '../core/http.Interceptor'; +import {BrowserStorageServerService, BrowserStorageService} from 'plate-commons'; +import {provideClientHydration} from "@angular/platform-browser"; + +export const ngZorroConfig: NzConfig = { + // 注意组件名称没有 nz 前缀 + message: { + nzTop: 50, + nzDuration: 5000, + nzAnimate: true, + nzPauseOnHover: true, + }, + notification: {nzTop: 240}, +}; /** - * provideZoneChangeDetection({eventCoalescing: true}) + * provideExperimentalZonelessChangeDetection(), */ export const appConfig: ApplicationConfig = { providers: [ - provideExperimentalZonelessChangeDetection(), - provideRouter(routes) + importProvidersFrom(BrowserAnimationsModule), + provideAnimationsAsync(), + provideNzConfig(ngZorroConfig), + provideHttpClient( + withFetch(), + withInterceptorsFromDi(), + withInterceptors([defaultInterceptor, authTokenInterceptor]), + withXsrfConfiguration({ + cookieName: 'XSRF-TOKEN', + headerName: 'X-XSRF-TOKEN', + }) + ), + {provide: TitleStrategy, useClass: PageTitleStrategy}, + {provide: BrowserStorageService, useClass: BrowserStorageServerService}, + provideZoneChangeDetection({eventCoalescing: true}), provideRouter(routes), provideClientHydration() ], }; diff --git a/ng-ui/projects/web/src/app/app.routes.ts b/ng-ui/projects/web/src/app/app.routes.ts index 8b5e5fb2..6decfb93 100644 --- a/ng-ui/projects/web/src/app/app.routes.ts +++ b/ng-ui/projects/web/src/app/app.routes.ts @@ -1,3 +1,15 @@ import {Routes} from '@angular/router'; +import {NotFoundComponent} from '../core/not-found.component'; -export const routes: Routes = []; +export const routes: Routes = [ + { + path: 'home', + loadChildren: () => import('../pages/pages.module').then(m => m.PagesModule), + }, + { + path: 'auth', + loadChildren: () => import('../core/security.module').then(m => m.SecurityModule), + }, + { path: '', pathMatch: 'full', redirectTo: 'auth' }, + { path: '**', component: NotFoundComponent }, +]; diff --git a/ng-ui/projects/web/src/core/auth.service.ts b/ng-ui/projects/web/src/core/auth.service.ts new file mode 100644 index 00000000..f3ff6bba --- /dev/null +++ b/ng-ui/projects/web/src/core/auth.service.ts @@ -0,0 +1,47 @@ +import {inject, Injectable} from '@angular/core'; +import {Subject} from 'rxjs'; +import {HttpErrorResponse} from '@angular/common/http'; +import {CanActivateChildFn, CanActivateFn, CanMatchFn, Router,} from '@angular/router'; + +export const authGuard: + | CanMatchFn + | CanActivateFn + | CanActivateChildFn = () => { + const auth = inject(AuthService); + const router = inject(Router); + if (auth.isLoggedIn) { + return true; + } + return router.parseUrl(auth.loginUrl); +}; + +@Injectable({providedIn: 'root'}) +export class AuthService { + public readonly loginUrl = '/auth/login'; + isLoggedIn = false; + private authenticatedSource = new Subject(); + authenticated$ = this.authenticatedSource.asObservable(); + private token = ''; + + authToken(): string { + if (!this.isLoggedIn) { + throw new HttpErrorResponse({ + error: 'Authenticate is incorrectness,please login again.', + status: 401, + }); + } + return this.token; + } + + login(token: string) { + this.isLoggedIn = true; + this.authenticatedSource.next(true); + this.token = token; + } + + logout(): void { + this.isLoggedIn = false; + this.authenticatedSource.next(false); + this.token = ''; + } +} diff --git a/ng-ui/projects/web/src/core/http.Interceptor.ts b/ng-ui/projects/web/src/core/http.Interceptor.ts new file mode 100644 index 00000000..920ed1c8 --- /dev/null +++ b/ng-ui/projects/web/src/core/http.Interceptor.ts @@ -0,0 +1,70 @@ +import {inject} from '@angular/core'; +import {HttpEvent, HttpHandlerFn, HttpRequest} from '@angular/common/http'; +import {catchError, finalize, Observable, throwError, timeout} from 'rxjs'; +import {AuthService} from './auth.service'; +import {LoadingService} from './loading.service'; +import {MessageService} from '../shared/message.service'; +import {Router} from '@angular/router'; +import {environment} from '../../environments/environment'; + +export function defaultInterceptor(req: HttpRequest, next: HttpHandlerFn): Observable> { + const _loading = inject(LoadingService); + const _message = inject(MessageService); + + _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 => { + if (errorResponse.error.message) { + _message.error(errorResponse.error.message); + return throwError(() => errorResponse.error.message); + } + console.error($localize`:@@errorMessage:Backend returned code ${errorResponse.status}, + body was: ${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.isLoggedIn) { + 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(() => $localize`:@@errorMessage401:身份验证无效,请重新登录。`); + } else if (errorResponse.status === 407) { + _auth.logout(); + _route.navigate([_auth.loginUrl]).then(); + return throwError(() => $localize`:@@errorMessage407:认证不正确,请重新登录。`); + } else if (errorResponse.status === 403) { + _auth.logout(); + _route.navigate([_auth.loginUrl]).then(); + return throwError(() => $localize`:@@errorMessage403:验证码令牌错误,请重新登录。`); + } else { + console.error(`Backend returned authToken code ${errorResponse.status}, body was: `, errorResponse.error); + return throwError(() => errorResponse); + } + }) + ); +} diff --git a/ng-ui/projects/web/src/core/loading.service.ts b/ng-ui/projects/web/src/core/loading.service.ts new file mode 100644 index 00000000..5bf68233 --- /dev/null +++ b/ng-ui/projects/web/src/core/loading.service.ts @@ -0,0 +1,17 @@ +import {Injectable} from '@angular/core'; +import {Observable, Subject} from 'rxjs'; + +@Injectable({providedIn: 'root'}) +export class LoadingService { + // Observable string sources + private progressSource: Subject = new Subject(); + progress$: Observable = this.progressSource.asObservable(); + + show(): void { + this.progressSource.next(true); + } + + hide(): void { + this.progressSource.next(false); + } +} diff --git a/ng-ui/projects/web/src/core/login/login.component.html b/ng-ui/projects/web/src/core/login/login.component.html new file mode 100644 index 00000000..2ff4a2bd --- /dev/null +++ b/ng-ui/projects/web/src/core/login/login.component.html @@ -0,0 +1,91 @@ +
+
+
+
+
登录你的账号
+
+
+
+
+ +
+ +
+ 请输入电子邮件或手机号。 +
+
+
+
+ +
+ +
+ 密码长度为8 ~ 20个字符,由字母和数字组成,不能包含空格、特殊字符或表情符号。 +
+
请输入密码。
+
+
+ + + 忘记密码? +
+
+ +
+
+
+
+
+ 社交账号登录 +
+
+ + + +
+ +
+
+
+
+
diff --git a/ng-ui/projects/web/src/core/login/login.component.scss b/ng-ui/projects/web/src/core/login/login.component.scss new file mode 100644 index 00000000..ca393ac3 --- /dev/null +++ b/ng-ui/projects/web/src/core/login/login.component.scss @@ -0,0 +1,22 @@ +:host { + min-height: 100%; + min-width: 100%; +} + +.page { + background-image: url(https://bing.biturl.top/?resolution=1920&format=image&index=0&mkt=zh-CN), url('/assets/th.jpg'), + linear-gradient(rgba(0, 0, 255, 0.5), rgba(255, 255, 0, 0.5)); + background-position: center; + background-repeat: no-repeat; + background-size: cover; +} + +.card { + width: 28rem; +} + +.bi { + font-size: 1.5rem; + margin-left: 0.1rem; + margin-right: 0.1rem; +} diff --git a/ng-ui/projects/web/src/core/login/login.component.ts b/ng-ui/projects/web/src/core/login/login.component.ts new file mode 100644 index 00000000..be4c6035 --- /dev/null +++ b/ng-ui/projects/web/src/core/login/login.component.ts @@ -0,0 +1,80 @@ +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators,} from '@angular/forms'; +import {Credentials, LoginService} from './login.service'; +import {ActivatedRoute, Router} from '@angular/router'; +import {Subject, takeUntil} from 'rxjs'; +import {NzFormModule} from 'ng-zorro-antd/form'; +import {NgIf} from '@angular/common'; + +@Component({ + selector: 'app-login', + standalone: true, + imports: [FormsModule, ReactiveFormsModule, NzFormModule, NgIf], + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'], +}) +export class LoginComponent implements OnInit, OnDestroy { + loginForm: FormGroup<{ + username: FormControl; + password: FormControl; + remember: FormControl; + }>; + + private componentDestroyed$: Subject = new Subject(); + + constructor( + private router: Router, + private route: ActivatedRoute, + private formBuilder: FormBuilder, + private loginSer: LoginService + ) { + this.loginForm = this.formBuilder.group({ + username: new FormControl('', [ + Validators.required, + Validators.minLength(5), + Validators.maxLength(64), + ]), + password: new FormControl('', [ + Validators.required, + Validators.minLength(6), + Validators.maxLength(64), + ]), + remember: new FormControl(false), + }); + } + + onSubmit(): void { + const credentials: Credentials = { + username: this.loginForm.value.username, + password: this.loginForm.value.password, + }; + + if (this.loginForm.value.remember) { + this.loginSer.setRememberMe(credentials); + } + this.login(credentials); + } + + ngOnInit(): void { + const credentials = this.loginSer.getRememberMe(); + if (credentials && credentials != null) { + this.login(credentials); + } + } + + ngOnDestroy(): void { + this.componentDestroyed$.next(); + this.componentDestroyed$.complete(); + } + + login(credentials: Credentials) { + const login = this.loginSer.login(credentials); + const result = login.pipe(takeUntil(this.componentDestroyed$)); + result.subscribe({ + next: () => { + this.router.navigate(['/home'], {relativeTo: this.route}).then(); + }, + error: e => console.log(e), + }); + } +} diff --git a/ng-ui/projects/web/src/core/login/login.service.ts b/ng-ui/projects/web/src/core/login/login.service.ts new file mode 100644 index 00000000..ffb7a637 --- /dev/null +++ b/ng-ui/projects/web/src/core/login/login.service.ts @@ -0,0 +1,76 @@ +import {HttpClient, HttpHeaders} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Observable, tap, throwError} from 'rxjs'; +import {AuthService} from '../../core/auth.service'; +import {BrowserStorageService} from 'plate-commons'; + +export interface Authentication { + token: string; + expires: number; + lastAccessTime: Date; +} + +export interface Credentials { + password: string | null | undefined; + username: string | null | undefined; +} + +@Injectable({ + providedIn: 'root', +}) +export class LoginService { + private readonly storageKey = 'credentials'; + + private credentials: Credentials | null | undefined = null; + + constructor( + private http: HttpClient, + private auth: AuthService, + private storage: BrowserStorageService + ) { + } + + login(credentials: Credentials): Observable { + if ( + credentials.username == undefined && + credentials.password == undefined + ) { + return throwError(() => '用户名和密码不能为[undefined]!'); + } + const headers: HttpHeaders = new HttpHeaders( + credentials + ? { + authorization: + 'Basic ' + + btoa(credentials.username + ':' + credentials.password), + } + : {} + ); + return this.http + .get('/oauth2/token', {headers: headers}) + .pipe(tap(authentication => this.auth.login(authentication.token))); + } + + setRememberMe(credentials: Credentials) { + let creStr = JSON.stringify(credentials); + creStr = btoa(creStr); + this.storage.set(this.storageKey, creStr); + this.credentials = credentials; + } + + getRememberMe() { + let creStr = this.storage.get(this.storageKey); + if (creStr) { + console.log(creStr); + creStr = atob(creStr); + this.credentials = JSON.parse(creStr); + } + return this.credentials; + } + + logout() { + this.auth.logout(); + this.storage.remove(this.storageKey); + this.credentials = null; + } +} diff --git a/ng-ui/projects/web/src/core/loginv1/login.component.html b/ng-ui/projects/web/src/core/loginv1/login.component.html new file mode 100644 index 00000000..94829e5b --- /dev/null +++ b/ng-ui/projects/web/src/core/loginv1/login.component.html @@ -0,0 +1,47 @@ + diff --git a/ng-ui/projects/web/src/core/loginv1/login.component.scss b/ng-ui/projects/web/src/core/loginv1/login.component.scss new file mode 100644 index 00000000..005a69ad --- /dev/null +++ b/ng-ui/projects/web/src/core/loginv1/login.component.scss @@ -0,0 +1,15 @@ +.login-form { + max-width: 300px; +} + +.login-form-margin { + margin-bottom: 16px; +} + +.login-form-forgot { + float: right; +} + +.login-form-button { + width: 100%; +} diff --git a/ng-ui/projects/web/src/core/loginv1/login.component.ts b/ng-ui/projects/web/src/core/loginv1/login.component.ts new file mode 100644 index 00000000..8bff2631 --- /dev/null +++ b/ng-ui/projects/web/src/core/loginv1/login.component.ts @@ -0,0 +1,55 @@ +import {NgIf} from '@angular/common'; +import {Component} from '@angular/core'; +import { + FormControl, + FormGroup, + FormsModule, + NonNullableFormBuilder, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import {NzButtonModule} from 'ng-zorro-antd/button'; +import {NzFormModule} from 'ng-zorro-antd/form'; +import {NzInputModule} from 'ng-zorro-antd/input'; + +@Component({ + selector: 'app-login-v1', + standalone: true, + imports: [ + FormsModule, + ReactiveFormsModule, + NzFormModule, + NzInputModule, + NzButtonModule, + NgIf, + ], + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'], +}) +export class LoginV1Component { + validateForm: FormGroup<{ + userName: FormControl; + password: FormControl; + remember: FormControl; + }> = this.fb.group({ + userName: ['', [Validators.required]], + password: ['', [Validators.required]], + remember: [true], + }); + + constructor(private fb: NonNullableFormBuilder) { + } + + submitForm(): void { + if (this.validateForm.valid) { + console.log('submit', this.validateForm.value); + } else { + Object.values(this.validateForm.controls).forEach(control => { + if (control.invalid) { + control.markAsDirty(); + control.updateValueAndValidity({onlySelf: true}); + } + }); + } + } +} diff --git a/ng-ui/projects/web/src/core/not-found.component.ts b/ng-ui/projects/web/src/core/not-found.component.ts new file mode 100644 index 00000000..165d987b --- /dev/null +++ b/ng-ui/projects/web/src/core/not-found.component.ts @@ -0,0 +1,26 @@ +import {Component} from '@angular/core'; +import {NzResultModule} from 'ng-zorro-antd/result'; + +@Component({ + selector: 'app-not-found', + standalone: true, + imports: [NzResultModule], + template: ` + +
+ Back Home +
+
+ `, + styles: ` + :host { + min-height: 100%; + min-width: 100%; + } + `, +}) +export class NotFoundComponent { +} diff --git a/ng-ui/projects/web/src/core/security-routing.module.ts b/ng-ui/projects/web/src/core/security-routing.module.ts new file mode 100644 index 00000000..0639209d --- /dev/null +++ b/ng-ui/projects/web/src/core/security-routing.module.ts @@ -0,0 +1,17 @@ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {LoginComponent} from './login/login.component'; +import {LoginV1Component} from './loginv1/login.component'; + +const routes: Routes = [ + {path: 'login', component: LoginComponent, title: '登录'}, + {path: 'login1', component: LoginV1Component, title: '登录V1'}, + {path: '', redirectTo: 'login', pathMatch: 'full'}, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class SecurityRoutingModule { +} diff --git a/ng-ui/projects/web/src/core/security.module.ts b/ng-ui/projects/web/src/core/security.module.ts new file mode 100644 index 00000000..e94c0877 --- /dev/null +++ b/ng-ui/projects/web/src/core/security.module.ts @@ -0,0 +1,17 @@ +import {NgModule, Optional, SkipSelf} from '@angular/core'; +import {BrowserStorageService} from 'plate-commons'; +import {SecurityRoutingModule} from './security-routing.module'; + +@NgModule({ + imports: [SecurityRoutingModule], + providers: [BrowserStorageService], +}) +export class SecurityModule { + constructor(@Optional() @SkipSelf() parentModule?: SecurityModule) { + if (parentModule) { + throw new Error( + 'SecurityModule is already loaded. Import it in the AppModule only' + ); + } + } +} diff --git a/ng-ui/projects/web/src/core/title-strategy.service.ts b/ng-ui/projects/web/src/core/title-strategy.service.ts new file mode 100644 index 00000000..5c2ef238 --- /dev/null +++ b/ng-ui/projects/web/src/core/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-ui/projects/web/src/index.html b/ng-ui/projects/web/src/index.html index 1fd53032..e46e3e5c 100644 --- a/ng-ui/projects/web/src/index.html +++ b/ng-ui/projects/web/src/index.html @@ -1,13 +1,23 @@ - - - - - Web - - - - - - - + + + + + + + Loading page .... + + + + + +
+
+
+ Loading... +
+ Loading... +
+
+
+ diff --git a/ng-ui/projects/web/src/main.ts b/ng-ui/projects/web/src/main.ts index e4260eff..d0a3c149 100644 --- a/ng-ui/projects/web/src/main.ts +++ b/ng-ui/projects/web/src/main.ts @@ -1,3 +1,5 @@ +/// + import {bootstrapApplication} from '@angular/platform-browser'; import {appConfig} from './app/app.config'; import {AppComponent} from './app/app.component'; diff --git a/ng-ui/projects/web/src/pages/home/home.component.html b/ng-ui/projects/web/src/pages/home/home.component.html new file mode 100644 index 00000000..ac0cf040 --- /dev/null +++ b/ng-ui/projects/web/src/pages/home/home.component.html @@ -0,0 +1,72 @@ + + + + + + + + + + + + @for (breadcrumb of breadcrumbs; track breadcrumb) { + {{ + breadcrumb.label + }} + } + + +
+ +
+
+ Ant Design ©2020 Implement By Angular +
+
+
diff --git a/ng-ui/projects/web/src/pages/home/home.component.scss b/ng-ui/projects/web/src/pages/home/home.component.scss new file mode 100644 index 00000000..1a841e0d --- /dev/null +++ b/ng-ui/projects/web/src/pages/home/home.component.scss @@ -0,0 +1,45 @@ +:host { + min-width: 100%; + min-height: 100%; +} + +.layout { + min-height: 100vh; +} + +.nz-header { + padding: 0 !important; +} + +.logo { + width: 14rem; + color: white; + float: left; + font-size: 2rem; + justify-content: center; + align-items: center; + padding-left: 2rem; +} + +.header-menu { + line-height: 4rem; + margin-right: 1rem; +} + +.sider-menu { + min-height: 100%; + border-right: 0; +} + +.inner-layout { + padding: 0 1.2rem 1.2rem; +} + +nz-breadcrumb { + margin: 1rem 0; +} + +nz-content { + background: #fff; + padding: 1rem; +} diff --git a/ng-ui/projects/web/src/pages/home/home.component.ts b/ng-ui/projects/web/src/pages/home/home.component.ts new file mode 100644 index 00000000..cb275a61 --- /dev/null +++ b/ng-ui/projects/web/src/pages/home/home.component.ts @@ -0,0 +1,85 @@ +import {Component, OnInit, Type} from '@angular/core'; +import {distinctUntilChanged, filter, map, Observable} from 'rxjs'; +import {Menu, MenusService} from '../system/menus/menus.service'; +import {ActivatedRoute, NavigationEnd, Params, PRIMARY_OUTLET, Resolve, ResolveFn, Router,} from '@angular/router'; + +export interface Breadcrumb { + label: string | ResolveFn | Type>; + params: Params; + url: string; +} + +@Component({ + selector: 'app-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.scss'], +}) +export class HomeComponent implements OnInit { + menus$: Observable | undefined; + breadcrumbs: Breadcrumb[] = []; + + constructor( + private router: Router, + private activatedRoute: ActivatedRoute, + private menusService: MenusService + ) { + } + + ngOnInit() { + this.initMenu(); + this.initBreadcrumb(); + } + + initMenu() { + const menuRequest: Menu = { + pcode: '0', + tenantCode: '0', + }; + this.menus$ = this.menusService.getMeMenus(menuRequest); + } + + initBreadcrumb() { + this.breadcrumbs = this.getBreadcrumbs(this.activatedRoute.root); + this.router.events + .pipe( + filter(event => event instanceof NavigationEnd), + distinctUntilChanged(), + map(() => this.getBreadcrumbs(this.activatedRoute.root)) + ) + .subscribe(event => (this.breadcrumbs = event)); + } + + getBreadcrumbs( + route: ActivatedRoute, + url = '', + breads: Breadcrumb[] = [] + ): Breadcrumb[] { + const children: ActivatedRoute[] = route.children; + if (children.length === 0) { + return breads; + } + for (const child of children) { + // verify primary route + if (child.outlet !== PRIMARY_OUTLET) { + continue; + } + if (!child.snapshot.routeConfig?.title || !child.snapshot.url.length) { + return this.getBreadcrumbs(child, url, breads); + } + const title = child.snapshot.routeConfig?.title; + const routeURL: string = child.snapshot.url + .map(segment => segment.path) + .join('/'); + + url += `/${routeURL}`; + const breadcrumb: Breadcrumb = { + label: title ? title : 'title', + params: child.snapshot.params, + url, + }; + breads.push(breadcrumb); + return this.getBreadcrumbs(child, url, breads); + } + return breads; + } +} diff --git a/ng-ui/projects/web/src/pages/home/index/index.component.html b/ng-ui/projects/web/src/pages/home/index/index.component.html new file mode 100644 index 00000000..2818fe10 --- /dev/null +++ b/ng-ui/projects/web/src/pages/home/index/index.component.html @@ -0,0 +1 @@ +
Home Index
diff --git a/ng-ui/projects/web/src/pages/home/index/index.component.scss b/ng-ui/projects/web/src/pages/home/index/index.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/ng-ui/projects/web/src/pages/home/index/index.component.ts b/ng-ui/projects/web/src/pages/home/index/index.component.ts new file mode 100644 index 00000000..69a33078 --- /dev/null +++ b/ng-ui/projects/web/src/pages/home/index/index.component.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'app-welcome', + templateUrl: './index.component.html', + styleUrls: ['./index.component.scss'], +}) +export class IndexComponent { +} diff --git a/ng-ui/projects/web/src/pages/layout/layout.module.ts b/ng-ui/projects/web/src/pages/layout/layout.module.ts new file mode 100644 index 00000000..a534b0ac --- /dev/null +++ b/ng-ui/projects/web/src/pages/layout/layout.module.ts @@ -0,0 +1,11 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {PageComponent} from './page/page.component'; + +@NgModule({ + declarations: [], + imports: [CommonModule, PageComponent], + exports: [PageComponent], +}) +export class LayoutModule { +} diff --git a/ng-ui/projects/web/src/pages/layout/page/page.component.html b/ng-ui/projects/web/src/pages/layout/page/page.component.html new file mode 100644 index 00000000..679710db --- /dev/null +++ b/ng-ui/projects/web/src/pages/layout/page/page.component.html @@ -0,0 +1 @@ +

page works!

diff --git a/ng-ui/projects/web/src/pages/layout/page/page.component.scss b/ng-ui/projects/web/src/pages/layout/page/page.component.scss new file mode 100644 index 00000000..634a4bdd --- /dev/null +++ b/ng-ui/projects/web/src/pages/layout/page/page.component.scss @@ -0,0 +1,4 @@ +:host { + min-width: 100%; + min-height: 100%; +} diff --git a/ng-ui/projects/web/src/pages/layout/page/page.component.ts b/ng-ui/projects/web/src/pages/layout/page/page.component.ts new file mode 100644 index 00000000..89bea5c5 --- /dev/null +++ b/ng-ui/projects/web/src/pages/layout/page/page.component.ts @@ -0,0 +1,10 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'app-page', + templateUrl: './page.component.html', + standalone: true, + styleUrl: './page.component.scss', +}) +export class PageComponent { +} diff --git a/ng-ui/projects/web/src/pages/pages.module.ts b/ng-ui/projects/web/src/pages/pages.module.ts new file mode 100644 index 00000000..d9d9389e --- /dev/null +++ b/ng-ui/projects/web/src/pages/pages.module.ts @@ -0,0 +1,31 @@ +import {NgModule, Optional, SkipSelf} from '@angular/core'; + +import {PagesRoutingModule} from './pages.routes'; + +import {IndexComponent} from './home/index/index.component'; +import {HomeComponent} from './home/home.component'; +import {SharedModule} from '../shared/shared.module'; +import {NzLayoutModule} from 'ng-zorro-antd/layout'; +import {NzMenuModule} from 'ng-zorro-antd/menu'; +import {NzSliderModule} from 'ng-zorro-antd/slider'; + +@NgModule({ + imports: [ + NzLayoutModule, + NzSliderModule, + NzMenuModule, + SharedModule, + PagesRoutingModule, + ], + declarations: [IndexComponent, HomeComponent], + exports: [], +}) +export class PagesModule { + constructor(@Optional() @SkipSelf() parentModule?: PagesModule) { + if (parentModule) { + throw new Error( + 'PagesModule is already loaded. Import it in the AppModule only' + ); + } + } +} diff --git a/ng-ui/projects/web/src/pages/pages.routes.ts b/ng-ui/projects/web/src/pages/pages.routes.ts new file mode 100644 index 00000000..f363a2b7 --- /dev/null +++ b/ng-ui/projects/web/src/pages/pages.routes.ts @@ -0,0 +1,35 @@ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {IndexComponent} from './home/index/index.component'; +import {HomeComponent} from './home/home.component'; +import {authGuard} from '../core/auth.service'; + +const routes: Routes = [ + { + path: '', + canActivate: [authGuard], + component: HomeComponent, + children: [ + { + path: 'system', + canActivateChild: [authGuard], + loadChildren: () => + import('./system/system.module').then(m => m.SystemModule), + title: '系统管理', + }, + { + path: '', + canActivateChild: [authGuard], + component: IndexComponent, + title: '首页', + }, + ], + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class PagesRoutingModule { +} diff --git a/ng-ui/projects/web/src/pages/system/groups/groups.component.css b/ng-ui/projects/web/src/pages/system/groups/groups.component.css new file mode 100644 index 00000000..5d4e87f3 --- /dev/null +++ b/ng-ui/projects/web/src/pages/system/groups/groups.component.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/ng-ui/projects/web/src/pages/system/groups/groups.component.html b/ng-ui/projects/web/src/pages/system/groups/groups.component.html new file mode 100644 index 00000000..41ad0f66 --- /dev/null +++ b/ng-ui/projects/web/src/pages/system/groups/groups.component.html @@ -0,0 +1 @@ +

groups works!

diff --git a/ng-ui/projects/web/src/pages/system/groups/groups.component.ts b/ng-ui/projects/web/src/pages/system/groups/groups.component.ts new file mode 100644 index 00000000..3fcf41a4 --- /dev/null +++ b/ng-ui/projects/web/src/pages/system/groups/groups.component.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'app-groups', + templateUrl: './groups.component.html', + styleUrl: './groups.component.css', +}) +export class GroupsComponent { +} diff --git a/ng-ui/projects/web/src/pages/system/menus/menu-form.component.ts b/ng-ui/projects/web/src/pages/system/menus/menu-form.component.ts new file mode 100644 index 00000000..385f1c82 --- /dev/null +++ b/ng-ui/projects/web/src/pages/system/menus/menu-form.component.ts @@ -0,0 +1,249 @@ +import {Component, EventEmitter, Output} from '@angular/core'; +import {FormBuilder, FormGroup, Validators} from '@angular/forms'; + +@Component({ + selector: 'app-menu-form', + template: ` + +
+ +
+ + 编码 + + + + + Please input your username! + + The username is redundant! + + + + + + 父级 + + + + + Please input your username! + + The username is redundant! + + + + +
+
+ + 租户 + + + + + Please input your username! + + The username is redundant! + + + + + + 类型 + + + + + Please input your username! + + The username is redundant! + + + + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + + + +
+
...
+
+ `, + styles: [], +}) +export class MenuFormComponent { + isVisible = false; + + @Output() event = new EventEmitter<{ + btn: string; + status: number; + data: null; + }>(); + + menuForm: FormGroup; + + constructor(private formBuilder: FormBuilder) { + this.menuForm = this.formBuilder.group({ + id: [null], + code: [null], + pcode: [null], + tenantCode: [null], + type: [null, Validators.required], + authority: [ + null, + Validators.required, + Validators.minLength(6), + Validators.maxLength(64), + ], + name: [null, Validators.required], + path: [ + null, + Validators.required, + Validators.pattern(/^(\/|(\/[a-zA-Z0-9_-]+)+)$/), + ], + sort: [null, Validators.required], + extend: [null], + permissions: [null], + icons: [null], + }); + } + + //表单提交 + onSubmit() { + console.log(this.menuForm.value); + } + + showModal(): void { + this.event.next({btn: 'show', status: 0, data: null}); + this.isVisible = true; + } + + handleOk(): void { + this.event.next({btn: 'ok', status: 200, data: null}); + this.isVisible = false; + } + + handleCancel(): void { + this.event.next({btn: 'cancel', status: -1, data: null}); + this.isVisible = false; + } +} diff --git a/ng-ui/projects/web/src/pages/system/menus/menus.component.html b/ng-ui/projects/web/src/pages/system/menus/menus.component.html new file mode 100644 index 00000000..826ed791 --- /dev/null +++ b/ng-ui/projects/web/src/pages/system/menus/menus.component.html @@ -0,0 +1,66 @@ +
+
+ +
+
+ +
+
+ + + + 编码 + 排序 + 菜单名称 + 权限标识 + 菜单类型 + 组件路径 + 创建时间 + 操作 + + + + + + + + {{ item.code }} + + {{ item.sort }} + {{ item.name }} + {{ item.authority }} + + {item.type, select, + FOLDER {FOLDER} + MENU {MENU} + LINK {LINK} + API {API} + } + + {{ item.path }} + {{ item.createdTime | date: 'short' }} + {{ item.path }} + + + + + + diff --git a/ng-ui/projects/web/src/pages/system/menus/menus.component.scss b/ng-ui/projects/web/src/pages/system/menus/menus.component.scss new file mode 100644 index 00000000..a7855c49 --- /dev/null +++ b/ng-ui/projects/web/src/pages/system/menus/menus.component.scss @@ -0,0 +1,9 @@ +:host { + min-height: 100%; + background-image: linear-gradient( + rgba(0, 0, 255, 0.5), + rgba(255, 255, 0, 0.5) + ); + background-position: center; + background-repeat: no-repeat; +} diff --git a/ng-ui/projects/web/src/pages/system/menus/menus.component.ts b/ng-ui/projects/web/src/pages/system/menus/menus.component.ts new file mode 100644 index 00000000..88bdd93b --- /dev/null +++ b/ng-ui/projects/web/src/pages/system/menus/menus.component.ts @@ -0,0 +1,89 @@ +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {Menu, MenusService} from './menus.service'; +import {Subject, takeUntil} from 'rxjs'; + +@Component({ + selector: 'app-menus', + templateUrl: './menus.component.html', + styleUrls: ['./menus.component.scss'], +}) +export class MenusComponent implements OnInit, OnDestroy { + listMenus: Menu[] = []; + mapOfExpandedData: Record = {}; + + private _subject: Subject = new Subject(); + + constructor(private menusService: MenusService) { + } + + collapse(array: Menu[], data: Menu, $event: boolean): void { + if (!$event) { + if (data.children) { + data.children.forEach(d => { + const target = array.find(a => a.code === d.code); + if (target) { + target.expand = false; + this.collapse(array, target, false); + } + }); + } else { + return; + } + } + } + + convertTreeToList(root: Menu): Menu[] { + const stack: Menu[] = []; + const array: Menu[] = []; + const hashMap = {}; + stack.push({...root, level: 0, expand: false}); + while (stack.length !== 0) { + const node = stack.pop(); + if (!node) { + continue; + } + this.visitNode(node, hashMap, array); + if (node.children) { + for (let i = node.children.length - 1; i >= 0; i--) { + stack.push({ + ...node.children[i], + level: node.level ? node.level + 1 : 1, + expand: false, + parent: node, + }); + } + } + } + + return array; + } + + visitNode(node: Menu, hashMap: Record, array: Menu[]): void { + if (!hashMap[node.code ? node.code : '0']) { + hashMap[node.code ? node.code : '0'] = true; + array.push(node); + } + } + + ngOnInit(): void { + const menuRequest: Menu = { + pcode: '0', + tenantCode: '0', + }; + this.menusService + .getMenus(menuRequest) + .pipe(takeUntil(this._subject)) + .subscribe(result => { + this.listMenus = result; + this.listMenus.forEach(item => { + this.mapOfExpandedData[item.code ? item.code : '0'] = + this.convertTreeToList(item); + }); + }); + } + + ngOnDestroy(): void { + this._subject.next(); + this._subject.complete(); + } +} diff --git a/ng-ui/projects/web/src/pages/system/menus/menus.service.ts b/ng-ui/projects/web/src/pages/system/menus/menus.service.ts new file mode 100644 index 00000000..085958c5 --- /dev/null +++ b/ng-ui/projects/web/src/pages/system/menus/menus.service.ts @@ -0,0 +1,96 @@ +import {Injectable} from '@angular/core'; +import {HttpClient, HttpParams} from '@angular/common/http'; +import {concatMap, delay, from, map, mergeMap, Observable, retry, toArray,} from 'rxjs'; + +export interface Menu { + id?: number; + code?: string; + pcode?: string; + tenantCode?: string; + type?: MenuType; + authority?: string; + name?: string; + path?: string; + sort?: number; + extend?: never; + creator?: UserAuditor; + updater?: UserAuditor; + createdTime?: Date; + updatedTime?: Date; + permissions?: Permission[]; + icons?: string; + children?: Menu[]; + level?: number; + expand?: boolean; + parent?: Menu; +} + +export interface UserAuditor { + code: string; + username: string; + name?: string; +} + +export enum MenuType { + FOLDER = 'FOLDER', + MENU = 'MENU', + LINK = 'LINK', + API = 'API', +} + +export enum HttpMethod { + GET = 'GET', + POST = 'POST', + PUT = 'PUT', + DELETE = 'DELETE', + ALL = 'ALL', +} + +export interface Permission { + method: HttpMethod; + name: string; + path: string; + authority: string; +} + +@Injectable({ + providedIn: 'root', +}) +export class MenusService { + constructor(private http: HttpClient) { + } + + childrenMap = (items: Menu[]) => { + return from(items).pipe( + delay(100), + mergeMap(item => { + return this.getChildren({pcode: item.code}).pipe( + map(children => { + item.children = children; + return item; + }) + ); + }), + retry(3) + ); + }; + + getMenus(request: Menu): Observable { + const params = new HttpParams({fromObject: request as never}); + return this.http + .get('/menus/search', {params: params}) + .pipe(concatMap(this.childrenMap), toArray(), retry(3)); + } + + getMeMenus(request: Menu): Observable { + const params = new HttpParams({fromObject: request as never}); + return this.http + .get('/menus/me', {params: params}) + .pipe(concatMap(this.childrenMap), toArray(), retry(3)); + } + + getChildren(request: Menu): Observable { + const params = new HttpParams({fromObject: request as never}); + return this.http.get('/menus/me', {params: params}); + } +} diff --git a/ng-ui/projects/web/src/pages/system/system.module.ts b/ng-ui/projects/web/src/pages/system/system.module.ts new file mode 100644 index 00000000..8719627d --- /dev/null +++ b/ng-ui/projects/web/src/pages/system/system.module.ts @@ -0,0 +1,19 @@ +import {NgModule} from '@angular/core'; +import {MenusComponent} from './menus/menus.component'; +import {SystemRoutingModule} from './system.routes'; +import {MenuFormComponent} from './menus/menu-form.component'; +import {SharedModule} from '../../shared/shared.module'; +import {GroupsComponent} from './groups/groups.component'; +import {UsersComponent} from './users/users.component'; + +@NgModule({ + declarations: [ + MenusComponent, + MenuFormComponent, + GroupsComponent, + UsersComponent, + ], + imports: [SharedModule, SystemRoutingModule], +}) +export class SystemModule { +} diff --git a/ng-ui/projects/web/src/pages/system/system.routes.ts b/ng-ui/projects/web/src/pages/system/system.routes.ts new file mode 100644 index 00000000..87211644 --- /dev/null +++ b/ng-ui/projects/web/src/pages/system/system.routes.ts @@ -0,0 +1,39 @@ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {MenusComponent} from './menus/menus.component'; +import {authGuard} from '../../core/auth.service'; +import {GroupsComponent} from './groups/groups.component'; +import {UsersComponent} from './users/users.component'; + +const routes: Routes = [ + { + path: 'users', + canActivate: [authGuard], + component: UsersComponent, + title: '用户管理', + }, + { + path: 'menus', + canActivate: [authGuard], + component: MenusComponent, + title: '菜单管理', + }, + { + path: 'groups', + canActivate: [authGuard], + component: GroupsComponent, + title: '角色管理', + }, + { + path: '', + redirectTo: 'menus', + pathMatch: 'full', + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class SystemRoutingModule { +} diff --git a/ng-ui/projects/web/src/pages/system/users/users.component.css b/ng-ui/projects/web/src/pages/system/users/users.component.css new file mode 100644 index 00000000..5d4e87f3 --- /dev/null +++ b/ng-ui/projects/web/src/pages/system/users/users.component.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/ng-ui/projects/web/src/pages/system/users/users.component.ts b/ng-ui/projects/web/src/pages/system/users/users.component.ts new file mode 100644 index 00000000..f7c39f20 --- /dev/null +++ b/ng-ui/projects/web/src/pages/system/users/users.component.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'app-users', + template: `

users works!

`, + styleUrl: './users.component.css', +}) +export class UsersComponent { +} diff --git a/ng-ui/projects/web/src/shared/icons-provider.module.ts b/ng-ui/projects/web/src/shared/icons-provider.module.ts new file mode 100644 index 00000000..191642f0 --- /dev/null +++ b/ng-ui/projects/web/src/shared/icons-provider.module.ts @@ -0,0 +1,12 @@ +import {NgModule} from '@angular/core'; +import {NzIconModule} from 'ng-zorro-antd/icon'; +import {UserOutline} from '@ant-design/icons-angular/icons'; + +const icons = [UserOutline]; + +@NgModule({ + imports: [NzIconModule.forRoot(icons)], + exports: [NzIconModule], +}) +export class IconsProviderModule { +} diff --git a/ng-ui/projects/web/src/shared/message.service.ts b/ng-ui/projects/web/src/shared/message.service.ts new file mode 100644 index 00000000..b091bdcd --- /dev/null +++ b/ng-ui/projects/web/src/shared/message.service.ts @@ -0,0 +1,31 @@ +import {Injectable} from '@angular/core'; +import {NzMessageService} from 'ng-zorro-antd/message'; + +@Injectable({ + providedIn: 'root', +}) +export class MessageService { + options = { + nzDuration: 5000, + nzAnimate: true, + nzPauseOnHover: true, + }; + + constructor(private _message: NzMessageService) { + } + + success(message: string, duration?: number): void { + this.options.nzDuration = duration ? duration : this.options.nzDuration; + this._message.success(message, this.options); + } + + error(message: string, duration?: number): void { + this.options.nzDuration = duration ? duration : this.options.nzDuration; + this._message.error(message, this.options); + } + + warning(message: string, duration?: number): void { + this.options.nzDuration = duration ? duration : this.options.nzDuration; + this._message.warning(message, this.options); + } +} diff --git a/ng-ui/projects/web/src/shared/shared-zorro.module.ts b/ng-ui/projects/web/src/shared/shared-zorro.module.ts new file mode 100644 index 00000000..9316f617 --- /dev/null +++ b/ng-ui/projects/web/src/shared/shared-zorro.module.ts @@ -0,0 +1,28 @@ +import {NgModule} from '@angular/core'; +import {NzSpinModule} from 'ng-zorro-antd/spin'; +import {NzBackTopModule} from 'ng-zorro-antd/back-top'; +import {NzResultModule} from 'ng-zorro-antd/result'; +import {NzBreadCrumbModule} from 'ng-zorro-antd/breadcrumb'; +import {NzButtonModule} from 'ng-zorro-antd/button'; +import {NzTableModule} from 'ng-zorro-antd/table'; +import {NzMessageModule} from 'ng-zorro-antd/message'; +import {NzModalModule} from 'ng-zorro-antd/modal'; +import {NzFormModule} from 'ng-zorro-antd/form'; +import {NzInputModule} from 'ng-zorro-antd/input'; + +@NgModule({ + exports: [ + NzMessageModule, + NzSpinModule, + NzBackTopModule, + NzResultModule, + NzBreadCrumbModule, + NzButtonModule, + NzTableModule, + NzModalModule, + NzFormModule, + NzInputModule, + ], +}) +export class SharedZorroModule { +} diff --git a/ng-ui/projects/web/src/shared/shared.module.ts b/ng-ui/projects/web/src/shared/shared.module.ts new file mode 100644 index 00000000..b272f87c --- /dev/null +++ b/ng-ui/projects/web/src/shared/shared.module.ts @@ -0,0 +1,29 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {SharedZorroModule} from './shared-zorro.module'; +import {RouterModule} from '@angular/router'; +import {IconsProviderModule} from './icons-provider.module'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + RouterModule, + ReactiveFormsModule, + IconsProviderModule, + SharedZorroModule, + ], + declarations: [], + exports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + RouterModule, + SharedZorroModule, + IconsProviderModule, + ], +}) +export class SharedModule { +} diff --git a/ng-ui/projects/web/src/styles.scss b/ng-ui/projects/web/src/styles.scss new file mode 100644 index 00000000..90d4ee00 --- /dev/null +++ b/ng-ui/projects/web/src/styles.scss @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git a/ng-ui/projects/web/tsconfig.app.json b/ng-ui/projects/web/tsconfig.app.json index 93e66fbe..cbacd186 100644 --- a/ng-ui/projects/web/tsconfig.app.json +++ b/ng-ui/projects/web/tsconfig.app.json @@ -5,7 +5,8 @@ "compilerOptions": { "outDir": "../../out-tsc/app", "types": [ - "node" + "node", + "@angular/localize" ] }, "files": [ diff --git a/ng-ui/projects/web/tsconfig.spec.json b/ng-ui/projects/web/tsconfig.spec.json index 0a43b832..3ed421a6 100644 --- a/ng-ui/projects/web/tsconfig.spec.json +++ b/ng-ui/projects/web/tsconfig.spec.json @@ -5,7 +5,8 @@ "compilerOptions": { "outDir": "../../out-tsc/spec", "types": [ - "jasmine" + "jasmine", + "@angular/localize" ] }, "include": [