Browse Source

feat: use simpler nitro instead of nestjs to implement mock service

vben 11 tháng trước cách đây
mục cha
commit
9987451647
100 tập tin đã thay đổi với 689 bổ sung1196 xóa
  1. 2 0
      .gitignore
  2. 2 0
      .prettierignore
  3. 2 1
      .vscode/settings.json
  4. 1 0
      apps/backend-mock/.env
  5. 1 4
      apps/backend-mock/README.md
  6. 15 0
      apps/backend-mock/api/auth/codes.ts
  7. 20 0
      apps/backend-mock/api/auth/login.post.ts
  8. 14 0
      apps/backend-mock/api/menu/all.ts
  9. 5 0
      apps/backend-mock/api/status.ts
  10. 1 0
      apps/backend-mock/api/test.get.ts
  11. 1 0
      apps/backend-mock/api/test.post.ts
  12. 14 0
      apps/backend-mock/api/user/info.ts
  13. 0 23
      apps/backend-mock/ecosystem.config.cjs
  14. 7 0
      apps/backend-mock/error.ts
  15. 0 20
      apps/backend-mock/http/auth.http
  16. 0 3
      apps/backend-mock/http/health.http
  17. 0 6
      apps/backend-mock/http/menu.http
  18. 0 10
      apps/backend-mock/nest-cli.json
  19. 6 0
      apps/backend-mock/nitro.config.ts
  20. 3 34
      apps/backend-mock/package.json
  21. 12 0
      apps/backend-mock/routes/[...].ts
  22. 0 34
      apps/backend-mock/src/app.module.ts
  23. 0 8
      apps/backend-mock/src/config/dev.yml
  24. 0 23
      apps/backend-mock/src/config/index.ts
  25. 0 8
      apps/backend-mock/src/config/prod.yml
  26. 0 1
      apps/backend-mock/src/core/decorator/index.ts
  27. 0 4
      apps/backend-mock/src/core/decorator/public.ts
  28. 0 40
      apps/backend-mock/src/core/filter/http-exception.filter.ts
  29. 0 1
      apps/backend-mock/src/core/filter/index.ts
  30. 0 2
      apps/backend-mock/src/core/guard/index.ts
  31. 0 23
      apps/backend-mock/src/core/guard/jwt-auth.guard.ts
  32. 0 5
      apps/backend-mock/src/core/guard/local-auth.guard.ts
  33. 0 1
      apps/backend-mock/src/core/interceptor/index.ts
  34. 0 37
      apps/backend-mock/src/core/interceptor/transform.interceptor.ts
  35. 0 1
      apps/backend-mock/src/core/pipe/index.ts
  36. 0 27
      apps/backend-mock/src/core/pipe/params.pipe.ts
  37. 0 51
      apps/backend-mock/src/main.ts
  38. 0 5
      apps/backend-mock/src/models/dto/auth.dto.ts
  39. 0 9
      apps/backend-mock/src/models/dto/user.dto.ts
  40. 0 21
      apps/backend-mock/src/models/entity/user.entity.ts
  41. 0 59
      apps/backend-mock/src/modules/auth/auth.controller.ts
  42. 0 33
      apps/backend-mock/src/modules/auth/auth.module.ts
  43. 0 94
      apps/backend-mock/src/modules/auth/auth.service.ts
  44. 0 26
      apps/backend-mock/src/modules/auth/jwt.strategy.ts
  45. 0 20
      apps/backend-mock/src/modules/auth/local.strategy.ts
  46. 0 29
      apps/backend-mock/src/modules/auth/refresh-token.strategy.ts
  47. 0 11
      apps/backend-mock/src/modules/health/health.controller.ts
  48. 0 8
      apps/backend-mock/src/modules/health/health.module.ts
  49. 0 157
      apps/backend-mock/src/modules/menu/menu.controller.ts
  50. 0 10
      apps/backend-mock/src/modules/menu/menu.module.ts
  51. 0 4
      apps/backend-mock/src/modules/menu/menu.service.ts
  52. 0 1
      apps/backend-mock/src/modules/mock/mock-db.json
  53. 0 23
      apps/backend-mock/src/modules/mock/mock.controller.ts
  54. 0 13
      apps/backend-mock/src/modules/mock/mock.interface.ts
  55. 0 11
      apps/backend-mock/src/modules/mock/mock.module.ts
  56. 0 80
      apps/backend-mock/src/modules/mock/mock.service.ts
  57. 0 11
      apps/backend-mock/src/modules/users/users.module.ts
  58. 0 18
      apps/backend-mock/src/modules/users/users.service.ts
  59. 0 13
      apps/backend-mock/src/types/config.ts
  60. 0 7
      apps/backend-mock/src/types/express.d.ts
  61. 0 2
      apps/backend-mock/src/types/index.ts
  62. 0 7
      apps/backend-mock/src/types/jwt.ts
  63. 0 5
      apps/backend-mock/src/utils/index.ts
  64. 1 23
      apps/backend-mock/tsconfig.json
  65. 178 0
      apps/backend-mock/utils/mock-data.ts
  66. 17 0
      apps/backend-mock/utils/response.ts
  67. 2 0
      apps/web-antd/.env.development
  68. 2 2
      apps/web-antd/package.json
  69. 17 0
      apps/web-antd/src/apis/core/auth.ts
  70. 1 1
      apps/web-antd/src/apis/core/index.ts
  71. 2 4
      apps/web-antd/src/apis/core/menu.ts
  72. 10 0
      apps/web-antd/src/apis/core/user.ts
  73. 1 0
      apps/web-antd/src/apis/demos/index.ts
  74. 1 1
      apps/web-antd/src/apis/demos/status.ts
  75. 2 1
      apps/web-antd/src/apis/index.ts
  76. 0 28
      apps/web-antd/src/apis/modules/user.ts
  77. 2 2
      apps/web-antd/src/forward/request.ts
  78. 8 5
      apps/web-antd/src/locales/langs/en-US.json
  79. 8 5
      apps/web-antd/src/locales/langs/zh-CN.json
  80. 56 43
      apps/web-antd/src/router/routes/modules/demos.ts
  81. 4 2
      apps/web-antd/src/router/routes/modules/vben.ts
  82. 2 2
      apps/web-antd/src/store/modules/access.ts
  83. 16 19
      apps/web-antd/src/views/demos/access/button-control.vue
  84. 4 4
      apps/web-antd/src/views/demos/access/index.vue
  85. 0 0
      apps/web-antd/src/views/demos/breadcrumb/lateral-detail.vue
  86. 0 0
      apps/web-antd/src/views/demos/breadcrumb/lateral.vue
  87. 0 0
      apps/web-antd/src/views/demos/breadcrumb/level-detail.vue
  88. 9 5
      apps/web-antd/src/views/demos/features/login-expired/index.vue
  89. 86 0
      apps/web-antd/src/views/demos/features/tabs/index.vue
  90. 2 2
      internal/lint-configs/commitlint-config/package.json
  91. 2 0
      internal/lint-configs/eslint-config/src/configs/ignores.ts
  92. 7 0
      internal/lint-configs/eslint-config/src/custom-config.ts
  93. 1 0
      internal/vite-config/package.json
  94. 6 0
      internal/vite-config/src/config/application.ts
  95. 0 2
      internal/vite-config/src/plugins/extra-app-config.ts
  96. 0 1
      internal/vite-config/src/plugins/importmap.ts
  97. 18 0
      internal/vite-config/src/plugins/index.ts
  98. 1 0
      internal/vite-config/src/plugins/inject-metadata.ts
  99. 89 0
      internal/vite-config/src/plugins/nitor-mock.ts
  100. 28 0
      internal/vite-config/src/plugins/print.ts

+ 2 - 0
.gitignore

@@ -5,6 +5,8 @@ dist-ssr
 dist.zip
 dist.tar
 dist.war
+.nitro
+.output
 *-dist.zip
 *-dist.tar
 *-dist.war

+ 2 - 0
.prettierignore

@@ -6,6 +6,8 @@ node_modules
 .nvmrc
 coverage
 CODEOWNERS
