import React, { useState, useEffect, useRef } from 'react'; type Option = { value: string; label: string; children?: Option[]; }; type MultiCascaderProps = { options: Option[]; defaultValue?: string[]; value?: string[]; onChange?: (value: string[]) => void; placeholder?: string; }; // 获取所有叶子节点的值 const getAllLeafValues = (option: Option): string[] => { if (!option.children || option.children.length === 0) { return [option.value]; } return option.children.flatMap(getAllLeafValues); }; // 检查节点是否所有子节点都被选中 const isAllChildrenChecked = (option: Option, selected: string[]): boolean => { if (!option.children || option.children.length === 0) { return selected.includes(option.value); } // 对于有子节点的节点,检查其所有叶子节点是否都被选中 const leafValues = getAllLeafValues(option); return leafValues.every(value => selected.includes(value)); }; // 检查节点是否有部分子节点被选中 const isSomeChildrenChecked = (option: Option, selected: string[]): boolean => { if (!option.children || option.children.length === 0) { return selected.includes(option.value); } // 对于有子节点的节点,检查其叶子节点是否有部分被选中 const leafValues = getAllLeafValues(option); return leafValues.some(value => selected.includes(value)); }; const MultiCascader: React.FC = ({ options, defaultValue = [], value, onChange, placeholder = '请选择' }) => { const [visible, setVisible] = useState(false); const [selected, setSelected] = useState(value ?? defaultValue); const [expandedKeys, setExpandedKeys] = useState>(new Set()); const containerRef = useRef(null); // 当外部 value 变化时,同步内部状态 useEffect(() => { if (value !== undefined) { setSelected(value); } }, [value]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setVisible(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const handleItemCheck = (option: Option, checked: boolean) => { const leafValues = getAllLeafValues(option); let newSelected: string[]; if (checked) { newSelected = Array.from(new Set([...selected, ...leafValues])); } else { newSelected = selected.filter(v => !leafValues.includes(v)); } setSelected(newSelected); onChange?.(newSelected); }; const getDisplayText = () => { if (selected.length === 0) return placeholder; return `已选择 ${selected.length} 项`; }; const toggleExpand = (key: string) => { setExpandedKeys(prev => { const newSet = new Set(prev); if (newSet.has(key)) { newSet.delete(key); } else { newSet.add(key); } return newSet; }); }; const renderOption = (option: Option, level: number = 0) => { const allChecked = isAllChildrenChecked(option, selected); const someChecked = isSomeChildrenChecked(option, selected); const hasChildren = option.children && option.children.length > 0; const isExpanded = expandedKeys.has(option.value); return (
0 ? "pl-4" : ""}>
{ if (el) el.indeterminate = !allChecked && someChecked; }} onChange={e => handleItemCheck(option, e.target.checked)} id={`cascader-${option.value}`} /> {hasChildren && ( )}
{hasChildren && isExpanded && (
{option.children?.map(child => ( {renderOption(child, level + 1)} ))}
)}
); }; return (
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setVisible(!visible); } }} onClick={() => setVisible(!visible)} > {getDisplayText()}
{visible && (
{options.map(option => renderOption(option))}
)}
); }; export default MultiCascader;