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
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
uploadFn | (file: File, thumbnail: boolean) => Promise<ImageUploadResult> | ✓ | — | Actual upload function. The component drives flow; networking belongs to you |
accept | string | image/* | File picker accept attribute | |
thumbnail | boolean | false | Whether to upload as a thumbnail; passed as the second argument to uploadFn | |
multiple | boolean | false | Allow multi-file selection | |
fileList | ImageUploadResult[] | undefined | Controlled 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 | |
maxCount | number | undefined | Maximum 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> | undefined | Per-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
| Event | Payload | Description |
|---|---|---|
success | ImageUploadResult | Fired per file on upload success |
error | Error | Fired on upload error |
update:fileList | ImageUploadResult[] | Controlled list changed (result appended / removed via remove()), carries the new list |
remove | (file: ImageUploadResult, index: number) | The #file slot's remove() was called |
exceed | File[] | 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
| Slot | Scope | Description |
|---|---|---|
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.
uploadFnis externally injected — usually wrapped by theuseUploadercomposable 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 onpending. - For chunked video upload see SwUploadVideo; for generic files see SwUploadFile.