feat: 接入字典管理接口
This commit is contained in:
parent
cf2c20ba22
commit
bd4937a8c7
@ -171,11 +171,15 @@ VALUES
|
||||
(2041700000000120101, 2041700000000100201, 'SystemMenuCreate', NULL, NULL, NULL, 'System:Menu:Create', 'button', '{"title":"common.create"}', 1, 0),
|
||||
(2041700000000120102, 2041700000000100201, 'SystemMenuEdit', NULL, NULL, NULL, 'System:Menu:Edit', 'button', '{"title":"common.edit"}', 1, 0),
|
||||
(2041700000000120103, 2041700000000100201, 'SystemMenuDelete', NULL, NULL, NULL, 'System:Menu:Delete', 'button', '{"title":"common.delete"}', 1, 0),
|
||||
(2041700000000100202, 2041700000000100002, 'SystemDept', '/system/dept', '/system/dept/list', NULL, 'System:Dept:List', 'menu', '{"icon":"carbon:container-services","title":"system.dept.title"}', 1, 3),
|
||||
(2041700000000100204, 2041700000000100002, 'SystemDict', '/system/dict', '/system/dict/list', NULL, 'System:Dict:List', 'menu', '{"icon":"carbon:data-structured","title":"system.dict.title"}', 1, 3),
|
||||
(2041700000000120207, 2041700000000100204, 'SystemDictCreate', NULL, NULL, NULL, 'System:Dict:Create', 'button', '{"title":"common.create"}', 1, 0),
|
||||
(2041700000000120208, 2041700000000100204, 'SystemDictEdit', NULL, NULL, NULL, 'System:Dict:Edit', 'button', '{"title":"common.edit"}', 1, 0),
|
||||
(2041700000000120209, 2041700000000100204, 'SystemDictDelete', NULL, NULL, NULL, 'System:Dict:Delete', 'button', '{"title":"common.delete"}', 1, 0),
|
||||
(2041700000000100202, 2041700000000100002, 'SystemDept', '/system/dept', '/system/dept/list', NULL, 'System:Dept:List', 'menu', '{"icon":"carbon:container-services","title":"system.dept.title"}', 1, 4),
|
||||
(2041700000000120201, 2041700000000100202, 'SystemDeptCreate', NULL, NULL, NULL, 'System:Dept:Create', 'button', '{"title":"common.create"}', 1, 0),
|
||||
(2041700000000120202, 2041700000000100202, 'SystemDeptEdit', NULL, NULL, NULL, 'System:Dept:Edit', 'button', '{"title":"common.edit"}', 1, 0),
|
||||
(2041700000000120203, 2041700000000100202, 'SystemDeptDelete', NULL, NULL, NULL, 'System:Dept:Delete', 'button', '{"title":"common.delete"}', 1, 0),
|
||||
(2041700000000100203, 2041700000000100002, 'SystemKtTableDemo', '/system/ktTableDemo', '/system/ktTableDemo/list', NULL, 'System:KtTableDemo:List', 'menu', '{"icon":"lucide:table-2","title":"system.ktTableDemo.title"}', 1, 4),
|
||||
(2041700000000100203, 2041700000000100002, 'SystemKtTableDemo', '/system/ktTableDemo', '/system/ktTableDemo/list', NULL, 'System:KtTableDemo:List', 'menu', '{"icon":"lucide:table-2","title":"system.ktTableDemo.title"}', 1, 5),
|
||||
(2041700000000120204, 2041700000000100203, 'SystemKtTableDemoCreate', NULL, NULL, NULL, 'System:KtTableDemo:Create', 'button', '{"title":"common.create"}', 1, 0),
|
||||
(2041700000000120205, 2041700000000100203, 'SystemKtTableDemoEdit', NULL, NULL, NULL, 'System:KtTableDemo:Edit', 'button', '{"title":"common.edit"}', 1, 0),
|
||||
(2041700000000120206, 2041700000000100203, 'SystemKtTableDemoDelete', NULL, NULL, NULL, 'System:KtTableDemo:Delete', 'button', '{"title":"common.delete"}', 1, 0),
|
||||
|
||||
@ -1,16 +1,31 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Post,
|
||||
Query,
|
||||
Res,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { DictService } from './dict.service';
|
||||
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiArrayResponse, ToolsService } from '@/common';
|
||||
import { DictDto } from './dict.dto';
|
||||
import {
|
||||
ApiArrayResponse,
|
||||
ApiPageResponse,
|
||||
vbenPage,
|
||||
vbenSuccess,
|
||||
} from '@/common';
|
||||
import {
|
||||
AdminDictBodyDto,
|
||||
AdminDictDto,
|
||||
AdminDictQueryDto,
|
||||
AdminDictUpdateDto,
|
||||
DictDto,
|
||||
} from './dict.dto';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
|
||||
const componentTypeDictExample = [
|
||||
@ -39,28 +54,78 @@ const chartDictExample = [
|
||||
@Controller('dict')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class DictController {
|
||||
constructor(
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly dictService: DictService,
|
||||
) {}
|
||||
constructor(private readonly dictService: DictService) {}
|
||||
|
||||
@ApiOperation({ summary: '获取字典分页列表' })
|
||||
@ApiPageResponse(AdminDictDto, [
|
||||
{
|
||||
id: '2041700000000300001',
|
||||
dictCode: 'COMPONENT_TYPE',
|
||||
label: '图表',
|
||||
value: '1',
|
||||
childrenCode: 'CHART',
|
||||
sort: 1,
|
||||
status: 1,
|
||||
},
|
||||
])
|
||||
@Get('list')
|
||||
async list(@Query() query: AdminDictQueryDto) {
|
||||
const page = await this.dictService.page(query);
|
||||
return vbenPage(page.items, page.total);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: '获取字典编码选项' })
|
||||
@Get('codes')
|
||||
async codes() {
|
||||
return vbenSuccess(await this.dictService.getDictCodeOptions());
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: '根据key获取字典' })
|
||||
@ApiQuery({ name: 'dictKey', type: String })
|
||||
@ApiArrayResponse(DictDto, componentTypeDictExample)
|
||||
@Get('getDictByKey')
|
||||
async getDictByKey(@Res() res, @Query('dictKey') dictKey: string) {
|
||||
async getDictByKey(@Query('dictKey') dictKey: string) {
|
||||
const dict = await this.dictService.getDictByKey(dictKey);
|
||||
|
||||
return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', dict));
|
||||
return vbenSuccess(dict);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: '根据组件类型获取组件字典' })
|
||||
@ApiQuery({ name: 'type', type: Number })
|
||||
@ApiArrayResponse(DictDto, chartDictExample)
|
||||
@Get('getComponentDictByType')
|
||||
async getComponentDictByType(@Res() res, @Query('type', ParseIntPipe) type) {
|
||||
async getComponentDictByType(@Query('type', ParseIntPipe) type) {
|
||||
const dict = await this.dictService.getComponentDictByType(type);
|
||||
|
||||
return res.send(this.toolsService.res(HttpStatus.OK, '操作成功', dict));
|
||||
return vbenSuccess(dict);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: '新增字典项' })
|
||||
@Post('save')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async save(@Body() body: AdminDictBodyDto) {
|
||||
return vbenSuccess(await this.dictService.save(body));
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: '编辑字典项' })
|
||||
@Post('update')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async update(@Body() body: AdminDictUpdateDto) {
|
||||
return vbenSuccess(await this.dictService.update(body));
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: '删除字典项' })
|
||||
@Delete(':id')
|
||||
async remove(@Param('id') id: string) {
|
||||
return vbenSuccess(await this.dictService.remove(id));
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: '启停字典项' })
|
||||
@Post('toggle')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiQuery({ name: 'id', type: String })
|
||||
@ApiQuery({ name: 'status', type: Number })
|
||||
async toggle(@Query('id') id: string, @Query('status') status: string) {
|
||||
return vbenSuccess(await this.dictService.toggle(id, Number(status)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
export class DictDto {
|
||||
@ApiProperty({
|
||||
@ -15,5 +16,120 @@ export class DictDto {
|
||||
@ApiProperty({
|
||||
example: 1,
|
||||
})
|
||||
value: number;
|
||||
value: number | string;
|
||||
}
|
||||
|
||||
export class AdminDictDto {
|
||||
@ApiProperty({
|
||||
example: '2041700000000300001',
|
||||
})
|
||||
id: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'COMPONENT_TYPE',
|
||||
})
|
||||
dictCode: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '图表',
|
||||
})
|
||||
label: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '1',
|
||||
})
|
||||
value: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: 'CHART',
|
||||
})
|
||||
childrenCode?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: 1,
|
||||
})
|
||||
sort?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: 1,
|
||||
})
|
||||
status?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: '2026-06-03T12:00:00.000Z',
|
||||
})
|
||||
createTime?: Date;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: '2026-06-03T12:00:00.000Z',
|
||||
})
|
||||
updateTime?: Date;
|
||||
}
|
||||
|
||||
export class AdminDictQueryDto {
|
||||
@ApiPropertyOptional()
|
||||
page?: number | string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
pageNo?: number | string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
pageSize?: number | string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
keyword?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
dictCode?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
label?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
value?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
childrenCode?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
status?: number | string;
|
||||
}
|
||||
|
||||
export class AdminDictBodyDto {
|
||||
@ApiProperty({
|
||||
example: 'COMPONENT_TYPE',
|
||||
})
|
||||
dictCode: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '图表',
|
||||
})
|
||||
label: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '1',
|
||||
})
|
||||
value: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: 'CHART',
|
||||
})
|
||||
childrenCode?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: 1,
|
||||
})
|
||||
sort?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: 1,
|
||||
})
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export class AdminDictUpdateDto extends PartialType(AdminDictBodyDto) {
|
||||
@ApiProperty({
|
||||
example: '2041700000000300001',
|
||||
})
|
||||
id: string;
|
||||
}
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
|
||||
import { HttpStatus, Injectable, OnApplicationBootstrap } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { setDictDecodeCache } from '@/common';
|
||||
import { Brackets, Repository } from 'typeorm';
|
||||
import { setDictDecodeCache, throwVbenError } from '@/common';
|
||||
import { AdminDict } from './admin-dict.entity';
|
||||
import {
|
||||
AdminDictBodyDto,
|
||||
AdminDictQueryDto,
|
||||
AdminDictUpdateDto,
|
||||
} from './dict.dto';
|
||||
|
||||
const COMPONENT_TYPE_DICT_KEY = 'COMPONENT_TYPE';
|
||||
|
||||
@ -32,6 +37,160 @@ export class DictService implements OnApplicationBootstrap {
|
||||
}));
|
||||
}
|
||||
|
||||
async page(query: AdminDictQueryDto = {}) {
|
||||
const pageNo = this.toPositiveNumber(query.pageNo ?? query.page, 1);
|
||||
const pageSize = this.toPositiveNumber(query.pageSize, 20);
|
||||
const builder = this.dictRepository
|
||||
.createQueryBuilder('dict')
|
||||
.where('dict.isDeleted = :isDeleted', { isDeleted: false });
|
||||
|
||||
const keyword = this.normalizeText(query.keyword);
|
||||
if (keyword) {
|
||||
builder.andWhere(
|
||||
new Brackets((subBuilder) => {
|
||||
subBuilder
|
||||
.where('dict.dictCode LIKE :keyword', {
|
||||
keyword: `%${keyword}%`,
|
||||
})
|
||||
.orWhere('dict.label LIKE :keyword', {
|
||||
keyword: `%${keyword}%`,
|
||||
})
|
||||
.orWhere('dict.value LIKE :keyword', {
|
||||
keyword: `%${keyword}%`,
|
||||
})
|
||||
.orWhere('dict.childrenCode LIKE :keyword', {
|
||||
keyword: `%${keyword}%`,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
this.applyLikeFilter(builder, 'dictCode', query.dictCode);
|
||||
this.applyLikeFilter(builder, 'label', query.label);
|
||||
this.applyLikeFilter(builder, 'value', query.value);
|
||||
this.applyLikeFilter(builder, 'childrenCode', query.childrenCode);
|
||||
|
||||
if (['0', '1'].includes(String(query.status))) {
|
||||
builder.andWhere('dict.status = :status', {
|
||||
status: Number(query.status),
|
||||
});
|
||||
}
|
||||
|
||||
const [items, total] = await builder
|
||||
.orderBy('dict.dictCode', 'ASC')
|
||||
.addOrderBy('dict.sort', 'ASC')
|
||||
.addOrderBy('dict.createTime', 'ASC')
|
||||
.skip((pageNo - 1) * pageSize)
|
||||
.take(pageSize)
|
||||
.getManyAndCount();
|
||||
|
||||
return {
|
||||
items: items.map((item) => this.serializeDict(item)),
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
async getDictCodeOptions() {
|
||||
const rows = await this.dictRepository
|
||||
.createQueryBuilder('dict')
|
||||
.select('DISTINCT dict.dictCode', 'dictCode')
|
||||
.where('dict.isDeleted = :isDeleted', { isDeleted: false })
|
||||
.orderBy('dict.dictCode', 'ASC')
|
||||
.getRawMany<{ dictCode: string }>();
|
||||
|
||||
return rows
|
||||
.filter((item) => !!item.dictCode)
|
||||
.map((item) => ({
|
||||
label: item.dictCode,
|
||||
value: item.dictCode,
|
||||
}));
|
||||
}
|
||||
|
||||
async save(body: AdminDictBodyDto) {
|
||||
const input = this.normalizeInput(body);
|
||||
const existing = await this.findByCodeValue(input.dictCode, input.value);
|
||||
if (existing && !existing.isDeleted) {
|
||||
throwVbenError('同一字典编码下的字典值已存在', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const entity = existing
|
||||
? this.dictRepository.merge(existing, {
|
||||
...input,
|
||||
isDeleted: false,
|
||||
})
|
||||
: this.dictRepository.create(input);
|
||||
|
||||
await this.dictRepository.save(entity);
|
||||
await this.refreshDecodeCache();
|
||||
return entity.id;
|
||||
}
|
||||
|
||||
async update(body: AdminDictUpdateDto) {
|
||||
const id = this.normalizeText(body.id);
|
||||
if (!id) throwVbenError('字典项ID不能为空', HttpStatus.BAD_REQUEST);
|
||||
|
||||
const dict = await this.dictRepository.findOne({
|
||||
where: {
|
||||
id,
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
if (!dict) throwVbenError('字典项不存在', HttpStatus.BAD_REQUEST);
|
||||
|
||||
const input = this.normalizeInput({
|
||||
childrenCode: body.childrenCode ?? dict.childrenCode,
|
||||
dictCode: body.dictCode ?? dict.dictCode,
|
||||
label: body.label ?? dict.label,
|
||||
sort: body.sort ?? dict.sort,
|
||||
status: body.status ?? dict.status,
|
||||
value: body.value ?? dict.value,
|
||||
});
|
||||
const existing = await this.findByCodeValue(input.dictCode, input.value);
|
||||
if (existing && existing.id !== id) {
|
||||
throwVbenError('同一字典编码下的字典值已存在', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
await this.dictRepository.save(
|
||||
this.dictRepository.merge(dict, {
|
||||
...input,
|
||||
}),
|
||||
);
|
||||
await this.refreshDecodeCache();
|
||||
return null;
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
const normalizedId = this.normalizeText(id);
|
||||
if (!normalizedId)
|
||||
throwVbenError('字典项ID不能为空', HttpStatus.BAD_REQUEST);
|
||||
|
||||
const dict = await this.dictRepository.findOne({
|
||||
where: {
|
||||
id: normalizedId,
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
if (!dict) throwVbenError('字典项不存在', HttpStatus.BAD_REQUEST);
|
||||
|
||||
await this.dictRepository.update(
|
||||
{ id: normalizedId },
|
||||
{
|
||||
isDeleted: true,
|
||||
},
|
||||
);
|
||||
await this.refreshDecodeCache();
|
||||
return null;
|
||||
}
|
||||
|
||||
async toggle(id: string, status: number) {
|
||||
const normalizedStatus = status === 1 ? 1 : 0;
|
||||
await this.update({
|
||||
id,
|
||||
status: normalizedStatus,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
async getDictItemsByKey(dictKey: string): Promise<AdminDictItem[]> {
|
||||
const list = await this.dictRepository.find({
|
||||
where: {
|
||||
@ -89,4 +248,79 @@ export class DictService implements OnApplicationBootstrap {
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
private applyLikeFilter(
|
||||
builder: ReturnType<Repository<AdminDict>['createQueryBuilder']>,
|
||||
field: keyof Pick<
|
||||
AdminDict,
|
||||
'childrenCode' | 'dictCode' | 'label' | 'value'
|
||||
>,
|
||||
value?: string,
|
||||
) {
|
||||
const normalizedValue = this.normalizeText(value);
|
||||
if (!normalizedValue) return;
|
||||
|
||||
builder.andWhere(`dict.${field} LIKE :${field}`, {
|
||||
[field]: `%${normalizedValue}%`,
|
||||
});
|
||||
}
|
||||
|
||||
private async findByCodeValue(dictCode: string, value: string) {
|
||||
return this.dictRepository.findOne({
|
||||
where: {
|
||||
dictCode,
|
||||
value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private normalizeInput(body: AdminDictBodyDto): Partial<AdminDict> {
|
||||
const dictCode = this.normalizeText(body.dictCode);
|
||||
const label = this.normalizeText(body.label);
|
||||
const value = this.normalizeText(body.value);
|
||||
|
||||
if (!dictCode) throwVbenError('字典编码不能为空', HttpStatus.BAD_REQUEST);
|
||||
if (!label) throwVbenError('字典标签不能为空', HttpStatus.BAD_REQUEST);
|
||||
if (!value) throwVbenError('字典值不能为空', HttpStatus.BAD_REQUEST);
|
||||
|
||||
return {
|
||||
childrenCode: this.normalizeNullableText(body.childrenCode),
|
||||
dictCode,
|
||||
label,
|
||||
sort: Number.isFinite(Number(body.sort)) ? Number(body.sort) : 0,
|
||||
status: Number(body.status) === 0 ? 0 : 1,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
private normalizeNullableText(value?: number | string | null) {
|
||||
const text = this.normalizeText(value);
|
||||
return text || null;
|
||||
}
|
||||
|
||||
private normalizeText(value?: number | string | null) {
|
||||
return value === undefined || value === null ? '' : String(value).trim();
|
||||
}
|
||||
|
||||
private serializeDict(dict: AdminDict) {
|
||||
return {
|
||||
childrenCode: dict.childrenCode,
|
||||
createTime: dict.createTime,
|
||||
dictCode: dict.dictCode,
|
||||
id: dict.id,
|
||||
label: dict.label,
|
||||
sort: dict.sort,
|
||||
status: dict.status,
|
||||
updateTime: dict.updateTime,
|
||||
value: dict.value,
|
||||
};
|
||||
}
|
||||
|
||||
private toPositiveNumber(
|
||||
value: number | string | undefined,
|
||||
fallback: number,
|
||||
) {
|
||||
const nextValue = Number(value);
|
||||
return Number.isFinite(nextValue) && nextValue > 0 ? nextValue : fallback;
|
||||
}
|
||||
}
|
||||
|
||||
@ -717,6 +717,7 @@ function itemExampleByPath(path: string) {
|
||||
return adminMenuExample();
|
||||
if (path.includes('/system/dept')) return adminDeptExample();
|
||||
if (path.includes('/system/role')) return adminRoleExample();
|
||||
if (path.includes('/dict')) return adminDictExample();
|
||||
if (path.includes('/component')) return componentExample();
|
||||
if (path.includes('/user')) return adminUserExample();
|
||||
if (path.includes('/timezone')) return { timezone: 'Asia/Shanghai' };
|
||||
@ -742,6 +743,20 @@ function adminLoginExample() {
|
||||
};
|
||||
}
|
||||
|
||||
function adminDictExample() {
|
||||
return {
|
||||
id: '2041700000000300001',
|
||||
dictCode: 'COMPONENT_TYPE',
|
||||
label: '图表',
|
||||
value: '1',
|
||||
childrenCode: 'CHART',
|
||||
sort: 1,
|
||||
status: 1,
|
||||
createTime: '2026-06-03T12:00:00.000Z',
|
||||
updateTime: '2026-06-03T12:00:00.000Z',
|
||||
};
|
||||
}
|
||||
|
||||
function adminUserExample() {
|
||||
return {
|
||||
id: '1000000000000000001',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user