ข้ามไปเนื้อหา
WhereUI

ค้นหาในเอกสาร

ค้นหาชื่อ component หรือหน้าเอกสาร

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

proptypeค่าเริ่มต้นคำอธิบาย
multiplebooleantrueเลือกได้หลายรูป (false = รูปใหม่แทนรูปเดิม)
onChange(files: File[]) => void—ส่งรายการไฟล์รูปปัจจุบันกลับทุกครั้งที่เพิ่มหรือลบ
disabledboolean—ปิดการใช้งาน

ข้อควรรู้

  • ปุ่ม "ถ่ายรูป" ใช้ 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 }