+.nitro
+.output
 
 
 **/*.svg

+ 2 - 1
.vscode/settings.json

@@ -191,5 +191,6 @@
     "tailwind.config.mjs": "postcss.*"
   },
   "commentTranslate.hover.enabled": true,
-  "i18n-ally.keystyle": "nested"
+  "i18n-ally.keystyle": "nested",
+  "commentTranslate.multiLineMerge": true
 }

+ 1 - 0
apps/backend-mock/.env

@@ -0,0 +1 @@
+PORT=5320

+ 1 - 4
apps/backend-mock/README.md

@@ -10,9 +10,6 @@ Vben Admin 数据 mock 服务,没有对接任何的数据库,所有数据都
 # development
 $ pnpm run start
 
-# watch mode
-$ pnpm run start:dev
-
 # production mode
-$ pnpm run start:prod
+$ pnpm run build
 ```

+ 15 - 0
apps/backend-mock/api/auth/codes.ts

@@ -0,0 +1,15 @@
+export default eventHandler((event) => {
+  const token = getHeader(event, 'Authorization');
+
+  if (!token) {
+    setResponseStatus(event, 401);
+    return useResponseError('UnauthorizedException', 'Unauthorized Exception');
+  }
+
+  const username = Buffer.from(token, 'base64').toString('utf8');
+
+  const codes =
+    MOCK_CODES.find((item) => item.username === username)?.codes ?? [];
+
+  return useResponseSuccess(codes);
+});

+ 20 - 0
apps/backend-mock/api/auth/login.post.ts

@@ -0,0 +1,20 @@
+export default defineEventHandler(async (event) => {
+  const { password, username } = await readBody(event);
+
+  const findUser = MOCK_USERS.find(
+    (item) => item.username === username && item.password === password,
+  );
+
+  if (!findUser) {
+    setResponseStatus(event, 403);
+    return useResponseError('UnauthorizedException', '用户名或密码错误');
+  }
+
+  const accessToken = Buffer.from(username).toString('base64');
+
+  return useResponseSuccess({
+    accessToken,
+    // TODO: refresh token
+    refreshToken: accessToken,
+  });
+});

+ 14 - 0
apps/backend-mock/api/menu/all.ts

@@ -0,0 +1,14 @@
+export default eventHandler((event) => {
+  const token = getHeader(event, 'Authorization');
+
+  if (!token) {
+    setResponseStatus(event, 401);
+    return useResponseError('UnauthorizedException', 'Unauthorized Exception');
+  }
+
+  const username = Buffer.from(token, 'base64').toString('utf8');
+
+  const menus =
+    MOCK_MENUS.find((item) => item.username === username)?.menus ?? [];
+  return useResponseSuccess(menus);
+});

+ 5 - 0
apps/backend-mock/api/status.ts

@@ -0,0 +1,5 @@
+export default eventHandler((event) => {
+  const { status } = getQuery(event);
+  setResponseStatus(event, Number(status));
+  return useResponseError(`${status}`);
+});

+ 1 - 0
apps/backend-mock/api/test.get.ts

@@ -0,0 +1 @@
+export default defineEventHandler(() => 'Test get handler');

+ 1 - 0
apps/backend-mock/api/test.post.ts

@@ -0,0 +1 @@
+export default defineEventHandler(() => 'Test post handler');

+ 14 - 0
apps/backend-mock/api/user/info.ts

@@ -0,0 +1,14 @@
+export default eventHandler((event) => {
+  const token = getHeader(event, 'Authorization');
+  if (!token) {
+    setResponseStatus(event, 401);
+    return useResponseError('UnauthorizedException', 'Unauthorized Exception');
+  }
+
+  const username = Buffer.from(token, 'base64').toString('utf8');
+
+  const user = MOCK_USERS.find((item) => item.username === username);
+
+  const { password: _pwd, ...userInfo } = user;
+  return useResponseSuccess(userInfo);
+});

+ 0 - 23
apps/backend-mock/ecosystem.config.cjs

@@ -1,23 +0,0 @@
-module.exports = {
-  apps: [
-    {
-      autorestart: true,
-      cwd: './',
-      env: {
-        NODE_ENV: 'production',
-      },
-      env_development: {
-        NODE_ENV: 'development',
-      },
-      env_production: {
-        NODE_ENV: 'production',
-      },
-      ignore_watch: ['node_modules', '.logs', 'dist'],
-      instances: 1,
-      max_memory_restart: '1G',
-      name: '@vben/backend-mock',
-      script: 'node dist/main.js',
-      watch: false,
-    },
-  ],
-};

+ 7 - 0
apps/backend-mock/error.ts

@@ -0,0 +1,7 @@
+import type { NitroErrorHandler } from 'nitropack';
+
+const errorHandler: NitroErrorHandler = function (error, event) {
+  event.res.end(`[error handler] ${error.stack}`);
+};
+
+export default errorHandler;

+ 0 - 20
apps/backend-mock/http/auth.http

@@ -1,20 +0,0 @@
-@port = 5320
-@type = application/json
-@token = Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MCwicm9sZXMiOlsiYWRtaW4iXSwidXNlcm5hbWUiOiJ2YmVuIiwiaWF0IjoxNzE5ODkwMTEwLCJleHAiOjE3MTk5NzY1MTB9.eyAFsQ2Jk_mAQGvrEL1jF9O6YmLZ_PSYj5aokL6fCuU
-POST http://localhost:{{port}}/api/auth/login HTTP/1.1
-content-type: {{ type }}
-
-{
-  "username": "vben",
-  "password": "123456"
-}
-
-
-###
-GET http://localhost:{{port}}/api/auth/getUserInfo HTTP/1.1
-content-type: {{ type }}
-Authorization: {{ token }}
-
-{
-  "username": "vben"
-}

+ 0 - 3
apps/backend-mock/http/health.http

@@ -1,3 +0,0 @@
-@port = 5320
-GET http://localhost:{{port}}/api HTTP/1.1
-content-type: application/json

+ 0 - 6
apps/backend-mock/http/menu.http

@@ -1,6 +0,0 @@
-@port = 5320
-@type = application/json
-@token = Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MCwicm9sZXMiOlsiYWRtaW4iXSwidXNlcm5hbWUiOiJ2YmVuIiwiaWF0IjoxNzE5ODkwMTEwLCJleHAiOjE3MTk5NzY1MTB9.eyAFsQ2Jk_mAQGvrEL1jF9O6YmLZ_PSYj5aokL6fCuU
-GET http://localhost:{{port}}/api/menu/getAll HTTP/1.1
-content-type: {{ type }}
-Authorization: {{ token }}

+ 0 - 10
apps/backend-mock/nest-cli.json

@@ -1,10 +0,0 @@
-{
-  "$schema": "https://json.schemastore.org/nest-cli",
-  "collection": "@nestjs/schematics",
-  "sourceRoot": "src",
-  "compilerOptions": {
-    "assets": ["**/*.yml", "**/*.json"],
-    "watchAssets": true,
-    "deleteOutDir": true
-  }
-}

+ 6 - 0
apps/backend-mock/nitro.config.ts

@@ -0,0 +1,6 @@
+import errorHandler from './error';
+
+export default defineNitroConfig({
+  devErrorHandler: errorHandler,
+  errorHandler: '~/error',
+});

+ 3 - 34
apps/backend-mock/package.json

@@ -6,41 +6,10 @@
   "license": "MIT",
   "author": "",
   "scripts": {
-    "build": "nest build",
-    "dev": "pnpm run start:dev",
-    "start": "cross-env NODE_ENV=development node dist/main",
-    "start:dev": "cross-env NODE_ENV=development DEBUG=true nest start --watch",
-    "start:prod": "nest build && cross-env NODE_ENV=production node dist/main"
+    "start": "nitro dev",
+    "build": "nitro build"
   },
   "dependencies": {
-    "@nestjs/common": "^10.3.10",
-    "@nestjs/config": "^3.2.3",
-    "@nestjs/core": "^10.3.10",
-    "@nestjs/jwt": "^10.2.0",
-    "@nestjs/passport": "^10.0.3",
-    "@nestjs/platform-express": "^10.3.10",
-    "@types/js-yaml": "^4.0.9",
-    "bcryptjs": "^2.4.3",
-    "class-transformer": "^0.5.1",
-    "class-validator": "^0.14.1",
-    "cross-env": "^7.0.3",
-    "joi": "^17.13.3",
-    "js-yaml": "^4.1.0",
-    "mockjs": "^1.1.0",
-    "passport": "^0.7.0",
-    "passport-jwt": "^4.0.1",
-    "passport-local": "^1.0.0",
-    "reflect-metadata": "^0.2.2",
-    "rxjs": "^7.8.1"
-  },
-  "devDependencies": {
-    "@nestjs/cli": "^10.4.2",
-    "@nestjs/schematics": "^10.1.2",
-    "@types/express": "^4.17.21",
-    "@types/mockjs": "^1.0.10",
-    "@types/node": "^20.14.11",
-    "nodemon": "^3.1.4",
-    "ts-node": "^10.9.2",
-    "typescript": "^5.5.3"
+    "nitropack": "latest"
   }
 }

+ 12 - 0
apps/backend-mock/routes/[...].ts

@@ -0,0 +1,12 @@
+export default defineEventHandler(() => {
+  return `
+<h1>Hello Vben Admin</h1>
+<h2>Mock service is starting</h2>
+<ul>
+<li><a href="/api/user">/api/user/info</a></li>
+<li><a href="/api/menu">/api/menu/all</a></li>
+<li><a href="/api/auth/codes">/api/auth/codes</a></li>
+<li><a href="/api/auth/login">/api/auth/login</a></li>
+</ul>
+`;
+});

+ 0 - 34
apps/backend-mock/src/app.module.ts

@@ -1,34 +0,0 @@
-import configuration from '@/config/index';
-import { Module } from '@nestjs/common';
-import { ConfigModule } from '@nestjs/config';
-import Joi from 'joi';
-
-import { AuthModule } from './modules/auth/auth.module';
-import { HealthModule } from './modules/health/health.module';
-import { MenuModule } from './modules/menu/menu.module';
-import { MockModule } from './modules/mock/mock.module';
-import { UsersModule } from './modules/users/users.module';
-
-@Module({
-  imports: [
-    ConfigModule.forRoot({
-      cache: true,
-      isGlobal: true,
-      load: [configuration],
-      validationOptions: {
-        abortEarly: true,
-        allowUnknown: true,
-      },
-      validationSchema: Joi.object({
-        NODE_ENV: Joi.string().valid('development', 'production', 'test'),
-        port: Joi.number(),
-      }),
-    }),
-    HealthModule,
-    AuthModule,
-    UsersModule,
-    MenuModule,
-    MockModule,
-  ],
-})
-export class AppModule {}

+ 0 - 8
apps/backend-mock/src/config/dev.yml

@@ -1,8 +0,0 @@
-NODE_ENV: development
-port: 5320
-apiPrefix: /api
-jwt:
-  secret: plonmGN4aSuMVnucrHuhnUoo49Wy
-  expiresIn: 1d
-  refreshSecret: 1lonmGN4aSuMVnucrHuhnUoo49Wy
-  refreshexpiresIn: 7d

+ 0 - 23
apps/backend-mock/src/config/index.ts

@@ -1,23 +0,0 @@
-import { readFileSync } from 'node:fs';
-import { join } from 'node:path';
-import process from 'node:process';
-
-import * as yaml from 'js-yaml';
-
-const configFileNameObj = {
-  development: 'dev',
-  production: 'prod',
-};
-
-const env = process.env.NODE_ENV;
-
-const configFactory = () => {
-  return yaml.load(
-    readFileSync(
-      join(process.cwd(), 'src', 'config', `${configFileNameObj[env]}.yml`),
-      'utf8',
-    ),
-  ) as Record<string, any>;
-};
-
-export default configFactory;

+ 0 - 8
apps/backend-mock/src/config/prod.yml

@@ -1,8 +0,0 @@
-NODE_ENV: production
-port: 5320
-apiPrefix: /api
-jwt:
-  secret: plonmGN4SuMVnucrHunUoo49Wy12
-  expiresIn: 1d
-  refreshSecret: 2lonmGN4aSuMVnucrHuhnUoo49Wy
-  refreshexpiresIn: 7d

+ 0 - 1
apps/backend-mock/src/core/decorator/index.ts

@@ -1 +0,0 @@
-export * from './public';

+ 0 - 4
apps/backend-mock/src/core/decorator/public.ts

@@ -1,4 +0,0 @@
-import { SetMetadata } from '@nestjs/common';
-
-export const IS_PUBLIC_KEY = 'isPublic';
-export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

+ 0 - 40
apps/backend-mock/src/core/filter/http-exception.filter.ts

@@ -1,40 +0,0 @@
-import {
-  ArgumentsHost,
-  Catch,
-  ExceptionFilter,
-  HttpException,
-  HttpStatus,
-  Logger,
-} from '@nestjs/common';
-import { Request, Response } from 'express';
-
-@Catch(HttpException)
-export class HttpExceptionFilter implements ExceptionFilter {
-  catch(exception: HttpException, host: ArgumentsHost) {
-    const ctx = host.switchToHttp();
-    const response = ctx.getResponse<Response>();
-    const request = ctx.getRequest<Request>();
-    const status =
-      exception instanceof HttpException
-        ? exception.getStatus()
-        : HttpStatus.INTERNAL_SERVER_ERROR;
-
-    const logFormat = `Request original url: ${request.originalUrl} Method: ${request.method} IP: ${request.ip} Status code: ${status} Response: ${exception.toString()}`;
-    Logger.error(logFormat);
-
-    const resultMessage = exception.message as any;
-    const message =
-      resultMessage || `${status >= 500 ? 'Service Error' : 'Client Error'}`;
-
-    const errorResponse = {
-      code: 1,
-      error: resultMessage,
-      message,
-      status,
-      url: request.originalUrl,
-    };
-    response.status(status);
-    response.header('Content-Type', 'application/json; charset=utf-8');
-    response.send(errorResponse);
-  }
-}

+ 0 - 1
apps/backend-mock/src/core/filter/index.ts

@@ -1 +0,0 @@
-export * from './http-exception.filter';

+ 0 - 2
apps/backend-mock/src/core/guard/index.ts

@@ -1,2 +0,0 @@
-export * from './jwt-auth.guard';
-export * from './local-auth.guard';

+ 0 - 23
apps/backend-mock/src/core/guard/jwt-auth.guard.ts

@@ -1,23 +0,0 @@
-import { ExecutionContext, Injectable } from '@nestjs/common';
-import { Reflector } from '@nestjs/core';
-import { AuthGuard } from '@nestjs/passport';
-
-import { IS_PUBLIC_KEY } from '../decorator/index';
-
-@Injectable()
-export class JwtAuthGuard extends AuthGuard('jwt') {
-  constructor(private reflector: Reflector) {
-    super();
-  }
-
-  canActivate(context: ExecutionContext) {
-    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
-      context.getHandler(),
-      context.getClass(),
-    ]);
-    if (isPublic) {
-      return true;
-    }
-    return super.canActivate(context);
-  }
-}

+ 0 - 5
apps/backend-mock/src/core/guard/local-auth.guard.ts

@@ -1,5 +0,0 @@
-import { Injectable } from '@nestjs/common';
-import { AuthGuard } from '@nestjs/passport';
-
-@Injectable()
-export class LocalAuthGuard extends AuthGuard('local') {}

+ 0 - 1
apps/backend-mock/src/core/interceptor/index.ts

@@ -1 +0,0 @@
-export * from './transform.interceptor';

+ 0 - 37
apps/backend-mock/src/core/interceptor/transform.interceptor.ts

@@ -1,37 +0,0 @@
-import {
-  CallHandler,
-  ExecutionContext,
-  Injectable,
-  Logger,
-  NestInterceptor,
-} from '@nestjs/common';
-import { Observable } from 'rxjs';
-import { map } from 'rxjs/operators';
-
-@Injectable()
-export class TransformInterceptor implements NestInterceptor {
-  public intercept(
-    context: ExecutionContext,
-    next: CallHandler,
-  ): Observable<any> {
-    const req = context.getArgByIndex(1).req;
-    return next.handle().pipe(
-      map((data) => {
-        const logFormat = `
-          Request original url: ${req.originalUrl}
-          Method: ${req.method}
-          IP: ${req.ip}
-          User: ${JSON.stringify(req.user)}
-          Response data: ${JSON.stringify(data)}
-         `;
-        Logger.debug(logFormat);
-        return {
-          code: 0,
-          data,
-          error: null,
-          message: 'ok',
-        };
-      }),
-    );
-  }
-}

+ 0 - 1
apps/backend-mock/src/core/pipe/index.ts

@@ -1 +0,0 @@
-export * from './params.pipe';

+ 0 - 27
apps/backend-mock/src/core/pipe/params.pipe.ts

@@ -1,27 +0,0 @@
-import {
-  BadRequestException,
-  HttpStatus,
-  ValidationPipe,
-  type ValidationPipeOptions,
-} from '@nestjs/common';
-
-class ParamsValidationPipe extends ValidationPipe {
-  constructor(options: ValidationPipeOptions = {}) {
-    super({
-      errorHttpStatusCode: HttpStatus.BAD_REQUEST,
-      exceptionFactory: (errors) => {
-        const message = Object.values(errors[0].constraints)[0];
-        return new BadRequestException({
-          message,
-          status: HttpStatus.BAD_REQUEST,
-        });
-      },
-      forbidNonWhitelisted: true,
-      transform: true,
-      whitelist: true,
-      ...options,
-    });
-  }
-}
-
-export { ParamsValidationPipe };

+ 0 - 51
apps/backend-mock/src/main.ts

@@ -1,51 +0,0 @@
-import type { AppConfig } from '@/types';
-
-import process from 'node:process';
-
-import { HttpExceptionFilter } from '@/core/filter';
-import { TransformInterceptor } from '@/core/interceptor';
-import { ParamsValidationPipe } from '@/core/pipe';
-import { type LogLevel } from '@nestjs/common';
-import { ConfigService } from '@nestjs/config';
-import { NestFactory, Reflector } from '@nestjs/core';
-
-import { AppModule } from './app.module';
-import { JwtAuthGuard } from './core/guard';
-
-async function bootstrap() {
-  const debug: LogLevel[] = process.env.DEBUG ? ['debug'] : [];
-  const loggerLevel: LogLevel[] = ['log', 'error', 'warn', ...debug];
-
-  const app = await NestFactory.create(AppModule, {
-    cors: true,
-    logger: loggerLevel,
-  });
-
-  // 获取 ConfigService 实例
-  const configService = app.get(ConfigService);
-
-  // 使用 ConfigService 获取配置值
-  const port = configService.get<AppConfig['port']>('port') || 3000;
-  const apiPrefix = configService.get<AppConfig['apiPrefix']>('apiPrefix');
-
-  // 全局注册拦截器
-  app.useGlobalInterceptors(new TransformInterceptor());
-
-  const reflector = app.get(Reflector);
-  app.useGlobalGuards(new JwtAuthGuard(reflector));
-
-  // 全局注册错误的过滤器
-  app.useGlobalFilters(new HttpExceptionFilter());
-
-  // 设置全局接口数据校验
-  app.useGlobalPipes(new ParamsValidationPipe());
-
-  app.setGlobalPrefix(apiPrefix);
-
-  await app.listen(port);
-
-  console.log(
-    `Application is running on: http://localhost:${port}${apiPrefix}`,
-  );
-}
-bootstrap();

+ 0 - 5
apps/backend-mock/src/models/dto/auth.dto.ts

@@ -1,5 +0,0 @@
-class RefreshTokenDto {
-  refreshToken: string;
-}
-
-export { RefreshTokenDto };

+ 0 - 9
apps/backend-mock/src/models/dto/user.dto.ts

@@ -1,9 +0,0 @@
-class CreateUserDto {
-  id: number;
-  password: string;
-  realName: string;
-  roles: string[];
-  username: string;
-}
-
-export { CreateUserDto };

+ 0 - 21
apps/backend-mock/src/models/entity/user.entity.ts

@@ -1,21 +0,0 @@
-class UserEntity {
-  id: number;
-  /**
-   * 密码
-   */
-  password: string;
-  /**
-   * 真实姓名
-   */
-  realName: string;
-  /**
-   * 角色
-   */
-  roles: string[];
-  /**
-   * 用户名
-   */
-  username: string;
-}
-
-export { UserEntity };

