mirror of
https://github.com/ccmjga/zhilu-admin
synced 2026-03-13 21:27:19 +08:00
新增部门子级查询功能,更新相关API接口和前端组件,优化部门树形结构展示,支持父子部门关系的处理
This commit is contained in:
@@ -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<DepartmentWithParentDto> 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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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": {
|
||||
|
||||
47
frontend/src/api/types/schema.d.ts
vendored
47
frontend/src/api/types/schema.d.ts
vendored
@@ -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?: {
|
||||
|
||||
87
frontend/src/components/common/department/DepartmentTree.vue
Normal file
87
frontend/src/components/common/department/DepartmentTree.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="department-tree">
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!departmentTree || departmentTree.length === 0" class="flex flex-col items-center justify-center py-8">
|
||||
<svg class="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"></path>
|
||||
</svg>
|
||||
<p class="mt-2 text-sm text-gray-500">暂无部门数据</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<!-- 操作按钮区域 -->
|
||||
<div class="flex justify-end mb-3 gap-2">
|
||||
<TableButton variant="secondary" size="xs" @click="toggleAllNodes" :title="isAllExpanded ? '收起所有部门' : '展开所有部门'">
|
||||
<template #icon>
|
||||
<svg v-if="isAllExpanded" class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
<svg v-else class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</template>
|
||||
{{ isAllExpanded ? '收起所有' : '展开所有' }}
|
||||
</TableButton>
|
||||
</div>
|
||||
|
||||
<!-- 树形结构内容区域 -->
|
||||
<div class="max-h-[500px] overflow-y-auto p-2 bg-gray-50 border border-gray-200 rounded shadow-inner">
|
||||
<TreeNodeComponent v-for="node in departmentTree" :key="node.id" :node="node" :expand-all="expandAll"
|
||||
@add-child="handleAddChild" @edit="handleEditNode" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TableButton from "@/components/tables/TableButton.vue";
|
||||
import type { DepartmentTreeNode } from "@/composables/department/useDepartmentQuery";
|
||||
import { computed, defineEmits, defineProps, ref, watch } from "vue";
|
||||
import TreeNodeComponent from "./TreeNodeComponent.vue";
|
||||
|
||||
// 定义组件属性
|
||||
const props = defineProps<{
|
||||
departmentTree: DepartmentTreeNode[];
|
||||
}>();
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits<{
|
||||
(e: "add-child", node: DepartmentTreeNode): void;
|
||||
(e: "edit", node: DepartmentTreeNode): void;
|
||||
}>();
|
||||
|
||||
// 控制所有节点展开/折叠状态
|
||||
const expandAll = ref<boolean>(true);
|
||||
|
||||
// 通过计算属性提供展开状态
|
||||
const isAllExpanded = computed(() => expandAll.value);
|
||||
|
||||
// 监听树数据变化,当数据改变时重置展开状态
|
||||
watch(
|
||||
() => props.departmentTree,
|
||||
() => {
|
||||
// 当树数据变化时,默认展开所有节点
|
||||
expandAll.value = true;
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// 切换所有节点的展开/折叠状态
|
||||
const toggleAllNodes = () => {
|
||||
expandAll.value = !expandAll.value;
|
||||
};
|
||||
|
||||
// 处理添加子部门
|
||||
const handleAddChild = (node: DepartmentTreeNode) => {
|
||||
emit("add-child", node);
|
||||
};
|
||||
|
||||
// 处理编辑节点
|
||||
const handleEditNode = (node: DepartmentTreeNode) => {
|
||||
emit("edit", node);
|
||||
};
|
||||
</script>
|
||||
129
frontend/src/components/common/department/TreeNodeComponent.vue
Normal file
129
frontend/src/components/common/department/TreeNodeComponent.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div class="relative mb-0.5">
|
||||
<div class="flex items-center py-1.5 px-2 cursor-pointer hover:bg-gray-100 rounded transition-colors group"
|
||||
@click="toggleExpand">
|
||||
<!-- 折叠/展开图标 -->
|
||||
<span class="mr-1.5 text-gray-500 cursor-pointer flex-shrink-0" v-if="node.children && node.children.length > 0">
|
||||
<svg v-if="localExpanded" class="w-4 h-4 transform transition-transform duration-200" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
<svg v-else class="w-4 h-4 transform transition-transform duration-200" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="mr-1.5 text-gray-500 invisible flex-shrink-0" v-else>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<!-- 部门名称 -->
|
||||
<span class="text-sm font-medium text-gray-800 flex-grow cursor-pointer truncate" :title="node.name">
|
||||
{{ node.name }}
|
||||
</span>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="ml-auto hidden group-hover:flex flex-shrink-0">
|
||||
<button @click.stop="$emit('add-child', node)"
|
||||
class="text-gray-500 hover:text-blue-600 focus:outline-none transition-colors p-0.5" title="添加子部门">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button @click.stop="$emit('edit', node)"
|
||||
class="text-gray-500 hover:text-blue-600 focus:outline-none transition-colors ml-1 p-0.5" title="编辑部门">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z">
|
||||
</path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<transition enter-active-class="transition-[height,opacity] duration-300 ease-in-out overflow-hidden"
|
||||
leave-active-class="transition-[height,opacity] duration-300 ease-in-out overflow-hidden" @enter="expandEnter"
|
||||
@after-enter="expandAfterEnter" @leave="expandLeave">
|
||||
<div v-if="localExpanded && node.children && node.children.length > 0"
|
||||
class="pl-4 border-l border-gray-200 ml-2 overflow-hidden">
|
||||
<TreeNodeComponent v-for="childNode in node.children" :key="childNode.id" :node="childNode"
|
||||
:expand-all="expandAll" @add-child="$emit('add-child', $event)" @edit="$emit('edit', $event)" />
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DepartmentTreeNode } from "@/composables/department/useDepartmentQuery";
|
||||
import { defineEmits, defineProps, ref, watch } from "vue";
|
||||
|
||||
// 定义组件属性
|
||||
const props = defineProps<{
|
||||
node: DepartmentTreeNode;
|
||||
expandAll: boolean;
|
||||
}>();
|
||||
|
||||
// 定义事件
|
||||
defineEmits<{
|
||||
(e: "add-child", node: DepartmentTreeNode): void;
|
||||
(e: "edit", node: DepartmentTreeNode): void;
|
||||
}>();
|
||||
|
||||
// 本地展开状态,默认展开
|
||||
const localExpanded = ref(true);
|
||||
|
||||
// 监听expandAll属性变化
|
||||
watch(
|
||||
() => props.expandAll,
|
||||
(newVal) => {
|
||||
localExpanded.value = newVal;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 切换展开状态
|
||||
const toggleExpand = () => {
|
||||
localExpanded.value = !localExpanded.value;
|
||||
};
|
||||
|
||||
// 展开动画处理函数
|
||||
const expandEnter = (element: Element) => {
|
||||
const el = element as HTMLElement;
|
||||
el.style.height = "0";
|
||||
el.style.opacity = "0";
|
||||
};
|
||||
|
||||
const expandAfterEnter = (element: Element) => {
|
||||
const el = element as HTMLElement;
|
||||
el.style.height = "";
|
||||
el.style.opacity = "";
|
||||
};
|
||||
|
||||
const expandLeave = (element: Element) => {
|
||||
const el = element as HTMLElement;
|
||||
el.style.height = `${el.scrollHeight}px`;
|
||||
|
||||
// 强制回流
|
||||
void el.offsetHeight;
|
||||
|
||||
el.style.height = "0";
|
||||
el.style.opacity = "0";
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.department-tree-node {
|
||||
position: relative;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.expand-enter-active,
|
||||
.expand-leave-active {
|
||||
transition: height 0.3s ease, opacity 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
7
frontend/src/components/common/department/index.ts
Normal file
7
frontend/src/components/common/department/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import DepartmentTree from "./DepartmentTree.vue";
|
||||
|
||||
export { DepartmentTree };
|
||||
|
||||
export default {
|
||||
DepartmentTree,
|
||||
};
|
||||
@@ -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<boolean> => {
|
||||
try {
|
||||
const response = await client.DELETE("/department", {
|
||||
params: {
|
||||
query: {
|
||||
id: departmentId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
return response.response.ok;
|
||||
} catch (error) {
|
||||
console.error("删除部门失败:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
return {
|
||||
deleteDepartment,
|
||||
|
||||
@@ -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<number>(0);
|
||||
const departments = ref<components["schemas"]["DepartmentRespDto"][]>();
|
||||
// 定义部门树节点类型
|
||||
export interface DepartmentTreeNode {
|
||||
id: number;
|
||||
name: string;
|
||||
parentId: number | null;
|
||||
children?: DepartmentTreeNode[];
|
||||
}
|
||||
|
||||
export function useDepartmentQuery() {
|
||||
// 部门列表数据
|
||||
const departments = ref<components["schemas"]["DepartmentRespDto"][]>([]);
|
||||
// 可用的部门列表(用于选择上级部门)
|
||||
const availableDepartments = ref<components["schemas"]["Department"][]>();
|
||||
// 部门树形结构数据
|
||||
const departmentTree = ref<DepartmentTreeNode[]>([]);
|
||||
// 总记录数
|
||||
const total = ref<number>(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<number, DepartmentTreeNode>();
|
||||
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,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<boolean> => {
|
||||
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 {
|
||||
|
||||
@@ -17,89 +17,116 @@
|
||||
</template>
|
||||
</TableFilterForm>
|
||||
|
||||
<!-- 移动端卡片布局 -->
|
||||
<div class="md:hidden">
|
||||
<MobileCardList :items="departments">
|
||||
<template #title="{ item }">
|
||||
{{ item.name }}
|
||||
</template>
|
||||
<template #content="{ item }">
|
||||
<div>
|
||||
<p class="text-xs font-medium text-gray-600">上级部门</p>
|
||||
<p class="text-sm text-gray-900 mt-0.5">{{ !item.parentName ? '无' : item.parentName }}</p>
|
||||
<!-- 内容布局:两栏布局,响应式处理 -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mt-6">
|
||||
<!-- 左侧:部门树状图(仅在桌面端显示) -->
|
||||
<div class="hidden lg:block lg:col-span-1">
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="p-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">部门结构</h2>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{ item }">
|
||||
<div class="flex gap-x-2">
|
||||
<TableButton variant="primary" size="xs" isMobile @click="handleUpsertDepartmentClick(item)">
|
||||
<template #icon>
|
||||
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
|
||||
<path fill-rule="evenodd"
|
||||
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</template>
|
||||
编辑
|
||||
</TableButton>
|
||||
<TableButton variant="danger" size="xs" isMobile @click="handleDeleteDepartmentClick(item)">
|
||||
<template #icon>
|
||||
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd"
|
||||
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</template>
|
||||
删除
|
||||
</TableButton>
|
||||
<div class="p-4">
|
||||
<DepartmentTree :departmentTree="departmentTree" @add-child="handleAddChildClick"
|
||||
@edit="handleEditDepartment" />
|
||||
</div>
|
||||
</template>
|
||||
</MobileCardList>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PC端表格布局 -->
|
||||
<div class="hidden md:block">
|
||||
<TableFormLayout :items="departments || []" :columns="columns">
|
||||
<template #parentName="{ item }">
|
||||
{{ !item.parentName ? '无' : item.parentName }}
|
||||
</template>
|
||||
<template #name="{ item }">
|
||||
{{ item.name }}
|
||||
</template>
|
||||
<template #actions="{ item }">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<TableButton variant="primary" @click="handleUpsertDepartmentClick(item)">
|
||||
<template #icon>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
|
||||
<path fill-rule="evenodd"
|
||||
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</template>
|
||||
编辑
|
||||
</TableButton>
|
||||
<TableButton variant="danger" @click="handleDeleteDepartmentClick(item)">
|
||||
<template #icon>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd"
|
||||
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</template>
|
||||
删除
|
||||
</TableButton>
|
||||
</div>
|
||||
</template>
|
||||
</TableFormLayout>
|
||||
</div>
|
||||
<!-- 右侧:部门表格(在移动端占满宽度) -->
|
||||
<div class="col-span-1 lg:col-span-2">
|
||||
<!-- 移动端卡片布局 -->
|
||||
<div class="md:hidden">
|
||||
<MobileCardList :items="departments">
|
||||
<template #title="{ item }">
|
||||
{{ item.name }}
|
||||
</template>
|
||||
<template #content="{ item }">
|
||||
<div>
|
||||
<p class="text-xs font-medium text-gray-600">上级部门</p>
|
||||
<p class="text-sm text-gray-900 mt-0.5">{{ !item.parentName ? '无' : item.parentName }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{ item }">
|
||||
<div class="flex gap-x-2">
|
||||
<TableButton variant="primary" size="xs" isMobile @click="handleUpsertDepartmentClick(item)">
|
||||
<template #icon>
|
||||
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z">
|
||||
</path>
|
||||
<path fill-rule="evenodd"
|
||||
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</template>
|
||||
编辑
|
||||
</TableButton>
|
||||
<TableButton variant="danger" size="xs" isMobile @click="handleDeleteDepartmentClick(item)">
|
||||
<template #icon>
|
||||
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd"
|
||||
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</template>
|
||||
删除
|
||||
</TableButton>
|
||||
</div>
|
||||
</template>
|
||||
</MobileCardList>
|
||||
</div>
|
||||
|
||||
<TablePagination :total="total" :pageChange="handlePageChange" />
|
||||
<!-- PC端表格布局 -->
|
||||
<div class="hidden md:block bg-white rounded-lg shadow">
|
||||
<div class="p-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">部门列表</h2>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<TableFormLayout :items="departments || []" :columns="columns">
|
||||
<template #parentName="{ item }">
|
||||
{{ !item.parentName ? '无' : item.parentName }}
|
||||
</template>
|
||||
<template #name="{ item }">
|
||||
{{ item.name }}
|
||||
</template>
|
||||
<template #actions="{ item }">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<TableButton variant="primary" @click="handleUpsertDepartmentClick(item)">
|
||||
<template #icon>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
|
||||
<path fill-rule="evenodd"
|
||||
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</template>
|
||||
编辑
|
||||
</TableButton>
|
||||
<TableButton variant="danger" @click="handleDeleteDepartmentClick(item)">
|
||||
<template #icon>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd"
|
||||
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</template>
|
||||
删除
|
||||
</TableButton>
|
||||
</div>
|
||||
</template>
|
||||
</TableFormLayout>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<TablePagination :total="total" :pageChange="handlePageChange" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DepartmentDeleteModal :id="'department-delete-modal'" :closeModal="() => {
|
||||
departmentDeleteModal!.hide();
|
||||
}" :onSubmit="handleDeleteDepartmentSubmit" title="确定删除该部门吗" content="删除部门"></DepartmentDeleteModal>
|
||||
|
||||
<DepartmentFormDialog :id="'department-upsert-modal'" :onSubmit="handleUpsertDepartmentSubmit" :closeModal="() => {
|
||||
availableDepartments = undefined
|
||||
departmentFormDialog!.hide();
|
||||
@@ -109,6 +136,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
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<ModalInterface>();
|
||||
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<HTMLElement>(
|
||||
"#department-upsert-modal",
|
||||
);
|
||||
const $deleteModalElement: HTMLElement | null = document.querySelector(
|
||||
const $deleteModalElement = document.querySelector<HTMLElement>(
|
||||
"#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 (
|
||||
|
||||
Reference in New Issue
Block a user