Убедитесь, что у вас настроен проект Next.js. Если нет, создайте его:
npx create-next-app мое-приложение --typescriptcd мое-приложение
Установите необходимые компоненты Shadc:
npx shadcn@latest init npx shadcn@latest добавить значок разделителя кнопки всплывающего окна команды
Создайте multi-select.tsx
в каталоге ваших components
:
// src/comComponents/multi-select.tsximport * as React from "react";import { cva, type VariantProps } from "class-variance-authority";import { ПроверитьЗначок, XКруг, ШевронВниз, XИконка, WandSparkles,} из "lucid-react"; импортировать { cn } из "@/lib/utils"; импортировать { Separator } из "@/comComponents/ui/separator"; импортировать { Button } из "@/comComponents/ui/ button";import { Значок } из "@/comComponents/ui/badge";import { Поповер, ПоповерКонтент, PopoverTrigger,} из "@/comComponents/ui/popover";import { Команда, КомандаПустой, Командная группа, КомандныйВвод, Командный элемент, Список команд, CommandSeparator,} from "@/comComponents/ui/command";/** * Варианты компонента с множественным выбором для обработки различных стилей. * Использует class-variance-authority (cva) для определения различных стилей на основе «варианта». */const multiSelectVariants = cva( "задержка перехода m-1 для облегчения входа-150 hover:-translate-y-1 hover:scale-110 продолжительность-300", {варианты: { вариант: {по умолчанию: "border-foreground/10 text-foreground bg-card hover:bg-card/80", Secondary: "border-foreground/10 bg-вторичный text-вторичный-передний план hover:bg- вторичный/80",destructive: "bg-destructive bg-destructive text-destructive-foreground hover:bg-destructive/80",inverted: "инвертированный", },},defaultVariants: { вариант: "default",}, });/** * Свойства для компонента MultiSelect */interface MultiSelectProps расширяет React.ButtonHTMLAttributes<HTMLButtonElement>,VariantProps<typeof multiSelectVariants> { /** * Массив объектов параметров, которые будут отображаться в компоненте с множественным выбором. * Каждый объект параметра имеет метку, значение и дополнительный значок. */ options: {/** Текст, отображаемый для параметра. */label: string;/** Уникальное значение, связанное с параметром. */value: string;/** Необязательный компонент значка для отображения рядом с параметром. */icon?: React.ComponentType<{ className?: string }>; }[]; /** * Функция обратного вызова срабатывает при изменении выбранных значений. * Получает массив новых выбранных значений. */ onValueChange: (значение: строка []) => void; /** Значения, выбранные по умолчанию при монтировании компонента. */ Значение по умолчанию?: строка[]; /** * Текст-заполнитель, который будет отображаться, если значения не выбраны. * Необязательно, по умолчанию «Выбрать параметры». */ заполнитель?: строка; /** * Продолжительность анимации визуальных эффектов (например, прыгающих значков) в секундах. * Необязательно, по умолчанию 0 (без анимации). */ анимация?: число; /** * Максимальное количество отображаемых элементов. Дополнительные выбранные элементы будут суммированы. * Необязательно, по умолчанию 3. */ maxCount?: число; /** * Модальность всплывающего окна. Если установлено значение true, взаимодействие с внешними элементами * будет отключено, и программам чтения с экрана будет виден только контент всплывающего окна. * Необязательно, по умолчанию установлено значение false. */ модальныйPopover?: логическое значение; /** * Если true, компонент с множественным выбором отображается как дочерний элемент другого компонента. * Необязательно, по умолчанию установлено значение false. */ asChild?: логическое значение; /** * Дополнительные имена классов для применения пользовательских стилей к компоненту с множественным выбором. * Необязательно, можно использовать для добавления пользовательских стилей. */ className?: string;}export const MultiSelect = React.forwardRef< HTMLButtonElement, Мультиселектпропс>( ({ options, onValueChange, вариант, defaultValue = [], заполнитель = «Выбрать параметры», анимация = 0, maxCount = 3, modalPopover = false, asChild = false, className, ... props},ref ) => {const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue);const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);const [isAnimating, setIsAnimating] = React.useState(false) ;const handleInputKeyDown = (событие: React.KeyboardEvent<HTMLInputElement>) => { if (event.key === "Enter") {setIsPopoverOpen(true); } else if (event.key === "Backspace" && !event.currentTarget.value) {const newSelectedValues = [...selectedValues];newSelectedValues.pop();setSelectedValues(newSelectedValues);onValueChange(newSelectedValues); }};const toggleOption = (опция: строка) => { const newSelectedValues = selectedValues.includes(опция)? selectedValues.filter((value) => value !== option): [...selectedValues, option]; setSelectedValues (newSelectedValues); onValueChange(newSelectedValues);};const handleClear = () => { setSelectedValues([]); onValueChange([]);};const handleTogglePopover = () => { setIsPopoverOpen((prev) => !prev);};constclearExtraOptions = () => { const newSelectedValues = selectedValues.slice(0, maxCount); setSelectedValues (newSelectedValues); onValueChange(newSelectedValues);};const toggleAll = () => { if (selectedValues.length === options.length) {handleClear(); } Еще {const allValues = options.map((option) => option.value);setSelectedValues(allValues);onValueChange(allValues); }};return ( <Popoveropen={isPopoverOpen}onOpenChange={setIsPopoverOpen}modal={modalPopover} ><PopoverTrigger asChild> <Buttonref={ref}{...props}onClick={handleTogglePopover}className={cn( "flex w-полный p-1 округлый-MD граница мин-ч-10 ч-авто items-center justify-between bg-inherit hover:bg-inherit", className)} >{selectedValues.length > 0 ? ( <div className="flex justify-between items-center w-full"><div className=" flex flex-wrap items-center"> {selectedValues.slice(0, maxCount).map((value) => {const option = options.find((o) => o.value === value);const IconComponent = option?.icon;return ( <Badgekey={value}className={cn( isAnimating ? "animate-bounce" : "", multiSelectVariants({variant }))}style={{animationDuration: `${animation}s` }} >{IconComponent && ( <IconComponent className="h-4 w-4 mr-2" />)}{option?.label}<XCircle className="ml-2 h-4 w-4 курсор-указатель" onClick={(event) => {event.stopPropagation();toggleOption(value }}/> </Badge>); })} {selectedValues.length > maxCount && (<Badge className={cn("bg-transparent) text-foreground border-foreground/1 hover:bg-transparent", isAnimating ? "animate-bounce" : "",multiSelectVariants({variant }) )} style={{animationDuration: `${animation}s` }}> {`+ ${selectedValues.length - maxCount} more`} <XCircleclassName= "ml-2 h-4 w-4 указатель курсора"onClick={(event) => { event.stopPropagation(); ClearExtraOptions();}} /></Badge> )</div><div className="flex items-center justify-between"> <XIconclassName="h-4 mx-2 курсор-указатель text-muted-foreground "onClick={(event) => { event.stopPropagation(); handleClear();}} /> <Separatororientation="vertical"className="flex min-h-6 h-full" /> <ChevronDown className="h-4 mx-2 курсор-указатель text-muted-foreground" / ></div> </div>) : ( <div className="flex items-center justify-between w-full mx-auto"><span className="text-sm" text-muted-foreground mx-3"> {placeholder</span><ChevronDown className="h-4 указатель курсора text-muted-foreground mx-2" /> </div>)} </Button>< /PopoverTrigger><PopoverContent className="w-auto p-0" align="start" onEscapeKeyDown={() => setIsPopoverOpen(false)}> <Command><CommandInput Placeholder="Search..." onKeyDown={handleInputKeyDown}/><CommandList> <CommandEmpty>Результатов не найдено.</CommandEmpty> <CommandGroup><CommandItem key="all" onSelect={toggleAll} className ="cursor-pointer"> <divclassName={cn( "mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary", selectedValues.length === options.length? "bg-primary text-primary-foreground": "opacity-50 [&_svg]:invisible") } ><CheckIcon className="h-4 w-4" /> </div> <span>(Выберите Все)</span></CommandItem>{options.map((option) => { const isSelected = selectedValues.includes(option.value); return (<CommandItem key={option.value} onSelect={() => toggleOption(option.value)} className="cursor-pointer"> <divclassName={cn( "mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary", isSelected? "bg-primary text-primary-foreground": "opacity-50 [&_svg]:invisible")} ><CheckIcon className="h-4 w-4" /> </div> {option.icon && (<option.icon className="mr-2 h-4 w-4 text) -muted-foreground" /> )} <span>{option.label</span></CommandItem> );})} </CommandGroup> <CommandSeparator /> <CommandGroup><div className="flex items-center justify-between"> {selectedValues.length > 0 && (<> <CommandImonSelect={handleClear}className="flex-1 justify-center курсор-указатель" >Очистить </CommandItem> <Separatororientation="vertical"className="flex min-h-6 h-full" /></> )} <CommandImonSelect={() => setIsPopoverOpen(false)}className="flex-1 justify-center курсор-указатель max-w-full" >Закрыть </CommandItem></div> </ CommandGroup></CommandList> </Command></PopoverContent>{animation > 0 && selectedValues.length > 0 && ( <WandSparklesclassName={cn( "курсор-указатель my-2 text-foreground bg-background w-3 h-3", isAnimating ? "" : "text-muted-foreground")}onClick={() => setIsAnimating(!isAnimating)} />)} </Popover>); });MultiSelect.displayName =</sp