Spaces:
Running
Running
import React, { useState, useRef, useEffect } from 'react'; | |
import { Category } from '../../types'; | |
import { useApp } from '../../contexts/AppContext'; | |
import CategoryBadge from './CategoryBadge'; | |
import ReactDOM from 'react-dom'; | |
interface CategorySelectorProps { | |
selectedCategory: string | Category; | |
onChange: (categoryId: string) => void; | |
className?: string; | |
} | |
const CategorySelector: React.FC<CategorySelectorProps> = ({ | |
selectedCategory, | |
onChange, | |
className = '' | |
}) => { | |
const { categories } = useApp(); | |
const [showDropdown, setShowDropdown] = useState(false); | |
const selectorRef = useRef<HTMLDivElement>(null); | |
const selectedCategoryObj = categories.find(c => | |
typeof selectedCategory === 'string' | |
? c._id === selectedCategory | |
: c._id === selectedCategory._id | |
); | |
// 点击外部关闭下拉菜单 | |
useEffect(() => { | |
const handleClickOutside = (event: MouseEvent) => { | |
if (selectorRef.current && !selectorRef.current.contains(event.target as Node)) { | |
setShowDropdown(false); | |
} | |
}; | |
if (showDropdown) { | |
document.addEventListener('mousedown', handleClickOutside); | |
} | |
return () => { | |
document.removeEventListener('mousedown', handleClickOutside); | |
}; | |
}, [showDropdown]); | |
// 切换下拉菜单显示状态 | |
const toggleDropdown = () => { | |
setShowDropdown(!showDropdown); | |
}; | |
const handleCategorySelect = (categoryId: string) => { | |
// 重要:确保这个函数被正确调用 | |
onChange(categoryId); | |
setShowDropdown(false); | |
}; | |
// 使用Portal将下拉菜单渲染到body | |
const renderDropdown = () => { | |
if (!showDropdown || !selectorRef.current) return null; | |
const rect = selectorRef.current.getBoundingClientRect(); | |
return ReactDOM.createPortal( | |
<div | |
style={{ | |
position: 'absolute', | |
zIndex: 9999, | |
width: '200px', | |
maxHeight: '300px', | |
overflowY: 'auto', | |
top: `${rect.bottom + window.scrollY}px`, | |
left: `${rect.left + window.scrollX}px`, | |
backgroundColor: 'white', | |
borderRadius: '0.375rem', | |
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', | |
border: '1px solid rgba(0, 0, 0, 0.05)' | |
}} | |
> | |
<div className="py-1" role="menu" aria-orientation="vertical"> | |
{categories.map((category) => ( | |
<div | |
key={category._id} | |
className="px-4 py-2 text-sm hover:bg-gray-100 cursor-pointer flex items-center" | |
// 重要: 使用独立的事件处理函数,不依赖冒泡 | |
onClick={(e) => { | |
e.preventDefault(); | |
e.stopPropagation(); | |
handleCategorySelect(category._id); | |
}} | |
> | |
<div | |
className="w-3 h-3 rounded-full mr-2" | |
style={{ backgroundColor: category.color }} | |
></div> | |
{category.name} | |
{(typeof selectedCategory === 'string' ? | |
selectedCategory === category._id : | |
selectedCategory._id === category._id) && ( | |
<svg | |
xmlns="http://www.w3.org/2000/svg" | |
width="16" | |
height="16" | |
viewBox="0 0 24 24" | |
fill="none" | |
stroke="currentColor" | |
strokeWidth="2" | |
strokeLinecap="round" | |
strokeLinejoin="round" | |
className="ml-auto" | |
> | |
<polyline points="20 6 9 17 4 12"></polyline> | |
</svg> | |
)} | |
</div> | |
))} | |
</div> | |
</div>, | |
document.body | |
); | |
}; | |
return ( | |
<div ref={selectorRef} className={`relative ${className}`}> | |
<div | |
className="flex items-center cursor-pointer" | |
onClick={toggleDropdown} | |
> | |
{selectedCategoryObj ? ( | |
<CategoryBadge category={selectedCategoryObj} /> | |
) : ( | |
<div className="ios-tag" style={{ backgroundColor: '#f0f0f0', color: '#666' }}> | |
选择分类 | |
</div> | |
)} | |
<svg | |
xmlns="http://www.w3.org/2000/svg" | |
width="16" | |
height="16" | |
viewBox="0 0 24 24" | |
fill="none" | |
stroke="currentColor" | |
strokeWidth="2" | |
strokeLinecap="round" | |
strokeLinejoin="round" | |
className="ml-1" | |
> | |
<polyline points="6 9 12 15 18 9"></polyline> | |
</svg> | |
</div> | |
{renderDropdown()} | |
</div> | |
); | |
}; | |
export default CategorySelector; |