'use client'; import * as React from 'react'; import { useState } from 'react'; import type { TermTreeNode } from '@fenghuo/common'; 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, IconTag, IconX } from '@tabler/icons-react'; import { cn } from '@nice/ui/lib/utils'; import { useTRPC } from '@fenghuo/client'; import { useQuery } from '@tanstack/react-query'; import { Term } from '@fenghuo/db'; import { Badge } from '@nice/ui/components/badge'; // 术语树节点接口 - 已移至 @fenghuo/common // 树形选择器属性 interface TermTreeSelectorProps { taxonomySlug: string; // 使用slug来获取分类 value?: string | string[]; // 支持单选和多选 onValueChange?: (value: string | string[]) => void; // 支持单选和多选 placeholder?: string; className?: string; disabled?: boolean; allowClear?: boolean; excludeIds?: string[]; // 排除的ID列表 multiple?: boolean; // 是否支持多选 maxSelections?: number; // 最大选择数量(仅多选模式) } // 树形节点组件 interface TreeNodeProps { node: TermTreeNode; level: number; selectedValues: string[]; // 改为数组以支持多选 expandedIds: Set; onSelect: (value: string, name: string) => void; onToggleExpand: (id: string) => void; multiple?: boolean; } function TreeNode({ node, level, selectedValues, expandedIds, onSelect, onToggleExpand, multiple }: TreeNodeProps) { const hasChildren = node.children && node.children.length > 0; const isExpanded = expandedIds.has(node.id); const isSelected = selectedValues.includes(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 TermSelect({ taxonomySlug, value, onValueChange, placeholder = '选择分类', className, disabled = false, allowClear = true, excludeIds = [], multiple = false, maxSelections, }: TermTreeSelectorProps) { const [open, setOpen] = useState(false); const [expandedIds, setExpandedIds] = useState>(new Set()); const [searchValue, setSearchValue] = useState(''); const trpc = useTRPC(); // 根据 taxonomySlug 获取术语树 const { data: termTree = [], isLoading, error, } = useQuery({ ...trpc.term.getTreeByTaxonomy.queryOptions({ taxonomySlug, }), enabled: !!taxonomySlug, }); // 过滤排除的术语 const terms = React.useMemo(() => { const filterExcluded = (nodes: TermTreeNode[]): Term[] => { return nodes .filter((node) => !excludeIds.includes(node.id)) .map((node) => ({ ...node, children: node.children ? filterExcluded(node.children) : undefined, })); }; return filterExcluded(termTree); }, [termTree, excludeIds]); // 构建术语树 - 后端已返回树形,只需排序 const buildTermTree = React.useMemo(() => { if (!terms || terms.length === 0) { return []; } // 递归排序 const sortNodes = (nodes: TermTreeNode[]): TermTreeNode[] => { return nodes .sort((a, b) => (a.order || 0) - (b.order || 0)) .map((node) => ({ ...node, children: node.children ? sortNodes(node.children) : undefined, })); }; return sortNodes(terms); }, [terms]); // 扁平化所有术语,用于查找和路径计算 const flatTerms = React.useMemo(() => { const flatten = (nodes: TermTreeNode[]): Term[] => { return nodes.reduce((acc, node) => { acc.push(node); if (node.children) { acc.push(...flatten(node.children)); } return acc; }, []); }; return flatten(terms); }, [terms]); // 标准化选中值为数组 const selectedValues = React.useMemo(() => { if (!value) return []; if (Array.isArray(value)) return value; return [value]; }, [value]); // 获取选中术语的信息 const selectedTerms = React.useMemo(() => { return selectedValues.map((val) => flatTerms.find((term) => term.id === val)).filter(Boolean) as Term[]; }, [selectedValues, flatTerms]); // 获取术语的完整路径 const getTermPath = React.useCallback( (termId: string): string => { const term = flatTerms.find((d) => d.id === termId); if (!term) return ''; const path: string[] = []; let current: Term | undefined = term; while (current) { path.unshift(current.name); current = flatTerms.find((d) => d.id === current?.parentId); } return path.join(' / '); }, [flatTerms], ); // 切换展开状态 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 (multiple) { // 多选模式 const newValues = [...selectedValues]; const index = newValues.indexOf(selectedValue); if (index >= 0) { // 已选中,取消选择 newValues.splice(index, 1); } else { // 未选中,添加选择(检查最大选择数) if (!maxSelections || newValues.length < maxSelections) { newValues.push(selectedValue); } } onValueChange?.(newValues); } else { // 单选模式 onValueChange?.(selectedValue); setOpen(false); } }; // 处理清除 const handleClear = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); onValueChange?.(multiple ? [] : ''); }; // 处理单个项目移除(仅多选模式) const handleRemoveItem = (termId: string, e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (multiple) { const newValues = selectedValues.filter((id) => id !== termId); onValueChange?.(newValues); } }; // 过滤术语(支持搜索) const filterTerms = React.useCallback((nodes: TermTreeNode[], search: string): TermTreeNode[] => { if (!search) return nodes; const filtered: TermTreeNode[] = []; const lowerCaseSearch = search.toLowerCase(); for (const node of nodes) { const matches = node.name.toLowerCase().includes(lowerCaseSearch); const filteredChildren = node.children ? filterTerms(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 filterTerms(buildTermTree, searchValue); }, [buildTermTree, searchValue, filterTerms]); // 处理搜索时的自动展开逻辑 React.useEffect(() => { if (searchValue) { // 在useEffect内部计算过滤结果,避免依赖filteredTree const filtered = filterTerms(buildTermTree, searchValue); if (filtered.length > 0) { const idsToExpand = new Set(); const collectExpandIds = (nodes: TermTreeNode[]) => { 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, filterTerms, buildTermTree]); // 渲染触发器内容 const renderTriggerContent = () => { if (selectedTerms.length === 0) { return {placeholder}; } if (multiple) { if (selectedTerms.length === 1) { return {getTermPath(selectedTerms[0]?.id || '')}; } else { return (
{selectedTerms.slice(0, 3).map((term) => ( {term.name}
handleRemoveItem(term.id, e)} className="ml-1 hover:bg-muted-foreground/20 rounded-sm p-0.5 cursor-pointer" role="button" tabIndex={0} aria-label="移除此项" onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); if (multiple) { const newValues = selectedValues.filter((id) => id !== term.id); onValueChange?.(newValues); } } }} >
))} {selectedTerms.length > 3 && ( +{selectedTerms.length - 1} )}
); } } else { return {getTermPath(selectedTerms[0]?.id || '')}; } }; // 加载状态 if (isLoading) { return ( ); } // 错误状态 if (error) { return ( ); } return ( 未找到分类 {/* 多选模式下显示已选择数量 */} {multiple && selectedTerms.length > 0 && (
已选择 {selectedTerms.length} 项{maxSelections && ` / ${maxSelections}`}
)} {filteredTree.map((node) => ( ))}
); }