import { useEffect, useRef, useState } from "react"; import { AnimatePresence, motion } from "motion/react"; import { Check, ChevronDown, X } from "lucide-react"; import { cn } from "@/lib/utils"; export type AdaptiveSelectOption = { value: string; label: string; disabled?: boolean; }; type AdaptiveSelectBaseProps = { options: AdaptiveSelectOption[]; placeholder?: string; sheetTitle?: string; disabled?: boolean; className?: string; searchable?: boolean; searchPlaceholder?: string; }; type AdaptiveSelectSingleProps = AdaptiveSelectBaseProps & { multiple?: false; value?: string; onChange: (value: string) => void; }; type AdaptiveSelectMultipleProps = AdaptiveSelectBaseProps & { multiple: true; value?: string[]; onChange: (value: string[]) => void; }; type AdaptiveSelectProps = AdaptiveSelectSingleProps | AdaptiveSelectMultipleProps; function useIsMobileViewport() { const [isMobile, setIsMobile] = useState(() => { if (typeof window === "undefined") { return false; } return window.matchMedia("(max-width: 639px)").matches; }); useEffect(() => { if (typeof window === "undefined") { return; } const mediaQuery = window.matchMedia("(max-width: 639px)"); const handleChange = () => setIsMobile(mediaQuery.matches); handleChange(); if (typeof mediaQuery.addEventListener === "function") { mediaQuery.addEventListener("change", handleChange); return () => mediaQuery.removeEventListener("change", handleChange); } mediaQuery.addListener(handleChange); return () => mediaQuery.removeListener(handleChange); }, []); return isMobile; } export function AdaptiveSelect({ options, placeholder = "请选择", sheetTitle, disabled = false, className, searchable = false, searchPlaceholder = "请输入关键字搜索", value, multiple = false, onChange, }: AdaptiveSelectProps) { const [open, setOpen] = useState(false); const [searchKeyword, setSearchKeyword] = useState(""); const containerRef = useRef(null); const isMobile = useIsMobileViewport(); const selectedValues = multiple ? Array.isArray(value) ? value : [] : typeof value === "string" && value ? [value] : []; const selectedLabel = selectedValues.length > 0 ? selectedValues .map((selectedValue) => options.find((option) => option.value === selectedValue)?.label) .filter((label): label is string => Boolean(label)) .join("、") : placeholder; const normalizedSearchKeyword = searchKeyword.trim().toLowerCase(); const visibleOptions = searchable && normalizedSearchKeyword ? options.filter((option) => { const label = option.label.toLowerCase(); const optionValue = option.value.toLowerCase(); return label.includes(normalizedSearchKeyword) || optionValue.includes(normalizedSearchKeyword); }) : options; useEffect(() => { if (!open || isMobile) { return; } const handlePointerDown = (event: MouseEvent) => { if (!containerRef.current?.contains(event.target as Node)) { setOpen(false); } }; const handleEscape = (event: KeyboardEvent) => { if (event.key === "Escape") { setOpen(false); } }; document.addEventListener("mousedown", handlePointerDown); document.addEventListener("keydown", handleEscape); return () => { document.removeEventListener("mousedown", handlePointerDown); document.removeEventListener("keydown", handleEscape); }; }, [isMobile, open]); useEffect(() => { if (!open || !isMobile) { return; } const previousOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = previousOverflow; }; }, [isMobile, open]); useEffect(() => { if (!open && searchKeyword) { setSearchKeyword(""); } }, [open, searchKeyword]); const handleSelect = (nextValue: string) => { if (multiple) { const currentValues = Array.isArray(value) ? value : []; const nextValues = currentValues.includes(nextValue) ? currentValues.filter((item) => item !== nextValue) : [...currentValues, nextValue]; (onChange as (value: string[]) => void)(nextValues); return; } (onChange as (value: string) => void)(nextValue); setOpen(false); }; const renderOption = (option: AdaptiveSelectOption) => { const isSelected = multiple ? selectedValues.includes(option.value) : option.value === value; return ( ); }; return (
{open && !isMobile ? ( {searchable ? (
setSearchKeyword(event.target.value)} placeholder={searchPlaceholder} className="w-full rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-700 outline-none transition-colors focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-700 dark:bg-slate-800/70 dark:text-slate-100" />
) : null}
{visibleOptions.length > 0 ? visibleOptions.map(renderOption) : (
未找到匹配选项
)}
) : null}
{open && isMobile ? ( <> setOpen(false)} />

{sheetTitle || placeholder}

{multiple ? "可多选" : "请选择一个选项"}

{searchable ? ( setSearchKeyword(event.target.value)} placeholder={searchPlaceholder} className="w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-700 outline-none transition-colors focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-700 dark:bg-slate-800/70 dark:text-slate-100" /> ) : null} {visibleOptions.length > 0 ? visibleOptions.map(renderOption) : (
未找到匹配选项
)} {multiple ? ( ) : null}
) : null}
); }