diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..bc18f3f5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "Lua.runtime.version": "Lua 5.1", + "Lua.diagnostics.globals": [ + "redis", + "KEYS", + "ARGV" + ] +} diff --git a/template/nest-next/eslint.config.mjs b/template/nest-next/eslint.config.mjs index 9b5aa9fb..559ae740 100644 --- a/template/nest-next/eslint.config.mjs +++ b/template/nest-next/eslint.config.mjs @@ -31,6 +31,7 @@ export default tseslint.config( '@typescript-eslint/no-unsafe-argument': 'warn', "prettier/prettier": ["error", { endOfLine: "auto" }], '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', }, }, ); diff --git a/template/nest-next/libs/shared/src/.generate/i18n.generated.ts b/template/nest-next/libs/shared/src/.generate/i18n.generated.ts index 53bb5764..f0dd80bf 100644 --- a/template/nest-next/libs/shared/src/.generate/i18n.generated.ts +++ b/template/nest-next/libs/shared/src/.generate/i18n.generated.ts @@ -1,5 +1,6 @@ /* DO NOT EDIT, file generated by nestjs-i18n */ +/* eslint-disable */ /* prettier-ignore */ import type { Path } from "nestjs-i18n"; /* prettier-ignore */ @@ -19,11 +20,13 @@ export type I18nTranslations = { "role": { "NOT_FOUND": string; "EXISTS": string; + "NOT_EMPTY": string; }; "user": { "NOT_FOUND": string; "EXISTS": string; "PASSWORD_INCORRECT": string; + "PASSWORD_NOT_MATCH": string; }; "auth": { "INVALID_TOKEN": string; @@ -43,6 +46,10 @@ export type I18nTranslations = { "EXISTS": string; "NOT_FOUND": string; }; + "i18n": { + "EXISTS": string; + "NOT_FOUND": string; + }; }; "validation": { "NOT_EMPTY": string; diff --git a/template/nest-next/libs/shared/src/constant.ts b/template/nest-next/libs/shared/src/constant.ts index 7fcaf550..9098f73a 100644 --- a/template/nest-next/libs/shared/src/constant.ts +++ b/template/nest-next/libs/shared/src/constant.ts @@ -1,3 +1,4 @@ -export const roleTotal = () => `tinypro:v1:role:total`; +export const roleTotal = (condition?: string) => + condition ? `tinypro:v1:role:total:${condition}` : `tinypro:v1:role:total`; export const userTotal = (condition?: string) => condition ? `tinypro:v1:user:total:${condition}` : `tinypro:v1:user:total`; diff --git a/template/nest-next/libs/shared/src/global-exception.filter.ts b/template/nest-next/libs/shared/src/global-exception.filter.ts index 52b5e6c1..ce247d7e 100644 --- a/template/nest-next/libs/shared/src/global-exception.filter.ts +++ b/template/nest-next/libs/shared/src/global-exception.filter.ts @@ -2,6 +2,7 @@ import { ArgumentsHost, Catch, ExceptionFilter, + HttpException, Injectable, Logger, } from '@nestjs/common'; @@ -20,7 +21,9 @@ export class GlobalExceptionFilter implements ExceptionFilter { const ctx = host.switchToHttp(); const response: Response = ctx.getResponse(); const request: Request = ctx.getRequest(); - const lang = request.headers['accept-language']?.split(',')[0] || 'enUS'; + const lang = + request.headers['accept-language']?.replace('-', '').split(',')[0] || + 'enUS'; if (exception instanceof DomainError) { const { code, message, details, args } = exception; const translatedMessage = this.i18n.translate(message, { args, lang }); @@ -30,6 +33,13 @@ export class GlobalExceptionFilter implements ExceptionFilter { details, }); } + if (exception instanceof HttpException && exception.getStatus() !== 500) { + const msg = exception.message; + return response.status(exception.getStatus()).json({ + statusCode: exception.getStatus(), + message: msg, + }); + } return response.status(500).json({ statusCode: 500, message: this.i18n.translate('exception.common.INTERNAL_ERROR', { lang }), diff --git a/template/nest-next/libs/shared/src/guard/reject.guard.ts b/template/nest-next/libs/shared/src/guard/reject.guard.ts index e02fd5c0..1c9664fc 100644 --- a/template/nest-next/libs/shared/src/guard/reject.guard.ts +++ b/template/nest-next/libs/shared/src/guard/reject.guard.ts @@ -9,6 +9,7 @@ import { Reflector } from '@nestjs/core'; import { I18nTranslations } from '../.generate/i18n.generated'; import { I18nContext } from 'nestjs-i18n'; import { ConfigureService } from '@app/configure'; +import { DomainError } from '../error.base'; @Injectable() export class RejectRequestGuard implements CanActivate { @@ -31,11 +32,15 @@ export class RejectRequestGuard implements CanActivate { if (!i18n) { return Promise.resolve(false); } - throw new HttpException( - i18n.t('exception.preview.REJECT_THIS_REQUEST', { - lang: I18nContext?.current()?.lang || 'enUS', - }), - HttpStatus.BAD_REQUEST, - ); + throw new RejectRequest(); + } +} + +export class RejectRequest extends DomainError { + constructor() { + super({ + message: 'exception.preview.REJECT_THIS_REQUEST', + code: HttpStatus.BAD_REQUEST, + }) } } diff --git a/template/nest-next/mikro-orm.config.ts b/template/nest-next/mikro-orm.config.ts index 49db622b..1efc4fb1 100644 --- a/template/nest-next/mikro-orm.config.ts +++ b/template/nest-next/mikro-orm.config.ts @@ -7,6 +7,7 @@ import config from './configs/config.json'; import { Role, RoleMenu, RolePermission } from './src/role'; import { User, UserRole } from './src/user'; import { I18n, Lang } from './src/i18'; +import { Application } from './src/application'; export default defineConfig({ entities: [ @@ -19,6 +20,7 @@ export default defineConfig({ UserRole, Lang, I18n, + Application, ], host: config.database.host, port: config.database.port, diff --git a/template/nest-next/package.json b/template/nest-next/package.json index 896a0fad..7c800fbe 100644 --- a/template/nest-next/package.json +++ b/template/nest-next/package.json @@ -41,6 +41,8 @@ "class-validator": "^0.14.1", "ioredis": "^5.11.1", "lodash.get": "^4.4.2", + "mockjs": "^1.1.0", + "nest-winston": "^1.10.2", "nestjs-i18n": "^10.5.1", "reflect-metadata": "^0.2.2", "rimraf": "5.0.1", diff --git a/template/nest-next/seeder/application-seeder.ts b/template/nest-next/seeder/application-seeder.ts new file mode 100644 index 00000000..0dc29776 --- /dev/null +++ b/template/nest-next/seeder/application-seeder.ts @@ -0,0 +1,116 @@ +import type { EntityManager } from '@mikro-orm/core'; +import { Seeder } from '@mikro-orm/seeder'; +import { Application } from '../src/application'; +import { log } from './utils/log'; + +const applicationData = [ + { + name: 'Tiny Design 设计体系', + description: + '华为云产品和服务的规范体系,包括交互视觉设计、业务流程、国际化、术语词条。', + tag: '[{ "type": "", "value": "机会点定义" }, { "type": "danger", "value": "交互设计" }]', + classify: 'design', + icon: 'card-list-application-default.png', + }, + { + name: 'Tiny DesignLink 设计流水线工具', + description: + '设计+协同+资源管理,一个工具就够了,在线原型设计、设计过程融入DevOps流程。', + tag: '[{ "type": "error", "value": "交互设计" }, { "type": "warning", "value": "视觉设计" }]', + classify: 'design', + icon: 'card-list-application-default.png', + }, + { + name: 'TinyUI3.0 开发工具 ', + description: 'Cloud Design System 提供了丰富的规范文档及开发组件。', + tag: '[{ "type": "success", "value": "开发" }]', + classify: 'dev', + icon: 'card-list-application-default.png', + }, + { + name: 'TinyPlus3.0 开发工具', + description: + 'TinyPlus3.0 是基于Angular + Typescript的Web前端云业务组件库。', + tag: '[{ "type": "success", "value": "开发" }]', + classify: 'dev', + icon: 'card-list-tiny-plus.png', + }, + { + name: 'Tiny Stage 工程工具 ', + description: + '一个跨平台的前端工程化cli工具,为开发提供一系列开发套件和工程插件', + tag: '[{ "type": "success", "value": "开发" }]', + classify: 'dev', + icon: 'card-list-console-framework.png', + }, + { + name: 'Tiny Flow 接口编排工具 ', + description: + '端到端的API编排解决方案,通过可视化编程的方式快速生成、发布、调试的API编排。', + tag: '[{ "type": "success", "value": "开发" }]', + classify: 'dev', + icon: 'card-list-console-framework.png', + }, + { + name: 'Tiny Gate 门禁系统', + description: + '门禁系统,通过卡点方式集成到伏羲流水线,在服务发布时生成预览页面。', + tag: '[{ "type": "info", "value": "测试验证" }]', + classify: 'dev', + icon: 'card-list-console-framework.png', + }, + { + name: 'Console Framework 控制台框架', + description: '华为云各服务快速构建管理控制台的平台。', + tag: '[{ "type": "success", "value": "开发" },{ "type": "info", "value": "测试验证" },{ "type": "warning", "value": "上线" }]', + classify: 'dev', + icon: 'card-list-console-framework.png', + }, + { + name: 'Nodejs Framework Nodejs应用', + description: + '基于egg的定制化web服务框架,让你快速上手Nodejs做BFF意见微服务。', + tag: '[{ "type": "success", "value": "开发" },{ "type": "info", "value": "测试验证" }]', + classify: 'dev', + icon: 'card-list-console-framework.png', + }, + { + name: 'Furion 前端体验监控', + description: + '提供端到端前端用户体验度量,让产品用户体验可度量、可监控、可优化。', + tag: '[{ "type": "", "value": "机会点定义" }]', + classify: 'dev', + icon: 'card-list-furion.png', + }, + { + name: 'Tiny Mock API 管理', + description: + '功能强大的API管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服务。', + tag: '[{ "type": "success", "value": "开发" },{ "type": "warning", "value": "视觉设计" }]', + classify: 'dev', + icon: 'card-list-application-default.png', + }, +]; + +export class ApplicationSeeder extends Seeder { + async run(em: EntityManager): Promise { + for (const app of applicationData) { + const existsData = await em.findOne(Application, { + name: app.name, + }); + if (existsData) { + log.info(`Application ${app.name} already exists`); + continue; + } + const application = em.create(Application, { + name: app.name, + description: app.description, + icon: app.icon, + tag: app.tag, + classify: app.classify, + }); + em.persist(application); + } + await em.flush(); + } +} diff --git a/template/nest-next/seeder/database-seeder.ts b/template/nest-next/seeder/database-seeder.ts index a1a81b2d..eb36333a 100644 --- a/template/nest-next/seeder/database-seeder.ts +++ b/template/nest-next/seeder/database-seeder.ts @@ -5,6 +5,7 @@ import { PermissionSeeder } from './permission-seeder'; import { RoleSeeder } from './role-seeder'; import { UserSeeder } from './user-seeder'; import { I18nRecordSeeder } from './i18n-seeder'; +import { ApplicationSeeder } from './application-seeder'; export class DatabaseSeeder extends Seeder { async run(em: EntityManager): Promise { @@ -15,6 +16,7 @@ export class DatabaseSeeder extends Seeder { RoleSeeder, UserSeeder, I18nRecordSeeder, + ApplicationSeeder, ]); }); } diff --git a/template/nest-next/src/app.module.ts b/template/nest-next/src/app.module.ts index c1cb687a..85d27b61 100644 --- a/template/nest-next/src/app.module.ts +++ b/template/nest-next/src/app.module.ts @@ -24,6 +24,9 @@ import { randomBytes } from 'crypto'; import { I18Module } from './i18/i18.module'; import { I18n, Lang } from './i18'; import { RejectRequestGuard } from '@app/shared'; +import { MockModule } from './mock/mock.module'; +import { ApplicationModule } from './application/application.module'; +import { Application } from './application'; @Module({ imports: [ @@ -84,6 +87,7 @@ import { RejectRequestGuard } from '@app/shared'; UserRole, Lang, I18n, + Application, ], host: configService.get('database.host'), port: configService.get('database.port'), @@ -113,6 +117,8 @@ import { RejectRequestGuard } from '@app/shared'; UserModule, AuthModule, I18Module, + MockModule, + ApplicationModule, ], providers: [ { diff --git a/template/nest-next/src/application/application.controller.ts b/template/nest-next/src/application/application.controller.ts new file mode 100644 index 00000000..7023abab --- /dev/null +++ b/template/nest-next/src/application/application.controller.ts @@ -0,0 +1,21 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { ApplicationService } from './application.service'; +import { + FindApplicationEntity, + FindApplicationsRequest, +} from './dto/find-applications'; +import { ApiOkResponse, ApiOperation } from '@nestjs/swagger'; + +@Controller('application') +export class ApplicationController { + constructor(private readonly applicationService: ApplicationService) {} + + @ApiOperation({ summary: '获取应用' }) + @ApiOkResponse({ + type: FindApplicationEntity, + }) + @Get() + async getAllApplication(@Query() searchInfo: FindApplicationsRequest) { + return this.applicationService.findAllAplication(searchInfo); + } +} diff --git a/template/nest-next/src/application/application.entity.ts b/template/nest-next/src/application/application.entity.ts new file mode 100644 index 00000000..69e82131 --- /dev/null +++ b/template/nest-next/src/application/application.entity.ts @@ -0,0 +1,31 @@ +import { type Opt } from '@mikro-orm/core'; +import { Entity, PrimaryKey, Property } from '@mikro-orm/decorators/legacy'; +import { v7 } from 'uuid'; + +export enum TagEnum { + DEFAULT = '', + SUCCESS = 'success', + INFO = 'info', + DANGER = 'danger', + WARNING = 'warning', +} + +export type ApplicationId = string & { readonly __brand: unique symbol }; +export const createApplicationId = () => v7() as ApplicationId; +export const toApplicationId = (val: string) => val as ApplicationId; + +@Entity({ tableName: 'application' }) +export class Application { + @PrimaryKey({ type: 'uuid' }) + id: Opt = createApplicationId(); + @Property({ type: 'text' }) + name: string; + @Property({ type: 'text' }) + description: string; + @Property({ type: 'text' }) + icon: string; + @Property({ type: 'text' }) + tag: string; + @Property({ type: 'text' }) + classify: string; +} diff --git a/template/nest-next/src/application/application.module.ts b/template/nest-next/src/application/application.module.ts new file mode 100644 index 00000000..9a2a6017 --- /dev/null +++ b/template/nest-next/src/application/application.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ApplicationService } from './application.service'; +import { ApplicationController } from './application.controller'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { Application } from './application.entity'; + +@Module({ + imports: [MikroOrmModule.forFeature([Application])], + controllers: [ApplicationController], + providers: [ApplicationService], +}) +export class ApplicationModule {} diff --git a/template/nest-next/src/application/application.service.ts b/template/nest-next/src/application/application.service.ts new file mode 100644 index 00000000..e699dca6 --- /dev/null +++ b/template/nest-next/src/application/application.service.ts @@ -0,0 +1,53 @@ +import { InjectRepository } from '@mikro-orm/nestjs'; +import { Injectable } from '@nestjs/common'; +import { Application } from './application.entity'; +import { EntityRepository } from '@mikro-orm/mysql'; +import { FindApplicationsRequest } from './dto/find-applications'; + +@Injectable() +export class ApplicationService { + constructor( + @InjectRepository(Application) + private readonly applicationRepository: EntityRepository, + ) {} + private parseTagSafely(tag: string): unknown { + try { + return JSON.parse(tag); + } catch { + return []; + } + } + async findAllAplication(query: FindApplicationsRequest) { + const { page, limit, keywords, classify } = query; + const skip = (page - 1) * limit; + const queryBuilder = + this.applicationRepository.createQueryBuilder('application'); + if (keywords) { + queryBuilder.where({ + $or: [ + { 'application.name': { $like: `%${keywords}%` } }, + { 'application.description': { $like: `%${keywords}%` } }, + { 'application.tag': { $like: `%${keywords}%` } }, + ], + }); + } + if (classify !== 'all') { + queryBuilder.andWhere({ + classify, + }); + } + const [application, cnt] = await queryBuilder + .offset(skip) + .limit(limit) + .getResultAndCount(); + return { + data: application.map((item) => { + return { + ...item, + tag: this.parseTagSafely(item.tag), + }; + }) as Application[], + total: cnt, + }; + } +} diff --git a/template/nest-next/src/application/dto/find-applications.ts b/template/nest-next/src/application/dto/find-applications.ts new file mode 100644 index 00000000..cf3ac650 --- /dev/null +++ b/template/nest-next/src/application/dto/find-applications.ts @@ -0,0 +1,69 @@ +import { ApiPropertyOptional, ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsInt, IsOptional, Max, Min } from 'class-validator'; + +export class FindApplicationsRequest { + @ApiPropertyOptional({ + description: '页数, 必须是一个大于零的正整数', + minimum: 1, + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page: number = 1; + + @ApiPropertyOptional({ + description: '页大小, 必须是一个正整数', + minimum: 1, + maximum: 100, + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + limit: number = 1; + + @ApiPropertyOptional({ description: '类型' }) + @IsOptional() + classify: string; + + @ApiPropertyOptional({ description: '关键字' }) + @IsOptional() + keywords: string; +} + +class FindApplicationItem { + @ApiProperty({ + description: '应用Tag', + }) + tag: unknown; + @ApiProperty({ + description: '应用 ID', + }) + id: number; + @ApiProperty({ + description: '应用名', + }) + name: string; + @ApiProperty({ + description: '应用图标', + }) + icon: string; + @ApiProperty({ + description: '应用类别', + }) + classify: string; +} + +export class FindApplicationEntity { + @ApiProperty({ + type: [FindApplicationItem], + }) + data: FindApplicationItem[]; + @ApiProperty({ + description: '应用总数', + }) + total: number; +} diff --git a/template/nest-next/src/application/dto/index.ts b/template/nest-next/src/application/dto/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/template/nest-next/src/application/index.ts b/template/nest-next/src/application/index.ts new file mode 100644 index 00000000..3fbd948e --- /dev/null +++ b/template/nest-next/src/application/index.ts @@ -0,0 +1 @@ +export * from './application.entity'; diff --git a/template/nest-next/src/auth/auth.guard.ts b/template/nest-next/src/auth/auth.guard.ts index 2f973e71..3a5fe772 100644 --- a/template/nest-next/src/auth/auth.guard.ts +++ b/template/nest-next/src/auth/auth.guard.ts @@ -6,6 +6,7 @@ import { TokenPayload } from './entity'; import { AuthService } from './auth.service'; import { ApiTokenService } from './api-token.service'; import { InvalidToken } from './errors'; +import { DomainError } from '@app/shared'; @Injectable() export class AuthGuard implements CanActivate { @@ -16,11 +17,11 @@ export class AuthGuard implements CanActivate { private readonly apiTokenService: ApiTokenService, ) {} async canActivate(ctx: ExecutionContext): Promise { - const isPubblic = this.reflector.getAllAndOverride('isPublic', [ + const isPublic = this.reflector.getAllAndOverride('isPublic', [ ctx.getHandler(), ctx.getClass(), ]); - if (isPubblic) { + if (isPublic) { return true; } const token = this.extractToken(ctx); @@ -42,10 +43,13 @@ export class AuthGuard implements CanActivate { return true; } await this.authService.tokenAlive(token); - } catch { + return true; + } catch (error) { + if (error instanceof DomainError) { + throw error; + } throw new InvalidToken(); } - return true; } private extractToken(ctx: ExecutionContext): string { const request: Request = ctx.switchToHttp().getRequest(); diff --git a/template/nest-next/src/i18/dto/batch-remove-i18.dto.ts b/template/nest-next/src/i18/dto/batch-remove-i18.dto.ts deleted file mode 100644 index 47894316..00000000 --- a/template/nest-next/src/i18/dto/batch-remove-i18.dto.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { I18nRecordId } from '../i18.entites'; - -export class BatchRemoveI18Records { - @ApiProperty({ - description: '想要删除的国际化字段ID数组', - type: [Number], - }) - ids: I18nRecordId[]; -} diff --git a/template/nest-next/src/i18/i18.controller.ts b/template/nest-next/src/i18/i18.controller.ts index 31c052b4..d2887059 100644 --- a/template/nest-next/src/i18/i18.controller.ts +++ b/template/nest-next/src/i18/i18.controller.ts @@ -22,7 +22,6 @@ import { import { I18 } from './dto/i18n-info.dto'; import { Permission, Reject } from '@app/shared'; import { CreateI18Dto } from './dto/create-i18.dto'; -import { BatchRemoveI18Records } from './dto/batch-remove-i18.dto'; import { UpdateI18Dto } from './dto/update-i18.dto'; import type { I18nRecordId, LangId } from './i18.entites'; import { FindAllI18n } from './dto/find-all-i18n.dto'; @@ -99,7 +98,7 @@ export class I18Controller { }) @Permission('i18n::query') @Get(':id') - findOne(@Param('id', ParseIntPipe) id: I18nRecordId) { + findOne(@Param('id') id: I18nRecordId) { return this.i18Service.findOne(id); } @@ -115,10 +114,7 @@ export class I18Controller { @Reject() @Permission('i18n::update') @Patch(':id') - update( - @Param('id', ParseIntPipe) id: I18nRecordId, - @Body() updateI18Dto: UpdateI18Dto, - ) { + update(@Param('id') id: I18nRecordId, @Body() updateI18Dto: UpdateI18Dto) { return this.i18Service.update(id, updateI18Dto); } @@ -134,7 +130,7 @@ export class I18Controller { @Reject() @Permission('i18n::remove') @Delete(':id') - remove(@Param('id', ParseIntPipe) id: I18nRecordId) { + remove(@Param('id') id: I18nRecordId) { return this.i18Service.remove(id); } @@ -145,7 +141,7 @@ export class I18Controller { @Reject() @Permission('i18n::batch-remove') @Post('/batch') - batchRemove(@Body() body: BatchRemoveI18Records) { - return this.i18Service.batchRemove(body.ids); + batchRemove(@Body() body: I18nRecordId[]) { + return this.i18Service.batchRemove(body); } } diff --git a/template/nest-next/src/i18/i18.service.ts b/template/nest-next/src/i18/i18.service.ts index b56d325e..9825234e 100644 --- a/template/nest-next/src/i18/i18.service.ts +++ b/template/nest-next/src/i18/i18.service.ts @@ -19,12 +19,9 @@ export class I18Service { @InjectRepository(Lang) private readonly lang: EntityRepository, ) {} async getFormat(lang: string) { - const data = await this.lang.find( - { name: lang ? lang : undefined }, - { - populate: ['i18n'], - }, - ); + const data = await this.lang.find(lang ? { name: lang } : {}, { + populate: ['i18n'], + }); const ret: Record> = {}; for (let i = 0; i < data.length; i++) { const { name, i18n } = data[i]; diff --git a/template/nest-next/src/i18n/enUS/exception.json b/template/nest-next/src/i18n/enUS/exception.json index 96fb09e2..4c2d3d04 100644 --- a/template/nest-next/src/i18n/enUS/exception.json +++ b/template/nest-next/src/i18n/enUS/exception.json @@ -12,12 +12,14 @@ }, "role":{ "NOT_FOUND": "Role not found", - "EXISTS": "Role {name} Exists" + "EXISTS": "Role {name} Exists", + "NOT_EMPTY": "There are users under the role, please clear them" }, "user":{ "NOT_FOUND": "User not found", "EXISTS": "User {email} Exists", - "PASSWORD_INCORRECT": "Password Incorrect" + "PASSWORD_INCORRECT": "Password Incorrect", + "PASSWORD_NOT_MATCH": "Password not match" }, "auth":{ "INVALID_TOKEN": "Invalid Token", diff --git a/template/nest-next/src/i18n/zhCN/exception.json b/template/nest-next/src/i18n/zhCN/exception.json index 70cd4be7..c6b894e1 100644 --- a/template/nest-next/src/i18n/zhCN/exception.json +++ b/template/nest-next/src/i18n/zhCN/exception.json @@ -1,12 +1,42 @@ { - "common":{ - "INTERNAL_ERROR": "内部错误" + "common": { + "INTERNAL_ERROR": "内部错误", + "FORBIDDEN": "禁止访问" }, - "permission":{ + "permission": { "NOT_FOUND": "权限不存在" }, - "menu":{ - "EXISTS": "{name}已存在", + "menu": { + "EXISTS": "菜单 {name} 已存在", "NOT_FOUND": "菜单不存在" + }, + "role": { + "NOT_FOUND": "角色不存在", + "EXISTS": "角色 {name} 已存在", + "NOT_EMPTY": "该角色下仍存在用户,请先清除用户" + }, + "user": { + "NOT_FOUND": "用户不存在", + "EXISTS": "用户 {email} 已存在", + "PASSWORD_INCORRECT": "密码错误", + "PASSWORD_NOT_MATCH": "密码不匹配" + }, + "auth": { + "INVALID_TOKEN": "无效的令牌", + "TOKEN_EXPIRED": "令牌已过期", + "SESSION_EXPIRED": "会话已过期" + }, + "preview": { + "REJECT_THIS_REQUEST": "服务器拒绝了该请求,演示模式下不允许修改、新增或删除数据" + }, + "lang": { + "NOT_EXISTS": "{lang} 不存在", + "EXISTS": "{name} 已存在", + "NOT_EXISTS_COMMON": "不存在", + "DELETE_LANG_CONFLICT": "请先清除 {name} 下的所有语言记录" + }, + "i18n": { + "EXISTS": "国际化字段已存在", + "NOT_FOUND": "未找到" } } diff --git a/template/nest-next/src/main.ts b/template/nest-next/src/main.ts index 43a28cd6..c95b8390 100644 --- a/template/nest-next/src/main.ts +++ b/template/nest-next/src/main.ts @@ -1,3 +1,7 @@ +import * as winston from 'winston'; +import 'winston-daily-rotate-file'; +import { utilities, WinstonModule } from 'nest-winston'; + import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { INestApplication, VersioningType } from '@nestjs/common'; @@ -6,7 +10,12 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { I18nValidationPipe } from 'nestjs-i18n'; async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule, { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + logger: WinstonModule.createLogger({ + instance: setupLogger(), + }), + }); app.enableVersioning({ type: VersioningType.MEDIA_TYPE, key: 'v=', @@ -38,4 +47,52 @@ function setupSwagger(app: INestApplication, configure: SwaggerConfigure = {}) { const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api', app, document); } + +function setupLogger() { + return winston.createLogger({ + // 日志选项 + transports: [ + new winston.transports.Console({ + level: 'info', + // 字符串拼接 + format: winston.format.combine( + winston.format.timestamp(), + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access + utilities.format.nestLike(), + ), + }), + // warn、error日志存储到/logs/application-日期.log文件中 + new winston.transports.DailyRotateFile({ + level: 'warn', + dirname: 'logs', + filename: 'application-%DATE%.log', + datePattern: 'YYYY-MM-DD-HH', + zippedArchive: true, + maxSize: '20m', + maxFiles: '14d', + format: winston.format.combine( + winston.format.timestamp(), + winston.format.simple(), + ), + }), + // info日志存储到/logs/info-日期.log文件中 + new winston.transports.DailyRotateFile({ + level: 'info', + dirname: 'logs', + filename: 'info-%DATE%.log', + datePattern: 'YYYY-MM-DD-HH', + zippedArchive: true, + // 文件大小 + maxSize: '20m', + // 最多14 天 + maxFiles: '14d', + format: winston.format.combine( + winston.format.timestamp(), + winston.format.simple(), + ), + }), + ], + }); +} + bootstrap().catch(console.error); diff --git a/template/nest-next/src/menu/menu.controller.ts b/template/nest-next/src/menu/menu.controller.ts index 81782c13..8ac02519 100644 --- a/template/nest-next/src/menu/menu.controller.ts +++ b/template/nest-next/src/menu/menu.controller.ts @@ -53,7 +53,7 @@ export class MenuController { @ApiOperation({ summary: '菜单修改' }) @ApiOkResponse({ type: UpdateMenuResponse }) - @Patch(':id') + @Patch('') @Reject() @Permission('menu::update') update(@Body() updateMenuDto: UpdateMenuDto) { diff --git a/template/nest-next/src/menu/menu.module.ts b/template/nest-next/src/menu/menu.module.ts index 63c4d76f..ccacd648 100644 --- a/template/nest-next/src/menu/menu.module.ts +++ b/template/nest-next/src/menu/menu.module.ts @@ -6,12 +6,18 @@ import { RemoveMenuCommandHandler, UpdateMenuCommandHandler, } from './commands'; -import { FindAllMenuQueryHandler, FindMenuHandler } from './queries'; +import { + FindAllMenuQueryHandler, + FindMenuHandler, + GetUserMenuService, +} from './queries'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { Menu } from './menu.entity'; +import { User } from '../user'; +import { RoleMenu } from '../role'; @Module({ - imports: [MikroOrmModule.forFeature([Menu])], + imports: [MikroOrmModule.forFeature([Menu, User, RoleMenu])], controllers: [MenuController], providers: [ MenuService, @@ -20,6 +26,7 @@ import { Menu } from './menu.entity'; UpdateMenuCommandHandler, FindAllMenuQueryHandler, FindMenuHandler, + GetUserMenuService, ], }) export class MenuModule {} diff --git a/template/nest-next/src/menu/menu.service.ts b/template/nest-next/src/menu/menu.service.ts index dccace11..026d7314 100644 --- a/template/nest-next/src/menu/menu.service.ts +++ b/template/nest-next/src/menu/menu.service.ts @@ -3,10 +3,8 @@ import { CreateMenuDto } from './dto/create-menu.dto'; import { UpdateMenuDto } from './dto/update-menu.dto'; import { CommandBus, QueryBus } from '@nestjs/cqrs'; import { CreateMenu, RemoveMenu, UpdateMenu } from './commands'; -import { convertToTree, FindAllMenu } from './queries'; +import { FindAllMenu, GetUserMenu } from './queries'; import { MenuId } from './menu.entity'; -import { GetUserInfo } from '../user/query'; -import { UserNotFound } from '../user'; @Injectable() export class MenuService { @@ -15,18 +13,7 @@ export class MenuService { private readonly cb: CommandBus, ) {} async findUserMenu(email: string) { - const [userInfo] = await this.cb.execute( - new GetUserInfo({ email: [email] }), - ); - if (!userInfo) { - throw new UserNotFound(); - } - const menus = userInfo.role.flatMap((role) => role.menus); - const maps = {}; - menus.forEach((menu) => { - maps[menu.id] = menu; - }); - return convertToTree(Object.values(maps)); + return this.qb.execute(new GetUserMenu({ email })); } create(createMenuDto: CreateMenuDto) { return this.cb.execute(new CreateMenu(createMenuDto)); diff --git a/template/nest-next/src/menu/queries/get-user-menu.query.ts b/template/nest-next/src/menu/queries/get-user-menu.query.ts new file mode 100644 index 00000000..8e0eb687 --- /dev/null +++ b/template/nest-next/src/menu/queries/get-user-menu.query.ts @@ -0,0 +1,63 @@ +import { IQueryHandler, Query, QueryHandler } from '@nestjs/cqrs'; +import { InjectRepository } from '@mikro-orm/nestjs'; +import { EntityRepository, FilterQuery } from '@mikro-orm/core'; +import { convertToTree, Menu } from '../../menu'; +import { User, UserId, UserNotFound } from '../../user'; +import { TreeNode } from '../types'; +import { RoleMenu } from '../../role'; + +export type GetUserInfoRequest = + | { + email: string; + id?: never; + } + | { + email?: never; + id: UserId; + }; + +export class GetUserMenu extends Query { + constructor(public readonly request: GetUserInfoRequest) { + super(); + } +} + +@QueryHandler(GetUserMenu) +export class GetUserMenuService implements IQueryHandler { + constructor( + @InjectRepository(User) + private readonly userRepository: EntityRepository, + @InjectRepository(RoleMenu) + private readonly roleMenuRepo: EntityRepository, + @InjectRepository(Menu) + private readonly menuRepo: EntityRepository, + ) {} + async execute(query: GetUserMenu): Promise { + const whereCondition: FilterQuery = {}; + if (query.request.email) { + whereCondition['email'] = query.request.email; + } + if (query.request.id) { + whereCondition['id'] = query.request.id; + } + const user = await this.userRepository.findOne( + { + ...whereCondition, + }, + { populate: ['role'] }, + ); + if (!user) { + throw new UserNotFound(); + } + const roles = user.role; + const roleIds = roles.map((role) => role.roleId); + const roleMenus = await this.roleMenuRepo.find({ + roleId: { $in: roleIds }, + }); + const menuIds = roleMenus.map((menu) => menu.menuId); + const menus = await this.menuRepo.find({ + id: { $in: menuIds }, + }); + return convertToTree(menus); + } +} diff --git a/template/nest-next/src/menu/queries/index.ts b/template/nest-next/src/menu/queries/index.ts index 714848d0..ff003e03 100644 --- a/template/nest-next/src/menu/queries/index.ts +++ b/template/nest-next/src/menu/queries/index.ts @@ -1,2 +1,3 @@ export * from './find-all-menu.query'; export * from './find-menu.query'; +export * from './get-user-menu.query'; diff --git a/template/nest-next/src/mock/data/application.ts b/template/nest-next/src/mock/data/application.ts new file mode 100644 index 00000000..2a6f4d3a --- /dev/null +++ b/template/nest-next/src/mock/data/application.ts @@ -0,0 +1,49 @@ +import { mock } from 'mockjs'; +import { successResponseWrap } from '../utils'; +const taskList = mock({ + 'list|60': [ + { + id: '@id', + name: 'Tiny Design ', + description: + 'HUA WEI CLOUD PRODUCT AND SERVICES DESIGN ,INCLUDES EXCHANGE DESIGN ,WORDS,INCLUDES EXCHANGE DESIGN ,WORDS', + 'tag|1-3': [ + { + 'type|1': ['success', '', 'info', 'danger', 'warning'], + 'value|1': ['dev', 'design', 'test', 'online'], + }, + ], + 'icon|1': ['logo-top2.png', 'info1.png', 'info2.png'], + }, + ], +}); + +let treeData = []; +const tableData = taskList.list; + +const req = [ + // list + { + url: '/api/application', + method: 'get', + response: (params: { body: any }) => { + console.log(params); + const { pageIndex = 1, pageSize = 10 } = JSON.parse( + JSON.stringify(params.body || {}) + ); + const index = pageIndex as number; + const size = pageSize as number; + const offset = (index - 1) * size; + const count = index * size; + treeData = tableData.slice(offset, count); + const data = mock({ + total: 60, + data: treeData, + }); + + return successResponseWrap(data); + }, + }, +] as const; + +export default req; diff --git a/template/nest-next/src/mock/data/board.ts b/template/nest-next/src/mock/data/board.ts new file mode 100644 index 00000000..9a443b44 --- /dev/null +++ b/template/nest-next/src/mock/data/board.ts @@ -0,0 +1,115 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { mock } from 'mockjs'; +import { successResponseWrap } from '../utils'; + +const initData = mock({ + options: [ + { + value: '1', + label: 'work.mock.employees', + }, + { + value: '2', + label: 'work.mock.onboard', + }, + { + value: '3', + label: 'work.mock.Test', + }, + ], +}); + +const initData1 = mock({ + options: [ + { + value: '1', + label: 'work.mock.week1', + }, + { + value: '2', + label: 'work.mock.week2', + }, + { + value: '3', + label: 'work.mock.week3', + }, + ], +}); + +const initData2 = mock({ + options: [ + { + value: 'work.mock.collectValue1', + description: 'work.mock.collectDescription1', + label1: 'work.mock.collectHotLabel1', + label2: 'work.mock.collectLabel2', + }, + { + value: 'work.mock.collectValue2', + description: 'work.mock.collectDescription2', + label1: 'work.mock.collectHotLabel1', + label2: 'work.mock.collectLabel3', + }, + { + value: 'work.mock.collectValue3', + description: 'work.mock.collectDescription3', + label1: 'work.mock.collectHotLabel1', + label2: 'work.mock.collectLabel4', + }, + { + value: 'work.mock.collectValue4', + description: 'work.mock.collectDescription4', + label1: 'work.mock.collectHotLabel1', + label2: 'work.mock.collectLabel5', + isNews: true + }, + ], +}); + +const changeDate = mock({ + options1: [101, 212, 122, 232], + options2: [323, 555, 425, 2221], + options3: [23234, 234, 989, 122], +}); + +const req = [ + { + url: '/api/user/getdata', + method: 'get', + response: () => { + return successResponseWrap(initData); + }, + }, + { + url: '/api/user/getrpractic', + method: 'get', + response: () => { + return successResponseWrap(initData1); + }, + }, + { + url: '/api/user/getrtrain', + method: 'get', + response: () => { + return successResponseWrap(initData2); + }, + }, + { + url: '/api/user/getselect', + method: 'post', + response: (data: any) => { + let result: any = null; + if (data.body === 1) { + result = successResponseWrap(changeDate.options1); + } else if (data.body === 2) { + result = successResponseWrap(changeDate.options2); + } else { + result = successResponseWrap(changeDate.options3); + } + return result; + }, + }, +] as const; + +export default req; diff --git a/template/nest-next/src/mock/data/forms.ts b/template/nest-next/src/mock/data/forms.ts new file mode 100644 index 00000000..5a518273 --- /dev/null +++ b/template/nest-next/src/mock/data/forms.ts @@ -0,0 +1,94 @@ +import { mock } from 'mockjs'; +import { successResponseWrap } from '../utils'; + +const initBase = mock({ + position: [ + { + value: '1', + label: 'position1', + }, + { + value: '2', + label: 'position2', + }, + { + value: '3', + label: 'position3', + }, + { + value: '4', + label: 'position4', + }, + ], + HR: [ + { + value: '1', + label: 'test01', + }, + { + value: '2', + label: 'test01', + }, + { + value: '3', + label: 'test03', + }, + ], + mentor: ['Teacher1', 'Teacher2', 'Teacher3', 'Teacher4'], + director: ['Director1', 'Director2', 'Director3', 'Director4'], + status: ['running', 'finished', 'delayed'], + department: [ + { + value: '1', + label: 'department01', + }, + { + value: '2', + label: 'department02', + }, + { + value: '3', + label: 'department03', + }, + ], +}); + +const req = [ + // init-base + { + url: '/api/base/getdata', + method: 'get', + response: () => { + return successResponseWrap(initBase); + }, + }, + + // init-step + { + url: '/api/step/getdata', + method: 'get', + response: () => { + return successResponseWrap(initBase); + }, + }, + + // submit + { + url: '/api/channel-form/submit', + method: 'post', + response: () => { + return successResponseWrap('ok'); + }, + }, + + // init-advance + { + url: '/api/advance/getdata', + method: 'get', + response: () => { + return successResponseWrap(initBase); + }, + }, +] as const; + +export default req; diff --git a/template/nest-next/src/mock/data/index.ts b/template/nest-next/src/mock/data/index.ts new file mode 100644 index 00000000..83f3a86c --- /dev/null +++ b/template/nest-next/src/mock/data/index.ts @@ -0,0 +1,17 @@ +import board from './board'; +import forms from './forms'; +import list from './list'; +import profile from './profile'; +import user from './user'; +import application from './application'; + +const data = [ + ...board, + ...list, + ...profile, + ...user, + ...forms, + ...application, +] as const; + +export default data; diff --git a/template/nest-next/src/mock/data/list.ts b/template/nest-next/src/mock/data/list.ts new file mode 100644 index 00000000..6a528b05 --- /dev/null +++ b/template/nest-next/src/mock/data/list.ts @@ -0,0 +1,81 @@ +import { mock } from 'mockjs'; +import { successResponseWrap } from '../utils'; +const taskList = mock({ + 'list|60': [ + { + id: '@id', + name: '@cname', + rank: '初级', + description: '一段描述文字', + createTime: '@datetime', + 'status|1': ['0', '1', '2'], + type: 'Tiny Design', + roles: '前端', + employeeNo: '@integer(10000000, 99999999)', + department: '公共服务', + departmentLevel: '二级', + workbenchName: 'work', + project: 'TinyDesign', + address: '西安研究所', + lastUpdateUser: '张三', + }, + ], +}); + +let treeData = []; +const tableData = taskList.list; + +const req = [ + // list + { + url: '/api/employee/getEmployee', + method: 'post', + response: (params: { body: any }) => { + const { pageIndex = 1, pageSize = 10 } = JSON.parse( + JSON.stringify(params.body) + ); + const index = pageIndex as number; + const size = pageSize as number; + const offset = (index - 1) * size; + const count = index * size; + treeData = tableData.slice(offset, count); + const data = mock({ + total: 60, + data: treeData, + }); + + return successResponseWrap(data); + }, + }, + { + url: '/api/employee/getEmployeeInfo', + method: 'post', + response: (params) => { + const { id } = params.body || {}; + return tableData.find((item) => item.id === id) || {}; + }, + }, + { + url: '/api/employee/updateEmployeeInfo', + method: 'post', + response: (params) => { + const { data } = params.body || {}; + let res = false; + tableData.find((item, index) => { + if (item.id === data.id) { + res = true; + Object.keys(data).forEach((key) => { + if (key !== 'id') { + item[key] = data[key]; + } + }); + return true; + } + }); + + return res; + }, + }, +] as const; + +export default req; diff --git a/template/nest-next/src/mock/data/profile.ts b/template/nest-next/src/mock/data/profile.ts new file mode 100644 index 00000000..3c952513 --- /dev/null +++ b/template/nest-next/src/mock/data/profile.ts @@ -0,0 +1,66 @@ +import { successResponseWrap } from '../utils'; +import { mock } from 'mockjs'; + +const initData = mock({ + Project: [ + 'baseForm.form.label.projectone', + 'baseForm.form.label.projecttwo', + 'baseForm.form.label.projectthree', + ], + tableData: [ + { + id: '1', + version: 'version1', + operation: 'offline', + updated: 'person1', + time: '2022-10-11', + }, + { + id: '2', + version: 'version2', + operation: 'offline', + updated: 'person2', + time: '2022-10-12', + }, + { + id: '3', + version: 'version3', + operation: 'online', + updated: 'person3', + time: '2022-10-13', + }, + { + id: '4', + version: 'version4', + operation: 'online', + updated: 'person4', + time: '2022-10-14', + }, + { + id: '5', + version: 'version5', + operation: 'online', + updated: 'person5', + time: '2022-10-15', + }, + { + id: '6', + version: 'version6', + operation: 'online', + updated: 'person6', + time: '2022-10-16', + }, + ], +}); + +const req = [ + { + url: '/api/detail/getdata', + method: 'get', + response: () => { + return successResponseWrap(initData); + }, + }, +] as const; + +export default req; diff --git a/template/nest-next/src/mock/data/setup.ts b/template/nest-next/src/mock/data/setup.ts new file mode 100644 index 00000000..f2353af3 --- /dev/null +++ b/template/nest-next/src/mock/data/setup.ts @@ -0,0 +1,227 @@ +import { mock } from 'mockjs'; + +export const initData = mock({ + chartData: [ + { + title: 'userInfo.week.1', + value: 1, + list: [ + { + type: 'userInfo.type.optionA', + status: 'userInfo.status.optionA', + len: 1, + bid: 'A', + pid: 'A', + }, + { + type: 'userInfo.type.optionC', + status: 'userInfo.status.optionB', + len: 5, + bid: 'c', + pid: 'B', + }, + { + type: 'userInfo.type.optionA', + status: 'userInfo.status.optionC', + len: 3, + bid: 'A', + pid: 'C', + }, + { + type: 'userInfo.type.optionA', + status: 'userInfo.status.optionA', + len: 1, + bid: 'A', + pid: 'A', + }, + { + type: 'userInfo.type.optionB', + status: 'userInfo.status.optionA', + len: 6, + bid: 'B', + pid: 'A', + }, + { + type: 'userInfo.type.optionA', + status: 'userInfo.status.optionC', + len: 1, + bid: 'A', + pid: 'C', + }, + { + type: 'userInfo.type.optionA', + status: 'userInfo.status.optionA', + len: 1, + bid: 'A', + pid: 'A', + }, + { + type: 'userInfo.type.optionA', + status: 'userInfo.status.optionB', + len: 1, + bid: 'A', + pid: 'B', + }, + { + type: 'userInfo.type.optionB', + status: 'userInfo.status.optionA', + len: 1, + bid: 'B', + pid: 'A', + }, + { + type: 'userInfo.type.optionA', + status: 'userInfo.status.optionC', + len: 1, + bid: 'A', + pid: 'C', + }, + { + type: 'userInfo.type.optionA', + status: 'userInfo.status.optionD', + len: 1, + bid: 'A', + pid: 'D', + }, + { + type: 'userInfo.type.optionC', + status: 'userInfo.status.optionD', + len: 1, + bid: 'C', + pid: 'D', + }, + { + type: 'userInfo.type.optionA', + status: 'userInfo.status.optionD', + len: 1, + bid: 'A', + pid: 'D', + }, + ], + }, + { title: 'userInfo.month.1', value: 0 }, + { title: 'userInfo.month.2', value: 0 }, + { title: 'userInfo.month.3', value: 0 }, + { title: 'userInfo.month.4', value: 0 }, + { title: 'userInfo.month.5', value: 0 }, + { title: 'userInfo.month.6', value: 0 }, + { title: 'userInfo.month.7', value: 0 }, + { title: 'userInfo.month.8', value: 0 }, + { title: 'userInfo.month.9', value: 0 }, + { title: 'userInfo.month.10', value: 0 }, + { title: 'userInfo.month.11', value: 0 }, + { title: 'userInfo.month.12', value: 0 }, + { title: 'userInfo.month.13', value: 0 }, + { title: 'userInfo.month.14', value: 0 }, + { title: 'userInfo.month.15', value: 0 }, + { title: 'userInfo.month.16', value: 0 }, + { title: 'userInfo.month.17', value: 0 }, + ], + tableData: [ + { + id: '1', + bid: 'A', + pid: 'D', + name: 'GFD Company', + time: '2021-12-18', + type: 'userInfo.type.optionA', + status: 'userInfo.status.optionD', + }, + { + id: '2', + bid: 'B', + pid: 'A', + name: 'WWWW Company', + time: '2021-11-18', + type: 'userInfo.type.optionB', + status: 'userInfo.status.optionA', + }, + { + id: '3', + bid: 'C', + pid: 'B', + name: 'TGBYX Company', + time: '2021-10-18', + type: 'userInfo.type.optionC', + status: 'userInfo.status.optionB', + }, + { + id: '4', + bid: 'B', + pid: 'D', + name: 'GF Company', + time: '2021-09-18', + type: 'userInfo.type.optionB', + status: 'userInfo.status.optionC', + }, + { + id: '5', + bid: 'C', + pid: 'C', + name: 'Property management company', + time: '2021-07-18', + type: 'userInfo.type.optionA', + status: 'userInfo.status.optionD', + }, + { + id: '6', + bid: 'A', + pid: 'C', + name: 'Property management company', + time: '2020-12-23', + type: 'userInfo.type.optionA', + status: 'userInfo.status.optionC', + }, + { + id: '7', + bid: 'B', + pid: 'C', + name: 'GF Company', + time: '2020-11-08', + type: 'userInfo.type.optionB', + status: 'userInfo.status.optionC', + }, + { + id: '8', + bid: 'B', + pid: 'C', + name: 'WWWW Company', + time: '2020-10-18', + type: 'userInfo.type.optionB', + status: 'userInfo.status.optionC', + }, + { + id: '9', + bid: 'C', + pid: 'D', + name: 'WWWW Company', + time: '2020-10-11', + type: 'userInfo.type.optionC', + status: 'userInfo.status.optionD', + }, + { + id: '10', + bid: 'C', + pid: 'D', + name: 'TGBYX Company', + time: '2020-06-18', + type: 'userInfo.type.optionC', + status: 'userInfo.status.optionD', + }, + ], + userInfo: { + userId: '10000', + username: 'admin', + department: 'Tiny-Vue-Pro', + employeeType: 'social recruitment', + role: 'admin', + job: 'Front end', + probationStart: '2021-04-19', + probationEnd: '2021-10-15', + probationDuration: '180', + protocolStart: '2021-04-19', + protocolEnd: '2024-04-19', + address: 'xian', + status: 'normal', + }, +}); diff --git a/template/nest-next/src/mock/data/user.ts b/template/nest-next/src/mock/data/user.ts new file mode 100644 index 00000000..340676de --- /dev/null +++ b/template/nest-next/src/mock/data/user.ts @@ -0,0 +1,65 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { successResponseWrap } from '../utils'; +import { initData } from './setup'; + +const positive = JSON.parse(JSON.stringify(initData.tableData)); +const negative = JSON.parse(JSON.stringify(initData.tableData.reverse())); +const initlist = JSON.parse(JSON.stringify(initData.chartData[0].list)); +const userInfo = JSON.parse(JSON.stringify(initData.userInfo)); + +const req = [ + { + url: '/api/user/data', + method: 'post', + response: (params: any) => { + const { sort, startTime, endTime, filterStatus, filterType } = JSON.parse( + JSON.stringify(params.body), + ); + initData.tableData = positive; + initData.chartData[0].list = initlist; + if (sort === 1 || sort === 3) { + initData.chartData[0].list.reverse(); + initData.tableData = positive; + return successResponseWrap(initData); + } + if (sort === 2 || sort === 4) { + initData.chartData[0].list.reverse(); + initData.tableData = negative; + return successResponseWrap(initData); + } + if ( + startTime !== '' || + endTime !== '' || + filterStatus.length !== 0 || + (filterType.length !== 0 && sort === undefined) + ) { + const start = new Date(JSON.parse(JSON.stringify(startTime))).getTime(); + const end = new Date(JSON.parse(JSON.stringify(endTime))).getTime(); + + const table = initData.tableData.filter(function (item: any) { + return ( + filterType.includes(item.bid) && + filterStatus.includes(item.pid) && + new Date(JSON.parse(JSON.stringify(item.time))).getTime() - start > + 0 && + new Date(JSON.parse(JSON.stringify(item.time))).getTime() - end < 0 + ); + }); + + const chart = initData.chartData[0].list.filter(function (item: any) { + return ( + filterType.includes(item.bid) && filterStatus.includes(item.pid) + ); + }); + initData.tableData = table; + initData.chartData[0].list = chart; + return successResponseWrap(initData); + } + return successResponseWrap(initData); + }, + }, +] as const; + +export default req; diff --git a/template/nest-next/src/mock/mock.controller.ts b/template/nest-next/src/mock/mock.controller.ts new file mode 100644 index 00000000..1e1238aa --- /dev/null +++ b/template/nest-next/src/mock/mock.controller.ts @@ -0,0 +1,39 @@ +import { + Controller, + Get, + HttpException, + HttpStatus, + Post, + Req, +} from '@nestjs/common'; +import type { Request } from 'express'; +import data from './data'; + +@Controller('mock') +export class MockController { + @Get('*path') + getMock(@Req() req: Request) { + const path = req.path.replace('/mock', ''); + const item = data.filter( + (dataItem) => dataItem.method === 'get' && dataItem.url === path, + ); + if (!item.length) { + throw new HttpException('not found', HttpStatus.NOT_FOUND); + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return item[0].response({ body: null }); + } + + @Post('*path') + postMock(@Req() req: Request) { + const path = req.path.replace('/mock', ''); + const item = data.filter( + (dataItem) => dataItem.method === 'post' && dataItem.url === path, + ); + if (!item.length) { + throw new HttpException('not found', HttpStatus.NOT_FOUND); + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return item[0].response({ body: req.body }); + } +} diff --git a/template/nest-next/src/mock/mock.module.ts b/template/nest-next/src/mock/mock.module.ts new file mode 100644 index 00000000..d94c724c --- /dev/null +++ b/template/nest-next/src/mock/mock.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { MockService } from './mock.service'; +import { MockController } from './mock.controller'; + +@Module({ + controllers: [MockController], + providers: [MockService], +}) +export class MockModule {} diff --git a/template/nest-next/src/mock/mock.service.ts b/template/nest-next/src/mock/mock.service.ts new file mode 100644 index 00000000..be140f36 --- /dev/null +++ b/template/nest-next/src/mock/mock.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class MockService {} diff --git a/template/nest-next/src/mock/utils.ts b/template/nest-next/src/mock/utils.ts new file mode 100644 index 00000000..6c72a33a --- /dev/null +++ b/template/nest-next/src/mock/utils.ts @@ -0,0 +1,41 @@ +export const successResponseWrapper = (data: unknown) => { + return { + data, + status: 'ok', + msg: '请求成功', + code: 20000, + }; +}; + +export const failResponseWrapper = ( + data: unknown, + msg: string, + code = 50000 +) => { + return { + data, + status: 'fail', + msg, + code, + }; +}; + +export const successResponseWrap = (data: unknown) => { + return { + data, + errMsg: '', + code: '0', + }; +}; + +export const failResponseWrap = ( + data: unknown, + errMsg: string, + code = '500' +) => { + return { + data, + errMsg, + code, + }; +}; diff --git a/template/nest-next/src/permission/dto/create-permission.dto.ts b/template/nest-next/src/permission/dto/create-permission.dto.ts index 23a64c14..ab81830c 100644 --- a/template/nest-next/src/permission/dto/create-permission.dto.ts +++ b/template/nest-next/src/permission/dto/create-permission.dto.ts @@ -16,10 +16,7 @@ export class CreatePermissionDto { @ApiProperty({ description: '权限在后台, 人类可读的短简介', }) - @IsNotEmpty({ - message: i18nValidationMessage('validation.NOT_EMPTY'), - }) - desc: string; + desc: string = ''; } export class CreatePermissionResponse { diff --git a/template/nest-next/src/permission/queries/get-all-permissions.query.ts b/template/nest-next/src/permission/queries/get-all-permissions.query.ts index 8475bab7..e62dee9c 100644 --- a/template/nest-next/src/permission/queries/get-all-permissions.query.ts +++ b/template/nest-next/src/permission/queries/get-all-permissions.query.ts @@ -31,10 +31,12 @@ export class GetAllPermissionQueryHandler implements IQueryHandler toRoleId(item.id)) }, + roleId: { $in: roles.map((item) => toRoleId(item.roleId)) }, }); const rolePermissionIds = rolePermissions.map((item) => item.permissionId); const permissions = await this.permissionRepository.find({ id: { $in: rolePermissionIds }, }); - if (!permissions.length) { return false; } - if (permissions.find((perm) => perm.name === '*')) { return true; } return permissionName.every( (name) => permissions.find((perm) => perm.name === name) !== undefined, ); - // return permissions.every((perm) => permissionName.includes(perm.name)); } } diff --git a/template/nest-next/src/role/assembler/find-role.ts b/template/nest-next/src/role/assembler/find-role.ts index 3d14245d..327524cc 100644 --- a/template/nest-next/src/role/assembler/find-role.ts +++ b/template/nest-next/src/role/assembler/find-role.ts @@ -12,7 +12,7 @@ export const assembleFindRole = ( return new FindRoleResponse({ id: role.id, name: role.name, - permissions: [...permission], + permission: [...permission], menus: menuTree, }); }; diff --git a/template/nest-next/src/role/commands/remove-role.command.ts b/template/nest-next/src/role/commands/remove-role.command.ts index 000c6658..df8ae24d 100644 --- a/template/nest-next/src/role/commands/remove-role.command.ts +++ b/template/nest-next/src/role/commands/remove-role.command.ts @@ -2,10 +2,11 @@ import { Command, CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { Role, RoleId, RoleMenu, RolePermission } from '../role.entity'; import { EntityManager, EntityRepository } from '@mikro-orm/core'; import { InjectRepository } from '@mikro-orm/nestjs'; -import { RoleNotFound } from '../errors'; +import { RoleHasUserError, RoleNotFound } from '../errors'; import { RedisService } from '@liaoliaots/nestjs-redis'; import Redis from 'ioredis'; import { roleTotal } from '@app/shared'; +import { UserRole } from '../../user'; export class RemoveRoleCommand extends Command { constructor(public readonly id: RoleId) { @@ -20,6 +21,8 @@ export class RemoveRoleHandler implements ICommandHandler { private readonly em: EntityManager, @InjectRepository(Role) private readonly role: EntityRepository, + @InjectRepository(UserRole) + private readonly userRoleRepo: EntityRepository, private readonly redisService: RedisService, ) { this.redis = this.redisService.getOrThrow(); @@ -29,7 +32,12 @@ export class RemoveRoleHandler implements ICommandHandler { if (!role) { throw new RoleNotFound(); } - // TODO: 查找是否有用户使用该角色,如果有则不能删除 + const user = await this.userRoleRepo.findOne({ + roleId: command.id, + }); + if (user) { + throw new RoleHasUserError(); + } await this.em.transactional(async (em) => { await em.nativeDelete(Role, { id: command.id }); await em.nativeDelete(RolePermission, { roleId: command.id }); diff --git a/template/nest-next/src/role/commands/update-role.command.ts b/template/nest-next/src/role/commands/update-role.command.ts index f0c93f35..328cf15e 100644 --- a/template/nest-next/src/role/commands/update-role.command.ts +++ b/template/nest-next/src/role/commands/update-role.command.ts @@ -31,27 +31,31 @@ export class UpdateRoleHandler implements ICommandHandler { ) {} async execute(command: UpdateRoleCommand): Promise { const { id, name } = command; - const role = await this.role.findOne(id); - if (!role) { - throw new RoleNotFound(); - } - if (name) { - role.name = name; - } - if (command.permissionIds) { - command.permissionIds - .map((pm) => { - return this.rolePermission.create({ roleId: id, permissionId: pm }); - }) - .forEach((perm) => this.em.persist(perm)); - } - if (command.menuIds) { - command.menuIds - .map((menuId) => this.roleMenu.create({ roleId: id, menuId })) - .forEach((menu) => this.em.persist(menu)); - } - this.em.persist(role); - await this.em.flush(); - return role.id; + return this.em.transactional(async (em) => { + const role = await this.role.findOne({ id }, { em }); + if (!role) { + throw new RoleNotFound(); + } + if (name) { + role.name = name; + } + if (command.permissionIds) { + await em.nativeDelete(RolePermission, { roleId: id }); + command.permissionIds + .map((pm) => { + return em.create(RolePermission, { roleId: id, permissionId: pm }); + }) + .forEach((pm) => em.persist(pm)); + } + if (command.menuIds) { + await em.nativeDelete(RoleMenu, { roleId: id }); + command.menuIds + .map((menuId) => em.create(RoleMenu, { roleId: id, menuId })) + .forEach((menu) => em.persist(menu)); + } + em.persist(role); + await em.flush(); + return role.id; + }); } } diff --git a/template/nest-next/src/role/dto/create-role.dto.ts b/template/nest-next/src/role/dto/create-role.dto.ts index c30ba029..30aad845 100644 --- a/template/nest-next/src/role/dto/create-role.dto.ts +++ b/template/nest-next/src/role/dto/create-role.dto.ts @@ -15,12 +15,12 @@ export class CreateRoleDto { }) @IsArray() @IsNotEmpty() - permissionIds: PermissionId[] = []; + permissionIds: PermissionId[]; @ApiProperty({ description: '菜单ID', type: [String], }) @IsArray() @IsNotEmpty() - menuIds: MenuId[] = []; + menuIds: MenuId[]; } diff --git a/template/nest-next/src/role/dto/find-role.dto.ts b/template/nest-next/src/role/dto/find-role.dto.ts index d0700ae7..25910e6b 100644 --- a/template/nest-next/src/role/dto/find-role.dto.ts +++ b/template/nest-next/src/role/dto/find-role.dto.ts @@ -48,7 +48,7 @@ export class FindRoleResponse { @ApiProperty({ description: '角色名称' }) name: string; @ApiProperty({ description: '角色所拥有的权限', type: [FindRolePermission] }) - permissions: FindRolePermission[]; + permission: FindRolePermission[]; @ApiProperty({ description: '角色所拥有的菜单', type: [FindRoleMenu] }) menus: FindRoleMenu[]; constructor(props: FindRoleResponse) { diff --git a/template/nest-next/src/role/dto/get-role-detail.dto.ts b/template/nest-next/src/role/dto/get-role-detail.dto.ts index 99b4eec4..b896e6a9 100644 --- a/template/nest-next/src/role/dto/get-role-detail.dto.ts +++ b/template/nest-next/src/role/dto/get-role-detail.dto.ts @@ -55,13 +55,13 @@ export class GetRoleInfo { @ApiProperty({ type: [RoleInfoItem], }) - item: RoleInfoItem[]; + items: RoleInfoItem[]; @ApiProperty({ type: PaginationMeta, }) meta: PaginationMeta; constructor(item: RoleInfoItem[], meta: PaginationMeta) { - this.item = item; + this.items = item; this.meta = meta; } } diff --git a/template/nest-next/src/role/errors/index.ts b/template/nest-next/src/role/errors/index.ts index 35e54164..0e61206e 100644 --- a/template/nest-next/src/role/errors/index.ts +++ b/template/nest-next/src/role/errors/index.ts @@ -1,2 +1,3 @@ export { RoleNotFound } from './role-not-found'; export { RoleExistsError } from './role-exists'; +export { RoleHasUserError } from './role-has-user'; diff --git a/template/nest-next/src/role/errors/role-has-user.ts b/template/nest-next/src/role/errors/role-has-user.ts new file mode 100644 index 00000000..01c85a13 --- /dev/null +++ b/template/nest-next/src/role/errors/role-has-user.ts @@ -0,0 +1,11 @@ +import { DomainError } from '@app/shared'; +import { HttpStatus } from '@nestjs/common'; + +export class RoleHasUserError extends DomainError { + constructor() { + super({ + message: 'exception.role.NOT_EMPTY', + code: HttpStatus.BAD_REQUEST, + }); + } +} diff --git a/template/nest-next/src/role/queries/find-all-role.query.ts b/template/nest-next/src/role/queries/find-all-role.query.ts index 949a857c..e920a01c 100644 --- a/template/nest-next/src/role/queries/find-all-role.query.ts +++ b/template/nest-next/src/role/queries/find-all-role.query.ts @@ -23,7 +23,7 @@ export class FindAllRoleHandler implements IQueryHandler { const filter = query.page && query.limit ? { - page: query.page, + offset: (query.page - 1) * query.limit, limit: query.limit, } : {}; @@ -31,7 +31,7 @@ export class FindAllRoleHandler implements IQueryHandler { ...filter, where: query.name ? { - name: query.name, + name: { $like: query.name }, } : undefined, }); diff --git a/template/nest-next/src/role/queries/get-role-total.query.ts b/template/nest-next/src/role/queries/get-role-total.query.ts index 396f1621..63d98e31 100644 --- a/template/nest-next/src/role/queries/get-role-total.query.ts +++ b/template/nest-next/src/role/queries/get-role-total.query.ts @@ -4,7 +4,6 @@ import Redis from 'ioredis'; import { Role } from '../role.entity'; import { EntityRepository } from '@mikro-orm/core'; import { InjectRepository } from '@mikro-orm/nestjs'; -import { roleTotal } from '@app/shared'; export class GetRoleTotal extends Query { constructor(public readonly name?: string) { @@ -23,12 +22,13 @@ export class GetRoleTotalHandler implements IQueryHandler { this.redis = this.redisService.getOrThrow(); } async execute(query: GetRoleTotal): Promise { - if (!query.name) { - const cnt = await this.roleRepo.count({ - name: query.name ? { $like: `${query.name}` } : undefined, - }); - return cnt; - } - return this.redis.get(roleTotal()).then((val) => (val ? Number(val) : 1)); + const cnt = await this.roleRepo.count( + query.name + ? { + name: { $like: `${query.name}` }, + } + : {}, + ); + return cnt; } } diff --git a/template/nest-next/src/role/role.module.ts b/template/nest-next/src/role/role.module.ts index 7e1cc0d1..94582e66 100644 --- a/template/nest-next/src/role/role.module.ts +++ b/template/nest-next/src/role/role.module.ts @@ -19,9 +19,12 @@ import { } from './commands'; import { OnPermissionRemoved } from './event-handler'; import { OnMenuRemoved } from './event-handler/on-menu-removed'; +import { UserRole } from '../user'; @Module({ - imports: [MikroOrmModule.forFeature([Role, RoleMenu, RolePermission])], + imports: [ + MikroOrmModule.forFeature([Role, RoleMenu, RolePermission, UserRole]), + ], controllers: [RoleController], providers: [ RoleService, diff --git a/template/nest-next/src/role/role.service.ts b/template/nest-next/src/role/role.service.ts index 66781c50..0d34ad97 100644 --- a/template/nest-next/src/role/role.service.ts +++ b/template/nest-next/src/role/role.service.ts @@ -28,8 +28,9 @@ export class RoleService { ) {} async remove(id: RoleId) { - const roleId = await this.cb.execute(new RemoveRoleCommand(id)); - return this.findRole(roleId); + const roleInfo = await this.findRole(id); + await this.cb.execute(new RemoveRoleCommand(id)); + return roleInfo; } async updateRole(dto: UpdateRoleDto) { @@ -81,7 +82,7 @@ export class RoleService { const [permissionIdMap, menuIdMap, total] = await Promise.all([ this.qb.execute(new FindRolePermissionId(roleIds)), this.qb.execute(new FindRoleMenuId(roleIds)), - this.qb.execute(new GetRoleTotal()), + this.qb.execute(new GetRoleTotal(name)), ]); const allPermIds = [...permissionIdMap.values()].flat(); const allMenuIds = [...menuIdMap.values()].flat(); @@ -91,6 +92,7 @@ export class RoleService { ]); const permMap = new Map(permissions.map((p) => [p.id, p])); const menuMap = new Map(menus.map((m) => [m.id, m])); + const roleDetail = assembleGetDetail( roles, permissionIdMap, diff --git a/template/nest-next/src/user/command/batch-remove-user.ts b/template/nest-next/src/user/command/batch-remove-user.ts index 122dcb28..d823942f 100644 --- a/template/nest-next/src/user/command/batch-remove-user.ts +++ b/template/nest-next/src/user/command/batch-remove-user.ts @@ -4,7 +4,7 @@ import { EventBus, ICommandHandler, } from '@nestjs/cqrs'; -import { User, UserId } from '../user.entity'; +import { User, UserId, UserRole } from '../user.entity'; import { EntityRepository } from '@mikro-orm/mysql'; import { InjectRepository } from '@mikro-orm/nestjs'; import { UserRemovedEvent } from '../events'; @@ -20,6 +20,8 @@ export class BatchRemoveUserService implements ICommandHandler constructor( @InjectRepository(User) private readonly userRepo: EntityRepository, + @InjectRepository(UserRole) + private readonly userRoleRepo: EntityRepository, private readonly eventBus: EventBus, ) {} async execute(command: BatchRemoveUser): Promise { @@ -30,6 +32,16 @@ export class BatchRemoveUserService implements ICommandHandler }); const userIds = removedUser.map((user) => user.id); await this.userRepo.getEntityManager().transactional(async (em) => { + await this.userRoleRepo.nativeDelete( + { + user: { + id: { + $in: userIds, + }, + }, + }, + { em }, + ); await this.userRepo.nativeDelete( { id: { diff --git a/template/nest-next/src/user/command/change-password.command.ts b/template/nest-next/src/user/command/change-password.command.ts index 018cc2d2..63789aef 100644 --- a/template/nest-next/src/user/command/change-password.command.ts +++ b/template/nest-next/src/user/command/change-password.command.ts @@ -13,6 +13,7 @@ import { UserPasswordChangedEvent } from '../events'; export class ChangePassword extends Command { constructor( public readonly email: string, + public readonly oldPassword: string, public readonly password: string, public readonly confirmPassword: string, ) { @@ -27,13 +28,15 @@ export class ChangePasswordService implements ICommandHandler { if (!user) { throw new UserNotFound(); } - console.log(command.password); - if (!user.verifyPassword(command.password)) { + if ( + !user.verifyPassword(command.oldPassword) || + command.oldPassword !== command.confirmPassword + ) { throw new PasswordIncorrect(); } user.changePassword(command.password); await this.userRepository.upsert(user); - this.eventBus.publish(new UserPasswordChangedEvent(user.id)); + await this.eventBus.publish(new UserPasswordChangedEvent(user.id)); return user.id; } constructor( diff --git a/template/nest-next/src/user/command/update-user-info.command.ts b/template/nest-next/src/user/command/update-user-info.command.ts index 5468d2e1..602e9d91 100644 --- a/template/nest-next/src/user/command/update-user-info.command.ts +++ b/template/nest-next/src/user/command/update-user-info.command.ts @@ -25,7 +25,10 @@ export class UpdateUserInfoService implements ICommandHandler { ) {} async execute({ dto }: UpdateUserInfo): Promise { const { email, roleIds } = dto; - const user = await this.userRepository.findOneOrFail({ email }); + const user = await this.userRepository.findOneOrFail( + { email }, + { populate: ['role'] }, + ); for (const key of Object.keys(dto)) { if (key === 'email' || key === 'roleIds') { continue; diff --git a/template/nest-next/src/user/command/update-user-password.command.ts b/template/nest-next/src/user/command/update-user-password.command.ts index 14fd9cb9..b6e99fdc 100644 --- a/template/nest-next/src/user/command/update-user-password.command.ts +++ b/template/nest-next/src/user/command/update-user-password.command.ts @@ -1,8 +1,14 @@ -import { Command, CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { + Command, + CommandHandler, + EventBus, + ICommandHandler, +} from '@nestjs/cqrs'; import { User, UserId } from '../user.entity'; import { InjectRepository } from '@mikro-orm/nestjs'; import { EntityRepository } from '@mikro-orm/core'; import { UserNotFound } from '../error'; +import { UserPasswordChangedEvent } from '../events'; export class UpdateUserPassword extends Command { constructor( @@ -18,6 +24,7 @@ export class UpdateUserPasswordService implements ICommandHandler, + private readonly eventBus: EventBus, ) {} async execute(command: UpdateUserPassword): Promise { const user = await this.userRepository.findOne({ email: command.email }); @@ -26,6 +33,7 @@ export class UpdateUserPasswordService implements ICommandHandler { constructor(public data: CreateUserDto) { @@ -35,6 +36,12 @@ export class CreateUserService implements ICommandHandler { address, status, } = data; + const dbUser = await this.userRepository.findOne({ + email, + }); + if (dbUser) { + throw new UserExistsError(dbUser.email); + } const roles = await this.roleRepository.findAll({ where: { id: { diff --git a/template/nest-next/src/user/dto/get-all-user.dto.ts b/template/nest-next/src/user/dto/get-all-user.dto.ts index b934227b..56989d5e 100644 --- a/template/nest-next/src/user/dto/get-all-user.dto.ts +++ b/template/nest-next/src/user/dto/get-all-user.dto.ts @@ -1,13 +1,18 @@ import { PaginationMeta, PaginationQueryDto } from '@app/shared'; -import { IsOptional, IsString } from 'class-validator'; +import { IsArray, IsOptional, IsString } from 'class-validator'; import { RoleId } from 'src/role'; import { UserInfo } from './get-user-info.dto'; +import { Transform } from 'class-transformer'; export class GetAllUserRequest extends PaginationQueryDto { @IsOptional() @IsString() name?: string; @IsOptional() + @Transform(({ value }) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return typeof value === 'string' ? value.split(',') : value; + }) @IsString({ each: true }) role?: RoleId[]; @IsOptional() @@ -16,10 +21,10 @@ export class GetAllUserRequest extends PaginationQueryDto { } export class UserList { - item: UserInfo[]; + items: UserInfo[]; meta: PaginationMeta; constructor(item: UserInfo[], meta: PaginationMeta) { - this.item = item; + this.items = item; this.meta = meta; } } diff --git a/template/nest-next/src/user/dto/update-pwd-user.dto.ts b/template/nest-next/src/user/dto/update-pwd-user.dto.ts index 9f0af798..fcc379a8 100644 --- a/template/nest-next/src/user/dto/update-pwd-user.dto.ts +++ b/template/nest-next/src/user/dto/update-pwd-user.dto.ts @@ -28,4 +28,11 @@ export class UpdatePwdUserDto { message: i18nValidationMessage('validation.NOT_EMPTY'), }) oldPassword: string; + @ApiProperty({ + description: '确认新密码', + }) + @IsNotEmpty({ + message: i18nValidationMessage('validation.NOT_EMPTY'), + }) + confirmNewPassword: string; } diff --git a/template/nest-next/src/user/error/password-not-match.error.ts b/template/nest-next/src/user/error/password-not-match.error.ts new file mode 100644 index 00000000..2a519054 --- /dev/null +++ b/template/nest-next/src/user/error/password-not-match.error.ts @@ -0,0 +1,11 @@ +import { DomainError } from "@app/shared"; +import { HttpStatus } from "@nestjs/common"; + +export class PasswordNotMatchError extends DomainError { + constructor() { + super({ + message: 'exception.user.PASSWORD_NOT_MATCH', + code: HttpStatus.BAD_REQUEST, + }) + } +} diff --git a/template/nest-next/src/user/error/user-exists.error.ts b/template/nest-next/src/user/error/user-exists.error.ts new file mode 100644 index 00000000..37affa39 --- /dev/null +++ b/template/nest-next/src/user/error/user-exists.error.ts @@ -0,0 +1,12 @@ +import { DomainError } from '@app/shared'; +import { HttpStatus } from '@nestjs/common'; + +export class UserExistsError extends DomainError { + constructor(email: string) { + super({ + message: 'exception.user.EXISTS', + code: HttpStatus.BAD_REQUEST, + params: { email }, + }); + } +} diff --git a/template/nest-next/src/user/query/get-all-user.query.ts b/template/nest-next/src/user/query/get-all-user.query.ts index 3cac51cd..b9cd9ef1 100644 --- a/template/nest-next/src/user/query/get-all-user.query.ts +++ b/template/nest-next/src/user/query/get-all-user.query.ts @@ -46,8 +46,8 @@ export class GetAllUserService implements IQueryHandler { whereCondition.name = { $like: name }; conditions.push(name); } - if (role?.length) { - whereCondition.role = { $in: role }; + if (role && role.length) { + whereCondition.role = { roleId: { $in: role } }; role.forEach((val) => conditions.push(val)); } if (email) { @@ -108,13 +108,7 @@ export class GetAllUserService implements IQueryHandler { }); infos.push(info); } - if (conditions.length) { - const cnt = await this.userRepository.count({ ...whereCondition }); - await this.redis.set(userTotal(md5(conditions)), cnt); - } - const total = await this.redis - .get(userTotal(conditions.length ? md5(conditions) : undefined)) - .then((value) => (!value ? 0 : Number.parseInt(value))); + const total = await this.userRepository.count({ ...whereCondition }); const meta = new PaginationMeta(limit, total, limit, page); return new UserList(infos, meta); } diff --git a/template/nest-next/src/user/user.service.ts b/template/nest-next/src/user/user.service.ts index 16dc1560..13d22785 100644 --- a/template/nest-next/src/user/user.service.ts +++ b/template/nest-next/src/user/user.service.ts @@ -15,6 +15,7 @@ import { RemoveUser } from './command/remove-user.command'; import { UpdateUserDto } from './dto/update-user.dto'; import { UpdatePwdUserDto } from './dto/update-pwd-user.dto'; import { UpdatePwdAdminDto } from './dto/update-pwd-admin.dto'; +import { PasswordNotMatchError } from './error/password-not-match.error'; @Injectable() export class UserService { @@ -25,26 +26,35 @@ export class UserService { async createUser(dto: CreateUserDto) { const userId = await this.cb.execute(new CreateUserCommand(dto)); - const [userInfo] = await this.qb.execute(new GetUserInfo({ id: [userId] })); return userInfo; } async removeUser(email: string) { - const id = await this.cb.execute(new RemoveUser(email)); - const [userInfo] = await this.qb.execute(new GetUserInfo({ id: [id] })); + const [userInfo] = await this.qb.execute( + new GetUserInfo({ email: [email] }), + ); + await this.cb.execute(new RemoveUser(email)); return userInfo; } async updateUserInfo(dto: UpdateUserDto) { - const id = await this.qb.execute(new UpdateUserInfo(dto)); + const id = await this.cb.execute(new UpdateUserInfo(dto)); const [userInfo] = await this.qb.execute(new GetUserInfo({ id: [id] })); return userInfo; } async changePassword(dto: UpdatePwdUserDto) { + if (dto.confirmNewPassword !== dto.newPassword) { + throw new PasswordNotMatchError(); + } await this.cb.execute( - new ChangePassword(dto.email, dto.oldPassword, dto.newPassword), + new ChangePassword( + dto.email, + dto.oldPassword, + dto.newPassword, + dto.confirmNewPassword, + ), ); } async resetPassword(dto: UpdatePwdAdminDto) { @@ -63,8 +73,8 @@ export class UserService { } async batchRemove(email: string[]) { - const userIds = await this.cb.execute(new BatchRemoveUser(email)); - const userInfos = await this.qb.execute(new GetUserInfo({ id: userIds })); + const userInfos = await this.qb.execute(new GetUserInfo({ email })); + await this.cb.execute(new BatchRemoveUser(email)); return userInfos; } } diff --git a/template/tinyvue/src/api/card.ts b/template/tinyvue/src/api/card.ts index 2594fc9b..08b22e23 100644 --- a/template/tinyvue/src/api/card.ts +++ b/template/tinyvue/src/api/card.ts @@ -7,7 +7,7 @@ export interface QueryTaskParams { } export function getServicesList(params: QueryTaskParams) { - return axios.get(`${import.meta.env.VITE_BASE_API}/application`, { + return axios.get(`/application`, { params, }) } diff --git a/template/tinyvue/src/api/dashboard.ts b/template/tinyvue/src/api/dashboard.ts index 1c8990a6..8d844d7c 100644 --- a/template/tinyvue/src/api/dashboard.ts +++ b/template/tinyvue/src/api/dashboard.ts @@ -6,7 +6,7 @@ export interface ContentDataRecord { } export function queryContentData() { - return axios.get(`${import.meta.env.VITE_BASE_API}/content-data`) + return axios.get(`/content-data`) } export interface PopularRecord { @@ -45,23 +45,23 @@ export interface QueryVisitDetailParmas { } export function queryPopularList(params: { type: string }) { - return axios.get(`${import.meta.env.VITE_BASE_API}/popular/list`, { params }) + return axios.get(`/popular/list`, { params }) } export function queryDataOverview(key: string) { - return axios.get(`${import.meta.env.VITE_BASE_API}/dashboard/data-overview/${key}`) + return axios.get(`/dashboard/data-overview/${key}`) } export function queryVisitData(params: { days: string }) { - return axios.get(`${import.meta.env.VITE_BASE_API}/dashboard/visit`, { params }) + return axios.get(`/dashboard/visit`, { params }) } export function queryPeriodVisits() { - return axios.get(`${import.meta.env.VITE_BASE_API}/dashboard/period-visits`) + return axios.get(`/dashboard/period-visits`) } export function queryVisitsDetail(params: QueryVisitDetailParmas) { - return axios.get(`${import.meta.env.VITE_BASE_API}/dashboard/visits-detail`, { + return axios.get(`/dashboard/visits-detail`, { params, }) } diff --git a/template/tinyvue/src/api/interceptor.ts b/template/tinyvue/src/api/interceptor.ts index 2d8dea18..f412cacc 100644 --- a/template/tinyvue/src/api/interceptor.ts +++ b/template/tinyvue/src/api/interceptor.ts @@ -3,7 +3,8 @@ import { Modal } from '@opentiny/vue' import locale from '@opentiny/vue-locale' import axios from 'axios' import router from '@/router' -import { clearToken, getToken } from '@/utils/auth' +import { clearToken, getRefreshToken, getToken, setRefreshToken, setToken } from '@/utils/auth' +import { flushToken } from './user' export interface HttpResponse { errMsg: string @@ -11,14 +12,17 @@ export interface HttpResponse { data: T } -const { VITE_API_BASE_URL, VITE_BASE_API, VITE_MOCK_IGNORE } = import.meta +const { VITE_BASE_API, VITE_MOCK_IGNORE } = import.meta .env || { VITE_BASE_API: '', VITE_MOCK_IGNORE: '' } -if (VITE_API_BASE_URL) { - axios.defaults.baseURL = VITE_API_BASE_URL +if (VITE_BASE_API) { + axios.defaults.baseURL = VITE_BASE_API } const ignoreMockApiList = VITE_MOCK_IGNORE?.split(',') || [] + +let refreshPromise: Promise | null = null + axios.interceptors.request.use( (config: AxiosRequestConfig): any => { const isProxy = ignoreMockApiList.includes(config.url) @@ -40,11 +44,10 @@ axios.interceptors.request.use( return config }, (error) => { - // do something return Promise.reject(error) }, ) -// add response interceptors + axios.interceptors.response.use( (response: AxiosResponse) => { const res = response @@ -62,12 +65,62 @@ axios.interceptors.response.use( }) } if (status === 401) { - Modal.message({ - message: locale.t('http.error.TokenExpire'), - status: 'error', - }) - clearToken() - router.replace({ name: 'login' }) + const originalRequest = error.config + if (originalRequest._retry) { + clearToken() + router.replace({ name: 'login' }) + Modal.message({ + message: locale.t('http.error.TokenExpire'), + status: 'error', + }) + return Promise.reject(error) + } + + originalRequest._retry = true + if (!getRefreshToken()) { + clearToken() + router.replace({ name: 'login' }) + Modal.message({ + message: locale.t('http.error.TokenExpire'), + status: 'error', + }) + return Promise.reject(error) + } + + if (!refreshPromise) { + refreshPromise = flushToken({ token: getRefreshToken() }) + .then((res) => { + const newAccessToken = res.data.accessToken + const newRefreshToken = res.data.refreshToken + setToken(newAccessToken) + setRefreshToken(newRefreshToken) + return newAccessToken + }) + .catch((err) => { + clearToken() + router.replace({ name: 'login' }) + Modal.message({ + message: locale.t('http.error.TokenExpire'), + status: 'error', + }) + return Promise.reject(err) + }) + .finally(() => { + refreshPromise = null + }) + } + + return refreshPromise + .then((newToken) => { + if (!originalRequest.headers) { + originalRequest.headers = {} + } + originalRequest.headers.Authorization = `Bearer ${newToken}` + return axios.request(originalRequest) + }) + .catch((err) => { + return Promise.reject(err) + }) } if (status === 400) { data.message = error.response.data.errors?.[0] ?? data.message diff --git a/template/tinyvue/src/api/lang.ts b/template/tinyvue/src/api/lang.ts index fc609f53..15aca26a 100644 --- a/template/tinyvue/src/api/lang.ts +++ b/template/tinyvue/src/api/lang.ts @@ -9,16 +9,16 @@ export interface CreateLangDTO { } export function getAllLang() { - return axios.get(`${import.meta.env.VITE_BASE_API}/lang`) + return axios.get(`/lang`) } export function createLang(data: CreateLangDTO) { - return axios.post(`${import.meta.env.VITE_BASE_API}/lang`, data) + return axios.post(`/lang`, data) } export function patchLang(data: Partial, id: number) { - return axios.patch(`${import.meta.env.VITE_BASE_API}/lang/${id}`, data) + return axios.patch(`/lang/${id}`, data) } export function deleteLang(id: number) { - return axios.delete<{ name: string }>(`${import.meta.env.VITE_BASE_API}/lang/${id}`) + return axios.delete<{ name: string }>(`/lang/${id}`) } diff --git a/template/tinyvue/src/api/local.ts b/template/tinyvue/src/api/local.ts index 9a61aec0..f47233bd 100644 --- a/template/tinyvue/src/api/local.ts +++ b/template/tinyvue/src/api/local.ts @@ -38,28 +38,28 @@ export interface CreateLocalReturn { type DeleteLocaleRet = Omit export function getLocalTable(lang?: string) { - return axios.get(`${import.meta.env.VITE_BASE_API}/i18/format`, { params: { lang } }) + return axios.get(`/i18/format`, { params: { lang } }) } export function getAllLocalItems(page?: number, limit?: number, all?: number, filters?: { [x: string]: number[] | string }) { - return axios.get(`${import.meta.env.VITE_BASE_API}/i18`, { + return axios.get(`/i18`, { params: { page, limit, all, ...filters }, }) } export function createLocalItem(data: CreateLocal) { - return axios.post(`${import.meta.env.VITE_BASE_API}/i18`, data) + return axios.post(`/i18`, data) } export function deleteLocale(id: number) { - return axios.delete(`${import.meta.env.VITE_BASE_API}/i18/${id}`) + return axios.delete(`/i18/${id}`) } export function patchLocal(id: number, data: Partial) { - return axios.patch(`${import.meta.env.VITE_BASE_API}/i18/${id}`, data) + return axios.patch(`/i18/${id}`, data) } export function batchDeleteLocal(ids: string[]) { - return axios.post(`${import.meta.env.VITE_BASE_API}/i18/batch`, ids) + return axios.post(`/i18/batch`, ids) } diff --git a/template/tinyvue/src/api/menu.ts b/template/tinyvue/src/api/menu.ts index 793ec51e..e5942a4d 100644 --- a/template/tinyvue/src/api/menu.ts +++ b/template/tinyvue/src/api/menu.ts @@ -35,21 +35,21 @@ export interface CreateMenuDto { } export function getAllMenu() { - return axios.get(`${import.meta.env.VITE_BASE_API}/menu`) + return axios.get(`/menu`) } export function getRoleMenu(email: string) { - return axios.get(`${import.meta.env.VITE_BASE_API}/menu/role/${email}`) + return axios.get(`/menu/role/${email}`) } export function updateMenu(data: any) { - return axios.patch(`${import.meta.env.VITE_BASE_API}/menu`, data) + return axios.patch(`/menu`, data) } export function deleteMenu(id: number, parentId: number) { - return axios.delete(`${import.meta.env.VITE_BASE_API}/menu?id=${id}&parentId=${parentId}`) + return axios.delete(`/menu?id=${id}&parentId=${parentId}`) } export function createMenu(data: any) { - return axios.post(`${import.meta.env.VITE_BASE_API}/menu`, data) + return axios.post(`/menu`, data) } diff --git a/template/tinyvue/src/api/message.ts b/template/tinyvue/src/api/message.ts index 1ff0e49c..f36707aa 100644 --- a/template/tinyvue/src/api/message.ts +++ b/template/tinyvue/src/api/message.ts @@ -14,7 +14,7 @@ export interface MessageRecord { export type MessageListType = MessageRecord[] export function queryMessageList() { - return axios.post(`${import.meta.env.VITE_BASE_API}/message/list`) + return axios.post(`/message/list`) } interface MessageStatus { @@ -22,7 +22,7 @@ interface MessageStatus { } export function setMessageStatus(data: MessageStatus) { - return axios.post(`${import.meta.env.VITE_BASE_API}/message/read`, data) + return axios.post(`/message/read`, data) } export interface ChatRecord { @@ -34,5 +34,5 @@ export interface ChatRecord { } export function queryChatList() { - return axios.post(`${import.meta.env.VITE_BASE_API}/chat/list`) + return axios.post(`/chat/list`) } diff --git a/template/tinyvue/src/api/permission.ts b/template/tinyvue/src/api/permission.ts index 591e39fd..86157676 100644 --- a/template/tinyvue/src/api/permission.ts +++ b/template/tinyvue/src/api/permission.ts @@ -7,19 +7,19 @@ export interface Permission { } export function getAllPermission(page?: number, limit?: number, name?: string) { - return axios.get(`${import.meta.env.VITE_BASE_API}/permission`, { + return axios.get(`/permission`, { params: { page, limit, name }, }) } export function updatePermission(data: any) { - return axios.patch(`${import.meta.env.VITE_BASE_API}/permission`, data) + return axios.patch(`/permission`, data) } export function deletePermission(id: number) { - return axios.delete(`${import.meta.env.VITE_BASE_API}/permission/${id}`) + return axios.delete(`/permission/${id}`) } export function createPermission(data: any) { - return axios.post(`${import.meta.env.VITE_BASE_API}/permission`, data) + return axios.post(`/permission`, data) } diff --git a/template/tinyvue/src/api/role.ts b/template/tinyvue/src/api/role.ts index fcff5a26..c45fc7d3 100644 --- a/template/tinyvue/src/api/role.ts +++ b/template/tinyvue/src/api/role.ts @@ -19,27 +19,27 @@ export interface GetAllRoleDetailRet { } export function getAllRole() { - return axios.get(`${import.meta.env.VITE_BASE_API}/role`) + return axios.get(`/role`) } export function getAllRoleDetail(page = 1, limit = 10, name?: string) { - return axios.get(`${import.meta.env.VITE_BASE_API}/role/detail`, { + return axios.get(`/role/detail`, { params: { page, limit, name }, }) } export function updateRole(data: any) { - return axios.patch(`${import.meta.env.VITE_BASE_API}/role`, data) + return axios.patch(`/role`, data) } -export function deleteRole(id: number) { - return axios.delete(`${import.meta.env.VITE_BASE_API}/role/${id}`) +export function deleteRole(id: string) { + return axios.delete(`/role/${id}`) } export function createRole(data: any) { - return axios.post(`${import.meta.env.VITE_BASE_API}/role`, data) + return axios.post(`/role`, data) } export function getRoleInfo(id: number) { - return axios.get(`${import.meta.env.VITE_BASE_API}/role/info/${id}`) + return axios.get(`/role/info/${id}`) } diff --git a/template/tinyvue/src/api/user.ts b/template/tinyvue/src/api/user.ts index 1f6f477d..bc51ee44 100644 --- a/template/tinyvue/src/api/user.ts +++ b/template/tinyvue/src/api/user.ts @@ -49,17 +49,17 @@ export interface RefreshToken { } export function flushToken(data: RefreshToken) { - return axios.post(`${import.meta.env.VITE_BASE_API}/auth/token/refresh`, data) + return axios.post(`/auth/token/refresh`, data) } export function login(data: LoginData) { - return axios.post(`${import.meta.env.VITE_BASE_API}/auth/login`, data) + return axios.post(`/auth/login`, data) } export function loginMail(data: LoginDataMail) { - return axios.post(`${import.meta.env.VITE_BASE_API}/mail/login`, data) + return axios.post(`/mail/login`, data) } export function logout(data: LogoutData) { - return axios.post(`${import.meta.env.VITE_BASE_API}/auth/logout`, data) + return axios.post(`/auth/logout`, data) } // 获取全部用户 @@ -84,20 +84,20 @@ export function getAllUser(page?: number, limit?: number, filter?: FilterType) { params.set(key, sql) } } - return axios.get(`${import.meta.env.VITE_BASE_API}/user?${params.toString()}`) + return axios.get(`/user?${params.toString()}`) } // 获取单个用户 export function getUserInfo(email?: string) { - return axios.get(`${import.meta.env.VITE_BASE_API}/user/info/${email ?? ''}`) + return axios.get(`/user/info/${email ?? ''}`) } export function deleteUser(email: string) { - return axios.delete(`${import.meta.env.VITE_BASE_API}/user/${email}`) + return axios.delete(`/user/${email}`) } export function updateUserInfo(data: any) { - return axios.patch(`${import.meta.env.VITE_BASE_API}/user/update`, data) + return axios.patch(`/user/update`, data) } export function getUserData(data?: UserData) { @@ -108,17 +108,17 @@ export function getUserData(data?: UserData) { } export function registerUser(data: any) { - return axios.post(`${import.meta.env.VITE_BASE_API}/user/reg`, data) + return axios.post(`/user/reg`, data) } export function updatePwdAdmin(data: any) { - return axios.patch(`${import.meta.env.VITE_BASE_API}/user/admin/updatePwd`, data) + return axios.patch(`/user/admin/updatePwd`, data) } export function updatePwdUser(data: any) { - return axios.patch(`${import.meta.env.VITE_BASE_API}/user/updatePwd`, data) + return axios.patch(`/user/updatePwd`, data) } export function batchDeleteUsers(emails: string[]) { - return axios.post(`${import.meta.env.VITE_BASE_API}/user/batch`, emails) + return axios.post(`/user/batch`, emails) } diff --git a/template/tinyvue/src/api/visualization.ts b/template/tinyvue/src/api/visualization.ts index cfe388ea..db9957bf 100644 --- a/template/tinyvue/src/api/visualization.ts +++ b/template/tinyvue/src/api/visualization.ts @@ -19,7 +19,7 @@ export interface DataChainGrowthRes { } } export function queryDataChainGrowth(data: DataChainGrowth) { - return axios.post(`${import.meta.env.VITE_BASE_API}/data-chain-growth`, data) + return axios.post(`/data-chain-growth`, data) } export interface PopularAuthorRes { @@ -32,7 +32,7 @@ export interface PopularAuthorRes { } export function queryPopularAuthor() { - return axios.get(`${import.meta.env.VITE_BASE_API}/popular-author/list`) + return axios.get(`/popular-author/list`) } export interface ContentPublishRecord { @@ -42,11 +42,11 @@ export interface ContentPublishRecord { } export function queryContentPublish() { - return axios.get(`${import.meta.env.VITE_BASE_API}/content-publish`) + return axios.get(`/content-publish`) } export function queryContentPeriodAnalysis() { - return axios.post(`${import.meta.env.VITE_BASE_API}/content-period-analysis`) + return axios.post(`/content-period-analysis`) } export interface PublicOpinionAnalysis { @@ -59,7 +59,7 @@ export interface PublicOpinionAnalysisRes { } export function queryPublicOpinionAnalysis(data: DataChainGrowth) { return axios.post( - `${import.meta.env.VITE_BASE_API}/public-opinion-analysis`, + `/public-opinion-analysis`, data, ) } @@ -69,5 +69,5 @@ export interface DataOverviewRes { } export function queryDataOverview() { - return axios.post(`${import.meta.env.VITE_BASE_API}/data-overview`) + return axios.post(`/data-overview`) } diff --git a/template/tinyvue/src/router/guard/index.ts b/template/tinyvue/src/router/guard/index.ts index 4d08e81d..c4e23ca7 100644 --- a/template/tinyvue/src/router/guard/index.ts +++ b/template/tinyvue/src/router/guard/index.ts @@ -5,9 +5,9 @@ import setupPermissionGuard from './permission' import { setupTabsGuard } from './tabs' function setupPageGuard(router: Router) { + setupMenuGuard(router) setupPermissionGuard(router) setupInfoGuard(router) - setupMenuGuard(router) setupTabsGuard(router) } diff --git a/template/tinyvue/src/store/modules/user/types.ts b/template/tinyvue/src/store/modules/user/types.ts index 0ea7c9c9..dc9499d8 100644 --- a/template/tinyvue/src/store/modules/user/types.ts +++ b/template/tinyvue/src/store/modules/user/types.ts @@ -1,13 +1,13 @@ export type RoleType = '' | '*' | 'admin' | 'user' export interface Role { - id: number + id: string name: string permission: { name: string desc: string - id: number + id: string }[] - menus: { id: number, name: string }[] + menus: { id: string, name: string }[] } export interface UserInfo { id: string diff --git a/template/tinyvue/src/views/menu/info/components/add-menu.vue b/template/tinyvue/src/views/menu/info/components/add-menu.vue index 73a3c739..2dbbf024 100644 --- a/template/tinyvue/src/views/menu/info/components/add-menu.vue +++ b/template/tinyvue/src/views/menu/info/components/add-menu.vue @@ -20,7 +20,7 @@ const props = defineProps<{ interface TreeSelectMenu { label: string - id: number + id: string; children: TreeSelectMenu[] } @@ -52,7 +52,7 @@ function cover(data: ITreeNodeData[]) { const dfs = (menu: ITreeNodeData) => { const ret: TreeSelectMenu = { label: menu.label, - id: Number(menu.id), + id: menu.id.toString(), children: [], } for (let i = 0; i < menu.children.length; i += 1) { diff --git a/template/tinyvue/src/views/menu/info/components/info-tab.vue b/template/tinyvue/src/views/menu/info/components/info-tab.vue index 8b418e5b..485aadfa 100644 --- a/template/tinyvue/src/views/menu/info/components/info-tab.vue +++ b/template/tinyvue/src/views/menu/info/components/info-tab.vue @@ -124,8 +124,7 @@ function onDelete(data: Node) { if (node.parentId === null) { node.parentId = -1 } - - deleteMenu(Number(node.id.toString()), node.parentId) + deleteMenu(node.id.toString(), node.parentId) .then(() => { TinyModal.message({ message: '删除成功', diff --git a/template/tinyvue/src/views/role/info/components/add-role.vue b/template/tinyvue/src/views/role/info/components/add-role.vue index 5f75a00f..c28195a2 100644 --- a/template/tinyvue/src/views/role/info/components/add-role.vue +++ b/template/tinyvue/src/views/role/info/components/add-role.vue @@ -76,6 +76,7 @@ defineExpose({ }, onConfirm, }) + - diff --git a/template/tinyvue/src/views/role/info/components/info-tab.vue b/template/tinyvue/src/views/role/info/components/info-tab.vue index cb840604..79588566 100644 --- a/template/tinyvue/src/views/role/info/components/info-tab.vue +++ b/template/tinyvue/src/views/role/info/components/info-tab.vue @@ -58,7 +58,7 @@ getAllMenu() setLoading(false) }) getAllPermission().then(({ data }) => { - permissions.value = data + permissions.value = data.items }) const pagerConfigSm = { component: TinyPager, diff --git a/template/tinyvue/src/views/role/info/components/role-table.vue b/template/tinyvue/src/views/role/info/components/role-table.vue index 8616ca9e..da8453d3 100644 --- a/template/tinyvue/src/views/role/info/components/role-table.vue +++ b/template/tinyvue/src/views/role/info/components/role-table.vue @@ -49,7 +49,7 @@ function onMenuUpdate(data: ITreeNodeData[], roldId: number, role: Role) { emits('menuUpdate', data, roldId, role) } -function onRoleDelete(id: number, row) { +function onRoleDelete(id: string, row) { setLoading(true) deleteRole(id) .then(() => { @@ -86,8 +86,11 @@ function getPermission(row: any) { } function onUpdate(args: any) { const menuIds = args.row.menus.map(menu => menu.id) + const permissionIds = args.row.permissionIds updateRole({ - ...args.row, + id: args.row.id, + name: args.row.name, + permissionIds, menuIds, }) .then(() => { @@ -154,8 +157,8 @@ defineExpose({ 'collapse-tags': true, 'value-key': 'id', 'options': props.permissions, - 'textField': 'name', - 'valueField': 'id', + 'text-field': 'name', + 'value-field': 'id', }, }" >