+ 0 - 59
apps/backend-mock/src/modules/auth/auth.controller.ts

@@ -1,59 +0,0 @@
-import type { RefreshTokenDto } from '@/models/dto/auth.dto';
-
-import { Public } from '@/core/decorator';
-import { LocalAuthGuard } from '@/core/guard';
-import {
-  Body,
-  Controller,
-  Get,
-  HttpCode,
-  HttpStatus,
-  Post,
-  Request,
-  UseGuards,
-} from '@nestjs/common';
-
-import { AuthService } from './auth.service';
-
-@Controller('auth')
-export class AuthController {
-  constructor(private authService: AuthService) {}
-
-  /**
-   * 获取用户权限码
-   * @param req
-   */
-  @Get('getAccessCodes')
-  @HttpCode(HttpStatus.OK)
-  async getAccessCodes(@Request() req: Request) {
-    return await this.authService.getAccessCodes(req.user.username);
-  }
-
-  /**
-   * 获取用户信息
-   * @param req
-   */
-  @Get('getUserInfo')
-  @HttpCode(HttpStatus.OK)
-  async getProfile(@Request() req: Request) {
-    return await this.authService.getUserInfo(req.user.username);
-  }
-
-  /**
-   * 用户登录
-   * @param req
-   */
-  @Public()
-  @UseGuards(LocalAuthGuard)
-  @Post('login')
-  @HttpCode(HttpStatus.OK)
-  async login(@Request() req: Request) {
-    return await this.authService.login(req.user);
-  }
-
-  @Post('refreshToken')
-  @HttpCode(HttpStatus.OK)
-  async refreshToken(@Body() refreshTokenDto: RefreshTokenDto) {
-    return this.authService.refresh(refreshTokenDto.refreshToken);
-  }
-}

+ 0 - 33
apps/backend-mock/src/modules/auth/auth.module.ts

@@ -1,33 +0,0 @@
-import type { JwtConfig } from '@/types';
-
-import { Module } from '@nestjs/common';
-import { ConfigService } from '@nestjs/config';
-import { JwtModule } from '@nestjs/jwt';
-
-import { UsersModule } from '../users/users.module';
-import { AuthController } from './auth.controller';
-import { AuthService } from './auth.service';
-import { JwtStrategy } from './jwt.strategy';
-import { LocalStrategy } from './local.strategy';
-import { JwtRefreshStrategy } from './refresh-token.strategy';
-
-@Module({
-  controllers: [AuthController],
-  exports: [AuthService],
-  imports: [
-    UsersModule,
-    JwtModule.registerAsync({
-      global: true,
-      inject: [ConfigService],
-      useFactory: async (configService: ConfigService) => {
-        const { expiresIn, secret } = configService.get<JwtConfig>('jwt');
-        return {
-          secret,
-          signOptions: { expiresIn },
-        };
-      },
-    }),
-  ],
-  providers: [AuthService, JwtStrategy, JwtRefreshStrategy, LocalStrategy],
-})
-export class AuthModule {}

+ 0 - 94
apps/backend-mock/src/modules/auth/auth.service.ts

