mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-02-02 18:28:40 +08:00
feat: use simpler nitro instead of nestjs to implement mock service
This commit is contained in:
parent
9ec91ac16d
commit
9987451647
2
.gitignore
vendored
2
.gitignore
vendored
@ -5,6 +5,8 @@ dist-ssr
|
|||||||
dist.zip
|
dist.zip
|
||||||
dist.tar
|
dist.tar
|
||||||
dist.war
|
dist.war
|
||||||
|
.nitro
|
||||||
|
.output
|
||||||
*-dist.zip
|
*-dist.zip
|
||||||
*-dist.tar
|
*-dist.tar
|
||||||
*-dist.war
|
*-dist.war
|
||||||
|
@ -6,6 +6,8 @@ node_modules
|
|||||||
.nvmrc
|
.nvmrc
|
||||||
coverage
|
coverage
|
||||||
CODEOWNERS
|
CODEOWNERS
|
||||||
|
.nitro
|
||||||
|
.output
|
||||||
|
|
||||||
|
|
||||||
**/*.svg
|
**/*.svg
|
||||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -191,5 +191,6 @@
|
|||||||
"tailwind.config.mjs": "postcss.*"
|
"tailwind.config.mjs": "postcss.*"
|
||||||
},
|
},
|
||||||
"commentTranslate.hover.enabled": true,
|
"commentTranslate.hover.enabled": true,
|
||||||
"i18n-ally.keystyle": "nested"
|
"i18n-ally.keystyle": "nested",
|
||||||
|
"commentTranslate.multiLineMerge": true
|
||||||
}
|
}
|
||||||
|
1
apps/backend-mock/.env
Normal file
1
apps/backend-mock/.env
Normal file
@ -0,0 +1 @@
|
|||||||
|
PORT=5320
|
@ -10,9 +10,6 @@ Vben Admin 数据 mock 服务,没有对接任何的数据库,所有数据都
|
|||||||
# development
|
# development
|
||||||
$ pnpm run start
|
$ pnpm run start
|
||||||
|
|
||||||
# watch mode
|
|
||||||
$ pnpm run start:dev
|
|
||||||
|
|
||||||
# production mode
|
# production mode
|
||||||
$ pnpm run start:prod
|
$ pnpm run build
|
||||||
```
|
```
|
||||||
|
15
apps/backend-mock/api/auth/codes.ts
Normal file
15
apps/backend-mock/api/auth/codes.ts
Normal file
@ -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
Normal file
20
apps/backend-mock/api/auth/login.post.ts
Normal file
@ -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
Normal file
14
apps/backend-mock/api/menu/all.ts
Normal file
@ -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
Normal file
5
apps/backend-mock/api/status.ts
Normal file
@ -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
Normal file
1
apps/backend-mock/api/test.get.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default defineEventHandler(() => 'Test get handler');
|
1
apps/backend-mock/api/test.post.ts
Normal file
1
apps/backend-mock/api/test.post.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default defineEventHandler(() => 'Test post handler');
|
14
apps/backend-mock/api/user/info.ts
Normal file
14
apps/backend-mock/api/user/info.ts
Normal file
@ -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);
|
||||||
|
});
|
@ -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
Normal file
7
apps/backend-mock/error.ts
Normal file
@ -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;
|
@ -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"
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
@port = 5320
|
|
||||||
GET http://localhost:{{port}}/api HTTP/1.1
|
|
||||||
content-type: application/json
|
|
@ -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 }}
|
|
@ -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
Normal file
6
apps/backend-mock/nitro.config.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import errorHandler from './error';
|
||||||
|
|
||||||
|
export default defineNitroConfig({
|
||||||
|
devErrorHandler: errorHandler,
|
||||||
|
errorHandler: '~/error',
|
||||||
|
});
|
@ -6,41 +6,10 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "",
|
"author": "",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nest build",
|
"start": "nitro dev",
|
||||||
"dev": "pnpm run start:dev",
|
"build": "nitro build"
|
||||||
"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"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^10.3.10",
|
"nitropack": "latest"
|
||||||
"@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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
apps/backend-mock/routes/[...].ts
Normal file
12
apps/backend-mock/routes/[...].ts
Normal file
@ -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>
|
||||||
|
`;
|
||||||
|
});
|
@ -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 {}
|
|
@ -1,8 +0,0 @@
|
|||||||
NODE_ENV: development
|
|
||||||
port: 5320
|
|
||||||
apiPrefix: /api
|
|
||||||
jwt:
|
|
||||||
secret: plonmGN4aSuMVnucrHuhnUoo49Wy
|
|
||||||
expiresIn: 1d
|
|
||||||
refreshSecret: 1lonmGN4aSuMVnucrHuhnUoo49Wy
|
|
||||||
refreshexpiresIn: 7d
|
|
@ -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;
|
|
@ -1,8 +0,0 @@
|
|||||||
NODE_ENV: production
|
|
||||||
port: 5320
|
|
||||||
apiPrefix: /api
|
|
||||||
jwt:
|
|
||||||
secret: plonmGN4SuMVnucrHunUoo49Wy12
|
|
||||||
expiresIn: 1d
|
|
||||||
refreshSecret: 2lonmGN4aSuMVnucrHuhnUoo49Wy
|
|
||||||
refreshexpiresIn: 7d
|
|
@ -1 +0,0 @@
|
|||||||
export * from './public';
|
|
@ -1,4 +0,0 @@
|
|||||||
import { SetMetadata } from '@nestjs/common';
|
|
||||||
|
|
||||||
export const IS_PUBLIC_KEY = 'isPublic';
|
|
||||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
|
@ -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 +0,0 @@
|
|||||||
export * from './http-exception.filter';
|
|
@ -1,2 +0,0 @@
|
|||||||
export * from './jwt-auth.guard';
|
|
||||||
export * from './local-auth.guard';
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class LocalAuthGuard extends AuthGuard('local') {}
|
|
@ -1 +0,0 @@
|
|||||||
export * from './transform.interceptor';
|
|
@ -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 +0,0 @@
|
|||||||
export * from './params.pipe';
|
|
@ -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 };
|
|
@ -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();
|
|
@ -1,5 +0,0 @@
|
|||||||
class RefreshTokenDto {
|
|
||||||
refreshToken: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { RefreshTokenDto };
|
|
@ -1,9 +0,0 @@
|
|||||||
class CreateUserDto {
|
|
||||||
id: number;
|
|
||||||
password: string;
|
|
||||||
realName: string;
|
|
||||||
roles: string[];
|
|
||||||
username: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { CreateUserDto };
|
|
@ -1,21 +0,0 @@
|
|||||||
class UserEntity {
|
|
||||||
id: number;
|
|
||||||
/**
|
|
||||||
* 密码
|
|
||||||
*/
|
|
||||||
password: string;
|
|
||||||
/**
|
|
||||||
* 真实姓名
|
|
||||||
*/
|
|
||||||
realName: string;
|
|
||||||
/**
|
|
||||||
* 角色
|
|
||||||
*/
|
|
||||||
roles: string[];
|
|
||||||
/**
|
|
||||||
* 用户名
|
|
||||||
*/
|
|
||||||
username: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { UserEntity };
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 {}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -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';
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { HealthController } from './health.controller';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
controllers: [HealthController],
|
|
||||||
})
|
|
||||||
export class HealthModule {}
|
|
@ -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 ?? [];
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 {}
|
|
@ -1,4 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class MenuService {}
|
|
@ -1 +0,0 @@
|
|||||||
{}
|
|
@ -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}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
interface User {
|
|
||||||
id: number;
|
|
||||||
password: string;
|
|
||||||
realName: string;
|
|
||||||
roles: string[];
|
|
||||||
username: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MockDatabaseData {
|
|
||||||
users: User[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { MockDatabaseData, User };
|
|
@ -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 {}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 {}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
vendored
7
apps/backend-mock/src/types/express.d.ts
vendored
@ -1,7 +0,0 @@
|
|||||||
import { UserEntity } from '@/models/entity/user.entity';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Request {
|
|
||||||
user?: UserEntity;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
export * from './config';
|
|
||||||
export * from './jwt';
|
|
@ -1,7 +0,0 @@
|
|||||||
interface JwtPayload {
|
|
||||||
id: number;
|
|
||||||
roles: string[];
|
|
||||||
username: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { JwtPayload };
|
|
@ -1,5 +0,0 @@
|
|||||||
function sleep(ms: number) {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
export { sleep };
|
|
@ -1,25 +1,3 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"extends": "./.nitro/types/tsconfig.json"
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
178
apps/backend-mock/utils/mock-data.ts
Normal file
178
apps/backend-mock/utils/mock-data.ts
Normal file
@ -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
Normal file
17
apps/backend-mock/utils/response.ts
Normal file
@ -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,
|
||||||
|
};
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
VITE_PUBLIC_PATH = /
|
VITE_PUBLIC_PATH = /
|
||||||
|
|
||||||
VITE_GLOB_API_URL=/api
|
VITE_GLOB_API_URL=/api
|
||||||
|
|
||||||
|
VITE_NITRO_MOCK = true
|
||||||
|
@ -16,9 +16,9 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "pnpm vite build",
|
"build": "pnpm vite build --mode production",
|
||||||
"build:analyze": "pnpm vite build --mode analyze",
|
"build:analyze": "pnpm vite build --mode analyze",
|
||||||
"dev": "pnpm vite",
|
"dev": "pnpm vite --mode development",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"typecheck": "vue-tsc --noEmit --skipLibCheck"
|
"typecheck": "vue-tsc --noEmit --skipLibCheck"
|
||||||
},
|
},
|
||||||
|
17
apps/web-antd/src/apis/core/auth.ts
Normal file
17
apps/web-antd/src/apis/core/auth.ts
Normal file
@ -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,3 +1,3 @@
|
|||||||
|
export * from './auth';
|
||||||
export * from './menu';
|
export * from './menu';
|
||||||
export * from './mock';
|
|
||||||
export * from './user';
|
export * from './user';
|
@ -5,8 +5,6 @@ import { requestClient } from '#/forward';
|
|||||||
/**
|
/**
|
||||||
* 获取用户所有菜单
|
* 获取用户所有菜单
|
||||||
*/
|
*/
|
||||||
async function getAllMenus() {
|
export async function getAllMenus() {
|
||||||
return requestClient.get<RouteRecordStringComponent[]>('/menu/getAll');
|
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
|
||||||
}
|
}
|
||||||
|
|
||||||
export { getAllMenus };
|
|
10
apps/web-antd/src/apis/core/user.ts
Normal file
10
apps/web-antd/src/apis/core/user.ts
Normal file
@ -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
Normal file
1
apps/web-antd/src/apis/demos/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './status';
|
@ -4,7 +4,7 @@ import { requestClient } from '#/forward';
|
|||||||
* 模拟任意状态码
|
* 模拟任意状态码
|
||||||
*/
|
*/
|
||||||
async function getMockStatus(status: string) {
|
async function getMockStatus(status: string) {
|
||||||
return requestClient.get('/mock/status', { params: { status } });
|
return requestClient.get('/status', { params: { status } });
|
||||||
}
|
}
|
||||||
|
|
||||||
export { getMockStatus };
|
export { getMockStatus };
|
@ -1,2 +1,3 @@
|
|||||||
export * from './modules';
|
export * from './core';
|
||||||
|
export * from './demos';
|
||||||
export type * from './types';
|
export type * from './types';
|
||||||
|
@ -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 };
|
|
@ -25,8 +25,8 @@ function createRequestClient() {
|
|||||||
tokenHandler: () => {
|
tokenHandler: () => {
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
return {
|
return {
|
||||||
refreshToken: `Bearer ${accessStore.refreshToken}`,
|
refreshToken: `${accessStore.refreshToken}`,
|
||||||
token: `Bearer ${accessStore.accessToken}`,
|
token: `${accessStore.accessToken}`,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
unAuthorizedHandler: async () => {
|
unAuthorizedHandler: async () => {
|
||||||
|
@ -38,11 +38,14 @@
|
|||||||
"title": "Features",
|
"title": "Features",
|
||||||
"hideChildrenInMenu": "Hide Menu Children",
|
"hideChildrenInMenu": "Hide Menu Children",
|
||||||
"loginExpired": "Login Expired",
|
"loginExpired": "Login Expired",
|
||||||
"breadcrumbNavigation": "Breadcrumb Navigation",
|
"tabs": "Tabs"
|
||||||
"breadcrumbLateral": "Lateral Mode",
|
},
|
||||||
"breadcrumbLateralDetail": "Lateral Mode Detail",
|
"breadcrumb": {
|
||||||
"breadcrumbLevel": "Level Mode",
|
"navigation": "Breadcrumb Navigation",
|
||||||
"breadcrumbLevelDetail": "Level Mode Detail"
|
"lateral": "Lateral Mode",
|
||||||
|
"lateralDetail": "Lateral Mode Detail",
|
||||||
|
"level": "Level Mode",
|
||||||
|
"levelDetail": "Level Mode Detail"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,11 +40,14 @@
|
|||||||
"title": "功能",
|
"title": "功能",
|
||||||
"hideChildrenInMenu": "隐藏子菜单",
|
"hideChildrenInMenu": "隐藏子菜单",
|
||||||
"loginExpired": "登录过期",
|
"loginExpired": "登录过期",
|
||||||
"breadcrumbNavigation": "面包屑导航",
|
"tabs": "标签页"
|
||||||
"breadcrumbLateral": "平级模式",
|
},
|
||||||
"breadcrumbLevel": "层级模式",
|
"breadcrumb": {
|
||||||
"breadcrumbLevelDetail": "层级模式详情",
|
"navigation": "面包屑导航",
|
||||||
"breadcrumbLateralDetail": "平级模式详情"
|
"lateral": "平级模式",
|
||||||
|
"level": "层级模式",
|
||||||
|
"levelDetail": "层级模式详情",
|
||||||
|
"lateralDetail": "平级模式详情"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: '/demos',
|
path: '/demos',
|
||||||
redirect: '/demos/access',
|
redirect: '/demos/access',
|
||||||
children: [
|
children: [
|
||||||
|
// 权限控制
|
||||||
{
|
{
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'mdi:shield-key-outline',
|
icon: 'mdi:shield-key-outline',
|
||||||
@ -87,6 +88,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// 功能
|
||||||
{
|
{
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'mdi:feature-highlight',
|
icon: 'mdi:feature-highlight',
|
||||||
@ -94,8 +96,17 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
name: 'Features',
|
name: 'Features',
|
||||||
path: 'features',
|
path: 'features',
|
||||||
redirect: '/demos/features/hide-menu-children',
|
redirect: '/demos/features/tabs',
|
||||||
children: [
|
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',
|
name: 'HideChildrenInMenuParent',
|
||||||
path: 'hide-children-in-menu',
|
path: 'hide-children-in-menu',
|
||||||
@ -127,35 +138,36 @@ const routes: RouteRecordRaw[] = [
|
|||||||
title: $t('page.demos.features.loginExpired'),
|
title: $t('page.demos.features.loginExpired'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 面包屑导航
|
||||||
{
|
{
|
||||||
name: 'BreadcrumbDemos',
|
name: 'BreadcrumbDemos',
|
||||||
path: 'breadcrumb',
|
path: 'breadcrumb',
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'lucide:navigation',
|
icon: 'lucide:navigation',
|
||||||
title: $t('page.demos.features.breadcrumbNavigation'),
|
title: $t('page.demos.breadcrumb.navigation'),
|
||||||
},
|
},
|
||||||
|
redirect: '/demos/breadcrumb/lateral',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'BreadcrumbLateral',
|
name: 'BreadcrumbLateral',
|
||||||
path: 'lateral',
|
path: 'lateral',
|
||||||
component: () =>
|
component: () => import('#/views/demos/breadcrumb/lateral.vue'),
|
||||||
import('#/views/demos/features/breadcrumb/lateral.vue'),
|
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'lucide:navigation',
|
icon: 'lucide:navigation',
|
||||||
title: $t('page.demos.features.breadcrumbLateral'),
|
title: $t('page.demos.breadcrumb.lateral'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'BreadcrumbLateralDetail',
|
name: 'BreadcrumbLateralDetail',
|
||||||
path: 'lateral-detail',
|
path: 'lateral-detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import('#/views/demos/breadcrumb/lateral-detail.vue'),
|
||||||
'#/views/demos/features/breadcrumb/lateral-detail.vue'
|
|
||||||
),
|
|
||||||
meta: {
|
meta: {
|
||||||
activePath: '/demos/features/breadcrumb/lateral',
|
activePath: '/demos/breadcrumb/lateral',
|
||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
title: $t('page.demos.features.breadcrumbLateralDetail'),
|
title: $t('page.demos.breadcrumb.lateralDetail'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -163,26 +175,24 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: 'level',
|
path: 'level',
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'lucide:navigation',
|
icon: 'lucide:navigation',
|
||||||
title: $t('page.demos.features.breadcrumbLevel'),
|
title: $t('page.demos.breadcrumb.level'),
|
||||||
},
|
},
|
||||||
|
redirect: '/demos/breadcrumb/level/detail',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'BreadcrumbLevelDetail',
|
name: 'BreadcrumbLevelDetail',
|
||||||
path: 'detail',
|
path: 'detail',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(
|
import('#/views/demos/breadcrumb/level-detail.vue'),
|
||||||
'#/views/demos/features/breadcrumb/level-detail.vue'
|
|
||||||
),
|
|
||||||
meta: {
|
meta: {
|
||||||
title: $t('page.demos.features.breadcrumbLevelDetail'),
|
title: $t('page.demos.breadcrumb.levelDetail'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
// 缺省页
|
||||||
},
|
|
||||||
{
|
{
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'mdi:lightbulb-error-outline',
|
icon: 'mdi:lightbulb-error-outline',
|
||||||
@ -231,6 +241,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// 菜单徽标
|
||||||
{
|
{
|
||||||
meta: {
|
meta: {
|
||||||
badgeType: 'dot',
|
badgeType: 'dot',
|
||||||
@ -275,6 +286,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// 外部链接
|
||||||
{
|
{
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'ic:round-settings-input-composite',
|
icon: 'ic:round-settings-input-composite',
|
||||||
@ -350,6 +362,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// 嵌套菜单
|
||||||
{
|
{
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'ic:round-menu',
|
icon: 'ic:round-menu',
|
||||||
|
@ -10,11 +10,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: BasicLayout,
|
component: BasicLayout,
|
||||||
meta: {
|
meta: {
|
||||||
badgeType: 'dot',
|
badgeType: 'dot',
|
||||||
|
badgeVariants: 'destructive',
|
||||||
icon: VBEN_LOGO_URL,
|
icon: VBEN_LOGO_URL,
|
||||||
order: 9999,
|
order: 9999,
|
||||||
title: 'Vben',
|
title: $t('page.vben.title'),
|
||||||
},
|
},
|
||||||
name: 'AboutLayout',
|
name: 'VbenProject',
|
||||||
path: '/vben-admin',
|
path: '/vben-admin',
|
||||||
redirect: '/vben-admin/about',
|
redirect: '/vben-admin/about',
|
||||||
children: [
|
children: [
|
||||||
@ -24,6 +25,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('#/views/_core/vben/about/index.vue'),
|
component: () => import('#/views/_core/vben/about/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
badgeType: 'dot',
|
badgeType: 'dot',
|
||||||
|
badgeVariants: 'destructive',
|
||||||
icon: 'lucide:copyright',
|
icon: 'lucide:copyright',
|
||||||
title: $t('page.vben.about'),
|
title: $t('page.vben.about'),
|
||||||
},
|
},
|
||||||
|
@ -11,7 +11,7 @@ import { useCoreAccessStore } from '@vben-core/stores';
|
|||||||
import { notification } from 'ant-design-vue';
|
import { notification } from 'ant-design-vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
import { getAccessCodes, getUserInfo, userLogin } from '#/apis';
|
import { getAccessCodes, getUserInfo, login } from '#/apis';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
export const useAccessStore = defineStore('access', () => {
|
export const useAccessStore = defineStore('access', () => {
|
||||||
@ -53,7 +53,7 @@ export const useAccessStore = defineStore('access', () => {
|
|||||||
let userInfo: UserInfo | null = null;
|
let userInfo: UserInfo | null = null;
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const { accessToken, refreshToken } = await userLogin(params);
|
const { accessToken, refreshToken } = await login(params);
|
||||||
|
|
||||||
// 如果成功获取到 accessToken
|
// 如果成功获取到 accessToken
|
||||||
// If accessToken is successfully obtained
|
// If accessToken is successfully obtained
|
||||||
|
@ -57,9 +57,9 @@ async function changeAccount(role: string) {
|
|||||||
<div class="text-foreground/80 mt-2">切换不同的账号,观察按钮变化。</div>
|
<div class="text-foreground/80 mt-2">切换不同的账号,观察按钮变化。</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-box mt-5 p-5 font-semibold">
|
<div class="card-box mt-5 p-5">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<span class="text-lg">当前角色:</span>
|
<span class="text-lg font-semibold">当前角色:</span>
|
||||||
<span class="text-primary mx-4 text-lg">
|
<span class="text-primary mx-4 text-lg">
|
||||||
{{ accessStore.userRoles?.[0] }}
|
{{ accessStore.userRoles?.[0] }}
|
||||||
</span>
|
</span>
|
||||||
@ -81,45 +81,42 @@ async function changeAccount(role: string) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-box mt-5 p-5 font-semibold">
|
<div class="card-box mt-5 p-5">
|
||||||
<div class="mb-3 text-lg">组件形式控制 - 权限码方式</div>
|
<div class="mb-3 text-lg font-semibold">组件形式控制 - 权限码方式</div>
|
||||||
<AccessControl :permissions="['AC_100100']" type="code">
|
<AccessControl :codes="['AC_100100']" type="code">
|
||||||
<Button class="mr-4"> Super 账号可见 ["AC_1000001"] </Button>
|
<Button class="mr-4"> Super 账号可见 ["AC_1000001"] </Button>
|
||||||
</AccessControl>
|
</AccessControl>
|
||||||
<AccessControl :permissions="['AC_100030']" type="code">
|
<AccessControl :codes="['AC_100030']" type="code">
|
||||||
<Button class="mr-4"> Admin 账号可见 ["AC_100010"] </Button>
|
<Button class="mr-4"> Admin 账号可见 ["AC_100010"] </Button>
|
||||||
</AccessControl>
|
</AccessControl>
|
||||||
<AccessControl :permissions="['AC_1000001']" type="code">
|
<AccessControl :codes="['AC_1000001']" type="code">
|
||||||
<Button class="mr-4"> User 账号可见 ["AC_1000001"] </Button>
|
<Button class="mr-4"> User 账号可见 ["AC_1000001"] </Button>
|
||||||
</AccessControl>
|
</AccessControl>
|
||||||
<AccessControl :permissions="['AC_100100', 'AC_100010']" type="code">
|
<AccessControl :codes="['AC_100100', 'AC_100010']" type="code">
|
||||||
<Button class="mr-4">
|
<Button class="mr-4">
|
||||||
Super & Admin 账号可见 ["AC_100100","AC_1000001"]
|
Super & Admin 账号可见 ["AC_100100","AC_1000001"]
|
||||||
</Button>
|
</Button>
|
||||||
</AccessControl>
|
</AccessControl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div v-if="accessMode === 'frontend'" class="card-box mt-5 p-5">
|
||||||
v-if="accessMode === 'frontend'"
|
<div class="mb-3 text-lg font-semibold">组件形式控制 - 用户角色方式</div>
|
||||||
class="card-box mt-5 p-5 font-semibold"
|
<AccessControl :codes="['super']">
|
||||||
>
|
|
||||||
<div class="mb-3 text-lg">组件形式控制 - 用户角色方式</div>
|
|
||||||
<AccessControl :permissions="['super']">
|
|
||||||
<Button class="mr-4"> Super 角色可见 </Button>
|
<Button class="mr-4"> Super 角色可见 </Button>
|
||||||
</AccessControl>
|
</AccessControl>
|
||||||
<AccessControl :permissions="['admin']">
|
<AccessControl :codes="['admin']">
|
||||||
<Button class="mr-4"> Admin 角色可见 </Button>
|
<Button class="mr-4"> Admin 角色可见 </Button>
|
||||||
</AccessControl>
|
</AccessControl>
|
||||||
<AccessControl :permissions="['user']">
|
<AccessControl :codes="['user']">
|
||||||
<Button class="mr-4"> User 角色可见 </Button>
|
<Button class="mr-4"> User 角色可见 </Button>
|
||||||
</AccessControl>
|
</AccessControl>
|
||||||
<AccessControl :permissions="['super', 'admin']">
|
<AccessControl :codes="['super', 'admin']">
|
||||||
<Button class="mr-4"> Super & Admin 角色可见 </Button>
|
<Button class="mr-4"> Super & Admin 角色可见 </Button>
|
||||||
</AccessControl>
|
</AccessControl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-box mt-5 p-5 font-semibold">
|
<div class="card-box mt-5 p-5">
|
||||||
<div class="mb-3 text-lg">函数形式控制</div>
|
<div class="mb-3 text-lg font-semibold">函数形式控制</div>
|
||||||
<Button v-if="hasAccessByCodes(['AC_100100'])" class="mr-4">
|
<Button v-if="hasAccessByCodes(['AC_100100'])" class="mr-4">
|
||||||
Super 账号可见 ["AC_1000001"]
|
Super 账号可见 ["AC_1000001"]
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -67,8 +67,8 @@ async function handleToggleAccessMode() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-box mt-5 p-5 font-semibold">
|
<div class="card-box mt-5 p-5">
|
||||||
<span class="text-lg">当前权限模式:</span>
|
<span class="text-lg font-semibold">当前权限模式:</span>
|
||||||
<span class="text-primary mx-4">{{
|
<span class="text-primary mx-4">{{
|
||||||
accessMode === 'frontend' ? '前端权限控制' : '后端权限控制'
|
accessMode === 'frontend' ? '前端权限控制' : '后端权限控制'
|
||||||
}}</span>
|
}}</span>
|
||||||
@ -76,9 +76,9 @@ async function handleToggleAccessMode() {
|
|||||||
切换为{{ accessMode === 'frontend' ? '后端' : '前端' }}权限模式
|
切换为{{ accessMode === 'frontend' ? '后端' : '前端' }}权限模式
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-box mt-5 p-5 font-semibold">
|
<div class="card-box mt-5 p-5">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<span class="text-lg">当前账号:</span>
|
<span class="text-lg font-semibold">当前账号:</span>
|
||||||
<span class="text-primary mx-4 text-lg">
|
<span class="text-primary mx-4 text-lg">
|
||||||
{{ accessStore.userRoles?.[0] }}
|
{{ accessStore.userRoles?.[0] }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -23,17 +23,21 @@ async function handleClick(type: LoginExpiredModeType) {
|
|||||||
<div class="card-box p-5">
|
<div class="card-box p-5">
|
||||||
<h1 class="text-xl font-semibold">登录过期演示</h1>
|
<h1 class="text-xl font-semibold">登录过期演示</h1>
|
||||||
<div class="text-foreground/80 mt-2">
|
<div class="text-foreground/80 mt-2">
|
||||||
401状态码转到登录页,登录成功后跳转回原页面。
|
接口请求遇到401状态码时,需要重新登录。有两种方式:
|
||||||
|
<div>1.转到登录页,登录成功后跳转回原页面</div>
|
||||||
|
<div>
|
||||||
|
2.弹出重新登录弹窗,登录后关闭弹窗,不进行任何页面跳转(刷新后调整登录页面)
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-box mt-5 p-5 font-semibold">
|
<div class="card-box mt-5 p-5">
|
||||||
<div class="mb-3 text-lg">跳转登录页面方式</div>
|
<div class="mb-3 text-lg font-semibold">跳转登录页面方式</div>
|
||||||
<Button type="primary" @click="handleClick('page')"> 点击触发 </Button>
|
<Button type="primary" @click="handleClick('page')"> 点击触发 </Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-box mt-5 p-5 font-semibold">
|
<div class="card-box mt-5 p-5">
|
||||||
<div class="mb-3 text-lg">登录弹窗方式</div>
|
<div class="mb-3 text-lg font-semibold">登录弹窗方式</div>
|
||||||
<Button type="primary" @click="handleClick('modal')"> 点击触发 </Button>
|
<Button type="primary" @click="handleClick('modal')"> 点击触发 </Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
86
apps/web-antd/src/views/demos/features/tabs/index.vue
Normal file
86
apps/web-antd/src/views/demos/features/tabs/index.vue
Normal file
@ -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>
|
@ -32,7 +32,7 @@
|
|||||||
"@commitlint/config-conventional": "^19.2.2",
|
"@commitlint/config-conventional": "^19.2.2",
|
||||||
"@vben/node-utils": "workspace:*",
|
"@vben/node-utils": "workspace:*",
|
||||||
"commitlint-plugin-function-rules": "^4.0.0",
|
"commitlint-plugin-function-rules": "^4.0.0",
|
||||||
"cz-git": "^1.9.3",
|
"cz-git": "^1.9.4",
|
||||||
"czg": "^1.9.3"
|
"czg": "^1.9.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,8 @@ export async function ignores(): Promise<Linter.FlatConfig[]> {
|
|||||||
'**/dist-*',
|
'**/dist-*',
|
||||||
'**/*-dist',
|
'**/*-dist',
|
||||||
'**/.husky',
|
'**/.husky',
|
||||||
|
'**/.nitro',
|
||||||
|
'**/.output',
|
||||||
'**/Dockerfile',
|
'**/Dockerfile',
|
||||||
'**/package-lock.json',
|
'**/package-lock.json',
|
||||||
'**/yarn.lock',
|
'**/yarn.lock',
|
||||||
|
@ -121,10 +121,17 @@ const customConfig: Linter.FlatConfig[] = [
|
|||||||
files: ['apps/backend-mock/**/**'],
|
files: ['apps/backend-mock/**/**'],
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/no-extraneous-class': 'off',
|
'@typescript-eslint/no-extraneous-class': 'off',
|
||||||
|
'n/prefer-global/buffer': 'off',
|
||||||
'no-console': 'off',
|
'no-console': 'off',
|
||||||
'unicorn/prefer-module': 'off',
|
'unicorn/prefer-module': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: ['internal/**/**'],
|
||||||
|
rules: {
|
||||||
|
'no-console': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export { customConfig };
|
export { customConfig };
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
"@jspm/generator": "^2.1.2",
|
"@jspm/generator": "^2.1.2",
|
||||||
"cheerio": "1.0.0-rc.12",
|
"cheerio": "1.0.0-rc.12",
|
||||||
"html-minifier-terser": "^7.2.0",
|
"html-minifier-terser": "^7.2.0",
|
||||||
|
"nitropack": "^2.9.7",
|
||||||
"resolve.exports": "^2.0.2",
|
"resolve.exports": "^2.0.2",
|
||||||
"vite-plugin-lib-inject-css": "^2.1.1",
|
"vite-plugin-lib-inject-css": "^2.1.1",
|
||||||
"vite-plugin-pwa": "^0.20.0",
|
"vite-plugin-pwa": "^0.20.0",
|
||||||
|
@ -33,6 +33,12 @@ function defineApplicationConfig(userConfigPromise: DefineApplicationOptions) {
|
|||||||
isBuild,
|
isBuild,
|
||||||
license: true,
|
license: true,
|
||||||
mode,
|
mode,
|
||||||
|
nitroMock: !isBuild,
|
||||||
|
nitroMockOptions: {},
|
||||||
|
print: !isBuild,
|
||||||
|
printInfoMap: {
|
||||||
|
'Vben Admin Docs': 'https://docs.vben.pro',
|
||||||
|
},
|
||||||
pwa: true,
|
pwa: true,
|
||||||
...application,
|
...application,
|
||||||
});
|
});
|
||||||
|
@ -47,10 +47,8 @@ async function viteExtraAppConfigPlugin({
|
|||||||
type: 'asset',
|
type: 'asset',
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(colors.cyan(`✨configuration file is build successfully!`));
|
console.log(colors.cyan(`✨configuration file is build successfully!`));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(
|
console.log(
|
||||||
colors.red(
|
colors.red(
|
||||||
`configuration file configuration file failed to package:\n${error}`,
|
`configuration file configuration file failed to package:\n${error}`,
|
||||||
|
@ -70,7 +70,6 @@ async function viteImportMapPlugin(
|
|||||||
if (options?.debug) {
|
if (options?.debug) {
|
||||||
(async () => {
|
(async () => {
|
||||||
for await (const { message, type } of generator.logStream()) {
|
for await (const { message, type } of generator.logStream()) {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(`${type}: ${message}`);
|
console.log(`${type}: ${message}`);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -23,6 +23,8 @@ import { viteImportMapPlugin } from './importmap';
|
|||||||
import { viteInjectAppLoadingPlugin } from './inject-app-loading';
|
import { viteInjectAppLoadingPlugin } from './inject-app-loading';
|
||||||
import { viteMetadataPlugin } from './inject-metadata';
|
import { viteMetadataPlugin } from './inject-metadata';
|
||||||
import { viteLicensePlugin } from './license';
|
import { viteLicensePlugin } from './license';
|
||||||
|
import { viteNitroMockPlugin } from './nitor-mock';
|
||||||
|
import { vitePrintPlugin } from './print';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取条件成立的 vite 插件
|
* 获取条件成立的 vite 插件
|
||||||
@ -99,6 +101,10 @@ async function loadApplicationPlugins(
|
|||||||
importmapOptions,
|
importmapOptions,
|
||||||
injectAppLoading,
|
injectAppLoading,
|
||||||
license,
|
license,
|
||||||
|
nitroMock,
|
||||||
|
nitroMockOptions,
|
||||||
|
print,
|
||||||
|
printInfoMap,
|
||||||
pwa,
|
pwa,
|
||||||
pwaOptions,
|
pwaOptions,
|
||||||
...commonOptions
|
...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,
|
condition: injectAppLoading,
|
||||||
plugins: async () => [await viteInjectAppLoadingPlugin(!!isBuild, env)],
|
plugins: async () => [await viteInjectAppLoadingPlugin(!!isBuild, env)],
|
||||||
|
@ -15,6 +15,7 @@ function resolvePackageVersion(
|
|||||||
|
|
||||||
async function resolveMonorepoDependencies() {
|
async function resolveMonorepoDependencies() {
|
||||||
const { packages } = await getPackages();
|
const { packages } = await getPackages();
|
||||||
|
|
||||||
const resultDevDependencies: Record<string, string> = {};
|
const resultDevDependencies: Record<string, string> = {};
|
||||||
const resultDependencies: Record<string, string> = {};
|
const resultDependencies: Record<string, string> = {};
|
||||||
const pkgsMeta: Record<string, string> = {};
|
const pkgsMeta: Record<string, string> = {};
|
||||||
|
89
internal/vite-config/src/plugins/nitor-mock.ts
Normal file
89
internal/vite-config/src/plugins/nitor-mock.ts
Normal file
@ -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
Normal file
28
internal/vite-config/src/plugins/print.ts
Normal file
@ -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 have changed in this diff Show More
Loading…
Reference in New Issue
Block a user