diff --git a/prisma/migrations/20261011141231_dict_builtin_color/migration.sql b/prisma/migrations/20261011141231_dict_builtin_color/migration.sql new file mode 100644 index 0000000..65ca5ee --- /dev/null +++ b/prisma/migrations/20261011141231_dict_builtin_color/migration.sql @@ -0,0 +1,9 @@ +-- AlterTable +ALTER TABLE `sys_dict_item` ADD COLUMN `built_in` BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN `color_type` VARCHAR(16) NULL; + +-- AlterTable +ALTER TABLE `sys_dict_type` ADD COLUMN `built_in` BOOLEAN NOT NULL DEFAULT false; + +-- CreateIndex +CREATE UNIQUE INDEX `sys_dict_item_tenant_code_dict_type_code_value_key` ON `sys_dict_item`(`tenant_code`, `dict_type_code`, `value`); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index daae274..c8532cd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -120,6 +120,7 @@ model DictType { code String @db.VarChar(64) name String @db.VarChar(64) status String @default("enabled") @db.VarChar(16) + builtIn Boolean @default(false) @map("built_in") remark String? @db.VarChar(255) createdAt DateTime @default(now()) @map("created_at") createdBy String? @map("created_by") @db.VarChar(36) @@ -140,11 +141,14 @@ model DictItem { value String @db.VarChar(64) sort Int @default(0) status String @default("enabled") @db.VarChar(16) + builtIn Boolean @default(false) @map("built_in") + colorType String? @map("color_type") @db.VarChar(16) createdAt DateTime @default(now()) @map("created_at") createdBy String? @map("created_by") @db.VarChar(36) updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") + @@unique([tenantCode, dictTypeCode, value]) @@index([tenantCode, dictTypeCode]) @@map("sys_dict_item") } diff --git a/prisma/seed.ts b/prisma/seed.ts index f1174bf..6d81ccd 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -70,6 +70,55 @@ const BUTTONS = [ { code: 'login-log:export', name: '登录日志导出', parent: 'login-log' }, ]; +/** 内置枚举字典(值对齐 src/common/enums.ts,仅展示层 label/颜色/排序可改)。 */ +const DICTS: { code: string; name: string; items: { label: string; value: string; colorType: string; sort: number }[] }[] = [ + { + code: 'common_status', + name: '通用状态', + items: [ + { label: '启用', value: 'enabled', colorType: 'success', sort: 1 }, + { label: '停用', value: 'disabled', colorType: 'info', sort: 2 }, + ], + }, + { + code: 'user_status', + name: '用户状态', + items: [ + { label: '启用', value: 'enabled', colorType: 'success', sort: 1 }, + { label: '停用', value: 'disabled', colorType: 'info', sort: 2 }, + { label: '锁定', value: 'locked', colorType: 'danger', sort: 3 }, + ], + }, + { + code: 'resource_type', + name: '资源类型', + items: [ + { label: '菜单', value: 'menu', colorType: 'primary', sort: 1 }, + { label: '按钮', value: 'button', colorType: 'warning', sort: 2 }, + { label: '接口', value: 'api', colorType: 'info', sort: 3 }, + ], + }, + { + code: 'data_scope', + name: '数据权限', + items: [ + { label: '全部', value: 'all', colorType: 'success', sort: 1 }, + { label: '本部门', value: 'dept', colorType: 'info', sort: 2 }, + { label: '本部门及以下', value: 'dept_and_child', colorType: 'warning', sort: 3 }, + { label: '本人', value: 'self', colorType: 'danger', sort: 4 }, + { label: '自定义', value: 'custom', colorType: 'primary', sort: 5 }, + ], + }, + { + code: 'login_status', + name: '登录状态', + items: [ + { label: '成功', value: 'success', colorType: 'success', sort: 1 }, + { label: '失败', value: 'fail', colorType: 'danger', sort: 2 }, + ], + }, +]; + async function main(): Promise { // 1. 租户 await prisma.tenant.upsert({ @@ -170,7 +219,32 @@ async function main(): Promise { }); } - console.log(`seed 完成:租户 ${TENANT}、用户 ${ADMIN_USERNAME}、角色 ${SUPER_ROLE}、资源 ${allResourceCodes.length} 个(已全量授权)。`); + // 8. 内置字典(类型 + 项,幂等) + for (const d of DICTS) { + await prisma.dictType.upsert({ + where: { tenantCode_code: { tenantCode: TENANT, code: d.code } }, + update: { name: d.name, status: 'enabled', builtIn: true }, + create: { tenantCode: TENANT, code: d.code, name: d.name, status: 'enabled', builtIn: true }, + }); + for (const it of d.items) { + await prisma.dictItem.upsert({ + where: { tenantCode_dictTypeCode_value: { tenantCode: TENANT, dictTypeCode: d.code, value: it.value } }, + update: { label: it.label, colorType: it.colorType, sort: it.sort, status: 'enabled', builtIn: true }, + create: { + tenantCode: TENANT, + dictTypeCode: d.code, + label: it.label, + value: it.value, + colorType: it.colorType, + sort: it.sort, + status: 'enabled', + builtIn: true, + }, + }); + } + } + + console.log(`seed 完成:租户 ${TENANT}、用户 ${ADMIN_USERNAME}、角色 ${SUPER_ROLE}、资源 ${allResourceCodes.length} 个(已全量授权)、内置字典 ${DICTS.length} 个。`); } main() diff --git a/src/dicts/dict-items.controller.ts b/src/dicts/dict-items.controller.ts index 59b5083..6dbe99d 100644 --- a/src/dicts/dict-items.controller.ts +++ b/src/dicts/dict-items.controller.ts @@ -38,6 +38,6 @@ export class DictItemsController { @RequirePermissions(Permissions.dict.delete) @AuditLog('dict', 'delete') remove(@Param('id') id: string) { - return this.service.softRemove(id); + return this.service.removeItem(id); } } diff --git a/src/dicts/dict-items.service.ts b/src/dicts/dict-items.service.ts index ab6c3b3..8ae954c 100644 --- a/src/dicts/dict-items.service.ts +++ b/src/dicts/dict-items.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { BaseCrudService } from '../common/base-crud.service.js'; import { PrismaService } from '../prisma/prisma.service.js'; import { CreateDictItemDto, UpdateDictItemDto } from './dto.js'; @@ -13,10 +13,22 @@ export class DictItemsService extends BaseCrudService { return this.create({ ...dto }); } - updateItem(id: string, dto: UpdateDictItemDto) { + /** 更新前拦截内置项 value 变更(label/sort/colorType 放行)。 */ + async updateItem(id: string, dto: UpdateDictItemDto) { + const item = await this.findById(id); + if (item?.builtIn && dto.value !== undefined && dto.value !== item.value) { + throw new BadRequestException('内置字典项 value 不可修改'); + } return this.update(id, { ...dto }); } + /** 删除前拦截内置项。 */ + async removeItem(id: string) { + const item = await this.findById(id); + if (item?.builtIn) throw new BadRequestException('内置字典项不可删除'); + return this.softRemove(id); + } + listByType(dictTypeCode?: string) { return this.list(dictTypeCode ? { dictTypeCode } : {}, { sort: 'asc' }); } diff --git a/src/dicts/dict-types.controller.ts b/src/dicts/dict-types.controller.ts index 8fff1d2..5b926dc 100644 --- a/src/dicts/dict-types.controller.ts +++ b/src/dicts/dict-types.controller.ts @@ -39,6 +39,6 @@ export class DictTypesController { @RequirePermissions(Permissions.dict.delete) @AuditLog('dict', 'delete') remove(@Param('id') id: string) { - return this.service.softRemove(id); + return this.service.removeType(id); } } diff --git a/src/dicts/dict-types.service.ts b/src/dicts/dict-types.service.ts index 0e76540..8414875 100644 --- a/src/dicts/dict-types.service.ts +++ b/src/dicts/dict-types.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { BaseCrudService } from '../common/base-crud.service.js'; import { PrismaService } from '../prisma/prisma.service.js'; import { CreateDictTypeDto, UpdateDictTypeDto } from './dto.js'; @@ -16,4 +16,11 @@ export class DictTypesService extends BaseCrudService { updateType(id: string, dto: UpdateDictTypeDto) { return this.update(id, { ...dto }); } + + /** 删除前拦截内置类型。 */ + async removeType(id: string) { + const type = await this.findById(id); + if (type?.builtIn) throw new BadRequestException('内置字典类型不可删除'); + return this.softRemove(id); + } } diff --git a/src/dicts/dicts.controller.ts b/src/dicts/dicts.controller.ts new file mode 100644 index 0000000..0e2d4af --- /dev/null +++ b/src/dicts/dicts.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Get } from '@nestjs/common'; +import { DictsService } from './dicts.service.js'; + +/** 字典聚合接口:前端登录后一次性拉取全部启用字典(仅登录态,无需权限码)。 */ +@Controller('dicts') +export class DictsController { + constructor(private readonly service: DictsService) {} + + @Get() + all() { + return this.service.allEnabled(); + } +} diff --git a/src/dicts/dicts.module.ts b/src/dicts/dicts.module.ts index db52765..e062f91 100644 --- a/src/dicts/dicts.module.ts +++ b/src/dicts/dicts.module.ts @@ -3,9 +3,11 @@ import { DictTypesController } from './dict-types.controller.js'; import { DictTypesService } from './dict-types.service.js'; import { DictItemsController } from './dict-items.controller.js'; import { DictItemsService } from './dict-items.service.js'; +import { DictsController } from './dicts.controller.js'; +import { DictsService } from './dicts.service.js'; @Module({ - controllers: [DictTypesController, DictItemsController], - providers: [DictTypesService, DictItemsService], + controllers: [DictTypesController, DictItemsController, DictsController], + providers: [DictTypesService, DictItemsService, DictsService], }) export class DictsModule {} diff --git a/src/dicts/dicts.service.ts b/src/dicts/dicts.service.ts new file mode 100644 index 0000000..b497f3f --- /dev/null +++ b/src/dicts/dicts.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service.js'; + +export interface DictAggregateItem { + label: string; + value: string; + colorType: string | null; + sort: number; +} + +export interface DictAggregateType { + code: string; + name: string; + items: DictAggregateItem[]; +} + +/** 字典聚合查询:当前租户全部启用字典类型 + 启用字典项。 */ +@Injectable() +export class DictsService { + constructor(private readonly prisma: PrismaService) {} + + async allEnabled(): Promise<{ list: DictAggregateType[] }> { + const [types, items] = await Promise.all([ + this.prisma.client.dictType.findMany({ where: { status: 'enabled' }, orderBy: { createdAt: 'asc' } }), + this.prisma.client.dictItem.findMany({ + where: { status: 'enabled' }, + orderBy: [{ sort: 'asc' }, { createdAt: 'asc' }], + }), + ]); + const list = types.map((t) => ({ + code: t.code, + name: t.name, + items: items + .filter((i) => i.dictTypeCode === t.code) + .map((i) => ({ label: i.label, value: i.value, colorType: i.colorType, sort: i.sort })), + })); + return { list }; + } +} diff --git a/src/dicts/dto.ts b/src/dicts/dto.ts index 16fb723..6ae3304 100644 --- a/src/dicts/dto.ts +++ b/src/dicts/dto.ts @@ -1,5 +1,8 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { IsIn, IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +/** 标签色(对应前端 TagType)。 */ +const COLOR_TYPES = ['primary', 'success', 'warning', 'danger', 'info'] as const; export class CreateDictTypeDto { @ApiProperty({ description: '字典类型编码' }) @@ -55,6 +58,11 @@ export class CreateDictItemDto { @IsOptional() @IsInt() sort?: number; + + @ApiPropertyOptional({ description: '标签色:primary/success/warning/danger/info' }) + @IsOptional() + @IsIn(COLOR_TYPES) + colorType?: string; } export class UpdateDictItemDto { @@ -77,4 +85,9 @@ export class UpdateDictItemDto { @IsOptional() @IsString() status?: string; + + @ApiPropertyOptional({ description: '标签色:primary/success/warning/danger/info' }) + @IsOptional() + @IsIn(COLOR_TYPES) + colorType?: string; }