@@ -1,94 +0,0 @@
-import type { UserEntity } from '@/models/entity/user.entity';
-import type { JwtConfig } from '@/types';
-
-import { UsersService } from '@/modules/users/users.service';
-import { Injectable, UnauthorizedException } from '@nestjs/common';
-import { ConfigService } from '@nestjs/config';
-import { JwtService } from '@nestjs/jwt';
-import bcrypt from 'bcryptjs';
-
-@Injectable()
-export class AuthService {
-  constructor(
-    private usersService: UsersService,
-    private jwtService: JwtService,
-    private configService: ConfigService,
-  ) {}
-
-  /**
-   * get user info
-   * @param username
-   */
-  async getAccessCodes(username: string): Promise<string[]> {
-    const user = await this.usersService.findOne(username);
-
-    const mockCodes = [
-      // super
-      {
-        codes: ['AC_100100', 'AC_100110', 'AC_100120', 'AC_100010'],
-        userId: 0,
-      },
-      {
-        // admin
-        codes: ['AC_100010', 'AC_100020', 'AC_100030'],
-        userId: 1,
-      },
-      {
-        // user
-        codes: ['AC_1000001', 'AC_1000002'],
-        userId: 2,
-      },
-    ];
-
-    return mockCodes.find((item) => item.userId === user.id)?.codes ?? [];
-  }
-
-  async getUserInfo(username: string): Promise<Omit<UserEntity, 'password'>> {
-    const user = await this.usersService.findOne(username);
-    const { password: _pass, ...userInfo } = user;
-    return userInfo;
-  }
-
-  /**
-   * user login
-   */
-  async login(userEntity: UserEntity): Promise<any> {
-    const { id, roles, username } = userEntity;
-
-    const payload = { id, roles, username };
-    const { refreshSecret, refreshexpiresIn } =
-      this.configService.get<JwtConfig>('jwt');
-    return {
-      accessToken: await this.jwtService.signAsync(payload),
-      refreshToken: this.jwtService.sign(payload, {
-        expiresIn: refreshexpiresIn,
-        secret: refreshSecret,
-      }),
-    };
-  }
-
-  async refresh(refreshToken: string) {
-    try {
-      const payload = this.jwtService.verify(refreshToken, {
-        secret: this.configService.get<JwtConfig>('jwt').refreshSecret,
-      });
-      const user = await this.usersService.findOne(payload.username);
-      if (!user) {
-        throw new UnauthorizedException();
-      }
-      return this.login(user);
-    } catch {
-      throw new UnauthorizedException();
-    }
-  }
-
-  async validateUser(username: string, password: string): Promise<any> {
-    const user = await this.usersService.findOne(username);
-    if (user && (await bcrypt.compare(password, user.password))) {
-      // 使用 bcrypt.compare 验证密码
-      const { password: _pass, ...result } = user;
-      return result;
-    }
-    return null;
-  }
-}

+ 0 - 26
apps/backend-mock/src/modules/auth/jwt.strategy.ts

@@ -1,26 +0,0 @@
-import type { JwtConfig, JwtPayload } from '@/types';
-
-import { Injectable } from '@nestjs/common';
-import { ConfigService } from '@nestjs/config';
-import { PassportStrategy } from '@nestjs/passport';
-import { ExtractJwt, Strategy } from 'passport-jwt';
-
-@Injectable()
-export class JwtStrategy extends PassportStrategy(Strategy) {
-  constructor(configService: ConfigService) {
-    super({
-      ignoreExpiration: false,
-      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
-      secretOrKey: configService.get<JwtConfig>('jwt').secret,
-    });
-  }
-
-  async validate(payload: JwtPayload) {
-    console.log('jwt strategy validate payload', payload);
-    return {
-      id: payload.id,
-      roles: payload.roles,
-      username: payload.username,
-    };
-  }
-}

+ 0 - 20
apps/backend-mock/src/modules/auth/local.strategy.ts

@@ -1,20 +0,0 @@
-import { Injectable, UnauthorizedException } from '@nestjs/common';
-import { PassportStrategy } from '@nestjs/passport';
-import { Strategy } from 'passport-local';
-
-import { AuthService } from './auth.service';
-
-@Injectable()
-export class LocalStrategy extends PassportStrategy(Strategy) {
-  constructor(private authService: AuthService) {
-    super();
-  }
-
-  async validate(username: string, password: string): Promise<any> {
-    const user = await this.authService.validateUser(username, password);
-    if (!user) {
-      throw new UnauthorizedException();
-    }
-    return user;
-  }
-}

+ 0 - 29
apps/backend-mock/src/modules/auth/refresh-token.strategy.ts

@@ -1,29 +0,0 @@
-import type { JwtConfig, JwtPayload } from '@/types';
-
-import { Injectable } from '@nestjs/common';
-import { ConfigService } from '@nestjs/config';
-import { PassportStrategy } from '@nestjs/passport';
-import { ExtractJwt, Strategy } from 'passport-jwt';
-
-@Injectable()
-export class JwtRefreshStrategy extends PassportStrategy(
-  Strategy,
-  'jwt-refresh',
-) {
-  constructor(configService: ConfigService) {
-    super({
-      ignoreExpiration: false,
-      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
-      secretOrKey: configService.get<JwtConfig>('jwt').refreshSecret,
-    });
-  }
-
-  async validate(payload: JwtPayload) {
-    console.log('jwt refresh strategy validate payload', payload);
-    return {
-      id: payload.id,
-      roles: payload.roles,
-      username: payload.username,
-    };
-  }
-}

+ 0 - 11
apps/backend-mock/src/modules/health/health.controller.ts

@@ -1,11 +0,0 @@
-import { Public } from '@/core/decorator';
-import { Controller, Get } from '@nestjs/common';
-
-@Controller()
-export class HealthController {
-  @Public()
-  @Get()
-  getHeart(): string {
-    return 'ok';
-  }
-}

+ 0 - 8
apps/backend-mock/src/modules/health/health.module.ts

@@ -1,8 +0,0 @@
-import { Module } from '@nestjs/common';
-
-import { HealthController } from './health.controller';
-
-@Module({
-  controllers: [HealthController],
-})
-export class HealthModule {}

+ 0 - 157
apps/backend-mock/src/modules/menu/menu.controller.ts

@@ -1,157 +0,0 @@
-import { sleep } from '@/utils';
-import { Controller, Get, HttpCode, HttpStatus, Request } from '@nestjs/common';
-
-@Controller('menu')
-export class MenuController {
-  /**
-   *  获取用户所有菜单
-   */
-  @Get('getAll')
-  @HttpCode(HttpStatus.OK)
-  async getAll(@Request() req: Request) {
-    // 模拟请求延迟
-    await sleep(500);
-    // 请求用户的id
-    const userId = req.user.id;
-
-    // TODO: 改为表方式获取
-    const dashboardMenus = [
-      {
-        component: 'BasicLayout',
-        meta: {
-          order: -1,
-          title: 'page.dashboard.title',
-        },
-        name: 'Dashboard',
-        path: '/',
-        redirect: '/analytics',
-        children: [
-          {
-            name: 'Analytics',
-            path: '/analytics',
-            component: '/dashboard/analytics/index',
-            meta: {
-              affixTab: true,
-              title: 'page.dashboard.analytics',
-            },
-          },
-          {
-            name: 'Workspace',
-            path: '/workspace',
-            component: '/dashboard/workspace/index',
-            meta: {
-              title: 'page.dashboard.workspace',
-            },
-          },
-        ],
-      },
-    ];
-
-    const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
-      const roleWithMenus = {
-        admin: {
-          component: '/demos/access/admin-visible',
-          meta: {
-            icon: 'mdi:button-cursor',
-            title: 'page.demos.access.adminVisible',
-          },
-          name: 'AccessAdminVisible',
-          path: 'admin-visible',
-        },
-        super: {
-          component: '/demos/access/super-visible',
-          meta: {
-            icon: 'mdi:button-cursor',
-            title: 'page.demos.access.superVisible',
-          },
-          name: 'AccessSuperVisible',
-          path: 'super-visible',
-        },
-        user: {
-          component: '/demos/access/user-visible',
-          meta: {
-            icon: 'mdi:button-cursor',
-            title: 'page.demos.access.userVisible',
-          },
-          name: 'AccessUserVisible',
-          path: 'user-visible',
-        },
-      };
-
-      return [
-        {
-          component: 'BasicLayout',
-          meta: {
-            icon: 'ic:baseline-view-in-ar',
-            keepAlive: true,
-            order: 1000,
-            title: 'page.demos.title',
-          },
-          name: 'Demos',
-          path: '/demos',
-          redirect: '/access',
-          children: [
-            {
-              name: 'Access',
-              path: '/access',
-              meta: {
-                icon: 'mdi:cloud-key-outline',
-                title: 'page.demos.access.backendPermissions',
-              },
-              redirect: '/access/page-control',
-              children: [
-                {
-                  name: 'AccessPageControl',
-                  path: 'page-control',
-                  component: '/demos/access/index',
-                  meta: {
-                    icon: 'mdi:page-previous-outline',
-                    title: 'page.demos.access.pageAccess',
-                  },
-                },
-                {
-                  name: 'AccessButtonControl',
-                  path: 'button-control',
-                  component: '/demos/access/button-control',
-                  meta: {
-                    icon: 'mdi:button-cursor',
-                    title: 'page.demos.access.buttonControl',
-                  },
-                },
-                {
-                  name: 'AccessMenuVisible403',
-                  path: 'menu-visible-403',
-                  component: '/demos/access/menu-visible-403',
-                  meta: {
-                    authority: ['no-body'],
-                    icon: 'mdi:button-cursor',
-                    menuVisibleWithForbidden: true,
-                    title: 'page.demos.access.menuVisible403',
-                  },
-                },
-                roleWithMenus[role],
-              ],
-            },
-          ],
-        },
-      ];
-    };
-
-    const MOCK_MENUS = [
-      {
-        menus: [...dashboardMenus, ...createDemosMenus('super')],
-        userId: 0,
-      },
-      {
-        menus: [...dashboardMenus, ...createDemosMenus('admin')],
-        userId: 1,
-      },
-      {
-        menus: [...dashboardMenus, ...createDemosMenus('user')],
-        userId: 2,
-      },
-    ];
-
-    return MOCK_MENUS.find((item) => item.userId === userId)?.menus ?? [];
-  }
-}

+ 0 - 10
apps/backend-mock/src/modules/menu/menu.module.ts

@@ -1,10 +0,0 @@
-import { Module } from '@nestjs/common';
-
-import { MenuController } from './menu.controller';
-import { MenuService } from './menu.service';
-
-@Module({
-  controllers: [MenuController],
-  providers: [MenuService],
-})
-export class MenuModule {}

+ 0 - 4
apps/backend-mock/src/modules/menu/menu.service.ts

@@ -1,4 +0,0 @@
-import { Injectable } from '@nestjs/common';
-
-@Injectable()
-export class MenuService {}

+ 0 - 1
apps/backend-mock/src/modules/mock/mock-db.json

