refactor: 抽取 SearchSelect 为公共组件
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
71
src/components/SearchSelect.tsx
Normal file
71
src/components/SearchSelect.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { useState, useEffect, useMemo, useRef } from 'react';
|
||||||
|
import { ChevronDown } from 'lucide-react';
|
||||||
|
|
||||||
|
export function SearchSelect({ value, onChange, options, placeholder, className }: {
|
||||||
|
value: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
options: string[];
|
||||||
|
placeholder: string;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e: MouseEvent) => {
|
||||||
|
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
||||||
|
};
|
||||||
|
document.addEventListener('mousedown', handler);
|
||||||
|
return () => document.removeEventListener('mousedown', handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
if (!query) return options;
|
||||||
|
const q = query.toLowerCase();
|
||||||
|
return options.filter((o) => o.toLowerCase().includes(q));
|
||||||
|
}, [options, query]);
|
||||||
|
|
||||||
|
const displayValue = value || '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className="relative">
|
||||||
|
<div
|
||||||
|
className={`flex items-center bg-white border border-gray-200 rounded-lg shadow-sm cursor-pointer transition-all focus-within:ring-2 focus-within:ring-blue-500/20 focus-within:border-blue-500 ${className || 'text-xs py-1.5 px-2'}`}
|
||||||
|
onClick={() => setOpen(!open)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="flex-1 outline-none bg-transparent min-w-0 text-inherit"
|
||||||
|
placeholder={displayValue || placeholder}
|
||||||
|
value={open ? query : displayValue}
|
||||||
|
onChange={(e) => { setQuery(e.target.value); if (!open) setOpen(true); }}
|
||||||
|
onFocus={() => { setOpen(true); setQuery(''); }}
|
||||||
|
/>
|
||||||
|
<ChevronDown size={14} className={`text-gray-400 shrink-0 transition-transform ${open ? 'rotate-180' : ''}`} />
|
||||||
|
</div>
|
||||||
|
{open && (
|
||||||
|
<div className="absolute z-50 left-0 right-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-xl max-h-48 overflow-auto">
|
||||||
|
<div
|
||||||
|
className="px-2 py-1.5 text-xs text-gray-400 cursor-pointer hover:bg-gray-50"
|
||||||
|
onClick={() => { onChange(''); setQuery(''); setOpen(false); }}
|
||||||
|
>
|
||||||
|
{placeholder}
|
||||||
|
</div>
|
||||||
|
{filtered.map((o) => (
|
||||||
|
<div
|
||||||
|
key={o}
|
||||||
|
className={`px-2 py-1.5 text-xs cursor-pointer hover:bg-blue-50 transition-colors ${o === value ? 'bg-blue-50 text-blue-600 font-bold' : 'text-gray-700'}`}
|
||||||
|
onClick={() => { onChange(o); setQuery(''); setOpen(false); }}
|
||||||
|
>
|
||||||
|
{o}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{filtered.length === 0 && (
|
||||||
|
<div className="px-2 py-3 text-xs text-gray-400 text-center">无匹配项</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user