From ea86342a0f8d29884c18efc832f355508fb9b832 Mon Sep 17 00:00:00 2001 From: Chuck1sn Date: Thu, 19 Jun 2025 15:29:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=83=A8=E9=97=A8=E5=AD=90?= =?UTF-8?q?=E7=BA=A7=E6=9F=A5=E8=AF=A2=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E7=9B=B8=E5=85=B3API=E6=8E=A5=E5=8F=A3=E5=92=8C?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E7=BB=84=E4=BB=B6=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=83=A8=E9=97=A8=E6=A0=91=E5=BD=A2=E7=BB=93=E6=9E=84=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=EF=BC=8C=E6=94=AF=E6=8C=81=E7=88=B6=E5=AD=90=E9=83=A8?= =?UTF-8?q?=E9=97=A8=E5=85=B3=E7=B3=BB=E7=9A=84=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mjga/controller/DepartmentController.java | 7 + .../mjga/repository/DepartmentRepository.java | 3 +- frontend/src/api/mocks/departmentHandlers.ts | 52 ++- frontend/src/api/schema/openapi.json | 63 ++++ frontend/src/api/types/schema.d.ts | 47 +++ .../common/department/DepartmentTree.vue | 87 +++++ .../common/department/TreeNodeComponent.vue | 129 ++++++++ .../src/components/common/department/index.ts | 7 + .../department/useDepartmentDelete.ts | 20 +- .../department/useDepartmentQuery.ts | 166 ++++++++-- .../department/useDepartmentUpsert.ts | 24 +- .../src/views/DepartmentManagementPage.vue | 306 ++++++++++++------ 12 files changed, 749 insertions(+), 162 deletions(-) create mode 100644 frontend/src/components/common/department/DepartmentTree.vue create mode 100644 frontend/src/components/common/department/TreeNodeComponent.vue create mode 100644 frontend/src/components/common/department/index.ts diff --git a/backend/src/main/java/com/zl/mjga/controller/DepartmentController.java b/backend/src/main/java/com/zl/mjga/controller/DepartmentController.java index 509a154..0a57244 100644 --- a/backend/src/main/java/com/zl/mjga/controller/DepartmentController.java +++ b/backend/src/main/java/com/zl/mjga/controller/DepartmentController.java @@ -4,6 +4,7 @@ import com.zl.mjga.dto.PageRequestDto; import com.zl.mjga.dto.PageResponseDto; import com.zl.mjga.dto.department.DepartmentQueryDto; import com.zl.mjga.dto.department.DepartmentRespDto; +import com.zl.mjga.dto.department.DepartmentWithParentDto; import com.zl.mjga.repository.DepartmentRepository; import com.zl.mjga.service.DepartmentService; import java.util.List; @@ -37,6 +38,12 @@ public class DepartmentController { return departmentService.queryAvailableParentDepartmentsBy(id); } + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_DEPARTMENT_PERMISSION)") + @GetMapping("/query-sub") + List querySubDepartment(@RequestParam(required = false) Long id) { + return departmentService.queryDepartmentAndSubsBy(id); + } + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_DEPARTMENT_PERMISSION)") @DeleteMapping() void deleteDepartment(@RequestParam Long id) { diff --git a/backend/src/main/java/com/zl/mjga/repository/DepartmentRepository.java b/backend/src/main/java/com/zl/mjga/repository/DepartmentRepository.java index 53005ba..548cbfb 100644 --- a/backend/src/main/java/com/zl/mjga/repository/DepartmentRepository.java +++ b/backend/src/main/java/com/zl/mjga/repository/DepartmentRepository.java @@ -37,7 +37,8 @@ public class DepartmentRepository extends DepartmentDao { DEPARTMENT.PARENT_ID, DEPARTMENT.NAME.cast(VARCHAR)) .from(DEPARTMENT) - .where(DEPARTMENT.ID.eq(id)) + .where(id == null ? noCondition() : DEPARTMENT.ID.eq(id)) + .and(DEPARTMENT.PARENT_ID.isNull()) .unionAll( select( DEPARTMENT.ID, diff --git a/frontend/src/api/mocks/departmentHandlers.ts b/frontend/src/api/mocks/departmentHandlers.ts index 1fd7e48..8d6aa10 100644 --- a/frontend/src/api/mocks/departmentHandlers.ts +++ b/frontend/src/api/mocks/departmentHandlers.ts @@ -3,13 +3,17 @@ import { http, HttpResponse } from "msw"; export default [ http.get("/department/page-query", () => { - const generateDepartment = () => ({ - id: faker.number.int({ min: 1, max: 100 }), - name: faker.company.name(), - parentId: faker.number.int({ min: 1, max: 100 }), - isBound: faker.datatype.boolean(), - parentName: faker.company.name(), - }); + const generateDepartment = () => { + // 20% 的概率生成 parentId 为 null 的数据 + const hasParent = faker.datatype.boolean(0.8); + return { + id: faker.number.int({ min: 1, max: 100 }), + name: faker.company.name(), + parentId: hasParent ? faker.number.int({ min: 1, max: 100 }) : null, + isBound: faker.datatype.boolean(), + parentName: hasParent ? faker.company.name() : null, + }; + }; const mockData = { data: faker.helpers.multiple(generateDepartment, { count: 10 }), total: 30, @@ -17,21 +21,41 @@ export default [ return HttpResponse.json(mockData); }), http.get("/department/query-available", () => { - const generateDepartment = () => ({ - id: faker.number.int({ min: 1, max: 30 }), - name: faker.company.name(), - parentId: faker.number.int({ min: 1, max: 30 }), - parentName: faker.company.name(), - }); + const generateDepartment = () => { + // 20% 的概率生成 parentId 为 null 的数据 + const hasParent = faker.datatype.boolean(0.8); + return { + id: faker.number.int({ min: 1, max: 30 }), + name: faker.company.name(), + parentId: hasParent ? faker.number.int({ min: 1, max: 30 }) : null, + parentName: hasParent ? faker.company.name() : null, + }; + }; const mockData = faker.helpers.multiple(generateDepartment, { count: 30 }); return HttpResponse.json(mockData); }), - http.post("/department", () => { console.log("Captured department upsert"); return HttpResponse.json(); }), + http.get("/department/query-sub", ({ request }) => { + const generateDepartment = () => { + // 20% 的概率生成 parentId 为 null 的数据 + const hasParent = faker.datatype.boolean(0.8); + return { + id: faker.number.int({ min: 1, max: 30 }), + name: faker.company.name(), + parentId: hasParent ? faker.number.int({ min: 1, max: 30 }) : null, + parentName: hasParent ? faker.company.name() : null, + }; + }; + const mockData = faker.helpers.multiple(generateDepartment, { + count: 30, + }); + + return HttpResponse.json(mockData); + }), http.delete("/department", () => { console.log("Captured department delete"); return HttpResponse.json(); diff --git a/frontend/src/api/schema/openapi.json b/frontend/src/api/schema/openapi.json index 252ef5b..0822105 100644 --- a/frontend/src/api/schema/openapi.json +++ b/frontend/src/api/schema/openapi.json @@ -1077,6 +1077,40 @@ } } }, + "/department/query-sub": { + "get": { + "tags": [ + "department-controller" + ], + "operationId": "querySubDepartment", + "parameters": [ + { + "name": "id", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DepartmentWithParentDto" + } + } + } + } + } + } + } + }, "/department/query-available": { "get": { "tags": [ @@ -1977,6 +2011,35 @@ } } }, + "DepartmentWithParentDto": { + "required": [ + "id", + "name", + "parentId", + "parentName", + "path" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "parentId": { + "type": "integer", + "format": "int64" + }, + "parentName": { + "type": "string" + }, + "path": { + "type": "string" + } + } + }, "DepartmentQueryDto": { "type": "object", "properties": { diff --git a/frontend/src/api/types/schema.d.ts b/frontend/src/api/types/schema.d.ts index 0279ced..80fea2c 100644 --- a/frontend/src/api/types/schema.d.ts +++ b/frontend/src/api/types/schema.d.ts @@ -532,6 +532,22 @@ export interface paths { patch?: never; trace?: never; }; + "/department/query-sub": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["querySubDepartment"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/department/query-available": { parameters: { query?: never; @@ -884,6 +900,15 @@ export interface components { total?: number; data?: components["schemas"]["PermissionRespDto"][]; }; + DepartmentWithParentDto: { + /** Format: int64 */ + id: number; + name: string; + /** Format: int64 */ + parentId: number; + parentName: string; + path: string; + }; DepartmentQueryDto: { /** Format: int64 */ userId?: number; @@ -1826,6 +1851,28 @@ export interface operations { }; }; }; + querySubDepartment: { + parameters: { + query?: { + id?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["DepartmentWithParentDto"][]; + }; + }; + }; + }; queryAvailableParentDepartmentsBy: { parameters: { query?: { diff --git a/frontend/src/components/common/department/DepartmentTree.vue b/frontend/src/components/common/department/DepartmentTree.vue new file mode 100644 index 0000000..3e33b60 --- /dev/null +++ b/frontend/src/components/common/department/DepartmentTree.vue @@ -0,0 +1,87 @@ + + + diff --git a/frontend/src/components/common/department/TreeNodeComponent.vue b/frontend/src/components/common/department/TreeNodeComponent.vue new file mode 100644 index 0000000..dab777f --- /dev/null +++ b/frontend/src/components/common/department/TreeNodeComponent.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/frontend/src/components/common/department/index.ts b/frontend/src/components/common/department/index.ts new file mode 100644 index 0000000..5969e43 --- /dev/null +++ b/frontend/src/components/common/department/index.ts @@ -0,0 +1,7 @@ +import DepartmentTree from "./DepartmentTree.vue"; + +export { DepartmentTree }; + +export default { + DepartmentTree, +}; diff --git a/frontend/src/composables/department/useDepartmentDelete.ts b/frontend/src/composables/department/useDepartmentDelete.ts index 7a40b5b..a9bfb1a 100644 --- a/frontend/src/composables/department/useDepartmentDelete.ts +++ b/frontend/src/composables/department/useDepartmentDelete.ts @@ -1,14 +1,20 @@ import client from "@/api/client"; export const useDepartmentDelete = () => { - const deleteDepartment = async (departmentId: number) => { - await client.DELETE("/department", { - params: { - query: { - id: departmentId, + const deleteDepartment = async (departmentId: number): Promise => { + try { + const response = await client.DELETE("/department", { + params: { + query: { + id: departmentId, + }, }, - }, - }); + }); + return response.response.ok; + } catch (error) { + console.error("删除部门失败:", error); + return false; + } }; return { deleteDepartment, diff --git a/frontend/src/composables/department/useDepartmentQuery.ts b/frontend/src/composables/department/useDepartmentQuery.ts index 0107316..90a0ed7 100644 --- a/frontend/src/composables/department/useDepartmentQuery.ts +++ b/frontend/src/composables/department/useDepartmentQuery.ts @@ -1,51 +1,151 @@ import client from "@/api/client"; +import type { components } from "@/api/types/schema"; import { ref } from "vue"; -import type { components } from "../../api/types/schema"; -export const useDepartmentQuery = () => { - const total = ref(0); - const departments = ref(); +// 定义部门树节点类型 +export interface DepartmentTreeNode { + id: number; + name: string; + parentId: number | null; + children?: DepartmentTreeNode[]; +} + +export function useDepartmentQuery() { + // 部门列表数据 + const departments = ref([]); + // 可用的部门列表(用于选择上级部门) const availableDepartments = ref(); + // 部门树形结构数据 + const departmentTree = ref([]); + // 总记录数 + const total = ref(0); - const fetchAvailableDepartments = async (id?: number) => { - const { data } = await client.GET("/department/query-available", { - params: { - query: { - id, - }, - }, - }); - availableDepartments.value = data ?? []; - }; + // 获取部门列表数据 const fetchDepartmentWith = async ( - param: { + params: { name?: string; - enable?: boolean; - userId?: number; - bindState?: "ALL" | "BIND" | "UNBIND"; - }, + } = {}, page = 1, - size = 10, + pageSize = 10, ) => { - const { data } = await client.GET("/department/page-query", { - params: { - query: { - pageRequestDto: { - page, - size, + try { + const response = await client.GET("/department/page-query", { + params: { + query: { + pageRequestDto: { + page, + size: pageSize, + }, + departmentQueryDto: { + name: params.name || "", + }, }, - departmentQueryDto: param, }, - }, - }); - total.value = !data || !data.total ? 0 : data.total; - departments.value = data?.data ?? []; + }); + + if (response.data) { + departments.value = response.data.data || []; + total.value = response.data.total || 0; + } + + return response.data?.data || []; + } catch (error) { + console.error("获取部门列表失败:", error); + return []; + } }; + + // 获取可用部门列表(用于选择上级部门) + const fetchAvailableDepartments = async (excludeId?: number) => { + try { + const response = await client.GET("/department/query-available", { + params: { + query: { + id: excludeId, + }, + }, + }); + + if (response.data) { + availableDepartments.value = response.data; + } + + return response.data || []; + } catch (error) { + console.error("获取可用部门列表失败:", error); + return []; + } + }; + + // 构建部门树形结构 + const buildDepartmentTree = ( + departments: components["schemas"]["DepartmentWithParentDto"][], + ): DepartmentTreeNode[] => { + // 创建映射表,方便快速查找 + const map = new Map(); + const roots: DepartmentTreeNode[] = []; + + // 先将所有部门添加到映射表中 + for (const dept of departments) { + if (dept.id) { + map.set(dept.id, { + id: dept.id, + name: dept.name || "", + parentId: dept.parentId || null, + children: [], + }); + } + } + + // 构建树形结构 + for (const dept of departments) { + if (!dept.id) continue; + + const node = map.get(dept.id); + if (!node) continue; + + if (dept.parentId && map.has(dept.parentId)) { + // 如果有父节点,将当前节点添加到父节点的children中 + const parent = map.get(dept.parentId); + parent?.children?.push(node); + } else { + // 没有父节点或父节点不存在,作为根节点 + roots.push(node); + } + } + + return roots; + }; + + // 获取部门树形结构 + const fetchDepartmentTree = async () => { + try { + const response = await client.GET("/department/query-sub", { + params: { + query: { + id: undefined, + }, + }, + }); + + if (response.data) { + departmentTree.value = buildDepartmentTree(response.data); + } + + return response.data || []; + } catch (error) { + console.error("获取部门树形结构失败:", error); + return []; + } + }; + return { - total, departments, availableDepartments, + departmentTree, + total, fetchDepartmentWith, fetchAvailableDepartments, + fetchDepartmentTree, }; -}; +} diff --git a/frontend/src/composables/department/useDepartmentUpsert.ts b/frontend/src/composables/department/useDepartmentUpsert.ts index aaebf4f..5b5d038 100644 --- a/frontend/src/composables/department/useDepartmentUpsert.ts +++ b/frontend/src/composables/department/useDepartmentUpsert.ts @@ -2,14 +2,22 @@ import client from "../../api/client"; import type { DepartmentUpsertModel } from "../../types/DepartmentTypes"; export const useDepartmentUpsert = () => { - const upsertDepartment = async (department: DepartmentUpsertModel) => { - await client.POST("/department", { - body: { - id: department.id, - name: department.name, - parentId: department.parentId ?? undefined, - }, - }); + const upsertDepartment = async ( + department: DepartmentUpsertModel, + ): Promise => { + try { + const response = await client.POST("/department", { + body: { + id: department.id, + name: department.name, + parentId: department.parentId ?? undefined, + }, + }); + return response.response.ok; + } catch (error) { + console.error("保存部门失败:", error); + return false; + } }; return { diff --git a/frontend/src/views/DepartmentManagementPage.vue b/frontend/src/views/DepartmentManagementPage.vue index ff7493d..ce381e2 100644 --- a/frontend/src/views/DepartmentManagementPage.vue +++ b/frontend/src/views/DepartmentManagementPage.vue @@ -17,89 +17,116 @@ - -
- - - - - -
+ + - - + +
+ +
+ + + + + +
- + + +
+ +
+
+ + import type { components } from "@/api/types/schema"; +import { DepartmentTree } from "@/components/common/department"; import PlusIcon from "@/components/icons/PlusIcon.vue"; import Breadcrumbs from "@/components/layout/Breadcrumbs.vue"; import DepartmentDeleteModal from "@/components/modals/ConfirmationDialog.vue"; @@ -120,13 +148,16 @@ import type { FilterItem } from "@/components/tables/TableFilterForm.vue"; import TableFormLayout from "@/components/tables/TableFormLayout.vue"; import TablePagination from "@/components/tables/TablePagination.vue"; import useDepartmentDelete from "@/composables/department/useDepartmentDelete"; -import { useDepartmentQuery } from "@/composables/department/useDepartmentQuery"; +import { + type DepartmentTreeNode, + useDepartmentQuery, +} from "@/composables/department/useDepartmentQuery"; import { useDepartmentUpsert } from "@/composables/department/useDepartmentUpsert"; import { useActionExcStore } from "@/composables/store/useActionExcStore"; import useAlertStore from "@/composables/store/useAlertStore"; import type { DepartmentUpsertModel } from "@/types/DepartmentTypes"; import { Modal, type ModalInterface, initFlowbite } from "flowbite"; -import { nextTick, onMounted, reactive, ref } from "vue"; +import { nextTick, onMounted, onUnmounted, reactive, ref } from "vue"; // 定义筛选配置 const filterConfig = [ @@ -160,9 +191,11 @@ const departmentDeleteModal = ref(); const { departments, availableDepartments, + departmentTree, fetchDepartmentWith, - total, fetchAvailableDepartments, + fetchDepartmentTree, + total, } = useDepartmentQuery(); const { deleteDepartment } = useDepartmentDelete(); @@ -177,42 +210,105 @@ const columns = [ { title: "操作", field: "actions" }, ]; -onMounted(async () => { - await fetchDepartmentWith({ - name: filterValues.departmentName, +// 同步更新部门数据和树形结构 +const refreshDepartmentData = async () => { + // 更新部门列表数据和树形结构 + await Promise.all([ + fetchDepartmentWith({ + name: filterValues.departmentName, + }), + fetchDepartmentTree(), + ]); +}; + +// 处理添加子部门点击 +const handleAddChildClick = async (parentNode: DepartmentTreeNode) => { + // 创建默认的空部门,但设置父部门ID + selectedDepartment.value = { + parentId: parentNode.id, + } as components["schemas"]["Department"]; + await fetchAvailableDepartments(); + await nextTick(() => { + departmentFormDialog.value?.show(); }); +}; + +// 处理编辑部门 +const handleEditDepartment = async (node: DepartmentTreeNode) => { + // 将节点转换为部门对象 + const department: components["schemas"]["Department"] = { + id: node.id, + name: node.name, + parentId: node.parentId !== null ? node.parentId : undefined, + }; + + await handleUpsertDepartmentClick(department); +}; + +onMounted(async () => { + // 初始化加载部门数据和树形结构 + await refreshDepartmentData(); + initFlowbite(); - const $upsertModalElement: HTMLElement | null = document.querySelector( + const $upsertModalElement = document.querySelector( "#department-upsert-modal", ); - const $deleteModalElement: HTMLElement | null = document.querySelector( + const $deleteModalElement = document.querySelector( "#department-delete-modal", ); + if ($upsertModalElement) { departmentFormDialog.value = new Modal($upsertModalElement, {}); } if ($deleteModalElement) { departmentDeleteModal.value = new Modal($deleteModalElement, {}); } + actionExcStore.setCallback((result) => { if (result) { - handleSearch(); + refreshDepartmentData(); } }); }); +// 组件卸载时清理资源 +onUnmounted(() => { + // 重置回调,避免内存泄漏 + actionExcStore.setCallback(() => {}); + + // 清理模态框 + departmentFormDialog.value?.hide(); + departmentDeleteModal.value?.hide(); +}); + const handleUpsertDepartmentSubmit = async ( department: DepartmentUpsertModel, ) => { - await upsertDepartment(department); + const success = await upsertDepartment(department); departmentFormDialog.value?.hide(); - alertStore.showAlert({ - content: "操作成功", - level: "success", - }); - await fetchDepartmentWith({ - name: filterValues.departmentName, - }); + + if (success) { + alertStore.showAlert({ + content: department.id ? "部门更新成功" : "部门创建成功", + level: "success", + }); + // 同时刷新部门列表和树形结构 + await refreshDepartmentData(); + + // 如果是新增或修改部门,重置筛选条件 + if (!department.id || selectedDepartment.value?.id !== department.id) { + filterValues.departmentName = ""; + } + } else { + alertStore.showAlert({ + content: "操作失败,请稍后重试", + level: "error", + }); + } + + // 操作完成后清空选中部门 + selectedDepartment.value = undefined; + availableDepartments.value = undefined; }; const handleUpsertDepartmentClick = async ( @@ -227,15 +323,27 @@ const handleUpsertDepartmentClick = async ( const handleDeleteDepartmentSubmit = async () => { if (!selectedDepartment.value?.id) return; - await deleteDepartment(selectedDepartment.value.id); + + const success = await deleteDepartment(selectedDepartment.value.id); departmentDeleteModal.value?.hide(); - alertStore.showAlert({ - content: "删除成功", - level: "success", - }); - await fetchDepartmentWith({ - name: filterValues.departmentName, - }); + + if (success) { + alertStore.showAlert({ + content: "部门删除成功", + level: "success", + }); + // 删除后清空筛选条件,并刷新数据 + filterValues.departmentName = ""; + await refreshDepartmentData(); + } else { + alertStore.showAlert({ + content: "删除失败,请稍后重试", + level: "error", + }); + } + + // 操作完成后清空选中部门 + selectedDepartment.value = undefined; }; const handleDeleteDepartmentClick = async (