Browse Source

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

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

2
.gitignore

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

2
.prettierignore

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

3
.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
apps/backend-mock/.env

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

5
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
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
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
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
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
apps/backend-mock/api/test.get.ts

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

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

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

14
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);
});

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
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;

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"
}

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

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 }}

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
apps/backend-mock/nitro.config.ts

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

37
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
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>
`;
});

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 {}

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

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;

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

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

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

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);

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);
}
}

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

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

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

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

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);
}
}

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') {}

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

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

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',
};
}),
);
}
}

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

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

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 };

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();

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

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

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 };

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 };

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);
}
}

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 {}

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;
}
}

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,
};
}
}

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;
}
}

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,
};
}
}

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';
}
}

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 {}

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 ?? [];
}
}

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 {}

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

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

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

@ -1 +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}`,
});
}
}

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 };

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 {}

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);
}
}

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 {}

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);
}
}

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 };

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;
}
}

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

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

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

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

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 };

24
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
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
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
apps/web-antd/.env.development

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

4
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
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');
}

2
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';

6
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
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
apps/web-antd/src/apis/demos/index.ts

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

2
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 };

3
apps/web-antd/src/apis/index.ts

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

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 };

4
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 () => {

13
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"
}
}
}

13
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": "平级模式详情"
}
}
}

99
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',

6
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'),
},

4
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

35
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>

8
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
apps/web-antd/src/views/demos/features/breadcrumb/lateral-detail.vue → apps/web-antd/src/views/demos/breadcrumb/lateral-detail.vue

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

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

14
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
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>

4
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
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
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
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
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,
});

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}`,

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
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
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
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
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

Loading…
Cancel
Save