ImageUpload
WhereUIเลือกรูปจากเครื่องหรือถ่ายจากกล้องหลังของมือถือได้ในปุ่มเดียว แสดงตัวอย่างรูปเป็นตารางและลบได้ทีละรูป — ไม่มีการอัปโหลดจริง
ยังไม่ได้เลือกรูป
ติดตั้ง
Terminal
pnpm dlx shadcn@latest add @whereui/image-uploadต้องผูก registry และติดตั้ง @whereui/theme นำหน้าเสมอเมื่อติดตั้งครั้งแรก
การใช้งาน
import { ImageUpload } from "@/components/ui/image-upload"const [images, setImages] = React.useState<File[]>([])<ImageUpload onChange={setImages} />ตัวอย่างเพิ่มเติม
เลือกได้รูปเดียว
รูปโปรไฟล์พนักงาน (รูปเดียว)
ยังไม่ได้เลือกรูปโปรไฟล์
Props
| prop | type | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
| multiple | boolean | true | เลือกได้หลายรูป (false = รูปใหม่แทนรูปเดิม) |
| onChange | (files: File[]) => void | — | ส่งรายการไฟล์รูปปัจจุบันกลับทุกครั้งที่เพิ่มหรือลบ |
| disabled | boolean | — | ปิดการใช้งาน |
ข้อควรรู้
- ปุ่ม "ถ่ายรูป" ใช้ input capture="environment" บนมือถือจะเปิดกล้องหลังโดยตรง บนเดสก์ท็อปจะเปิดหน้าต่างเลือกไฟล์เหมือนกัน
- ตัวอย่างรูปใช้ URL ในเครื่อง (object URL) — คืนหน่วยความจำให้ระบบอัตโนมัติเมื่อลบรูปหรือถอด component
ซอร์สโค้ด
components/ui/image-upload.tsx
"use client"import * as React from "react"import { CameraIcon, ImageIcon, XIcon } from "lucide-react"import { cn } from "cn"import { Button } from "@/components/ui/button"function ImageUpload({ multiple = true, disabled, onChange, className, ...props}: Omit<React.ComponentProps<"div">, "onChange"> & { /** เลือกได้หลายรูป */ multiple?: boolean /** ส่งรายการไฟล์รูปปัจจุบันกลับทุกครั้งที่เพิ่ม/ลบ (ไม่อัปโหลดจริง) */ onChange?: (files: File[]) => void disabled?: boolean}) { const [items, setItems] = React.useState<{ file: File; url: string }[]>([]) const fileInputRef = React.useRef<HTMLInputElement>(null) const cameraInputRef = React.useRef<HTMLInputElement>(null) // คืน object URL ทั้งหมดเมื่อ unmount กัน memory leak React.useEffect(() => { return () => { for (const item of items) URL.revokeObjectURL(item.url) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const add = (incoming: File[]) => { if (!incoming.length) return const next = multiple ? [...items, ...incoming.map((file) => ({ file, url: URL.createObjectURL(file) }))] : incoming.map((file) => ({ file, url: URL.createObjectURL(file) })) setItems(next) onChange?.(next.map((item) => item.file)) } const removeAt = (index: number) => { URL.revokeObjectURL(items[index].url) const next = items.filter((_, i) => i !== index) setItems(next) onChange?.(next.map((item) => item.file)) } return ( <div data-slot="image-upload" className={cn("flex w-full min-w-0 flex-col gap-2", className)} {...props} > <div className="flex flex-wrap gap-2"> <Button type="button" variant="outline" size="sm" disabled={disabled} onClick={() => fileInputRef.current?.click()} > <ImageIcon data-icon="inline-start" aria-hidden="true" /> เลือกรูป </Button> <Button type="button" variant="outline" size="sm" disabled={disabled} onClick={() => cameraInputRef.current?.click()} > <CameraIcon data-icon="inline-start" aria-hidden="true" /> ถ่ายรูป </Button> </div> {/* ปุ่ม "เลือกรูป" เปิดแกลเลอรี · ปุ่ม "ถ่ายรูป" เปิดกล้องหลังบนมือถือ */} <input ref={fileInputRef} type="file" accept="image/*" multiple={multiple} disabled={disabled} className="hidden" onChange={(e) => { add(Array.from(e.target.files ?? []).filter((f) => f.type.startsWith("image/"))) e.target.value = "" }} /> <input ref={cameraInputRef} type="file" accept="image/*" capture="environment" disabled={disabled} className="hidden" onChange={(e) => { add(Array.from(e.target.files ?? []).filter((f) => f.type.startsWith("image/"))) e.target.value = "" }} /> {items.length > 0 && ( <ul className="grid grid-cols-3 gap-2 sm:grid-cols-4"> {items.map((item, index) => ( <li key={item.url} className="relative aspect-square overflow-hidden rounded-lg border border-border bg-surface" > {/* alt ใส่ชื่อไฟล์ไว้ให้ screen reader อ่าน */} <img src={item.url} alt={item.file.name} className="size-full object-cover" /> <button type="button" aria-label={`ลบรูป ${item.file.name}`} onClick={() => removeAt(index)} className="absolute top-1 right-1 flex size-6 cursor-pointer items-center justify-center rounded-full bg-background/80 text-foreground outline-none select-none hover:bg-background focus-visible:ring-3 focus-visible:ring-ring/50 [&_svg]:size-3.5 [&_svg]:pointer-events-none" > <XIcon aria-hidden="true" /> </button> </li> ))} </ul> )} </div> )}export { ImageUpload }