promptmanager / src /components /Category /CategorySelector.tsx
samlax12's picture
Update src/components/Category/CategorySelector.tsx
71c410d verified
raw
history blame
4.74 kB
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;