From 90b6ce4aba93b9c6f7fa37315c069e936b38e7b2 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Mon, 6 Jul 2026 00:09:04 +0800 Subject: [PATCH] feat: add Departments management page with tree + user list --- apps/admin/src/App.tsx | 10 + apps/admin/src/pages/Departments/index.tsx | 409 +++++++++++++++++++++ 2 files changed, 419 insertions(+) create mode 100644 apps/admin/src/pages/Departments/index.tsx diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index bebc481..ffe3303 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -26,6 +26,7 @@ import PermissionsPage from './pages/Permissions'; import AttendancePage from './pages/Attendance'; import TeacherWorkspacePage from './pages/TeacherWorkspace'; import NotificationsPage from './pages/Notifications'; +import DepartmentsPage from './pages/Departments'; import PermissionRoute from './components/PermissionRoute'; const PrivateRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { @@ -233,6 +234,15 @@ const App: React.FC = () => { /> } /> + + + + + } + /> diff --git a/apps/admin/src/pages/Departments/index.tsx b/apps/admin/src/pages/Departments/index.tsx new file mode 100644 index 0000000..0572b0f --- /dev/null +++ b/apps/admin/src/pages/Departments/index.tsx @@ -0,0 +1,409 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import { + Tree, + Card, + Button, + Modal, + Form, + Input, + Select, + InputNumber, + Table, + Row, + Col, + Popconfirm, + Space, + message, + Tag, + Descriptions, + Empty, + Spin, +} from 'antd'; +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, +} from '@ant-design/icons'; +import type { DataNode } from 'antd/es/tree'; +import api from '../../api'; +import PermissionButton from '../../components/PermissionButton'; + +interface DepartmentItem { + id: number; + name: string; + parentId: number | null; + type: string; + sortOrder: number; + status: string; + children?: DepartmentItem[]; +} + +interface UserInfo { + id: number; + username: string; + name: string; + isActive: boolean; +} + +interface DeptMember { + id: number; + userId: number; + departmentId: number; + isDefault: boolean; + user: UserInfo; +} + +function departmentToTreeNode(dept: DepartmentItem): DataNode { + return { + key: String(dept.id), + title: dept.name, + children: dept.children?.map(departmentToTreeNode), + }; +} + +const TYPE_LABELS: Record = { + campus: '校区', + department: '部门', +}; + +const TYPE_COLORS: Record = { + campus: 'blue', + department: 'green', +}; + +const DepartmentsPage: React.FC = () => { + const [treeData, setTreeData] = useState([]); + const [flatDepts, setFlatDepts] = useState([]); + const [selectedId, setSelectedId] = useState(null); + const [selectedDept, setSelectedDept] = useState(null); + const [members, setMembers] = useState([]); + const [membersLoading, setMembersLoading] = useState(false); + const [treeLoading, setTreeLoading] = useState(false); + const [modalOpen, setModalOpen] = useState(false); + const [editing, setEditing] = useState(null); + const [form] = Form.useForm(); + + const fetchTree = useCallback(async () => { + setTreeLoading(true); + try { + const data = await api.get('/departments/tree') as unknown as DepartmentItem[]; + const flat = await api.get('/departments') as unknown as DepartmentItem[]; + setFlatDepts(flat); + setTreeData(data.map(departmentToTreeNode)); + } catch { + message.error('加载部门树失败'); + } finally { + setTreeLoading(false); + } + }, []); + + useEffect(() => { + fetchTree(); + }, [fetchTree]); + + const fetchDetail = useCallback(async (id: number) => { + try { + const dept = await api.get(`/departments/${id}`) as unknown as DepartmentItem; + setSelectedDept(dept); + setMembersLoading(true); + const users = await api.get(`/departments/${id}/users`) as unknown as DeptMember[]; + setMembers(users); + } catch { + message.error('加载部门详情失败'); + } finally { + setMembersLoading(false); + } + }, []); + + const handleSelect = useCallback( + (keys: React.Key[]) => { + if (keys.length === 0) { + setSelectedId(null); + setSelectedDept(null); + setMembers([]); + return; + } + const id = String(keys[0]); + setSelectedId(id); + fetchDetail(Number(id)); + }, + [fetchDetail], + ); + + const handleAdd = useCallback(() => { + setEditing(null); + form.resetFields(); + if (selectedId) { + form.setFieldsValue({ parentId: Number(selectedId) }); + } + setModalOpen(true); + }, [selectedId, form]); + + const handleAddChild = useCallback(() => { + if (!selectedDept) return; + setEditing(null); + form.resetFields(); + form.setFieldsValue({ parentId: selectedDept.id }); + setModalOpen(true); + }, [selectedDept, form]); + + const handleEdit = useCallback(() => { + if (!selectedDept) return; + setEditing(selectedDept); + form.setFieldsValue({ + name: selectedDept.name, + parentId: selectedDept.parentId, + type: selectedDept.type, + sortOrder: selectedDept.sortOrder, + }); + setModalOpen(true); + }, [selectedDept, form]); + + const handleSubmit = useCallback(async () => { + try { + const values = await form.validateFields(); + const payload = { + name: values.name, + parentId: values.parentId || null, + type: values.type || 'department', + sortOrder: values.sortOrder ?? 0, + }; + if (editing) { + await api.put(`/departments/${editing.id}`, payload); + message.success('更新成功'); + } else { + await api.post('/departments', payload); + message.success('创建成功'); + } + setModalOpen(false); + await fetchTree(); + if (editing && selectedId) { + fetchDetail(editing.id); + } + } catch { + // form validation error: do nothing + } + }, [editing, fetchTree, fetchDetail, selectedId, form]); + + const handleDelete = useCallback(async () => { + if (!selectedDept) return; + try { + await api.delete(`/departments/${selectedDept.id}`); + message.success('删除成功'); + setSelectedId(null); + setSelectedDept(null); + setMembers([]); + await fetchTree(); + } catch (err: unknown) { + const msg = + err && typeof err === 'object' && 'message' in err + ? (err as { message: string }).message + : '删除失败'; + message.error(msg); + } + }, [selectedDept, fetchTree]); + + const memberColumns = [ + { title: 'ID', dataIndex: 'userId', key: 'userId', width: 60 }, + { title: '用户名', dataIndex: ['user', 'username'], key: 'username' }, + { title: '姓名', dataIndex: ['user', 'name'], key: 'name' }, + { + title: '默认部门', + dataIndex: 'isDefault', + key: 'isDefault', + width: 80, + render: (v: boolean) => (v ? : null), + }, + ]; + + return ( +
+
+

组织架构管理

+ + + } + onClick={handleAdd} + > + 新增 + + +
+ + + + + {treeLoading ? ( + + ) : ( + + )} + + + + + {selectedDept ? ( + + + {selectedDept.name} + + {TYPE_LABELS[selectedDept.type] || selectedDept.type} + + + } + extra={ + + } + onClick={handleEdit} + > + 编辑 + + } + onClick={handleAddChild} + > + 新增子部门 + + 0 + ? '该部门下存在子部门,可能无法删除' + : undefined} + onConfirm={handleDelete} + okText="确定" + cancelText="取消" + > + + + + } + > + + {selectedDept.id} + + + {TYPE_LABELS[selectedDept.type] || selectedDept.type} + + + + {selectedDept.sortOrder} + + + {selectedDept.parentId ?? '无(顶级)'} + + + + {selectedDept.status} + + + + + + + + + + ) : ( + + + + )} + + + + setModalOpen(false)} + destroyOnHidden + > +
+ + + + + + + + + + + + +
+ + ); +}; + +export default DepartmentsPage;