Browse Source

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

vben 8 months ago
parent
commit
9987451647
100 changed files with 689 additions and 1196 deletions
  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',
+  };
+};

Some files were not shown because too many files changed in this diff