ตรวจสอบให้แน่ใจว่าคุณได้ตั้งค่าโปรเจ็กต์ Next.js แล้ว ถ้าไม่ ให้สร้างขึ้นมา:
npx สร้างแอปถัดไป my-app --typescriptcd my-app
ติดตั้งส่วนประกอบ shadcn ที่จำเป็น:
npx shadcn@ล่าสุด init npx shadcn@latest เพิ่มคำสั่งตัวคั่นปุ่มป๊อปโอเวอร์
สร้าง multi-select.tsx
ในไดเร็กทอรี components
ของคุณ:
// src/components/multi-select.tsximport * เป็น React จาก "react"; นำเข้า { cva พิมพ์ VariantProps } จาก "class-variance-authority"; นำเข้า { ตรวจสอบไอคอน, เอ็กซ์เซอร์เคิล, เชฟรอนดาวน์, เอ็กซ์ไอคอน, WandSparkles,} จาก "lucide-react"; นำเข้า { cn } จาก "@/lib/utils"; นำเข้า { ตัวคั่น } จาก "@/components/ui/separator"; นำเข้า { ปุ่ม } จาก "@/components/ui/ ปุ่ม";นำเข้า { ป้าย } จาก "@/components/ui/badge";นำเข้า { ป๊อปโอเวอร์ เนื้อหาป๊อปโอเวอร์, PopoverTrigger,} จาก "@/components/ui/popover";นำเข้า { สั่งการ, คำสั่งว่าง, กลุ่มคำสั่ง, คำสั่งอินพุต, รายการคำสั่ง, รายการคำสั่ง, CommandSeparator,} จาก "@/components/ui/command";/** * ตัวแปรสำหรับองค์ประกอบแบบเลือกหลายรายการเพื่อจัดการสไตล์ที่แตกต่างกัน * ใช้ class-variance-authority (cva) เพื่อกำหนดสไตล์ที่แตกต่างกันตามเสา "variant" */const multiSelectVariants = cva( "m-1 การเปลี่ยนแปลงความง่ายในการเข้า - ออกล่าช้า -150 โฮเวอร์: -translate-y-1 โฮเวอร์: สเกล -110 ระยะเวลา -300", {ตัวแปร: { ตัวแปร: {ค่าเริ่มต้น: "border-foreground/10 text-foreground bg-card hover:bg-card/80",secondary: "border-foreground/10 bg-secondary text-secondary-foreground hover:bg- รอง/80", ทำลายล้าง: "เส้นขอบโปร่งใส bg- ทำลายข้อความ - ทำลายล้างเบื้องหน้า hover:bg-destructive/80",inverted: "inverted", },},defaultVariants: { ตัวแปร: "default",}, });/** * อุปกรณ์ประกอบฉากสำหรับส่วนประกอบ MultiSelect */อินเทอร์เฟซ MultiSelectProps ขยาย React.ButtonHTMLAttributes<HTMLButtonElement>,VariantProps<typeof multiSelectVariants> { /** * อาร์เรย์ของออบเจ็กต์ตัวเลือกที่จะแสดงในองค์ประกอบแบบเลือกหลายรายการ * แต่ละออบเจ็กต์ตัวเลือกมีป้ายกำกับ ค่า และไอคอนเสริม - ตัวเลือก: {/** ข้อความที่จะแสดงสำหรับตัวเลือก */label: string;/** ค่าเฉพาะที่เกี่ยวข้องกับตัวเลือก */value: string;/** องค์ประกอบไอคอนเสริมที่จะแสดงข้างตัวเลือก */icon?: React.ComponentType<{ className?: string }>; - /** * ฟังก์ชั่นการโทรกลับจะถูกทริกเกอร์เมื่อค่าที่เลือกเปลี่ยนแปลง * รับอาร์เรย์ของค่าที่เลือกใหม่ - onValueChange: (ค่า: string[]) => เป็นโมฆะ; /** ค่าเริ่มต้นที่เลือกเมื่อส่วนประกอบเมานต์ - ค่าดีฟอลต์?: สตริง[]; /** * ข้อความตัวยึดตำแหน่งที่จะแสดงเมื่อไม่ได้เลือกค่าใด ๆ * ไม่บังคับ ค่าเริ่มต้นคือ "เลือกตัวเลือก" - ตัวยึดตำแหน่ง?: สตริง; /** * ระยะเวลาของแอนิเมชันเป็นวินาทีสำหรับเอฟเฟกต์ภาพ (เช่น ป้ายเด้ง) * ไม่บังคับ ค่าเริ่มต้นเป็น 0 (ไม่มีภาพเคลื่อนไหว) - แอนิเมชั่น?: หมายเลข; /** * จำนวนรายการที่จะแสดงสูงสุด รายการที่เลือกเพิ่มเติมจะถูกสรุป * ไม่บังคับ ค่าเริ่มต้นคือ 3 */ maxCount?: หมายเลข; /** * กิริยาของป๊อปโอเวอร์ เมื่อตั้งค่าเป็นจริง การโต้ตอบกับองค์ประกอบภายนอก * จะถูกปิดใช้งาน และเฉพาะเนื้อหาป๊อปโอเวอร์เท่านั้นที่จะปรากฏแก่โปรแกรมอ่านหน้าจอ * ไม่บังคับ ค่าเริ่มต้นเป็นเท็จ - เป็นกิริยาช่วยPopover?: บูลีน; /** * หากเป็นจริง จะแสดงผลองค์ประกอบแบบเลือกหลายรายการเป็นรายการย่อยขององค์ประกอบอื่น * ไม่บังคับ ค่าเริ่มต้นเป็นเท็จ - asChild?: บูลีน; /** * ชื่อคลาสเพิ่มเติมเพื่อใช้สไตล์ที่กำหนดเองกับองค์ประกอบแบบเลือกหลายรายการ * ไม่จำเป็น สามารถใช้เพื่อเพิ่มสไตล์ที่กำหนดเองได้ - className?: string;} ส่งออก const MultiSelect = React.forwardRef< องค์ประกอบปุ่ม HTML, อุปกรณ์เลือกหลายรายการ>( ({ options, onValueChange, variety, defaultValue = [], placeholder = "เลือกตัวเลือก", ภาพเคลื่อนไหว = 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); } อื่น ๆ ถ้า (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 (ค่าที่เลือกใหม่); onValueChange(newSelectedValues);};const handleClear = () => { setSelectedValues([]); onValueChange([]);};const handleTogglePopover = () => { setIsPopoverOpen((prev) => !prev);};const clearExtraOptions = () => { const newSelectedValues = SelectValues.slice(0, maxCount); setSelectedValues (ค่าที่เลือกใหม่); onValueChange(newSelectedValues);};const toggleAll = () => { if (selectedValues.length === options.length) {handleClear(); } else {const allValues = options.map((ตัวเลือก) => option.value);setSelectedValues(allValues);onValueChange(allValues); }};return ( <Popoveropen={isPopoverOpen}onOpenChange={setIsPopoverOpen}modal={modalPopover} ><PopoverTrigger asChild> <Buttonref={ref}{...props}onClick={handleTogglePopover}className={cn( "ดิ้น w-full p-1 เส้นขอบโค้งมน md min-h-10 h-auto items-center จัดชิดขอบ-ระหว่าง bg-inherited 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 cursor-pointer" onClick={(event) => {event.stopPropagation();toggleOption(value); /> </Badge>); })} {selectedValues.length > maxCount && (<ป้าย className={cn("bg-transparent text-foreground) เส้นขอบเบื้องหน้า / 1 โฮเวอร์: 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 เคอร์เซอร์-พอยน์เตอร์ ข้อความปิดเสียงเบื้องหน้า "onClick={(event) => { event.stopPropagation(); handleClear();}} /> <Separatororientation="vertical"className="flex min-h-6 h-full" /> <ChevronDown className="h-4 mx-2 เคอร์เซอร์-ตัวชี้ข้อความ-ปิดเสียงเบื้องหน้า" / ></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 cursor-pointer text-muted-foreground mx-2" /> </div>)} </ปุ่ม></PopoverTrigger><PopoverContent className="w-auto p-0" align="start" onEscapeKeyDown={() => setIsPopoverOpen(false)}> <Command><CommandInput placeholder="ค้นหา..." onKeyDown={handleInputKeyDown}/><CommandList> <CommandEmpty>ไม่พบผลลัพธ์</CommandEmpty> <CommandGroup><CommandItem key="all" onSelect={toggleAll} className="cursor-pointer"> <divclassName={cn( "mr-2 flex h-4 w-4 รายการ-กึ่งกลางชิดขอบ-กึ่งกลางโค้งมน-sm เส้นขอบชายแดน-หลัก", 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 ดิ้น h-4 w-4 รายการ-กึ่งกลางชิดขอบ-กึ่งกลางขอบโค้งมน-sm เส้นขอบ-หลัก", isSelected? "bg-primary text-primary-foreground": "ความทึบ-50 [&_svg]:มองไม่เห็น")} ><CheckIcon className="h-4 w-4" /> </div> {option.icon && (<option.icon className="mr-2 h-4 w-4 ข้อความ -ปิดเสียงเบื้องหน้า" /> )} <span>{option.label}</span></CommandItem> );})} </CommandGroup> <CommandSeparator /> <CommandGroup><div className="flex items-center justify-between"> {selectedValues.length > 0 && (<> <CommandItemonSelect={handleClear}className="flex-1 justify-center cursor-pointer">ล้าง </ CommandItem> <Separatororientation="vertical"className="flex min-h-6 h-full" /></> )} <CommandItemonSelect={() => setIsPopoverOpen(false)}className="flex-1 justify-center cursor-pointer max-w-full">ปิด </CommandItem></div> </CommandGroup></CommandList> < /Command></PopoverContent>{ภาพเคลื่อนไหว > 0 && SelectedValues.length > 0 && ( <WandSparklesclassName={cn( "cursor-pointer my-2 text-foreground bg-พื้นหลัง w-3 h-3", isAnimating ? "" : "text-muted-foreground")}onClick={() => setIsAnimating(!isAnimating)} />)} </ป๊อปโอเวอร์>); });MultiSelect.displayName =</sp