feat: 增加字典树接口

This commit is contained in:
sunlei 2026-06-03 23:08:23 +08:00
parent bd4937a8c7
commit 40180ee0b0
3 changed files with 277 additions and 0 deletions

View File

@ -23,6 +23,7 @@ import {
AdminDictBodyDto, AdminDictBodyDto,
AdminDictDto, AdminDictDto,
AdminDictQueryDto, AdminDictQueryDto,
AdminDictTreeDto,
AdminDictUpdateDto, AdminDictUpdateDto,
DictDto, DictDto,
} from './dict.dto'; } from './dict.dto';
@ -74,6 +75,35 @@ export class DictController {
return vbenPage(page.items, page.total); return vbenPage(page.items, page.total);
} }
@ApiOperation({ summary: '获取字典树列表' })
@ApiArrayResponse(AdminDictTreeDto, [
{
id: '2041700000000300001',
dictCode: 'COMPONENT_TYPE',
label: '图表',
value: '1',
childrenCode: 'CHART',
sort: 1,
status: 1,
treeKey: '2041700000000300001',
children: [
{
id: '2041700000000300002',
dictCode: 'CHART',
label: '折线图',
value: '1',
sort: 1,
status: 1,
treeKey: '2041700000000300001/2041700000000300002',
},
],
},
])
@Get('tree')
async tree(@Query() query: AdminDictQueryDto) {
return vbenSuccess(await this.dictService.tree(query));
}
@ApiOperation({ summary: '获取字典编码选项' }) @ApiOperation({ summary: '获取字典编码选项' })
@Get('codes') @Get('codes')
async codes() { async codes() {

View File

@ -66,6 +66,18 @@ export class AdminDictDto {
updateTime?: Date; updateTime?: Date;
} }
export class AdminDictTreeDto extends AdminDictDto {
@ApiProperty({
example: '2041700000000300001/2041700000000300002',
})
treeKey: string;
@ApiPropertyOptional({
type: () => [AdminDictTreeDto],
})
children?: AdminDictTreeDto[];
}
export class AdminDictQueryDto { export class AdminDictQueryDto {
@ApiPropertyOptional() @ApiPropertyOptional()
page?: number | string; page?: number | string;

View File

@ -17,6 +17,23 @@ export type AdminDictItem = {
value: string; value: string;
}; };
type AdminDictSerialized = {
childrenCode?: string | null;
createTime?: Date;
dictCode: string;
id: string;
label: string;
sort?: number;
status?: number;
updateTime?: Date;
value: string;
};
type AdminDictTreeItem = AdminDictSerialized & {
children?: AdminDictTreeItem[];
treeKey: string;
};
@Injectable() @Injectable()
export class DictService implements OnApplicationBootstrap { export class DictService implements OnApplicationBootstrap {
constructor( constructor(
@ -106,6 +123,23 @@ export class DictService implements OnApplicationBootstrap {
})); }));
} }
async tree(query: AdminDictQueryDto = {}) {
const items = await this.dictRepository.find({
where: {
isDeleted: false,
},
order: {
dictCode: 'ASC',
sort: 'ASC',
createTime: 'ASC',
},
});
const serializedItems = items.map((item) => this.serializeDict(item));
const visibleItems = this.filterTreeItems(serializedItems, query);
return this.buildDictTree(visibleItems);
}
async save(body: AdminDictBodyDto) { async save(body: AdminDictBodyDto) {
const input = this.normalizeInput(body); const input = this.normalizeInput(body);
const existing = await this.findByCodeValue(input.dictCode, input.value); const existing = await this.findByCodeValue(input.dictCode, input.value);
@ -274,6 +308,207 @@ export class DictService implements OnApplicationBootstrap {
}); });
} }
private buildDictTree(items: AdminDictSerialized[]): AdminDictTreeItem[] {
const byDictCode = this.groupItemsByDictCode(items);
const dictCodes = new Set(items.map((item) => item.dictCode));
const referencedCodes = new Set(
items
.map((item) => this.normalizeText(item.childrenCode))
.filter((childrenCode) => childrenCode && dictCodes.has(childrenCode)),
);
const rootCodes = [...dictCodes].filter(
(code) => !referencedCodes.has(code),
);
const targetRootCodes = rootCodes.length > 0 ? rootCodes : [...dictCodes];
return items
.filter((item) => targetRootCodes.includes(item.dictCode))
.map((item) =>
this.createTreeNode(
item,
byDictCode,
item.id,
new Set([item.dictCode]),
),
);
}
private createTreeNode(
item: AdminDictSerialized,
byDictCode: Map<string, AdminDictSerialized[]>,
treeKey: string,
pathCodes: Set<string>,
): AdminDictTreeItem {
const childrenCode = this.normalizeText(item.childrenCode);
const children =
childrenCode && !pathCodes.has(childrenCode)
? byDictCode.get(childrenCode)
: undefined;
const nextPathCodes = new Set(pathCodes);
if (childrenCode) nextPathCodes.add(childrenCode);
const node: AdminDictTreeItem = {
...item,
treeKey,
};
if (children?.length) {
node.children = children.map((child) =>
this.createTreeNode(
child,
byDictCode,
`${treeKey}/${child.id}`,
nextPathCodes,
),
);
}
return node;
}
private filterTreeItems(
items: AdminDictSerialized[],
query: AdminDictQueryDto,
) {
if (!this.hasTreeFilter(query)) return items;
const byDictCode = this.groupItemsByDictCode(items);
const parentsByChildrenCode = this.groupParentsByChildrenCode(items);
const visibleIds = new Set<string>();
const matchedItems = items.filter((item) =>
this.matchesTreeFilter(item, query),
);
matchedItems.forEach((item) => {
this.collectRelatedTreeItems(
item,
byDictCode,
parentsByChildrenCode,
visibleIds,
);
});
return items.filter((item) => visibleIds.has(item.id));
}
private collectRelatedTreeItems(
item: AdminDictSerialized,
byDictCode: Map<string, AdminDictSerialized[]>,
parentsByChildrenCode: Map<string, AdminDictSerialized[]>,
visibleIds: Set<string>,
) {
if (visibleIds.has(item.id)) return;
visibleIds.add(item.id);
const parents = parentsByChildrenCode.get(item.dictCode) || [];
parents.forEach((parent) =>
this.collectRelatedTreeItems(
parent,
byDictCode,
parentsByChildrenCode,
visibleIds,
),
);
const childrenCode = this.normalizeText(item.childrenCode);
if (!childrenCode) return;
const children = byDictCode.get(childrenCode) || [];
children.forEach((child) =>
this.collectRelatedTreeItems(
child,
byDictCode,
parentsByChildrenCode,
visibleIds,
),
);
}
private groupItemsByDictCode(items: AdminDictSerialized[]) {
const map = new Map<string, AdminDictSerialized[]>();
items.forEach((item) => {
const list = map.get(item.dictCode) || [];
list.push(item);
map.set(item.dictCode, list);
});
return map;
}
private groupParentsByChildrenCode(items: AdminDictSerialized[]) {
const map = new Map<string, AdminDictSerialized[]>();
items.forEach((item) => {
const childrenCode = this.normalizeText(item.childrenCode);
if (!childrenCode) return;
const list = map.get(childrenCode) || [];
list.push(item);
map.set(childrenCode, list);
});
return map;
}
private hasTreeFilter(query: AdminDictQueryDto) {
return (
[
query.childrenCode,
query.dictCode,
query.keyword,
query.label,
query.value,
].some((value) => !!this.normalizeText(value)) ||
['0', '1'].includes(String(query.status))
);
}
private matchesTreeFilter(
item: AdminDictSerialized,
query: AdminDictQueryDto,
) {
const keyword = this.normalizeText(query.keyword);
if (
keyword &&
![item.childrenCode, item.dictCode, item.label, item.value].some(
(value) => this.includesText(value, keyword),
)
) {
return false;
}
if (!this.matchesLike(item.dictCode, query.dictCode)) return false;
if (!this.matchesLike(item.label, query.label)) return false;
if (!this.matchesLike(item.value, query.value)) return false;
if (!this.matchesLike(item.childrenCode, query.childrenCode)) return false;
if (['0', '1'].includes(String(query.status))) {
return Number(item.status) === Number(query.status);
}
return true;
}
private matchesLike(
value: number | string | null | undefined,
keyword?: string,
) {
const normalizedKeyword = this.normalizeText(keyword);
if (!normalizedKeyword) return true;
return this.includesText(value, normalizedKeyword);
}
private includesText(
value: number | string | null | undefined,
keyword: string,
) {
return this.normalizeText(value)
.toLowerCase()
.includes(keyword.toLowerCase());
}
private normalizeInput(body: AdminDictBodyDto): Partial<AdminDict> { private normalizeInput(body: AdminDictBodyDto): Partial<AdminDict> {
const dictCode = this.normalizeText(body.dictCode); const dictCode = this.normalizeText(body.dictCode);
const label = this.normalizeText(body.label); const label = this.normalizeText(body.label);