file-upload

Basic file upload

Basic file upload

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

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

<!--
  Оригинал использует IconPlaceholder для дефолтной иконки аватара —
  смонтирована всегда (пока файл не выбран). Заменено инлайновым <svg>
  (путь lucide "circle-user-round"), идентичным react.tsx.
-->
<script setup lang="ts">
import { Button } from "@/components/ui/button"
import { useFileUpload } from "@/composables/use-file-upload"

const [state, { removeFile, openFileDialog, handleFileChange }] = useFileUpload({
  accept: "image/*",
})
</script>

<template>
  <div class="flex flex-col items-center gap-2">
    <div class="inline-flex items-center gap-2 align-top">
      <div
        class="border-input rounded-md relative flex size-9 shrink-0 items-center justify-center overflow-hidden border"
        :aria-label="state.files[0]?.preview ? 'Preview of uploaded image' : 'Default user avatar'"
      >
        <img
          v-if="state.files[0]?.preview"
          class="size-full object-cover"
          :src="state.files[0]?.preview"
          alt="Preview of uploaded image"
          width="32"
          height="32"
        />
        <svg
          v-else
          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="opacity-60"
        ><path d="M18 20a6 6 0 0 0-12 0" /><circle cx="12" cy="10" r="4" /><circle cx="12" cy="12" r="10" /></svg>
      </div>
      <div class="relative inline-block">
        <Button aria-haspopup="dialog" @click="openFileDialog">
          {{ state.files[0]?.file.name ? "Change image" : "Upload image" }}
        </Button>
        <input
          type="file"
          accept="image/*"
          class="sr-only"
          aria-label="Upload image file"
          tabindex="-1"
          @change="handleFileChange"
        />
      </div>
    </div>
    <div v-if="state.files[0]?.file.name" class="inline-flex gap-2 text-xs">
      <p class="text-muted-foreground truncate" aria-live="polite">{{ state.files[0]?.file.name }}</p>
      <button
        class="text-destructive cursor-pointer font-medium hover:underline"
        :aria-label="`Remove ${state.files[0]?.file.name}`"
        @click="removeFile(state.files[0]?.id ?? '')"
      >
        Remove
      </button>
    </div>
    <div v-else class="inline-flex gap-2 text-xs">
      <p class="text-muted-foreground truncate" aria-live="polite">No image attached</p>
    </div>
  </div>
</template>

Установка

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

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

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