From dea4ae80b6825f73da9ec82c7687b9a12f6c0067 Mon Sep 17 00:00:00 2001 From: dark Date: Sat, 14 Feb 2026 11:40:54 +0800 Subject: [PATCH] feat: register routes for MyPage, MenuManagement, RoleManagement, OrganizationManagement --- frontend/react-shadcn/pc/src/App.tsx | 12 + .../pc/src/components/layout/MainLayout.tsx | 9 +- .../src/pages/OrganizationManagementPage.tsx | 796 ++++++++++++++++++ 3 files changed, 815 insertions(+), 2 deletions(-) create mode 100644 frontend/react-shadcn/pc/src/pages/OrganizationManagementPage.tsx diff --git a/frontend/react-shadcn/pc/src/App.tsx b/frontend/react-shadcn/pc/src/App.tsx index 23ecbb8..0dba8f7 100644 --- a/frontend/react-shadcn/pc/src/App.tsx +++ b/frontend/react-shadcn/pc/src/App.tsx @@ -3,9 +3,15 @@ import { AuthProvider } from './contexts/AuthContext' import { ProtectedRoute } from './components/layout/ProtectedRoute' import { MainLayout } from './components/layout/MainLayout' import { LoginPage } from './pages/LoginPage' +import { SSOCallbackPage } from './pages/SSOCallbackPage' import { DashboardPage } from './pages/DashboardPage' import { UserManagementPage } from './pages/UserManagementPage' import { SettingsPage } from './pages/SettingsPage' +import { FileManagementPage } from './pages/FileManagementPage' +import { MyPage } from './pages/MyPage' +import { MenuManagementPage } from './pages/MenuManagementPage' +import { RoleManagementPage } from './pages/RoleManagementPage' +import { OrganizationManagementPage } from './pages/OrganizationManagementPage' function App() { return ( @@ -13,6 +19,7 @@ function App() { } /> + } /> } /> } /> } /> + } /> + } /> + } /> + } /> + } /> } /> diff --git a/frontend/react-shadcn/pc/src/components/layout/MainLayout.tsx b/frontend/react-shadcn/pc/src/components/layout/MainLayout.tsx index 24c4981..73fdeb2 100644 --- a/frontend/react-shadcn/pc/src/components/layout/MainLayout.tsx +++ b/frontend/react-shadcn/pc/src/components/layout/MainLayout.tsx @@ -1,15 +1,20 @@ -import { Outlet } from 'react-router-dom' +import { Outlet, useLocation } from 'react-router-dom' import { Sidebar } from './Sidebar' import { Header } from './Header' const pageTitles: Record = { + '/my': { title: '我的', subtitle: '个人信息与机构' }, '/dashboard': { title: '仪表盘', subtitle: '系统概览与数据统计' }, '/users': { title: '用户管理', subtitle: '管理系统用户账号' }, + '/files': { title: '文件管理', subtitle: '上传与管理系统文件' }, + '/roles': { title: '角色管理', subtitle: '管理系统角色与权限' }, + '/menus': { title: '菜单管理', subtitle: '配置系统导航菜单' }, + '/organizations': { title: '机构管理', subtitle: '管理组织架构与成员' }, '/settings': { title: '系统设置', subtitle: '配置系统参数' }, } export function MainLayout() { - const pathname = window.location.pathname + const { pathname } = useLocation() const pageInfo = pageTitles[pathname] || { title: 'BASE', subtitle: '' } return ( diff --git a/frontend/react-shadcn/pc/src/pages/OrganizationManagementPage.tsx b/frontend/react-shadcn/pc/src/pages/OrganizationManagementPage.tsx new file mode 100644 index 0000000..255e849 --- /dev/null +++ b/frontend/react-shadcn/pc/src/pages/OrganizationManagementPage.tsx @@ -0,0 +1,796 @@ +import { useState, useEffect, useCallback } from 'react' +import { Plus, Search, Edit2, Trash2, Users, ChevronRight, ChevronDown } from 'lucide-react' +import { Button } from '@/components/ui/Button' +import { Input } from '@/components/ui/Input' +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card' +import { Modal } from '@/components/ui/Modal' +import { + Table, + TableHeader, + TableBody, + TableRow, + TableHead, + TableCell, +} from '@/components/ui/Table' +import type { OrgInfo, CreateOrgRequest, UpdateOrgRequest, OrgMember, RoleInfo } from '@/types' +import { apiClient } from '@/services/api' + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +type FlatOrg = OrgInfo & { depth: number } + +function flattenOrgTree(items: OrgInfo[], depth = 0): FlatOrg[] { + const result: FlatOrg[] = [] + for (const item of items) { + result.push({ ...item, depth }) + if (item.children && item.children.length > 0) { + result.push(...flattenOrgTree(item.children, depth + 1)) + } + } + return result +} + +function collectOrgOptions(items: OrgInfo[], depth = 0): { id: number; name: string; depth: number }[] { + const result: { id: number; name: string; depth: number }[] = [] + for (const item of items) { + result.push({ id: item.id, name: item.name, depth }) + if (item.children && item.children.length > 0) { + result.push(...collectOrgOptions(item.children, depth + 1)) + } + } + return result +} + +const STATUS_LABELS: Record = { + 1: { text: '正常', className: 'bg-green-500/20 text-green-400 border border-green-500/30' }, + 0: { text: '停用', className: 'bg-red-500/20 text-red-400 border border-red-500/30' }, +} + +// --------------------------------------------------------------------------- +// Component +// --------------------------------------------------------------------------- + +export function OrganizationManagementPage() { + // --- Org tree state --- + const [orgTree, setOrgTree] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + const [searchQuery, setSearchQuery] = useState('') + const [expandedIds, setExpandedIds] = useState>(new Set()) + + // --- Create / Edit modal --- + const [isModalOpen, setIsModalOpen] = useState(false) + const [editingOrg, setEditingOrg] = useState(null) + const [formData, setFormData] = useState>({ + parentId: 0, + name: '', + code: '', + leader: '', + phone: '', + email: '', + sortOrder: 0, + }) + + // --- Delete confirmation --- + const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false) + const [orgToDelete, setOrgToDelete] = useState(null) + const [isDeleting, setIsDeleting] = useState(false) + const [deleteError, setDeleteError] = useState(null) + + // --- Member management modal --- + const [memberModalOpen, setMemberModalOpen] = useState(false) + const [memberOrg, setMemberOrg] = useState(null) + const [members, setMembers] = useState([]) + const [isMembersLoading, setIsMembersLoading] = useState(false) + const [roles, setRoles] = useState([]) + const [newMemberUserId, setNewMemberUserId] = useState('') + const [newMemberRoleId, setNewMemberRoleId] = useState(0) + const [editingMemberUserId, setEditingMemberUserId] = useState(null) + const [editingMemberRoleId, setEditingMemberRoleId] = useState(0) + + // --------------------------------------------------------------------------- + // Data fetching + // --------------------------------------------------------------------------- + + const fetchOrganizations = useCallback(async () => { + try { + setIsLoading(true) + setError(null) + const response = await apiClient.getOrganizations() + setOrgTree(response.list || []) + // Expand all by default on first load + const allIds = flattenOrgTree(response.list || []).map((o) => o.id) + setExpandedIds(new Set(allIds)) + } catch (err) { + console.error('Failed to fetch organizations:', err) + setError('获取组织列表失败,请稍后重试') + setOrgTree([]) + } finally { + setIsLoading(false) + } + }, []) + + useEffect(() => { + fetchOrganizations() + }, [fetchOrganizations]) + + const fetchMembers = async (orgId: number) => { + try { + setIsMembersLoading(true) + const response = await apiClient.getOrgMembers(orgId) + setMembers(response.list || []) + } catch (err) { + console.error('Failed to fetch members:', err) + setMembers([]) + } finally { + setIsMembersLoading(false) + } + } + + const fetchRoles = async () => { + try { + const response = await apiClient.getRoles() + setRoles(response.list || []) + } catch (err) { + console.error('Failed to fetch roles:', err) + setRoles([]) + } + } + + // --------------------------------------------------------------------------- + // Org CRUD handlers + // --------------------------------------------------------------------------- + + const resetForm = () => { + setFormData({ parentId: 0, name: '', code: '', leader: '', phone: '', email: '', sortOrder: 0 }) + setEditingOrg(null) + } + + const openModal = (org?: FlatOrg) => { + if (org) { + setEditingOrg(org) + setFormData({ + parentId: org.parentId, + name: org.name, + code: org.code, + leader: org.leader, + phone: org.phone, + email: org.email, + sortOrder: org.sortOrder, + }) + } else { + resetForm() + } + setIsModalOpen(true) + } + + const handleCreateOrg = async () => { + try { + await apiClient.createOrganization(formData as CreateOrgRequest) + await fetchOrganizations() + setIsModalOpen(false) + resetForm() + } catch (err) { + console.error('Failed to create organization:', err) + alert('创建组织失败') + } + } + + const handleUpdateOrg = async () => { + if (!editingOrg) return + try { + const data: UpdateOrgRequest = { ...formData } + await apiClient.updateOrganization(editingOrg.id, data) + await fetchOrganizations() + setIsModalOpen(false) + resetForm() + } catch (err) { + console.error('Failed to update organization:', err) + alert('更新组织失败') + } + } + + const openDeleteConfirm = (org: FlatOrg) => { + setDeleteError(null) + + // Reject if has children + if (org.children && org.children.length > 0) { + setDeleteError('该组织包含子组织,无法删除。请先删除或转移子组织。') + setOrgToDelete(org) + setDeleteConfirmOpen(true) + return + } + + // Reject if has members + if (org.memberCount > 0) { + setDeleteError('该组织包含成员,无法删除。请先移除所有成员。') + setOrgToDelete(org) + setDeleteConfirmOpen(true) + return + } + + setOrgToDelete(org) + setDeleteConfirmOpen(true) + } + + const handleDeleteOrg = async () => { + if (!orgToDelete || deleteError) return + try { + setIsDeleting(true) + await apiClient.deleteOrganization(orgToDelete.id) + setDeleteConfirmOpen(false) + setOrgToDelete(null) + await fetchOrganizations() + } catch (err) { + console.error('Failed to delete organization:', err) + alert('删除组织失败') + } finally { + setIsDeleting(false) + } + } + + // --------------------------------------------------------------------------- + // Member management handlers + // --------------------------------------------------------------------------- + + const openMemberModal = async (org: FlatOrg) => { + setMemberOrg(org) + setMemberModalOpen(true) + setNewMemberUserId('') + setNewMemberRoleId(0) + setEditingMemberUserId(null) + await Promise.all([fetchMembers(org.id), fetchRoles()]) + } + + const handleAddMember = async () => { + if (!memberOrg || !newMemberUserId || !newMemberRoleId) return + try { + await apiClient.addOrgMember(memberOrg.id, Number(newMemberUserId), newMemberRoleId) + setNewMemberUserId('') + setNewMemberRoleId(0) + await fetchMembers(memberOrg.id) + await fetchOrganizations() + } catch (err) { + console.error('Failed to add member:', err) + alert('添加成员失败') + } + } + + const handleUpdateMemberRole = async (userId: number) => { + if (!memberOrg || !editingMemberRoleId) return + try { + await apiClient.updateOrgMember(memberOrg.id, userId, editingMemberRoleId) + setEditingMemberUserId(null) + await fetchMembers(memberOrg.id) + } catch (err) { + console.error('Failed to update member role:', err) + alert('更新成员角色失败') + } + } + + const handleRemoveMember = async (userId: number) => { + if (!memberOrg) return + try { + await apiClient.removeOrgMember(memberOrg.id, userId) + await fetchMembers(memberOrg.id) + await fetchOrganizations() + } catch (err) { + console.error('Failed to remove member:', err) + alert('移除成员失败') + } + } + + // --------------------------------------------------------------------------- + // Tree expand / collapse + // --------------------------------------------------------------------------- + + const toggleExpand = (id: number) => { + setExpandedIds((prev) => { + const next = new Set(prev) + if (next.has(id)) { + next.delete(id) + } else { + next.add(id) + } + return next + }) + } + + // --------------------------------------------------------------------------- + // Derived data + // --------------------------------------------------------------------------- + + const flatOrgs = flattenOrgTree(orgTree) + const orgOptions = collectOrgOptions(orgTree) + + // Filter: show rows whose name or code matches, plus their ancestors + const filteredOrgs = searchQuery + ? flatOrgs.filter( + (o) => + o.name.toLowerCase().includes(searchQuery.toLowerCase()) || + o.code.toLowerCase().includes(searchQuery.toLowerCase()) || + o.leader.toLowerCase().includes(searchQuery.toLowerCase()) + ) + : flatOrgs.filter((_) => { + return true + }) + + // For collapse filtering: determine visible rows considering expanded state + const visibleOrgs = (() => { + if (searchQuery) return filteredOrgs + + const visible: FlatOrg[] = [] + const depthStack: number[] = [] // tracks the depth of hidden subtrees + + for (const org of flatOrgs) { + // If this org is at a depth deeper than a collapsed parent, skip it + if (depthStack.length > 0 && org.depth > depthStack[depthStack.length - 1]) { + continue + } + + // Clean up the depth stack + while (depthStack.length > 0 && org.depth <= depthStack[depthStack.length - 1]) { + depthStack.pop() + } + + visible.push(org) + + // If this org has children and is not expanded, push its depth to mark subtree as hidden + if (org.children && org.children.length > 0 && !expandedIds.has(org.id)) { + depthStack.push(org.depth) + } + } + + return visible + })() + + // --------------------------------------------------------------------------- + // Render helpers + // --------------------------------------------------------------------------- + + const renderStatusBadge = (status: number) => { + const config = STATUS_LABELS[status] || { text: '未知', className: 'bg-gray-500/20 text-gray-400' } + return ( + + {config.text} + + ) + } + + // --------------------------------------------------------------------------- + // JSX + // --------------------------------------------------------------------------- + + return ( +
+ {/* Header */} + + +
+
+ setSearchQuery(e.target.value)} + leftIcon={} + /> +
+ +
+
+
+ + {/* Error */} + {error && ( +
+ {error} + +
+ )} + + {/* Organizations Table */} + + + 组织列表 ({flatOrgs.length}) + + +
+ + + + 名称 + 编码 + 负责人 + 联系电话 + 成员数 + 状态 + 操作 + + + + {isLoading ? ( + + 加载中... + + ) : visibleOrgs.length === 0 ? ( + + 暂无数据 + + ) : ( + visibleOrgs.map((org) => { + const hasChildren = org.children && org.children.length > 0 + const isExpanded = expandedIds.has(org.id) + + return ( + + +
+ {hasChildren ? ( + + ) : ( + + )} + {org.name} +
+
+ {org.code} + {org.leader || '-'} + {org.phone || '-'} + + + {org.memberCount} + + + {renderStatusBadge(org.status)} + +
+ + + +
+
+
+ ) + }) + )} +
+
+
+
+
+ + {/* Create / Edit Modal */} + { + setIsModalOpen(false) + resetForm() + }} + title={editingOrg ? '编辑组织' : '新增组织'} + size="md" + footer={ + <> + + + + } + > +
+ {/* Parent Org */} +
+ + +
+ + setFormData({ ...formData, name: e.target.value })} + /> + setFormData({ ...formData, code: e.target.value })} + /> + setFormData({ ...formData, leader: e.target.value })} + /> + setFormData({ ...formData, phone: e.target.value })} + /> + setFormData({ ...formData, email: e.target.value })} + /> + setFormData({ ...formData, sortOrder: Number(e.target.value) })} + /> +
+
+ + {/* Delete Confirmation Modal */} + { + setDeleteConfirmOpen(false) + setOrgToDelete(null) + setDeleteError(null) + }} + title="确认删除" + size="sm" + footer={ + <> + + {!deleteError && ( + + )} + + } + > +
+ {deleteError ? ( +
+

{deleteError}

+
+ ) : ( + <> +

+ 确定要删除组织 {orgToDelete?.name} 吗? +

+

此操作不可恢复,请谨慎操作。

+ + )} +
+
+ + {/* Member Management Modal */} + { + setMemberModalOpen(false) + setMemberOrg(null) + setMembers([]) + setEditingMemberUserId(null) + }} + title={`成员管理 — ${memberOrg?.name || ''}`} + size="lg" + > +
+ {/* Add Member Section */} +
+

添加成员

+
+
+ setNewMemberUserId(e.target.value)} + /> +
+
+ + +
+ +
+
+ + {/* Members List */} +
+

+ 当前成员 ({members.length}) +

+
+ + + + 用户名 + 角色 + 加入时间 + 操作 + + + + {isMembersLoading ? ( + + 加载中... + + ) : members.length === 0 ? ( + + 暂无成员 + + ) : ( + members.map((member) => ( + + {member.username} + + {editingMemberUserId === member.userId ? ( + + ) : ( + + {member.roleName} + + )} + + + {new Date(member.createdAt).toLocaleDateString('zh-CN')} + + +
+ {editingMemberUserId === member.userId ? ( + <> + + + + ) : ( + <> + + + + )} +
+
+
+ )) + )} +
+
+
+
+
+
+
+ ) +}