'use client' import { useEffect, useMemo, useRef, useState } from 'react' import { useGSAP } from '@gsap/react' import gsap from 'gsap' import { Check, ChevronDown } from 'lucide-react' import { fieldControlClass } from '@/components/upload/upload-field-styles' gsap.registerPlugin(useGSAP) interface MaterialTypeSelectProps { id: string value: string options: string[] counts: Record onChange: (value: string) => void disabled?: boolean } export default function MaterialTypeSelect({ id, value, options, counts, onChange, disabled = false, }: MaterialTypeSelectProps) { const containerRef = useRef(null) const triggerRef = useRef(null) const menuRef = useRef(null) const [isOpen, setIsOpen] = useState(false) const [activeIndex, setActiveIndex] = useState(0) const rankedOptions = useMemo(() => { if (!options.some((option) => (counts[option] || 0) > 0)) return options return [...options].sort((left, right) => (counts[right] || 0) - (counts[left] || 0)) }, [options, counts]) const hasCounts = rankedOptions.some((option) => (counts[option] || 0) > 0) useEffect(() => { if (!isOpen) return setActiveIndex(Math.max(0, rankedOptions.indexOf(value))) menuRef.current?.focus() }, [isOpen, value, rankedOptions]) useEffect(() => { if (!isOpen) return const onPointerDown = (event: PointerEvent) => { if (!containerRef.current?.contains(event.target as Node)) setIsOpen(false) } document.addEventListener('pointerdown', onPointerDown) return () => document.removeEventListener('pointerdown', onPointerDown) }, [isOpen]) useEffect(() => { if (disabled) setIsOpen(false) }, [disabled]) useEffect(() => { if (!isOpen) return menuRef.current?.querySelector(`[data-option-index="${activeIndex}"]`)?.scrollIntoView?.({ block: 'nearest' }) }, [activeIndex, isOpen]) useGSAP(() => { if (!isOpen || !menuRef.current || typeof window.matchMedia !== 'function') return const motion = gsap.matchMedia() motion.add('(prefers-reduced-motion: no-preference)', () => { gsap.fromTo(menuRef.current, { autoAlpha: 0, y: -6 }, { autoAlpha: 1, y: 0, duration: 0.16, ease: 'power2.out', clearProps: 'opacity,visibility,transform', }) }) return () => motion.revert() }, { scope: containerRef, dependencies: [isOpen], revertOnUpdate: true }) const close = () => { setIsOpen(false) triggerRef.current?.focus() } const select = (nextValue: string) => { onChange(nextValue) close() } const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Escape' && isOpen) { event.preventDefault() close() return } if (event.key === 'Tab' && isOpen) { setIsOpen(false) return } if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault() if (!isOpen) { setIsOpen(true) } else { setActiveIndex((index) => Math.max(0, Math.min(rankedOptions.length - 1, index + (event.key === 'ArrowDown' ? 1 : -1)))) } return } if (!isOpen) return if (event.key === 'Home' || event.key === 'End') { event.preventDefault() setActiveIndex(event.key === 'Home' ? 0 : rankedOptions.length - 1) } else if ((event.key === 'Enter' || event.key === ' ') && rankedOptions[activeIndex]) { event.preventDefault() select(rankedOptions[activeIndex]) } } return (
{isOpen && (
{hasCounts &&
按素材数量排序
} {rankedOptions.map((option, index) => (
select(option)} onPointerMove={() => setActiveIndex(index)} className={`flex min-h-10 cursor-pointer items-center justify-between gap-3 rounded-[8px] px-2.5 py-2 text-[14px] tracking-[-0.02em] ${index === activeIndex ? 'bg-[#f3f4f5]' : 'hover:bg-[#f3f4f5]'} ${option === value ? 'font-medium' : ''}`} > {hasCounts && {(counts[option] || 0).toLocaleString('zh-CN')}}
))}
)}
) }