Просмотр исходного кода

✨ feat(areas): 实现省市区树形结构API和前端优化

- 后端新增AreaService服务类,提供树形结构构建、子树查询、路径查询等功能
- 新增树形结构API接口,支持完整树、层级树、子树和路径查询
- 前端页面重构树形数据获取方式,使用新的/tree接口替代原有分页查询
- 定义AreaNode接口规范树形数据结构,优化类型定义

♻️ refactor(areas): 优化树形结构构建逻辑

- 将前端树形构建逻辑迁移至后端处理,减少前端计算负担
- 移除前端buildTree函数,新增convertToAreaNode类型转换函数
- 优化API响应数据结构,直接返回构建好的树形数据
yourname 4 месяцев назад
Родитель
Сommit
52b8a762e5

+ 23 - 32
src/client/admin/pages/Areas.tsx

@@ -23,6 +23,17 @@ type AreaResponse = InferResponseType<typeof areaClient.$get, 200>['data'][0];
 type CreateAreaRequest = InferRequestType<typeof areaClient.$post>['json'];
 type UpdateAreaRequest = InferRequestType<typeof areaClient[':id']['$put']>['json'];
 
+// 树形节点类型
+interface AreaNode {
+  id: number;
+  name: string;
+  code: string;
+  level: number;
+  parentId: number | null;
+  isDisabled: number;
+  children?: AreaNode[];
+}
+
 // 统一操作处理函数
 const handleOperation = async (operation: () => Promise<any>) => {
   try {
@@ -89,16 +100,10 @@ export const AreasPage: React.FC = () => {
   const { data: treeData, isLoading: isTreeLoading } = useQuery({
     queryKey: ['areas-tree'],
     queryFn: async () => {
-      const res = await areaClient.$get({
-        query: {
-          page: 1,
-          pageSize: 1000, // 获取所有数据用于构建树
-          relations: ['children']
-        }
-      });
+      const res = await areaClient.tree.$get();
       if (res.status !== 200) throw new Error('获取省市区树形数据失败');
       const response = await res.json();
-      return buildTree(response.data);
+      return response.data;
     },
     staleTime: 5 * 60 * 1000,
     gcTime: 10 * 60 * 1000,
@@ -243,30 +248,16 @@ export const AreasPage: React.FC = () => {
     setIsStatusDialogOpen(true);
   };
 
-  // 构建树形结构
-  const buildTree = (areas: AreaResponse[]): AreaResponse[] => {
-    const areaMap = new Map<number, AreaResponse>();
-    const tree: AreaResponse[] = [];
-
-    // 创建映射
-    areas.forEach(area => {
-      areaMap.set(area.id, { ...area, children: [] });
-    });
-
-    // 构建树
-    areas.forEach(area => {
-      const node = areaMap.get(area.id)!;
-      if (area.parentId === null || area.parentId === 0) {
-        tree.push(node);
-      } else {
-        const parent = areaMap.get(area.parentId);
-        if (parent) {
-          parent.children!.push(node);
-        }
-      }
-    });
-
-    return tree;
+  // 转换 AreaResponse 为 AreaNode
+  const convertToAreaNode = (area: AreaResponse): AreaNode => {
+    return {
+      id: area.id,
+      name: area.name,
+      code: area.code,
+      level: area.level,
+      parentId: area.parentId,
+      isDisabled: area.isDisabled
+    };
   };
 
   // 切换节点展开状态

+ 6 - 1
src/server/api/admin/areas/index.ts

@@ -7,6 +7,7 @@ import {
   getAreaSchema,
   areaListResponseSchema
 } from '@/server/modules/areas/area.schema';
+import treeRoutes from './tree';
 
 // 使用通用CRUD路由创建省市区管理API
 export default createCrudRoutes({
@@ -18,4 +19,8 @@ export default createCrudRoutes({
   searchFields: ['name', 'code'],
   relations: ['parent', 'children'],
   middleware: [authMiddleware]
-})
+})
+
+// 合并树形结构路由
+.route('/tree', treeRoutes);
+

+ 294 - 0
src/server/api/admin/areas/tree.ts

@@ -0,0 +1,294 @@
+import { OpenAPIHono } from '@hono/zod-openapi';
+import { createRoute, z } from '@hono/zod-openapi';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+import { AreaService } from '@/server/modules/areas/area.service';
+import { AreaLevel } from '@/server/modules/areas/area.entity';
+
+
+// 获取完整树形结构
+const getAreaTreeRoute = createRoute({
+  method: 'get',
+  path: '/tree',
+  description: '获取完整的省市区树形结构',
+  tags: ['省市区管理'],
+  middleware: [authMiddleware],
+  responses: {
+    200: {
+      description: '成功获取树形结构',
+      content: {
+        'application/json': {
+          schema: z.object({
+            success: z.boolean(),
+            data: z.array(z.object({
+              id: z.number(),
+              parentId: z.number().nullable(),
+              name: z.string(),
+              level: z.number(),
+              code: z.string(),
+              isDisabled: z.number(),
+              children: z.array(z.any()).optional()
+            }))
+          })
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: z.object({
+            success: z.boolean(),
+            error: z.string()
+          })
+        }
+      }
+    }
+  }
+});
+
+// 根据层级获取树形结构
+const getAreaTreeByLevelRoute = createRoute({
+  method: 'get',
+  path: '/tree/level/{level}',
+  description: '根据层级获取树形结构',
+  tags: ['省市区管理'],
+  middleware: [authMiddleware],
+  request: {
+    params: z.object({
+      level: z.nativeEnum(AreaLevel)
+    })
+  },
+  responses: {
+    200: {
+      description: '成功获取层级树形结构',
+      content: {
+        'application/json': {
+          schema: z.object({
+            success: z.boolean(),
+            data: z.array(z.object({
+              id: z.number(),
+              parentId: z.number().nullable(),
+              name: z.string(),
+              level: z.number(),
+              code: z.string(),
+              isDisabled: z.number(),
+              children: z.array(z.any()).optional()
+            }))
+          })
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: z.object({
+            success: z.boolean(),
+            error: z.string()
+          })
+        }
+      }
+    }
+  }
+});
+
+// 获取子树
+const getSubTreeRoute = createRoute({
+  method: 'get',
+  path: '/tree/{id}',
+  description: '获取指定节点的子树',
+  tags: ['省市区管理'],
+  middleware: [authMiddleware],
+  request: {
+    params: z.object({
+      id: z.coerce.number().positive('区域ID必须为正整数')
+    })
+  },
+  responses: {
+    200: {
+      description: '成功获取子树',
+      content: {
+        'application/json': {
+          schema: z.object({
+            success: z.boolean(),
+            data: z.object({
+              id: z.number(),
+              parentId: z.number().nullable(),
+              name: z.string(),
+              level: z.number(),
+              code: z.string(),
+              isDisabled: z.number(),
+              children: z.array(z.any()).optional()
+            }).nullable()
+          })
+        }
+      }
+    },
+    404: {
+      description: '区域不存在',
+      content: {
+        'application/json': {
+          schema: z.object({
+            success: z.boolean(),
+            error: z.string()
+          })
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: z.object({
+            success: z.boolean(),
+            error: z.string()
+          })
+        }
+      }
+    }
+  }
+});
+
+// 获取区域路径
+const getAreaPathRoute = createRoute({
+  method: 'get',
+  path: '/path/{id}',
+  description: '获取区域路径(从根节点到当前节点)',
+  tags: ['省市区管理'],
+  middleware: [authMiddleware],
+  request: {
+    params: z.object({
+      id: z.coerce.number().positive('区域ID必须为正整数')
+    })
+  },
+  responses: {
+    200: {
+      description: '成功获取区域路径',
+      content: {
+        'application/json': {
+          schema: z.object({
+            success: z.boolean(),
+            data: z.array(z.object({
+              id: z.number(),
+              parentId: z.number().nullable(),
+              name: z.string(),
+              level: z.number(),
+              code: z.string(),
+              isDisabled: z.number()
+            }))
+          })
+        }
+      }
+    },
+    404: {
+      description: '区域不存在',
+      content: {
+        'application/json': {
+          schema: z.object({
+            success: z.boolean(),
+            error: z.string()
+          })
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: z.object({
+            success: z.boolean(),
+            error: z.string()
+          })
+        }
+      }
+    }
+  }
+});
+
+const app = new OpenAPIHono()
+  // 注册路由 - 使用链式结构
+  .openapi(getAreaTreeRoute, async (c) => {
+    const areaService = new AreaService();
+    try {
+      const treeData = await areaService.getAreaTree();
+      return c.json({
+        success: true,
+        data: treeData
+      }, 200);
+    } catch (error) {
+      console.error('获取省市区树形结构失败:', error);
+      return c.json({
+        success: false,
+        error: '获取省市区树形结构失败'
+      }, 500);
+    }
+  })
+  .openapi(getAreaTreeByLevelRoute, async (c) => {
+    const areaService = new AreaService();
+    try {
+      const { level } = c.req.valid('param');
+      const treeData = await areaService.getAreaTreeByLevel(level);
+      return c.json({
+        success: true,
+        data: treeData
+      }, 200);
+    } catch (error) {
+      console.error('获取层级树形结构失败:', error);
+      return c.json({
+        success: false,
+        error: '获取层级树形结构失败'
+      }, 500);
+    }
+  })
+  .openapi(getSubTreeRoute, async (c) => {
+    const areaService = new AreaService();
+    try {
+      const { id } = c.req.valid('param');
+      const subTree = await areaService.getSubTree(id);
+
+      if (!subTree) {
+        return c.json({
+          success: false,
+          error: '区域不存在'
+        }, 404);
+      }
+
+      return c.json({
+        success: true,
+        data: subTree
+      }, 200);
+    } catch (error) {
+      console.error('获取子树失败:', error);
+      return c.json({
+        success: false,
+        error: '获取子树失败'
+      }, 500);
+    }
+  })
+  .openapi(getAreaPathRoute, async (c) => {
+    const areaService = new AreaService();
+    try {
+      const { id } = c.req.valid('param');
+      const path = await areaService.getAreaPath(id);
+
+      if (path.length === 0) {
+        return c.json({
+          success: false,
+          error: '区域不存在'
+        }, 404);
+      }
+
+      return c.json({
+        success: true,
+        data: path
+      }, 200);
+    } catch (error) {
+      console.error('获取区域路径失败:', error);
+      return c.json({
+        success: false,
+        error: '获取区域路径失败'
+      }, 500);
+    }
+  });
+
+export default app;

+ 150 - 0
src/server/modules/areas/area.service.ts

@@ -0,0 +1,150 @@
+import { AppDataSource } from '@/server/data-source';
+import { AreaEntity, AreaLevel } from './area.entity';
+import { DisabledStatus } from '@/share/types';
+
+export class AreaService {
+  private areaRepository = AppDataSource.getRepository(AreaEntity);
+
+  /**
+   * 获取完整的省市区树形结构
+   */
+  async getAreaTree(): Promise<AreaEntity[]> {
+    const areas = await this.areaRepository.find({
+      where: { isDeleted: 0 },
+      relations: ['children'],
+      order: {
+        level: 'ASC',
+        name: 'ASC'
+      }
+    });
+
+    // 构建树形结构
+    return this.buildTree(areas);
+  }
+
+  /**
+   * 根据层级获取树形结构
+   */
+  async getAreaTreeByLevel(level: AreaLevel): Promise<AreaEntity[]> {
+    const areas = await this.areaRepository.find({
+      where: {
+        level,
+        isDeleted: 0
+      },
+      relations: ['children'],
+      order: {
+        name: 'ASC'
+      }
+    });
+
+    return areas;
+  }
+
+  /**
+   * 获取指定节点的子树
+   */
+  async getSubTree(areaId: number): Promise<AreaEntity | null> {
+    const area = await this.areaRepository.findOne({
+      where: {
+        id: areaId,
+        isDeleted: 0
+      },
+      relations: ['children'],
+    });
+
+    if (!area) return null;
+
+    // 递归加载所有子节点
+    await this.loadChildrenRecursively(area);
+    return area;
+  }
+
+  /**
+   * 递归加载所有子节点
+   */
+  private async loadChildrenRecursively(area: AreaEntity): Promise<void> {
+    if (area.children && area.children.length > 0) {
+      for (const child of area.children) {
+        const fullChild = await this.areaRepository.findOne({
+          where: {
+            id: child.id,
+            isDeleted: 0
+          },
+          relations: ['children'],
+        });
+
+        if (fullChild) {
+          child.children = fullChild.children;
+          await this.loadChildrenRecursively(child);
+        }
+      }
+    }
+  }
+
+  /**
+   * 构建树形结构
+   */
+  private buildTree(areas: AreaEntity[]): AreaEntity[] {
+    const areaMap = new Map<number, AreaEntity>();
+    const tree: AreaEntity[] = [];
+
+    // 创建映射
+    areas.forEach(area => {
+      areaMap.set(area.id, area);
+    });
+
+    // 构建树
+    areas.forEach(area => {
+      if (area.parentId === null || area.parentId === 0) {
+        tree.push(area);
+      } else {
+        const parent = areaMap.get(area.parentId);
+        if (parent && parent.children) {
+          parent.children.push(area);
+        }
+      }
+    });
+
+    return tree;
+  }
+
+  /**
+   * 获取区域路径(从根节点到当前节点)
+   */
+  async getAreaPath(areaId: number): Promise<AreaEntity[]> {
+    const path: AreaEntity[] = [];
+    let currentArea = await this.areaRepository.findOne({
+      where: { id: areaId, isDeleted: 0 }
+    });
+
+    while (currentArea) {
+      path.unshift(currentArea);
+
+      if (currentArea.parentId === null || currentArea.parentId === 0) {
+        break;
+      }
+
+      currentArea = await this.areaRepository.findOne({
+        where: { id: currentArea.parentId, isDeleted: 0 }
+      });
+    }
+
+    return path;
+  }
+
+  /**
+   * 获取所有启用状态的省市区
+   */
+  async getEnabledAreas(): Promise<AreaEntity[]> {
+    return this.areaRepository.find({
+      where: {
+        isDeleted: 0,
+        isDisabled: DisabledStatus.ENABLED
+      },
+      order: {
+        level: 'ASC',
+        name: 'ASC'
+      }
+    });
+  }
+}