@@ -1 +0,0 @@
-{}

+ 0 - 23
apps/backend-mock/src/modules/mock/mock.controller.ts

@@ -1,23 +0,0 @@
-import type { Response } from 'express';
-
-import { Controller, Get, Query, Res } from '@nestjs/common';
-
-@Controller('mock')
-export class MockController {
-  /**
-   * 用于模拟任意的状态码
-   * @param res
-   */
-  @Get('status')
-  async mockAnyStatus(
-    @Res() res: Response,
-    @Query() { status }: { status: string },
-  ) {
-    res.status(Number.parseInt(status, 10)).send({
-      code: 1,
-      data: null,
-      error: null,
-      message: `code is ${status}`,
-    });
-  }
-}

+ 0 - 13
apps/backend-mock/src/modules/mock/mock.interface.ts

@@ -1,13 +0,0 @@
-interface User {
-  id: number;
-  password: string;
-  realName: string;
-  roles: string[];
-  username: string;
-}
-
-interface MockDatabaseData {
-  users: User[];
-}
-
-export type { MockDatabaseData, User };

+ 0 - 11
apps/backend-mock/src/modules/mock/mock.module.ts

@@ -1,11 +0,0 @@
-import { Module } from '@nestjs/common';
-
-import { MockController } from './mock.controller';
-import { MockService } from './mock.service';
-
-@Module({
-  controllers: [MockController],
-  exports: [MockService],
-  providers: [MockService],
-})
-export class MockModule {}

+ 0 - 80
apps/backend-mock/src/modules/mock/mock.service.ts

@@ -1,80 +0,0 @@
-import type { MockDatabaseData } from './mock.interface';
-
-import fs from 'node:fs';
-import path from 'node:path';
-
-import { Injectable, type OnModuleInit } from '@nestjs/common';
-import bcrypt from 'bcryptjs';
-
-@Injectable()
-export class MockService implements OnModuleInit {
-  private data: MockDatabaseData;
-  private readonly filePath: string;
-
-  constructor() {
-    this.filePath = path.join(__dirname, '.', 'mock-db.json');
-    this.loadData();
-  }
-
-  private loadData() {
-    const fileData = fs.readFileSync(this.filePath, 'utf8');
-    this.data = JSON.parse(fileData);
-  }
-
-  private saveData() {
-    fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
-  }
-
-  addItem(collection: string, item: any) {
-    this.data[collection].push(item);
-    this.saveData();
-    return item;
-  }
-
-  clearCollection(collection: string) {
-    this.data[collection] = [];
-    this.saveData();
-    return this.data[collection];
-  }
-
-  findAll(collection: string) {
-    return this.data[collection];
-  }
-
-  findOneById(collection: string, id: number) {
-    return this.data[collection].find((item) => item.id === id);
-  }
-
-  async onModuleInit() {
-    // 清空表,并初始化两条数据
-    await this.clearCollection('users');
-
-    // 密码哈希
-    const hashPassword = await bcrypt.hash('123456', 10);
-
-    await this.addItem('users', {
-      id: 0,
-      password: hashPassword,
-      realName: 'Vben',
-      roles: ['super'],
-      username: 'vben',
-    });
-
-    await this.addItem('users', {
-      id: 1,
-      password: hashPassword,
-      realName: 'Admin',
-      roles: ['admin'],
-      username: 'admin',
-    });
-    await this.addItem('users', {
-      id: 2,
-      password: hashPassword,
-      realName: 'Jack',
-      roles: ['user'],
-      username: 'jack',
-    });
-    const count = await this.findAll('users').length;
-    console.log('Database has been initialized with seed data, count:', count);
-  }
-}

+ 0 - 11
apps/backend-mock/src/modules/users/users.module.ts

@@ -1,11 +0,0 @@
-import { Module } from '@nestjs/common';
-
-import { MockModule } from '../mock/mock.module';
-import { UsersService } from './users.service';
-
-@Module({
-  exports: [UsersService],
-  imports: [MockModule],
-  providers: [UsersService],
-})
-export class UsersModule {}

+ 0 - 18
apps/backend-mock/src/modules/users/users.service.ts

@@ -1,18 +0,0 @@
-import { UserEntity } from '@/models/entity/user.entity';
-import { Injectable } from '@nestjs/common';
-
-import { MockService } from '../mock/mock.service';
-
-@Injectable()
-export class UsersService {
-  constructor(private mockService: MockService) {}
-
-  /**
-   * Find user by username
-   * @param username
-   */
-  async findOne(username: string): Promise<UserEntity | undefined> {
-    const allUsers = await this.mockService.findAll('users');
-    return allUsers.find((user) => user.username === username);
-  }
-}

+ 0 - 13
apps/backend-mock/src/types/config.ts

@@ -1,13 +0,0 @@
-interface AppConfig {
-  NODE_ENV: string;
-  apiPrefix: string;
-  port: number;
-}
-
-interface JwtConfig {
-  expiresIn: string;
-  refreshSecret: string;
-  refreshexpiresIn: string;
-  secret: string;
-}
-export type { AppConfig, JwtConfig };

+ 0 - 7
apps/backend-mock/src/types/express.d.ts

@@ -1,7 +0,0 @@
-import { UserEntity } from '@/models/entity/user.entity';
-
-declare global {
-  interface Request {
-    user?: UserEntity;
-  }
-}

+ 0 - 2
apps/backend-mock/src/types/index.ts

@@ -1,2 +0,0 @@
-export * from './config';
-export * from './jwt';

+ 0 - 7
apps/backend-mock/src/types/jwt.ts

@@ -1,7 +0,0 @@
-interface JwtPayload {
-  id: number;
-  roles: string[];
-  username: string;
-}
-
-export { JwtPayload };

+ 0 - 5
apps/backend-mock/src/utils/index.ts

@@ -1,5 +0,0 @@
-function sleep(ms: number) {
-  return new Promise((resolve) => setTimeout(resolve, ms));
-}
-
-export { sleep };

+ 1 - 23
apps/backend-mock/tsconfig.json

@@ -1,25 +1,3 @@
 {
-  "compilerOptions": {
-    "incremental": true,
-    "target": "ES2021",
-    "emitDecoratorMetadata": true,
-    "experimentalDecorators": true,
-    "baseUrl": "./",
-    "module": "commonjs",
-    "paths": {
-      "@/*": ["./src/*"]
-    },
-    "strictBindCallApply": false,
-    "strictNullChecks": false,
-    "noFallthroughCasesInSwitch": false,
-    "noImplicitAny": false,
-    "declaration": true,
-    "outDir": "./dist",
-    "removeComments": true,
-    "sourceMap": true,
-    "allowSyntheticDefaultImports": true,
-    "esModuleInterop": true,
-    "forceConsistentCasingInFileNames": false,
-    "skipLibCheck": true
-  }
+  "extends": "./.nitro/types/tsconfig.json"
 }

+ 178 - 0
apps/backend-mock/utils/mock-data.ts

@@ -0,0 +1,178 @@
+export const MOCK_USERS = [
+  {
+    id: 0,
+    password: '123456',
+    realName: 'Vben',
+    roles: ['super'],
+    username: 'vben',
+  },
+  {
+    id: 1,
+    password: '123456',
+    realName: 'Admin',
+    roles: ['admin'],
+    username: 'admin',
+  },
+  {
+    id: 2,
+    password: '123456',
+    realName: 'Jack',
+    roles: ['user'],
+    username: 'jack',
+  },
+];
+
+export const MOCK_CODES = [
+  // super
+  {
+    codes: ['AC_100100', 'AC_100110', 'AC_100120', 'AC_100010'],
+    username: 'vben',
+  },
+  {
+    // admin
+    codes: ['AC_100010', 'AC_100020', 'AC_100030'],
+    username: 'admin',
+  },
+  {
+    // user
+    codes: ['AC_1000001', 'AC_1000002'],
+    username: 'jack',
+  },
+];
+
+const dashboardMenus = [
+  {
+    component: 'BasicLayout',
+    meta: {
+      order: -1,
+      title: 'page.dashboard.title',
+    },
+    name: 'Dashboard',
+    path: '/',
+    redirect: '/analytics',
+    children: [
+      {
+        name: 'Analytics',
+        path: '/analytics',
+        component: '/dashboard/analytics/index',
+        meta: {
+          affixTab: true,
+          title: 'page.dashboard.analytics',
+        },
+      },
+      {
+        name: 'Workspace',
+        path: '/workspace',
+        component: '/dashboard/workspace/index',
+        meta: {
+          title: 'page.dashboard.workspace',
+        },
+      },
+    ],
+  },
+];
+
+const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
+  const roleWithMenus = {
+    admin: {
+      component: '/demos/access/admin-visible',
+      meta: {
+        icon: 'mdi:button-cursor',
+        title: 'page.demos.access.adminVisible',
+      },
+      name: 'AccessAdminVisible',
+      path: 'admin-visible',
+    },
+    super: {
+      component: '/demos/access/super-visible',
+      meta: {
+        icon: 'mdi:button-cursor',
+        title: 'page.demos.access.superVisible',
+      },
+      name: 'AccessSuperVisible',
+      path: 'super-visible',
+    },
+    user: {
+      component: '/demos/access/user-visible',
+      meta: {
+        icon: 'mdi:button-cursor',
+        title: 'page.demos.access.userVisible',
+      },
+      name: 'AccessUserVisible',
+      path: 'user-visible',
+    },
+  };
+
+  return [
+    {
+      component: 'BasicLayout',
+      meta: {
+        icon: 'ic:baseline-view-in-ar',
+        keepAlive: true,
+        order: 1000,
+        title: 'page.demos.title',
+      },
+      name: 'Demos',
+      path: '/demos',
+      redirect: '/access',
+      children: [
+        {
+          name: 'Access',
+          path: 'access',
+          meta: {
+            icon: 'mdi:cloud-key-outline',
+            title: 'page.demos.access.backendPermissions',
+          },
+          redirect: '/demos/access/page-control',
+          children: [
+            {
+              name: 'AccessPageControl',
+              path: 'page-control',
+              component: '/demos/access/index',
+              meta: {
+                icon: 'mdi:page-previous-outline',
+                title: 'page.demos.access.pageAccess',
+              },
+            },
+            {
+              name: 'AccessButtonControl',
+              path: 'button-control',
+              component: '/demos/access/button-control',
+              meta: {
+                icon: 'mdi:button-cursor',
+                title: 'page.demos.access.buttonControl',
+              },
+            },
+            {
+              name: 'AccessMenuVisible403',
+              path: 'menu-visible-403',
+              component: '/demos/access/menu-visible-403',
+              meta: {
+                authority: ['no-body'],
+                icon: 'mdi:button-cursor',
+                menuVisibleWithForbidden: true,
+                title: 'page.demos.access.menuVisible403',
+              },
+            },
+            roleWithMenus[role],
+          ],
+        },
+      ],
+    },
+  ];
+};
+
+export const MOCK_MENUS = [
+  {
+    menus: [...dashboardMenus, ...createDemosMenus('super')],
+    username: 'vben',
+  },
+  {
+    menus: [...dashboardMenus, ...createDemosMenus('admin')],
+    username: 'admin',
+  },
+  {
+    menus: [...dashboardMenus, ...createDemosMenus('user')],
+    username: 'user',
+  },
+];

