file-upload

Basic drag and drop file upload

Basic drag and drop file upload

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

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

<!-- Иконки "plus"/"file"/"x" — путь lucide, идентичны react.tsx. -->
<script setup lang="ts">
import { Button } from "@/components/ui/button"
import { formatBytes, useFileUpload, type FileMetadata } from "@/composables/use-file-upload"
import ErrorAlert from "./_shared/ErrorAlert.vue"

const maxFiles = 3
const maxSize = 2 * 1024 * 1024

const [state, { removeFile, handleDragEnter, handleDragLeave, handleDragOver, handleDrop, openFileDialog, handleFileChange }] = useFileUpload({
  maxFiles,
  maxSize,
  accept: "image/*",
  multiple: true,
})

const isImage = (file: File | FileMetadata) => file.type.startsWith("image/")
</script>

<template>
  <div class="w-full max-w-lg">
    <div
      class="border-border rounded-lg flex items-center gap-3 border border-dashed p-4 transition-colors"
      :class="state.isDragging ? 'border-primary bg-primary/5' : 'border-muted-foreground/25 hover:border-muted-foreground/50'"
      @dragenter="handleDragEnter"
      @dragleave="handleDragLeave"
      @dragover="handleDragOver"
      @drop="handleDrop"
    >
      <input type="file" accept="image/*" multiple class="sr-only" @change="handleFileChange" />

      <Button size="sm" :class="state.isDragging && 'animate-bounce'" @click="openFileDialog">
        <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="h-4 w-4"
        ><path d="M5 12h14" /><path d="M12 5v14" /></svg>
        Add files
      </Button>

      <div class="flex flex-1 items-center gap-2">
        <p v-if="state.files.length === 0" class="text-muted-foreground text-sm">
          {{ `Drop files here or click to browse (max ${maxFiles} files)` }}
        </p>
        <template v-else>
          <div v-for="fileItem in state.files" :key="fileItem.id" class="group/item relative shrink-0">
            <img
              v-if="isImage(fileItem.file) && fileItem.preview"
              :src="fileItem.preview"
              :alt="fileItem.file.name"
              class="h-12 w-12 rounded-lg border object-cover"
              :title="`${fileItem.file.name} (${formatBytes(fileItem.file.size)})`"
            />
            <div
              v-else
              class="bg-muted flex h-12 w-12 items-center justify-center rounded-lg border"
              :title="`${fileItem.file.name} (${formatBytes(fileItem.file.size)})`"
            >
              <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="text-muted-foreground h-5 w-5"
              ><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" /><path d="M14 2v4a2 2 0 0 0 2 2h4" /></svg>
            </div>

            <Button
              variant="outline"
              size="icon"
              class="absolute -end-2 -top-2 size-5 rounded-full opacity-0 shadow-md transition-opacity group-hover/item:opacity-100"
              @click="removeFile(fileItem.id)"
            >
              <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"
              ><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
            </Button>
          </div>
        </template>
      </div>

      <div v-if="state.files.length > 0" class="text-muted-foreground shrink-0 text-xs">{{ `${state.files.length}/${maxFiles}` }}</div>
    </div>

    <ErrorAlert :errors="state.errors" />
  </div>
</template>

src/file-upload/_shared/ErrorAlert.vue

<!--
  Общий блок "File upload error(s)" (`registry-reui/bases/radix/hooks/
  use-file-upload.ts` пробрасывает `errors: string[]`), повторяется во всех
  паттернах категории с одинаковой разметкой. Деталь реализации — не
  экспортируется из index.ts (тот же приём, что `calendar/_shared/
  CCalendar.vue`). В проверяемом (пустом/дефолтном) состоянии `errors`
  всегда пуст, поэтому этот компонент никогда не монтируется на диф.
-->
<script setup lang="ts">
import { Alert, AlertDescription, AlertTitle } from "@/components/reui/alert"

defineProps<{ errors: string[] }>()
</script>

<template>
  <Alert v-if="errors.length > 0" variant="destructive" class="mt-5">
    <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"
    ><circle cx="12" cy="12" r="10" /><line x1="12" x2="12" y1="8" y2="12" /><line x1="12" x2="12.01" y1="16" y2="16" /></svg>
    <AlertTitle>File upload error(s)</AlertTitle>
    <AlertDescription>
      <p v-for="(error, index) in errors" :key="index" class="last:mb-0">{{ error }}</p>
    </AlertDescription>
  </Alert>
</template>

Установка

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

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

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