'use client'; import * as React from 'react'; import { useState, useMemo } from 'react'; import { Button } from '@nice/ui/components/button'; import { Input } from '@nice/ui/components/input'; import { Badge } from '@nice/ui/components/badge'; import { IconSearch, IconPlus, IconBuilding, IconBuildingBank, IconUsers } from '@tabler/icons-react'; import { UsersDataTable } from '@/components/organization/users-data-table'; import { UserDialog } from '@/components/organization/user-dialog'; import { PageInfo, SiteHeader } from '@/components/site-header'; import { useTRPC, useOrganization, useUser } from '@fenghuo/client'; import { useQuery } from '@tanstack/react-query'; import { toast } from '@nice/ui/components/sonner'; import { TreeSelector, type TreeNode, type TreeDragConfig } from '@/components/selector'; import { type OrganizationTreeNode, UserWithRelations, userWithRelationsSelect } from '@fenghuo/common'; import { type OrganizationDialogState, type UserDialogState } from '@/components/organization/types'; import { OrganizationDialog } from '@/components/organization/organization-dialog'; import { useSetPageInfo } from '@/components/providers/dashboard-provider'; export default function OrganizationPage() { const trpc = useTRPC(); // 状态管理 - 将所有状态声明放在一起 const [selectedOrganization, setSelectedOrganization] = useState(null); const [organizationDialog, setOrganizationDialog] = useState({ open: false, }); const [userDialog, setUserDialog] = useState({ open: false, }); const [organizationSearchQuery, setOrganizationSearchQuery] = useState(''); // 分页和筛选状态 const [pagination, setPagination] = useState({ page: 1, pageSize: 10, }); const [searchQuery, setSearchQuery] = useState(''); const [selectedRole, setSelectedRole] = useState(''); // 获取组织树数据(使用 tRPC) const { data: rawOrganizationTree = [], isLoading: organizationsLoading } = useQuery({ ...trpc.organization.getTree.queryOptions({ includeInactive: false, include: { terms: { include: { taxonomy: true, }, }, }, }), }); // 将 OrganizationTreeNode 转换为与 TreeSelector 兼容的 TreeNode const organizationTree = useMemo(() => { const convertNode = (node: OrganizationTreeNode): TreeNode => ({ ...node, slug: node.slug ?? undefined, description: node.description ?? undefined, parentId: node.parentId ?? undefined, path: node.path ?? undefined, deletedAt: node.deletedAt ?? undefined, children: node.children ? node.children.map(convertNode) : [], }); return rawOrganizationTree.map(convertNode); }, [rawOrganizationTree]); // 构建用户查询的 where 条件 const buildUserWhereCondition = useMemo(() => { const conditions: any = { deletedAt: null, OR: [{ organization: { deletedAt: null } }, { organizationId: null }], }; // 搜索条件 - 支持用户名搜索 if (searchQuery) { conditions.username = { contains: searchQuery, mode: 'insensitive', // 忽略大小写 }; } // 角色筛选 if (selectedRole) { conditions.userRoles = { some: { role: { id: selectedRole, }, }, }; } // 部门筛选 - 考虑选中的部门及其子部门 if (selectedOrganization) { // 获取选中部门及其所有子部门的ID const findOrganizationInTree = (depts: any[], targetId: string): any => { for (const dept of depts) { if (dept.id === targetId) { return dept; } if (dept.children) { const found = findOrganizationInTree(dept.children, targetId); if (found) return found; } } return null; }; const selectedDeptWithChildren = findOrganizationInTree(organizationTree, selectedOrganization.id); if (selectedDeptWithChildren) { const organizationIds = getAllOrganizationIds(selectedDeptWithChildren as OrganizationTreeNode); conditions.organizationId = { in: organizationIds, }; } else { conditions.organizationId = selectedOrganization.id; } } return conditions; }, [searchQuery, selectedRole, selectedOrganization, organizationTree]); // 获取用户数据(使用服务端分页) const { data: usersResponse, isLoading: usersLoading } = useQuery({ ...trpc.user.findManyWithPagination.queryOptions({ page: pagination.page, pageSize: pagination.pageSize, where: buildUserWhereCondition, select: userWithRelationsSelect, orderBy: { createdAt: 'desc', }, }), }); // 获取角色列表用于筛选选项 const { data: rolesResponse } = useQuery({ ...trpc.role.findManyWithPagination.queryOptions({ page: 1, pageSize: 100, where: { isActive: true, }, }), }); // 处理用户数据 const userData = useMemo(() => { if (usersLoading || !usersResponse || !usersResponse.items) return []; return usersResponse.items }, [usersResponse, usersLoading]); // 处理可用角色选项 const availableRoles = useMemo(() => { if (!rolesResponse?.items) return []; return rolesResponse.items.map((role) => ({ value: role.id, label: role.name, })); }, [rolesResponse]); // 分页信息 const paginationInfo = useMemo(() => { if (!usersResponse) { return { totalPages: 0, totalCount: 0, hasNextPage: false, hasPreviousPage: false, }; } return { totalPages: usersResponse.totalPages, totalCount: usersResponse.totalCount, hasNextPage: usersResponse.hasNextPage, hasPreviousPage: usersResponse.hasPreviousPage, }; }, [usersResponse]); // 过滤部门数据 - 修改为支持已有树形结构的搜索 const filteredOrganizations = React.useMemo(() => { if (!organizationSearchQuery) return organizationTree; const filterOrganizations = (depts: TreeNode[]): TreeNode[] => { return depts.reduce((acc, dept) => { const matchesCurrent = dept.name.toLowerCase().includes(organizationSearchQuery.toLowerCase()) || dept.description?.toLowerCase().includes(organizationSearchQuery.toLowerCase()); const filteredChildren = dept.children ? filterOrganizations(dept.children as TreeNode[]) : []; // 如果当前部门匹配或有匹配的子部门,就包含这个部门 if (matchesCurrent || filteredChildren.length > 0) { acc.push({ ...dept, children: filteredChildren, }); } return acc; }, []); }; return filterOrganizations(organizationTree); }, [organizationTree, organizationSearchQuery]); // 拖拽排序处理函数 const handleDragEnd = async (oldData: TreeNode[], newData: TreeNode[]) => { // 提取排序变化 const changes = extractOrderChanges(oldData, newData); // 如果有变化,调用后端API更新排序 if (changes.length > 0) { try { await organizationMutations.updateDeptOrder.mutateAsync( changes.map((change) => ({ id: change.id, order: change.newOrder })), ); toast.success('部门排序已更新'); } catch (error: any) { console.error('排序更新失败:', error); toast.error('排序更新失败: ' + (error.message || '未知错误')); } } }; // 提取排序变化的辅助函数 const extractOrderChanges = (oldData: TreeNode[], newData: TreeNode[]): Array<{ id: string; newOrder: number }> => { const changes: Array<{ id: string; newOrder: number }> = []; const compareLevel = (oldNodes: TreeNode[], newNodes: TreeNode[], level = 0) => { newNodes.forEach((newNode, index) => { const oldNode = oldNodes.find((n) => n.id === newNode.id); if (oldNode && oldNode.order !== newNode.order) { changes.push({ id: newNode.id, newOrder: newNode.order || index, }); } // 递归处理子节点 if (newNode.children && oldNode?.children) { compareLevel(oldNode.children as TreeNode[], newNode.children as TreeNode[], level + 1); } }); }; compareLevel(oldData, newData); return changes; }; // 拖拽配置 const dragConfig: TreeDragConfig = { enabled: true, showHandle: true, onDragEnd: handleDragEnd, onDragStart: (event) => { console.log('开始拖拽:', event.active.id); }, }; // 处理分页变化 const handlePaginationChange = (newPagination: { page: number; pageSize: number }) => { setPagination(newPagination); }; // 处理搜索变化 const handleSearchChange = (query: string) => { setSearchQuery(query); // 搜索时重置到第一页 setPagination((prev) => ({ ...prev, page: 1 })); }; // 处理角色筛选变化 const handleRoleChange = (role: string) => { setSelectedRole(role); // 筛选时重置到第一页 setPagination((prev) => ({ ...prev, page: 1 })); }; // 处理部门筛选变化 - 当选中部门时自动应用筛选 React.useEffect(() => { // 当选中部门变化时,重置到第一页 setPagination((prev) => ({ ...prev, page: 1 })); }, [selectedOrganization]); // 处理部门选择 - 支持取消选中 const handleOrganizationSelect = (organization: TreeNode) => { // 如果点击的是已选中的部门,则取消选中 if (selectedOrganization && selectedOrganization.id === organization.id) { setSelectedOrganization(null); } else { // 否则选中新的部门 setSelectedOrganization(organization); } }; // 处理编辑部门 const handleEditOrganization = (organization: TreeNode) => { setOrganizationDialog({ open: true, organization: organization as OrganizationTreeNode, mode: 'edit', }); }; // 处理删除部门 const handleDeleteOrganization = (organization: TreeNode) => { if (confirm(`确定要删除部门 "${organization.name}" 吗?这将同时删除该部门下的所有子部门。`)) { organizationMutations.softDeleteByIds.mutate( { ids: [organization.id] }, { onSuccess: () => { toast.success('部门删除成功'); // 如果删除的是当前选中的部门,清空选择 if (selectedOrganization?.id === organization.id) { setSelectedOrganization(null); } }, onError: (error) => { toast.error('删除失败: ' + error.message); }, }, ); } }; // 处理添加子部门 const handleAddChildOrganization = (parentId: string) => { setOrganizationDialog({ open: true, parentId, mode: 'addChild', }); }; // 处理添加根部门 const handleAddRootOrganization = () => { setOrganizationDialog({ open: true, mode: 'add', }); }; // 添加 organization mutations const organizationMutations = useOrganization(); // 修改 handleSaveOrganization 方法的参数类型和实现 const handleSaveOrganization = (data: { name: string; slug: string; description: string; parentId?: string; organizationTypeId?: string; professionIds?: string[]; }) => { // 构建术语连接数组 const termConnections: Array<{ id: string }> = []; if (data.organizationTypeId) { termConnections.push({ id: data.organizationTypeId }); } if (data.professionIds && data.professionIds.length > 0) { data.professionIds.forEach((professionId) => { termConnections.push({ id: professionId }); }); } // 构建要保存的数据 const createData = { name: data.name, slug: data.slug || null, // 使用用户输入的 slug description: data.description || null, parentId: data.parentId || null, // 关联术语 terms: termConnections.length > 0 ? { connect: termConnections, } : undefined, }; // 检查是否为编辑模式 if (organizationDialog.organization && organizationDialog.mode === 'edit') { // 更新部门时,需要先断开所有术语连接,再重新连接 const updateData = { ...createData, terms: { // 先断开所有现有的术语连接 set: termConnections, // 使用 set 替换所有关联 }, }; // 更新部门 organizationMutations.update.mutate( { where: { id: organizationDialog.organization.id }, data: updateData, }, { onSuccess: () => { toast.success('部门更新成功'); setOrganizationDialog({ open: false }); }, onError: (error: any) => { console.error('更新失败:', error); let errorMessage = '更新失败'; if (error.message) { if ( error.message.includes('already exists') || error.message.includes('same') || error.message.includes('unique') || error.message.includes('Unique constraint') ) { errorMessage = '该部门名称或别名已存在,请使用其他名称'; } else { errorMessage = error.message; } } toast.error(errorMessage); }, }, ); } else { // 创建新部门 organizationMutations.create.mutate( { data: createData, }, { onSuccess: () => { toast.success('部门创建成功'); setOrganizationDialog({ open: false }); }, onError: (error: any) => { console.error('创建失败:', error); let errorMessage = '创建失败'; if (error.message) { if ( error.message.includes('already exists') || error.message.includes('same') || error.message.includes('unique') || error.message.includes('Unique constraint') ) { errorMessage = '该部门名称或别名已存在,请使用其他名称'; } else { errorMessage = error.message; } } toast.error(errorMessage); }, }, ); } }; // 处理添加用户 const handleAddUser = () => { setUserDialog({ open: true, mode: 'add', }); }; const { create: createUser } = useUser(); // 处理保存用户 const handleSaveUser = (data: { username: string; roleIds: string[]; organizationId: string; password: string; description?: string; // 添加可选的个人简介字段 }) => { const { roleIds, ...others } = data createUser.mutate( { data: { ...others, roles: { connect: roleIds.map(id => ({ id })) } } }, // 由于我们传入完整的 data 对象,roleIds 字段会自动包含在内 { onSuccess: () => { toast.success('用户创建成功'); setUserDialog({ open: false }); }, onError: (error: any) => { console.error('创建失败:', error); let errorMessage = '创建失败'; if (error.message) { if ( error.message.includes('already exists') || error.message.includes('same') || error.message.includes('unique') || error.message.includes('Unique constraint') ) { errorMessage = '该用户名已存在,请使用其他名称'; } else { errorMessage = error.message; } } toast.error(errorMessage); }, }, ); }; useSetPageInfo({ title: '组织管理', subtitle: '管理组织结构,支持人员、组织的创建、编辑、删除以及查看等操作', }) return (
{/* 左侧:部门树 */}
{/* 标题区域 */}

组织结构

选择一个组织来管理其用户

{/* 搜索框 */}
setOrganizationSearchQuery(e.target.value)} className="pl-10" />
{/* 部门树 */}
{organizationsLoading ? (
加载中...
) : filteredOrganizations.length > 0 ? (
{ if (node.level === 0) { return IconBuildingBank; } return IconBuilding; }, }} actionConfig={{ onEdit: handleEditOrganization, onDelete: handleDeleteOrganization, onAddChild: handleAddChildOrganization, editLabel: '编辑部门', deleteLabel: '删除部门', addChildLabel: '添加下级', }} displayConfig={{ showDescription: true, selectedClassName: 'bg-primary/10 border border-primary/20', hoverClassName: 'hover:bg-muted/50 border border-transparent', }} indentSize={20} containerized={false} className="space-y-1 bg-white" dragConfig={dragConfig} />
) : (
{organizationSearchQuery ? '没有找到匹配的部门' : '暂无部门'}
{!organizationSearchQuery &&
点击上方按钮创建第一个组织
}
)}
{/* 右侧:用户管理 */}
{/* 标题区域 */}

用户管理

{selectedOrganization && ( {selectedOrganization.name} )}

{selectedOrganization ? `显示 ${selectedOrganization.name} 及其下属部门的用户` : `共 ${paginationInfo.totalCount} 名用户`}

{/* 用户内容区域 */}
{ }} // 部门筛选通过左侧树形选择处理 isLoading={usersLoading} availableRoles={availableRoles} onAddUser={handleAddUser} />
{/* 部门编辑对话框 */} setOrganizationDialog({ open: false })} onSave={handleSaveOrganization} /> {/* 用户编辑对话框 */} setUserDialog({ open: false })} onSave={handleSaveUser} />
); } // 递归获取部门及其所有子部门的ID function getAllOrganizationIds(organization: OrganizationTreeNode): string[] { const ids = [organization.id]; if (organization.children) { organization.children.forEach((child) => { ids.push(...getAllOrganizationIds(child)); }); } return ids; }