file-upload

Image upload with sorting.

Image upload with sorting.

Загрузка превью…

src/file-upload/c-file-upload-8.vue

<!--
  Оригинал реализует drag-and-drop/сортировку через свой useState/dnd,
  `Sortable` (reui) и `toast.success` при реордере — тост не смонтирован
  статично (см. кейс `sonner`), не воспроизведён. Диф снимает только
  начальное состояние (см. react.tsx). Иконки — путь lucide
  "grip-vertical"/"x"/"cloud-upload". `https://picsum.photos/...` заменён
  PIXEL.
-->
<script setup lang="ts">
import { formatBytes } from "@/composables/use-file-upload"
import { Sortable, SortableItem, SortableItemHandle } from "@/components/reui/sortable"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"

const PIXEL = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAUwAOw=="
const maxFiles = 5
const maxSize = 10 * 1024 * 1024

const allImages = [
  { id: "default-1", src: PIXEL, alt: "Product view 1" },
  { id: "default-2", src: PIXEL, alt: "Product view 2" },
  { id: "default-3", src: PIXEL, alt: "Product view 3" },
  { id: "default-4", src: PIXEL, alt: "Product view 4" },
  { id: "default-5", src: PIXEL, alt: "Product view 5" },
]
</script>

<template>
  <div class="w-full max-w-4xl">
    <div class="mb-4 text-center">
      <p class="text-muted-foreground text-sm">
        {{ `Upload up to ${maxFiles} images (JPG, PNG, GIF, WebP, max ${formatBytes(maxSize)} each). ` }}
        <br />
        Drag and drop images to reorder.
      </p>
    </div>

    <div class="mb-6">
      <Sortable
        :value="allImages.map((item) => item.id)"
        :get-item-value="(item: unknown) => item as string"
        strategy="grid"
        class="grid auto-rows-fr grid-cols-5 gap-2.5"
      >
        <SortableItem v-for="item in allImages" :key="item.id" :value="item.id">
          <div
            class="bg-accent/50 group/item border-border hover:bg-accent/70 rounded-md relative flex shrink-0 items-center justify-center border shadow-none transition-all duration-200 hover:z-10 data-[dragging=true]:z-50"
          >
            <img :src="item.src" class="rounded-md pointer-events-none h-[120px] w-full object-cover" :alt="item.alt" />

            <SortableItemHandle class="absolute start-2 top-2 cursor-grab opacity-0 group-hover/item:opacity-100 active:cursor-grabbing">
              <Button variant="outline" size="icon" class="size-6 rounded-full dark:bg-zinc-800 hover:dark:bg-zinc-700">
                <svg
                  xmlns="http://www.w3.org/2000/svg"
                  width="24"
                  height="24"
                  viewBox="0 0 24 24"
                  fill="none"
                  stroke="currentColor"
                  stroke-width="2"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                  class="size-3.5"
                ><circle cx="9" cy="12" r="1" /><circle cx="9" cy="5" r="1" /><circle cx="9" cy="19" r="1" /><circle cx="15" cy="12" r="1" /><circle cx="15" cy="5" r="1" /><circle cx="15" cy="19" r="1" /></svg>
              </Button>
            </SortableItemHandle>

            <Button
              variant="outline"
              size="icon"
              class="absolute end-2 top-2 size-6 rounded-full opacity-0 shadow-sm group-hover/item:opacity-100 dark:bg-zinc-800 hover:dark:bg-zinc-700"
            >
              <svg
                xmlns="http://www.w3.org/2000/svg"
                width="24"
                height="24"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                stroke-width="2"
                stroke-linecap="round"
                stroke-linejoin="round"
                class="size-3.5"
              ><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
            </Button>
          </div>
        </SortableItem>
      </Sortable>
    </div>

    <Card class="rounded-md border-dashed shadow-none transition-colors border-muted-foreground/25 hover:border-muted-foreground/50">
      <CardContent class="text-center">
        <div class="border-border mx-auto mb-3 flex size-[32px] items-center justify-center rounded-full border">
          <svg
            xmlns="http://www.w3.org/2000/svg"
            width="24"
            height="24"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
            class="size-4"
          ><path d="M12 13v8" /><path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242" /><path d="m8 17 4-4 4 4" /></svg>
        </div>
        <h3 class="text-2sm text-foreground mb-0.5 font-medium">Choose a file or drag &amp; drop here.</h3>
        <span class="text-secondary-foreground mb-3 block text-xs font-normal">{{ `JPEG, PNG, up to ${formatBytes(maxSize)}.` }}</span>
        <Button size="sm">Browse File</Button>
      </CardContent>
    </Card>
  </div>
</template>

Установка

npx shadcn-vue@latest add https://revueui.rootapi.dev/r/c-file-upload-8.json

Зависимости реестра

Источник: порт из ReUI (Keenthemes, MIT)