diff --git a/src/admin/dict/dict.controller.ts b/src/admin/dict/dict.controller.ts index 3c3666c..74def63 100644 --- a/src/admin/dict/dict.controller.ts +++ b/src/admin/dict/dict.controller.ts @@ -23,6 +23,7 @@ import { AdminDictBodyDto, AdminDictDto, AdminDictQueryDto, + AdminDictTreeDto, AdminDictUpdateDto, DictDto, } from './dict.dto'; @@ -74,6 +75,35 @@ export class DictController { 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: '获取字典编码选项' }) @Get('codes') async codes() { diff --git a/src/admin/dict/dict.dto.ts b/src/admin/dict/dict.dto.ts index 5861432..3205142 100644 --- a/src/admin/dict/dict.dto.ts +++ b/src/admin/dict/dict.dto.ts @@ -66,6 +66,18 @@ export class AdminDictDto { updateTime?: Date; } +export class AdminDictTreeDto extends AdminDictDto { + @ApiProperty({ + example: '2041700000000300001/2041700000000300002', + }) + treeKey: string; + + @ApiPropertyOptional({ + type: () => [AdminDictTreeDto], + }) + children?: AdminDictTreeDto[]; +} + export class AdminDictQueryDto { @ApiPropertyOptional() page?: number | string; diff --git a/src/admin/dict/dict.service.ts b/src/admin/dict/dict.service.ts index 62067e1..c374fc7 100644 --- a/src/admin/dict/dict.service.ts +++ b/src/admin/dict/dict.service.ts @@ -17,6 +17,23 @@ export type AdminDictItem = { 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() export class DictService implements OnApplicationBootstrap { 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) { const input = this.normalizeInput(body); 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, + treeKey: string, + pathCodes: Set, + ): 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(); + 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, + parentsByChildrenCode: Map, + visibleIds: Set, + ) { + 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(); + + 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(); + + 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 { const dictCode = this.normalizeText(body.dictCode); const label = this.normalizeText(body.label);