'use client'; import * as React from 'react'; import { useState } from 'react'; import { Button } from '@nice/ui/components/button'; import { Popover, PopoverContent, PopoverTrigger } from '@nice/ui/components/popover'; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from '@nice/ui/components/command'; import { IconChevronDown, IconChevronRight, IconCheck, IconBuilding, IconX } from '@tabler/icons-react'; import { cn } from '@nice/ui/lib/utils'; import { useTRPC } from '@fenghuo/client'; import { useQuery } from '@tanstack/react-query'; import type { OrganizationTreeNode } from '@fenghuo/common'; import { Organization } from '@fenghuo/db'; import { Badge } from '@nice/ui/components/badge'; // 树形选择器属性 interface OrganizationTreeSelectorProps { value?: string | string[]; // 支持单选和多选 onValueChange?: (value: string | string[]) => void; placeholder?: string; className?: string; disabled?: boolean; allowClear?: boolean; multiple?: boolean; // 是否支持多选 excludeIds?: string[]; // 排除的部门ID列表,用于编辑时排除自身及子部门 modal?: boolean; // 是否为模态模式,用于在 Dialog 中解决滚轮问题 } // 树形节点组件 interface TreeNodeProps { node: OrganizationTreeNode; level: number; selectedValue?: string | string[]; multiple?: boolean; expandedIds: Set; onSelect: (value: string, name: string) => void; onToggleExpand: (id: string) => void; } function TreeNode({ node, level, selectedValue, multiple, expandedIds, onSelect, onToggleExpand }: TreeNodeProps) { const hasChildren = node.children && node.children.length > 0; const isExpanded = expandedIds.has(node.id); // 判断是否选中 - 兼容单选和多选 const isSelected = React.useMemo(() => { if (!selectedValue) return false; return Array.isArray(selectedValue) ? selectedValue.includes(node.id) : selectedValue === node.id; }, [selectedValue, node.id]); const handleSelect = () => { onSelect(node.id, node.name); }; const handleToggleExpand = (e: React.MouseEvent) => { e.stopPropagation(); if (hasChildren) { onToggleExpand(node.id); } }; return ( <>
{/* 展开/折叠图标 */}
{hasChildren ? ( ) : (
)}
{/* 部门图标 */} {/* 部门名称 */} {node.name}
{/* 选中状态指示 */} {isSelected && } {/* 子节点 */} {hasChildren && isExpanded && ( <> {node.children!.map((child) => ( ))} )} ); } // 主组件 export function DeptSelect({ value, onValueChange, placeholder = '选择部门', className, disabled = false, allowClear = true, multiple = false, excludeIds = [], modal = false, }: OrganizationTreeSelectorProps) { const [open, setOpen] = useState(false); const [expandedIds, setExpandedIds] = useState>(new Set()); const [searchValue, setSearchValue] = useState(''); const trpc = useTRPC(); // 使用 tRPC 获取组织树数据 const { data: rawOrganizationTree = [], isLoading, error, } = useQuery({ ...trpc.organization.getTree.queryOptions({ includeInactive: false, }), }); // 过滤排除的部门 const organizationTree = React.useMemo(() => { const filterExcluded = (nodes: OrganizationTreeNode[]): OrganizationTreeNode[] => { return nodes .filter((node) => !excludeIds.includes(node.id)) .map((node) => ({ ...node, children: node.children ? filterExcluded(node.children) : [], })); }; return filterExcluded(rawOrganizationTree); }, [rawOrganizationTree, excludeIds]); // 构建部门树 - 由于后端已经返回树形结构并排序,客户端只需处理过滤后的树 const buildOrganizationTree = React.useMemo(() => { if (!organizationTree || organizationTree.length === 0) { return []; } return organizationTree; }, [organizationTree]); // 扁平化所有部门,用于查找和路径计算 const flatOrganizations = React.useMemo(() => { const flatten = (nodes: OrganizationTreeNode[]): Organization[] => { return nodes.reduce((acc, node) => { acc.push(node); if (node.children) { acc.push(...flatten(node.children)); } return acc; }, []); }; return flatten(organizationTree); }, [organizationTree]); // 获取选中的部门列表 const selectedOrganizations = React.useMemo(() => { if (!value) return []; const selectedIds = Array.isArray(value) ? value : [value]; return flatOrganizations.filter((org) => selectedIds.includes(org.id)); }, [flatOrganizations, value]); // 获取部门的完整路径 const getOrganizationPath = React.useCallback( (deptId: string): string => { const dept = flatOrganizations.find((d) => d.id === deptId); if (!dept) return ''; const path: string[] = []; let current: Organization | undefined = dept; while (current) { path.unshift(current.name); current = flatOrganizations.find((d) => d.id === current?.parentId); } return path.join(' / '); }, [flatOrganizations], ); // 切换展开状态 const toggleExpand = (id: string) => { setExpandedIds((prev) => { const newSet = new Set(prev); if (newSet.has(id)) { newSet.delete(id); } else { newSet.add(id); } return newSet; }); }; // 处理选择逻辑 const handleSelect = (selectedValue: string, selectedName: string) => { if (!onValueChange) return; if (multiple) { const currentValues = Array.isArray(value) ? value : value ? [value] : []; if (currentValues.includes(selectedValue)) { // 取消选择 const newValues = currentValues.filter((id) => id !== selectedValue); onValueChange(newValues); } else { // 添加选择 onValueChange([...currentValues, selectedValue]); } } else { // 单选模式 onValueChange(selectedValue); setOpen(false); } }; // 处理清除 const handleClear = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); onValueChange?.(multiple ? [] : ''); }; // 处理单个部门移除(仅多选模式) const handleRemoveOrganization = (orgId: string, e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (multiple) { const currentValues = Array.isArray(value) ? value : value ? [value] : []; const newValues = currentValues.filter((id) => id !== orgId); onValueChange?.(newValues); } }; // 过滤部门(支持搜索) const filterOrganizations = React.useCallback( (nodes: OrganizationTreeNode[], search: string): OrganizationTreeNode[] => { if (!search) return nodes; const filtered: OrganizationTreeNode[] = []; for (const node of nodes) { const matches = node.name.toLowerCase().includes(search.toLowerCase()); const filteredChildren = node.children ? filterOrganizations(node.children, search) : []; if (matches || filteredChildren.length > 0) { filtered.push({ ...node, children: filteredChildren.length > 0 ? filteredChildren : node.children, }); } } return filtered; }, [], ); const filteredTree = React.useMemo(() => { return filterOrganizations(buildOrganizationTree, searchValue); }, [buildOrganizationTree, searchValue, filterOrganizations]); // 处理搜索时的自动展开逻辑 React.useEffect(() => { if (searchValue) { // 在useEffect内部计算过滤结果,避免依赖filteredTree const filtered = filterOrganizations(buildOrganizationTree, searchValue); if (filtered.length > 0) { const idsToExpand = new Set(); const collectExpandIds = (nodes: OrganizationTreeNode[]) => { nodes.forEach(node => { // 如果节点名称匹配搜索条件,展开该节点 if (node.name.toLowerCase().includes(searchValue.toLowerCase())) { idsToExpand.add(node.id); } // 如果有子节点,展开该节点并递归处理子节点 if (node.children && node.children.length > 0) { idsToExpand.add(node.id); collectExpandIds(node.children); } }); }; collectExpandIds(filtered); // 批量更新展开状态 setExpandedIds(prev => new Set([...prev, ...idsToExpand])); } } }, [searchValue, buildOrganizationTree, filterOrganizations]); // 渲染触发器内容 const renderTriggerContent = () => { if (selectedOrganizations.length === 0) { return placeholder; } if (multiple) { if (selectedOrganizations.length === 1) { return getOrganizationPath(selectedOrganizations[0]!.id); } else { return (
{selectedOrganizations.slice(0, 2).map((org) => ( {org.name} handleRemoveOrganization(org.id, e)} className="ml-1 hover:bg-muted-foreground/20 rounded-sm p-0.5 cursor-pointer inline-flex items-center justify-center" role="button" tabIndex={0} aria-label={`移除部门 ${org.name}`} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleRemoveOrganization(org.id, e as any); } }} > ))} {selectedOrganizations.length > 2 && ( +{selectedOrganizations.length - 2} )}
); } } return getOrganizationPath(selectedOrganizations[0]!.id); }; // 检查是否选中 const isSelected = (orgId: string) => { if (!value) return false; return Array.isArray(value) ? value.includes(orgId) : value === orgId; }; // 加载状态 if (isLoading) { return ( ); } // 错误状态 if (error) { return ( ); } return ( 未找到部门 {/* 多选模式下显示已选择数量 */} {multiple && selectedOrganizations.length > 0 && (
已选择 {selectedOrganizations.length} 个部门
)} {/* 树形部门列表 */} {filteredTree.map((node) => ( ))}
); } // 导出便捷的单选和多选组件 export function SingleDeptSelector( props: Omit & { onValueChange?: (value: string) => void; }, ) { const handleValueChange = (value: string | string[]) => { if (props.onValueChange && typeof value === 'string') { props.onValueChange(value); } }; return ; } export function MultipleDeptSelector( props: Omit & { onValueChange?: (value: string[]) => void; }, ) { const handleValueChange = (value: string | string[]) => { if (props.onValueChange && Array.isArray(value)) { props.onValueChange(value); } }; return ; }