+ 17 - 0
apps/backend-mock/utils/response.ts

@@ -0,0 +1,17 @@
+export function useResponseSuccess<T = any>(data: T) {
+  return {
+    code: 0,
+    data,
+    error: null,
+    message: 'ok',
+  };
+}
+
+export function useResponseError(message: string, error: any = null) {
+  return {
+    code: -1,
+    data: null,
+    error,
+    message,
+  };
+}

+ 2 - 0
apps/web-antd/.env.development

@@ -1,3 +1,5 @@
 VITE_PUBLIC_PATH = /
 
 VITE_GLOB_API_URL=/api
+
+VITE_NITRO_MOCK = true

+ 2 - 2
apps/web-antd/package.json

@@ -16,9 +16,9 @@
   },
   "type": "module",
   "scripts": {
-    "build": "pnpm vite build",
+    "build": "pnpm vite build --mode production",
     "build:analyze": "pnpm vite build --mode analyze",
-    "dev": "pnpm vite",
+    "dev": "pnpm vite --mode development",
     "preview": "vite preview",
     "typecheck": "vue-tsc --noEmit --skipLibCheck"
   },

+ 17 - 0
apps/web-antd/src/apis/core/auth.ts

@@ -0,0 +1,17 @@
+import type { UserApi } from '../types';
+
+import { requestClient } from '#/forward';
+
+/**
+ * 登录
+ */
+export async function login(data: UserApi.LoginParams) {
+  return requestClient.post<UserApi.LoginResult>('/auth/login', data);
+}
+
+/**
+ * 获取用户权限码
+ */
+export async function getAccessCodes() {
+  return requestClient.get<string[]>('/auth/codes');
+}

+ 1 - 1
apps/web-antd/src/apis/modules/index.ts → apps/web-antd/src/apis/core/index.ts

@@ -1,3 +1,3 @@
+export * from './auth';
 export * from './menu';
-export * from './mock';
 export * from './user';

+ 2 - 4
apps/web-antd/src/apis/modules/menu.ts → apps/web-antd/src/apis/core/menu.ts

@@ -5,8 +5,6 @@ import { requestClient } from '#/forward';
 /**
  * 获取用户所有菜单
  */
-async function getAllMenus() {
-  return requestClient.get<RouteRecordStringComponent[]>('/menu/getAll');
+export async function getAllMenus() {
+  return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
 }
-
-export { getAllMenus };

+ 10 - 0
apps/web-antd/src/apis/core/user.ts

@@ -0,0 +1,10 @@
+import type { UserInfo } from '@vben/types';
+
+import { requestClient } from '#/forward';
+
+/**
+ * 获取用户信息
+ */
+export async function getUserInfo() {
+  return requestClient.get<UserInfo>('/user/info');
+}

+ 1 - 0
apps/web-antd/src/apis/demos/index.ts

@@ -0,0 +1 @@
+export * from './status';

+ 1 - 1
apps/web-antd/src/apis/modules/mock.ts → apps/web-antd/src/apis/demos/status.ts

@@ -4,7 +4,7 @@ import { requestClient } from '#/forward';
  * 模拟任意状态码
  */
 async function getMockStatus(status: string) {
-  return requestClient.get('/mock/status', { params: { status } });
+  return requestClient.get('/status', { params: { status } });
 }
 
 export { getMockStatus };

+ 2 - 1
apps/web-antd/src/apis/index.ts

@@ -1,2 +1,3 @@
-export * from './modules';
+export * from './core';
+export * from './demos';
 export type * from './types';

+ 0 - 28
apps/web-antd/src/apis/modules/user.ts

@@ -1,28 +0,0 @@
-import type { UserInfo } from '@vben/types';
-
-import type { UserApi } from '../types';
-
-import { requestClient } from '#/forward';
-
-/**
- * 登录
- */
-async function userLogin(data: UserApi.LoginParams) {
-  return requestClient.post<UserApi.LoginResult>('/auth/login', data);
-}
-
-/**
- * 获取用户信息
- */
-async function getUserInfo() {
-  return requestClient.get<UserInfo>('/auth/getUserInfo');
-}
-
-/**
- * 获取用户权限码
- */
-async function getAccessCodes() {
-  return requestClient.get<string[]>('/auth/getAccessCodes');
-}
-
-export { getAccessCodes, getUserInfo, userLogin };

+ 2 - 2
apps/web-antd/src/forward/request.ts

@@ -25,8 +25,8 @@ function createRequestClient() {
         tokenHandler: () => {
           const accessStore = useAccessStore();
           return {
-            refreshToken: `Bearer ${accessStore.refreshToken}`,
-            token: `Bearer ${accessStore.accessToken}`,
+            refreshToken: `${accessStore.refreshToken}`,
+            token: `${accessStore.accessToken}`,
           };
         },
         unAuthorizedHandler: async () => {

+ 8 - 5
apps/web-antd/src/locales/langs/en-US.json

@@ -38,11 +38,14 @@
         "title": "Features",
         "hideChildrenInMenu": "Hide Menu Children",
         "loginExpired": "Login Expired",
-        "breadcrumbNavigation": "Breadcrumb Navigation",
-        "breadcrumbLateral": "Lateral Mode",
-        "breadcrumbLateralDetail": "Lateral Mode Detail",
-        "breadcrumbLevel": "Level Mode",
-        "breadcrumbLevelDetail": "Level Mode Detail"
+        "tabs": "Tabs"
+      },
+      "breadcrumb": {
+        "navigation": "Breadcrumb Navigation",
+        "lateral": "Lateral Mode",
+        "lateralDetail": "Lateral Mode Detail",
+        "level": "Level Mode",
+        "levelDetail": "Level Mode Detail"
       }
     }
   }

+ 8 - 5
apps/web-antd/src/locales/langs/zh-CN.json

@@ -40,11 +40,14 @@
         "title": "功能",
         "hideChildrenInMenu": "隐藏子菜单",
         "loginExpired": "登录过期",
-        "breadcrumbNavigation": "面包屑导航",
-        "breadcrumbLateral": "平级模式",
-        "breadcrumbLevel": "层级模式",
-        "breadcrumbLevelDetail": "层级模式详情",
-        "breadcrumbLateralDetail": "平级模式详情"
+        "tabs": "标签页"
+      },
+      "breadcrumb": {
+        "navigation": "面包屑导航",
+        "lateral": "平级模式",
+        "level": "层级模式",
+        "levelDetail": "层级模式详情",
+        "lateralDetail": "平级模式详情"
       }
     }
   }

+ 56 - 43
apps/web-antd/src/router/routes/modules/demos.ts

