SwUploadImage

Image upload component, headless: renders no UI of its own. The actual upload (network layer) is injected via the uploadFn prop, and state is exposed through the default slot (isOver / pending / open) so you fully own the visuals. Supports click and drag-and-drop selection, concurrent multi-file upload, with per-file success / error emits.

Basic usage (button)

Simplest form: a button opens the picker; the preview shows after upload completes.

Basic (button + result preview)

加载中...

Custom trigger area (#default slot)

The classic "+ upload card": the default slot fully takes over the trigger UI; the + turns into a preview once uploaded.

+ upload card (slot customization)

加载中...

Drag and drop

The slot's isOver flags drag-hover, useful for highlighting the drop zone.

Drag + click dual channel

加载中...

Multiple images

With multiple enabled you can pick or drop several images at once; uploads run concurrently and success emits per file.

Multi-file

加载中...

Error handling

When uploadFn rejects, the error event fires per file.

Upload failure feedback (@error)

加载中...

Controlled list (fileList + #file + maxCount + beforeUpload)

With v-model:file-list the component manages the uploaded list: successful results are appended automatically, and the #file slot renders each item with preview + removal; maxCount caps the count (an over-limit selection is rejected as a batch and emits exceed), and beforeUpload runs per-file validation (size / duplicate name / type — return false to drop).

<template>
  <SwUploadImage
    v-model:file-list="images"
    :upload-fn="uploadImage"
    :max-count="9"
    :before-upload="checkSize"
    multiple
    @exceed="useToast().warning('最多只能选择 9 张图片')"
  >
    <template #default="{ open, pending }">
      <SwButton :loading="pending" @click="open">选择图片</SwButton>
    </template>
    <template #file="{ file, remove }">
      <span class="thumb">
        <SwImage :src="file.value" alt="已传图片" ratio="1/1" />
        <SwButton aria-label="删除" @click="remove">✕</SwButton>
      </span>
    </template>
  </SwUploadImage>
</template>

<script setup lang="ts">
const images = ref<ImageUploadResult[]>([])
const checkSize = (file: File) =>
  file.size <= 10 * 1024 * 1024 || (useToast().warning('图片不能超过 10MB'), false)
</script>

Props

NameTypeRequiredDefaultDescription
uploadFn(file: File, thumbnail: boolean) => Promise<ImageUploadResult>✓—Actual upload function. The component drives flow; networking belongs to you
acceptstringimage/*File picker accept attribute
thumbnailbooleanfalseWhether to upload as a thumbnail; passed as the second argument to uploadFn
multiplebooleanfalseAllow multi-file selection
fileListImageUploadResult[]undefinedControlled list of uploaded files (v-model:file-list). When provided, successful results are appended automatically; pair with the #file slot for preview + removal. Omit to keep the "single-shot shell" behavior
maxCountnumberundefinedMaximum file count (including the current fileList). A selection that would exceed it is rejected as a whole batch and emits exceed
beforeUpload(file: File, batch: File[]) => boolean | Promise<boolean>undefinedPer-file veto hook: return false to drop that file (others still upload; if all are dropped the batch is aborted). Size / duplicate-name / custom checks and user feedback belong in the hook

Events

EventPayloadDescription
successImageUploadResultFired per file on upload success
errorErrorFired on upload error
update:fileListImageUploadResult[]Controlled list changed (result appended / removed via remove()), carries the new list
remove(file: ImageUploadResult, index: number)The #file slot's remove() was called
exceedFile[]Selection exceeded maxCount, carries the selected files (the whole batch was rejected)

ImageUploadResult shape:

type ImageUploadResult = { id: string; value: string; width: number; height: number }

Slots

SlotScopeDescription
default{ isOver, pending, open }Drag-hover state, uploading state, function to open the file picker
file{ file, index, remove }Rendered once per item when fileList is provided (preview / delete UI is yours to draw); remove() removes the item and emits remove

Notes

  • The component is headless: no built-in CSS, UI is entirely owned by your slot content.
  • uploadFn is externally injected — usually wrapped by the useUploader composable and bound to your real backend endpoint. The demos use mock implementations to illustrate the flow.
  • While uploading (pending=true) the component silently rejects repeated triggers — your slot UI must disable the trigger element based on pending.
  • For chunked video upload see SwUploadVideo; for generic files see SwUploadFile.