'use client'; import { ArrowDownWideNarrow, ArrowUpNarrowWide, ArrowUpDown } from 'lucide-react'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; export type SearchFilterKey = 'source' | 'title' | 'year' | 'yearOrder'; export interface SearchFilterOption { label: string; value: string; } export interface SearchFilterCategory { key: SearchFilterKey; label: string; options: SearchFilterOption[]; } interface SearchResultFilterProps { categories: SearchFilterCategory[]; values: Partial>; onChange: (values: Record) => void; } const DEFAULTS: Record = { source: 'all', title: 'all', year: 'all', yearOrder: 'none', }; const SearchResultFilter: React.FC = ({ categories, values, onChange }) => { const [activeCategory, setActiveCategory] = useState(null); const [dropdownPosition, setDropdownPosition] = useState<{ x: number; y: number; width: number }>({ x: 0, y: 0, width: 0 }); const categoryRefs = useRef>({}); const dropdownRef = useRef(null); const mergedValues = useMemo(() => { return { ...DEFAULTS, ...values, } as Record; }, [values]); const calculateDropdownPosition = (categoryKey: SearchFilterKey) => { const element = categoryRefs.current[categoryKey]; if (element) { const rect = element.getBoundingClientRect(); const viewportWidth = window.innerWidth; const isMobile = viewportWidth < 768; let x = rect.left; // 为标题筛选设置更大的最小宽度,其他保持原来的最小宽度 const minWidth = categoryKey === 'title' ? 400 : 240; let dropdownWidth = Math.max(rect.width, minWidth); let useFixedWidth = false; if (isMobile) { const padding = 16; const maxWidth = viewportWidth - padding * 2; dropdownWidth = Math.min(dropdownWidth, maxWidth); useFixedWidth = true; if (x + dropdownWidth > viewportWidth - padding) { x = viewportWidth - dropdownWidth - padding; } if (x < padding) { x = padding; } } setDropdownPosition({ x, y: rect.bottom, width: useFixedWidth ? dropdownWidth : rect.width }); } }; const handleCategoryClick = (categoryKey: SearchFilterKey) => { if (activeCategory === categoryKey) { setActiveCategory(null); } else { setActiveCategory(categoryKey); calculateDropdownPosition(categoryKey); } }; const handleOptionSelect = (categoryKey: SearchFilterKey, optionValue: string) => { const newValues = { ...mergedValues, [categoryKey]: optionValue, } as Record; onChange(newValues); setActiveCategory(null); }; const getDisplayText = (categoryKey: SearchFilterKey) => { const category = categories.find((cat) => cat.key === categoryKey); if (!category) return ''; const value = mergedValues[categoryKey]; if (!value || value === DEFAULTS[categoryKey]) return category.label; const option = category.options.find((opt) => opt.value === value); return option?.label || category.label; }; const isDefaultValue = (categoryKey: SearchFilterKey) => { const value = mergedValues[categoryKey]; return !value || value === DEFAULTS[categoryKey]; }; const isOptionSelected = (categoryKey: SearchFilterKey, optionValue: string) => { const value = mergedValues[categoryKey] ?? DEFAULTS[categoryKey]; return value === optionValue; }; useEffect(() => { const handleScroll = () => { // 滚动时直接关闭面板,而不是重新计算位置 if (activeCategory) { setActiveCategory(null); } }; const handleResize = () => { if (activeCategory) calculateDropdownPosition(activeCategory); }; // 监听 body 滚动事件,因为该项目的滚动容器是 document.body document.body.addEventListener('scroll', handleScroll, { passive: true }); window.addEventListener('resize', handleResize); return () => { document.body.removeEventListener('scroll', handleScroll); window.removeEventListener('resize', handleResize); }; }, [activeCategory]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( dropdownRef.current && !dropdownRef.current.contains(event.target as Node) && !Object.values(categoryRefs.current).some((ref) => ref && ref.contains(event.target as Node)) ) { setActiveCategory(null); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); return ( <>
{categories.map((category) => (
{ categoryRefs.current[category.key] = el; }} className='relative'>
))} {/* 通用年份排序切换按钮 */}
{activeCategory && createPortal(
{categories.find((cat) => cat.key === activeCategory)?.options.map((option) => ( ))}
, document.body )} ); }; export default SearchResultFilter;