@@ -16,6 +16,7 @@ const routes: RouteRecordRaw[] = [
     path: '/demos',
     redirect: '/demos/access',
     children: [
+      // 权限控制
       {
         meta: {
           icon: 'mdi:shield-key-outline',
@@ -87,6 +88,7 @@ const routes: RouteRecordRaw[] = [
           },
         ],
       },
+      // 功能
       {
         meta: {
           icon: 'mdi:feature-highlight',
@@ -94,8 +96,17 @@ const routes: RouteRecordRaw[] = [
         },
         name: 'Features',
         path: 'features',
-        redirect: '/demos/features/hide-menu-children',
+        redirect: '/demos/features/tabs',
         children: [
+          {
+            name: 'FeatureTabsDemo',
+            path: 'tabs',
+            component: () => import('#/views/demos/features/tabs/index.vue'),
+            meta: {
+              icon: 'lucide:app-window',
+              title: $t('page.demos.features.tabs'),
+            },
+          },
           {
             name: 'HideChildrenInMenuParent',
             path: 'hide-children-in-menu',
@@ -127,62 +138,61 @@ const routes: RouteRecordRaw[] = [
               title: $t('page.demos.features.loginExpired'),
             },
           },
+        ],
+      },
+      // 面包屑导航
+      {
+        name: 'BreadcrumbDemos',
+        path: 'breadcrumb',
+        meta: {
+          icon: 'lucide:navigation',
+          title: $t('page.demos.breadcrumb.navigation'),
+        },
+        redirect: '/demos/breadcrumb/lateral',
+        children: [
+          {
+            name: 'BreadcrumbLateral',
+            path: 'lateral',
+            component: () => import('#/views/demos/breadcrumb/lateral.vue'),
+            meta: {
+              icon: 'lucide:navigation',
+              title: $t('page.demos.breadcrumb.lateral'),
+            },
+          },
+          {
+            name: 'BreadcrumbLateralDetail',
+            path: 'lateral-detail',
+            component: () =>
+              import('#/views/demos/breadcrumb/lateral-detail.vue'),
+            meta: {
+              activePath: '/demos/breadcrumb/lateral',
+              hideInMenu: true,
+              title: $t('page.demos.breadcrumb.lateralDetail'),
+            },
+          },
           {
-            name: 'BreadcrumbDemos',
-            path: 'breadcrumb',
+            name: 'BreadcrumbLevel',
+            path: 'level',
             meta: {
               icon: 'lucide:navigation',
-              title: $t('page.demos.features.breadcrumbNavigation'),
+              title: $t('page.demos.breadcrumb.level'),
             },
+            redirect: '/demos/breadcrumb/level/detail',
             children: [
               {
-                name: 'BreadcrumbLateral',
-                path: 'lateral',
-                component: () =>
-                  import('#/views/demos/features/breadcrumb/lateral.vue'),
-                meta: {
-                  icon: 'lucide:navigation',
-                  title: $t('page.demos.features.breadcrumbLateral'),
-                },
-              },
-              {
-                name: 'BreadcrumbLateralDetail',
-                path: 'lateral-detail',
+                name: 'BreadcrumbLevelDetail',
+                path: 'detail',
                 component: () =>
-                  import(
-                    '#/views/demos/features/breadcrumb/lateral-detail.vue'
-                  ),
+                  import('#/views/demos/breadcrumb/level-detail.vue'),
                 meta: {
-                  activePath: '/demos/features/breadcrumb/lateral',
-                  hideInMenu: true,
-                  title: $t('page.demos.features.breadcrumbLateralDetail'),
+                  title: $t('page.demos.breadcrumb.levelDetail'),
                 },
               },
-              {
-                name: 'BreadcrumbLevel',
-                path: 'level',
-                meta: {
-                  icon: 'lucide:navigation',
-                  title: $t('page.demos.features.breadcrumbLevel'),
-                },
-                children: [
-                  {
-                    name: 'BreadcrumbLevelDetail',
-                    path: 'detail',
-                    component: () =>
-                      import(
-                        '#/views/demos/features/breadcrumb/level-detail.vue'
-                      ),
-                    meta: {
-                      title: $t('page.demos.features.breadcrumbLevelDetail'),
-                    },
-                  },
-                ],
-              },
             ],
           },
         ],
       },
+      // 缺省页
       {
         meta: {
           icon: 'mdi:lightbulb-error-outline',
@@ -231,6 +241,7 @@ const routes: RouteRecordRaw[] = [
           },
         ],
       },
+      // 菜单徽标
       {
         meta: {
           badgeType: 'dot',
@@ -275,6 +286,7 @@ const routes: RouteRecordRaw[] = [
           },
         ],
       },
+      // 外部链接
       {
         meta: {
           icon: 'ic:round-settings-input-composite',
@@ -350,6 +362,7 @@ const routes: RouteRecordRaw[] = [
           },
         ],
       },
+      // 嵌套菜单
       {
         meta: {
           icon: 'ic:round-menu',

+ 4 - 2
apps/web-antd/src/router/routes/modules/vben.ts

@@ -10,11 +10,12 @@ const routes: RouteRecordRaw[] = [
     component: BasicLayout,
     meta: {
       badgeType: 'dot',
+      badgeVariants: 'destructive',
       icon: VBEN_LOGO_URL,
       order: 9999,
-      title: 'Vben',
+      title: $t('page.vben.title'),
     },
-    name: 'AboutLayout',
+    name: 'VbenProject',
     path: '/vben-admin',
     redirect: '/vben-admin/about',
     children: [
@@ -24,6 +25,7 @@ const routes: RouteRecordRaw[] = [
         component: () => import('#/views/_core/vben/about/index.vue'),
         meta: {
           badgeType: 'dot',
+          badgeVariants: 'destructive',
           icon: 'lucide:copyright',
           title: $t('page.vben.about'),
         },

+ 2 - 2
apps/web-antd/src/store/modules/access.ts

@@ -11,7 +11,7 @@ import { useCoreAccessStore } from '@vben-core/stores';
 import { notification } from 'ant-design-vue';
 import { defineStore } from 'pinia';
 
-import { getAccessCodes, getUserInfo, userLogin } from '#/apis';
+import { getAccessCodes, getUserInfo, login } from '#/apis';
 import { $t } from '#/locales';
 
 export const useAccessStore = defineStore('access', () => {
@@ -53,7 +53,7 @@ export const useAccessStore = defineStore('access', () => {
     let userInfo: UserInfo | null = null;
     try {
       loading.value = true;
-      const { accessToken, refreshToken } = await userLogin(params);
+      const { accessToken, refreshToken } = await login(params);
 
       // 如果成功获取到 accessToken
       // If accessToken is successfully obtained

+ 16 - 19
apps/web-antd/src/views/demos/access/button-control.vue

@@ -57,9 +57,9 @@ async function changeAccount(role: string) {
       <div class="text-foreground/80 mt-2">切换不同的账号,观察按钮变化。</div>
     </div>
 
-    <div class="card-box mt-5 p-5 font-semibold">
+    <div class="card-box mt-5 p-5">
       <div class="mb-3">
-        <span class="text-lg">当前角色:</span>
+        <span class="text-lg font-semibold">当前角色:</span>
         <span class="text-primary mx-4 text-lg">
           {{ accessStore.userRoles?.[0] }}
         </span>
@@ -81,45 +81,42 @@ async function changeAccount(role: string) {
       </Button>
     </div>
 
-    <div class="card-box mt-5 p-5 font-semibold">
-      <div class="mb-3 text-lg">组件形式控制 - 权限码方式</div>
-      <AccessControl :permissions="['AC_100100']" type="code">
+    <div class="card-box mt-5 p-5">
+      <div class="mb-3 text-lg font-semibold">组件形式控制 - 权限码方式</div>
+      <AccessControl :codes="['AC_100100']" type="code">
         <Button class="mr-4"> Super 账号可见 ["AC_1000001"] </Button>
       </AccessControl>
-      <AccessControl :permissions="['AC_100030']" type="code">
+      <AccessControl :codes="['AC_100030']" type="code">
         <Button class="mr-4"> Admin 账号可见 ["AC_100010"] </Button>
       </AccessControl>
-      <AccessControl :permissions="['AC_1000001']" type="code">
+      <AccessControl :codes="['AC_1000001']" type="code">
         <Button class="mr-4"> User 账号可见 ["AC_1000001"] </Button>
       </AccessControl>
-      <AccessControl :permissions="['AC_100100', 'AC_100010']" type="code">
+      <AccessControl :codes="['AC_100100', 'AC_100010']" type="code">
         <Button class="mr-4">
           Super & Admin 账号可见 ["AC_100100","AC_1000001"]
         </Button>
       </AccessControl>
     </div>
 
-    <div
-      v-if="accessMode === 'frontend'"
-      class="card-box mt-5 p-5 font-semibold"
-    >
-      <div class="mb-3 text-lg">组件形式控制 - 用户角色方式</div>
-      <AccessControl :permissions="['super']">
+    <div v-if="accessMode === 'frontend'" class="card-box mt-5 p-5">
+      <div class="mb-3 text-lg font-semibold">组件形式控制 - 用户角色方式</div>
+      <AccessControl :codes="['super']">
         <Button class="mr-4"> Super 角色可见 </Button>
       </AccessControl>
-      <AccessControl :permissions="['admin']">
+      <AccessControl :codes="['admin']">
         <Button class="mr-4"> Admin 角色可见 </Button>
       </AccessControl>
-      <AccessControl :permissions="['user']">
+      <AccessControl :codes="['user']">
         <Button class="mr-4"> User 角色可见 </Button>
       </AccessControl>
-      <AccessControl :permissions="['super', 'admin']">
+      <AccessControl :codes="['super', 'admin']">
         <Button class="mr-4"> Super & Admin 角色可见 </Button>
       </AccessControl>
     </div>
 
-    <div class="card-box mt-5 p-5 font-semibold">
-      <div class="mb-3 text-lg">函数形式控制</div>
+    <div class="card-box mt-5 p-5">
+      <div class="mb-3 text-lg font-semibold">函数形式控制</div>
       <Button v-if="hasAccessByCodes(['AC_100100'])" class="mr-4">
         Super 账号可见 ["AC_1000001"]
       </Button>

+ 4 - 4
apps/web-antd/src/views/demos/access/index.vue

@@ -67,8 +67,8 @@ async function handleToggleAccessMode() {
       </div>
     </div>
 
-    <div class="card-box mt-5 p-5 font-semibold">
-      <span class="text-lg">当前权限模式:</span>
+    <div class="card-box mt-5 p-5">
+      <span class="text-lg font-semibold">当前权限模式:</span>
       <span class="text-primary mx-4">{{
         accessMode === 'frontend' ? '前端权限控制' : '后端权限控制'
       }}</span>
@@ -76,9 +76,9 @@ async function handleToggleAccessMode() {
         切换为{{ accessMode === 'frontend' ? '后端' : '前端' }}权限模式
       </Button>
     </div>
-    <div class="card-box mt-5 p-5 font-semibold">
+    <div class="card-box mt-5 p-5">
       <div class="mb-3">
-        <span class="text-lg">当前账号:</span>
+        <span class="text-lg font-semibold">当前账号:</span>
         <span class="text-primary mx-4 text-lg">
           {{ accessStore.userRoles?.[0] }}
         </span>

+ 0 - 0
apps/web-antd/src/views/demos/features/breadcrumb/lateral-detail.vue → apps/web-antd/src/views/demos/breadcrumb/lateral-detail.vue


+ 0 - 0
apps/web-antd/src/views/demos/features/breadcrumb/lateral.vue → apps/web-antd/src/views/demos/breadcrumb/lateral.vue


+ 0 - 0
apps/web-antd/src/views/demos/features/breadcrumb/level-detail.vue → apps/web-antd/src/views/demos/breadcrumb/level-detail.vue


+ 9 - 5
apps/web-antd/src/views/demos/features/login-expired/index.vue

@@ -23,17 +23,21 @@ async function handleClick(type: LoginExpiredModeType) {
     <div class="card-box p-5">
       <h1 class="text-xl font-semibold">登录过期演示</h1>
       <div class="text-foreground/80 mt-2">
-        401状态码转到登录页,登录成功后跳转回原页面。
+        接口请求遇到401状态码时,需要重新登录。有两种方式:
+        <div>1.转到登录页,登录成功后跳转回原页面</div>
+        <div>
+          2.弹出重新登录弹窗,登录后关闭弹窗,不进行任何页面跳转(刷新后调整登录页面)
+        </div>
       </div>
     </div>
 
-    <div class="card-box mt-5 p-5 font-semibold">
-      <div class="mb-3 text-lg">跳转登录页面方式</div>
+    <div class="card-box mt-5 p-5">
+      <div class="mb-3 text-lg font-semibold">跳转登录页面方式</div>
       <Button type="primary" @click="handleClick('page')"> 点击触发 </Button>
     </div>
 
-    <div class="card-box mt-5 p-5 font-semibold">
-      <div class="mb-3 text-lg">登录弹窗方式</div>
+    <div class="card-box mt-5 p-5">
+      <div class="mb-3 text-lg font-semibold">登录弹窗方式</div>
       <Button type="primary" @click="handleClick('modal')"> 点击触发 </Button>
     </div>
   </div>

+ 86 - 0
apps/web-antd/src/views/demos/features/tabs/index.vue

@@ -0,0 +1,86 @@
+<script lang="ts" setup>
+import { useRouter } from 'vue-router';
+
+import { useTabs } from '@vben/hooks';
+
+import { Button } from 'ant-design-vue';
+
+defineOptions({ name: 'FeatureTabsDemo' });
+
+const router = useRouter();
+// const newTabTitle = ref('');
+const {
+  closeAllTabs,
+  closeCurrentTab,
+  closeLeftTabs,
+  closeOtherTabs,
+  closeRightTabs,
+  closeTabByKey,
+  refreshTab,
+} = useTabs();
+
+function openTab() {
+  // 这里就是路由跳转,也可以用path
+  router.push({ name: 'VbenAbout' });
+}
+</script>
+
+<template>
+  <div class="p-5">
+    <div class="card-box p-5">
+      <h1 class="text-xl font-semibold">标签页</h1>
+      <div class="text-foreground/80 mt-2">用于需要操作标签页的场景</div>
+    </div>
+
+    <div class="card-box mt-5 p-5">
+      <div class="text-lg font-semibold">打开/关闭标签页</div>
+      <div class="text-foreground/80 my-3">
+        如果标签页存在,直接跳转切换。如果标签页不存在,则打开新的标签页。
+      </div>
+      <div class="flex flex-wrap gap-3">
+        <Button type="primary" @click="openTab"> 打开 "关于" 标签页 </Button>
+        <Button type="primary" @click="closeTabByKey('/vben-admin/about')">
+          关闭 "关于" 标签页
+        </Button>
+      </div>
+    </div>
+
+    <div class="card-box mt-5 p-5">
+      <div class="text-lg font-semibold">标签页操作</div>
+      <div class="text-foreground/80 my-3">用于动态控制标签页的各种操作</div>
+      <div class="flex flex-wrap gap-3">
+        <Button type="primary" @click="closeCurrentTab()">
+          关闭当前标签页
+        </Button>
+        <Button type="primary" @click="closeLeftTabs()">
+          关闭左侧标签页
+        </Button>
+        <Button type="primary" @click="closeRightTabs()">
+          关闭右侧标签页
+        </Button>
+        <Button type="primary" @click="closeAllTabs()"> 打开所有标签页 </Button>
+        <Button type="primary" @click="closeOtherTabs()">
+          关闭其他标签页
+        </Button>
+        <Button type="primary" @click="refreshTab()"> 刷新当前标签页 </Button>
+      </div>
+    </div>
+
+    <div class="card-box mt-5 p-5">
+      <div class="text-lg font-semibold">动态标题</div>
+      <div class="text-foreground/80 my-3">
+        该操作不会影响页面标题,仅修改Tab标题
+      </div>
+      <!-- <div class="flex flex-wrap items-center gap-3">
+        <Input
+          v-model="newTabTitle"
+          class="w-30"
+          placeholder="请输入新的标题"
+        />
+        <Button type="primary" @click="closeCurrentTab()">
+          关闭当前标签页 {{ newTabTitle }}
+        </Button>
+      </div> -->
+    </div>
+  </div>
+</template>

+ 2 - 2
internal/lint-configs/commitlint-config/package.json

@@ -32,7 +32,7 @@
     "@commitlint/config-conventional": "^19.2.2",
     "@vben/node-utils": "workspace:*",
     "commitlint-plugin-function-rules": "^4.0.0",
-    "cz-git": "^1.9.3",
-    "czg": "^1.9.3"
+    "cz-git": "^1.9.4",
+    "czg": "^1.9.4"
   }
 }

+ 2 - 0
internal/lint-configs/eslint-config/src/configs/ignores.ts

@@ -9,6 +9,8 @@ export async function ignores(): Promise<Linter.FlatConfig[]> {
         '**/dist-*',
         '**/*-dist',
         '**/.husky',
+        '**/.nitro',
+        '**/.output',
         '**/Dockerfile',
         '**/package-lock.json',
         '**/yarn.lock',

+ 7 - 0
internal/lint-configs/eslint-config/src/custom-config.ts

@@ -121,10 +121,17 @@ const customConfig: Linter.FlatConfig[] = [
     files: ['apps/backend-mock/**/**'],
     rules: {
       '@typescript-eslint/no-extraneous-class': 'off',
+      'n/prefer-global/buffer': 'off',
       'no-console': 'off',
       'unicorn/prefer-module': 'off',
     },
   },
+  {
+    files: ['internal/**/**'],
+    rules: {
+      'no-console': 'off',
+    },
+  },
 ];
 
 export { customConfig };

+ 1 - 0
internal/vite-config/package.json

@@ -31,6 +31,7 @@
     "@jspm/generator": "^2.1.2",
     "cheerio": "1.0.0-rc.12",
     "html-minifier-terser": "^7.2.0",
+    "nitropack": "^2.9.7",
     "resolve.exports": "^2.0.2",
     "vite-plugin-lib-inject-css": "^2.1.1",
     "vite-plugin-pwa": "^0.20.0",

+ 6 - 0
internal/vite-config/src/config/application.ts

@@ -33,6 +33,12 @@ function defineApplicationConfig(userConfigPromise: DefineApplicationOptions) {
       isBuild,
       license: true,
       mode,
+      nitroMock: !isBuild,
+      nitroMockOptions: {},
+      print: !isBuild,
+      printInfoMap: {
+        'Vben Admin Docs': 'https://docs.vben.pro',
+      },
       pwa: true,
       ...application,
     });

+ 0 - 2
internal/vite-config/src/plugins/extra-app-config.ts

@@ -47,10 +47,8 @@ async function viteExtraAppConfigPlugin({
           type: 'asset',
         });
 
-        // eslint-disable-next-line no-console
         console.log(colors.cyan(`✨configuration file is build successfully!`));
       } catch (error) {
-        // eslint-disable-next-line no-console
         console.log(
           colors.red(
             `configuration file configuration file failed to package:\n${error}`,

+ 0 - 1
internal/vite-config/src/plugins/importmap.ts

@@ -70,7 +70,6 @@ async function viteImportMapPlugin(
   if (options?.debug) {
     (async () => {
       for await (const { message, type } of generator.logStream()) {
-        // eslint-disable-next-line no-console
         console.log(`${type}: ${message}`);
       }
     })();

+ 18 - 0
internal/vite-config/src/plugins/index.ts

@@ -23,6 +23,8 @@ import { viteImportMapPlugin } from './importmap';
 import { viteInjectAppLoadingPlugin } from './inject-app-loading';
 import { viteMetadataPlugin } from './inject-metadata';
 import { viteLicensePlugin } from './license';
+import { viteNitroMockPlugin } from './nitor-mock';
+import { vitePrintPlugin } from './print';
 
 /**
  * 获取条件成立的 vite 插件
@@ -99,6 +101,10 @@ async function loadApplicationPlugins(
     importmapOptions,
     injectAppLoading,
     license,
+    nitroMock,
+    nitroMockOptions,
+    print,
+    printInfoMap,
     pwa,
     pwaOptions,
     ...commonOptions
@@ -120,6 +126,18 @@ async function loadApplicationPlugins(
         ];
       },
     },
+    {
+      condition: print,
+      plugins: async () => {
+        return [await vitePrintPlugin({ infoMap: printInfoMap })];
+      },
+    },
+    {
+      condition: nitroMock,
+      plugins: async () => {
+        return [await viteNitroMockPlugin(nitroMockOptions)];
+      },
+    },
     {
       condition: injectAppLoading,
       plugins: async () => [await viteInjectAppLoadingPlugin(!!isBuild, env)],

+ 1 - 0
internal/vite-config/src/plugins/inject-metadata.ts

@@ -15,6 +15,7 @@ function resolvePackageVersion(
 
 async function resolveMonorepoDependencies() {
   const { packages } = await getPackages();
+
   const resultDevDependencies: Record<string, string> = {};
   const resultDependencies: Record<string, string> = {};
   const pkgsMeta: Record<string, string> = {};

+ 89 - 0
internal/vite-config/src/plugins/nitor-mock.ts

@@ -0,0 +1,89 @@
+import type { PluginOption } from 'vite';
+
+import type { NitroMockPluginOptions } from '../typing';
+
+import { colors, consola, getPackage } from '@vben/node-utils';
+
+import { build, createDevServer, createNitro, prepare } from 'nitropack';
+
+const hmrKeyRe = /^runtimeConfig\.|routeRules\./;
+
+export const viteNitroMockPlugin = ({
+  mockServerPackage = '@vben/backend-mock',
+  port = 5320,
+  verbose = true,
+}: NitroMockPluginOptions = {}): PluginOption => {
+  return {
+    async configureServer(server) {
+      const pkg = await getPackage(mockServerPackage);
+      if (!pkg) {
+        consola.error(`Package ${mockServerPackage} not found.`);
+        return;
+      }
+
+      runNitroServer(pkg.dir, port, verbose);
+
+      const _printUrls = server.printUrls;
+      server.printUrls = () => {
+        _printUrls();
+
+        consola.log(
+          `  ${colors.green('➜')}  ${colors.bold('Nitro Mock Server')}: ${colors.cyan(`http://localhost:${port}/api`)}`,
+        );
+      };
+    },
+    enforce: 'pre',
+    name: 'vite:mock-server',
+  };
+};
+
+async function runNitroServer(rootDir: string, port: number, verbose: boolean) {
+  let nitro: any;
+  const reload = async () => {
+    if (nitro) {
+      consola.info('Restarting dev server...');
+      if ('unwatch' in nitro.options._c12) {
+        await nitro.options._c12.unwatch();
+      }
+      await nitro.close();
+    }
+    nitro = await createNitro(
+      {
+        dev: true,
+        preset: 'nitro-dev',
+        rootDir,
+      },
+      {
+        c12: {
+          async onUpdate({ getDiff, newConfig }) {
+            const diff = getDiff();
+            if (diff.length === 0) {
+              return;
+            }
+            verbose &&
+              consola.info(
+                `Nitro config updated:\n${diff
+                  .map((entry) => `  ${entry.toString()}`)
+                  .join('\n')}`,
+              );
+            await (diff.every((e) => hmrKeyRe.test(e.key))
+              ? nitro.updateConfig(newConfig.config)
+              : reload());
+          },
+        },
+        watch: true,
+      },
+    );
+    nitro.hooks.hookOnce('restart', reload);
+    const server = createDevServer(nitro);
+    await server.listen(port, { showURL: false });
+    await prepare(nitro);
+    await build(nitro);
+
+    if (verbose) {
+      console.log('');
+      consola.success(colors.bold(colors.green('Nitro Mock Server started.')));
+    }
+  };
+  await reload();
+}

+ 28 - 0
internal/vite-config/src/plugins/print.ts

@@ -0,0 +1,28 @@
+import type { PluginOption } from 'vite';
+
+import type { PrintPluginOptions } from '../typing';
+
+import { colors } from '@vben/node-utils';
+
+export const vitePrintPlugin = (
+  options: PrintPluginOptions = {},
+): PluginOption => {
+  const { infoMap = {} } = options;
+
+  return {
+    configureServer(server) {
+      const _printUrls = server.printUrls;
+      server.printUrls = () => {
+        _printUrls();
+
+        for (const [key, value] of Object.entries(infoMap)) {
+          console.log(
+            `  ${colors.green('➜')}  ${colors.bold(key)}: ${colors.cyan(value)}`,
+          );
+        }
+      };
+    },
+    enforce: 'pre',
+    name: 'vite:print-info',
+  };
+};

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác