useUploadWithProgress

View source
File upload composable with real-time progress monitoring, supporting business validation, timeout and abort.

Usage

useUploadWithProgress is built on the native XMLHttpRequest and reuses @movk/nuxt's endpoint, auth, business validation, Toast and movk:api:* hook system. The returned data is the business data unwrapped by dataKey, consistent with useApiFetch<T>.

<script setup lang="ts">
const selected = ref<File[] | null>(null)
const { progress, status, data, error, upload, abort } = useUploadWithProgress<{
  files: Array<{ name: string, size: number }>
}>()

async function handleUpload() {
  const { data, error: err, aborted } = await upload('/api/upload', selected.value!, {
    fieldName: 'files',
    onSuccess: d => console.log('Upload succeeded:', d.files)
  })

  if (aborted) return
  if (err) console.error(err)
  else console.log(data?.files)
}
</script>

<template>
  <UFileUpload v-model="selected" multiple />
  <UButton :disabled="!selected?.length" @click="handleUpload">Upload</UButton>
  <UProgress v-if="status === 'pending'" :model-value="progress ?? undefined" :max="100" />
  <UButton v-if="status === 'pending'" @click="abort">Cancel</UButton>
</template>
Use this for scenarios that require displaying upload progress. For small files where progress is not needed, you can use $api directly.

Basic Multi-File Upload

fieldName determines the FormData field name (defaults to 'file'); upload() returns { data, error, aborted }:

拖拽或点击选择文件
0% · 空闲
<script setup lang="ts">
const STATUS = {
  idle: { color: 'neutral', label: '空闲' },
  pending: { color: 'info', label: '上传中' },
  success: { color: 'success', label: '完成' },
  error: { color: 'error', label: '失败' },
  aborted: { color: 'warning', label: '已取消' }
} as const

const selected = ref<File[] | null>(null)
const basic = useUploadWithProgress<{ files: Array<{ name: string, size: number }> }>()
</script>

<template>
  <div class="flex flex-col gap-3">
    <UFileUpload v-model="selected" multiple label="拖拽或点击选择文件" />
    <div class="flex items-center gap-2">
      <UButton
        :loading="basic.status.value === 'pending'"
        :disabled="!selected?.length"
        icon="i-lucide-upload"
        @click="basic.upload('/upload', selected!, { fieldName: 'files' })"
      >
        开始上传
      </UButton>
      <UButton
        v-if="basic.status.value === 'pending'"
        color="error"
        variant="soft"
        icon="i-lucide-x"
        @click="basic.abort"
      >
        中止
      </UButton>
      <UBadge :color="STATUS[basic.status.value].color" variant="subtle">
        {{ basic.progress.value ?? 0 }}% · {{ STATUS[basic.status.value].label }}
      </UBadge>
    </div>
    <UProgress :model-value="basic.progress.value ?? undefined" :max="100" />
    <UAlert
      v-if="basic.error.value"
      color="error"
      variant="subtle"
      :description="basic.error.value.message"
    />
    <UCard v-if="basic.data.value">
      <pre class="text-xs overflow-auto">{{ basic.data.value }}</pre>
    </UCard>
  </div>
</template>

Single File, Multiple Files and Extra Fields

Pass a File for single-file upload or a File[] for multiple files (appended under the same fieldName); fields injects extra form fields submitted alongside the file:

await upload('/api/upload', file, { fieldName: 'file' })
await upload('/api/upload', [f1, f2, f3], { fieldName: 'files' })

await upload('/api/upload', file, {
  fieldName: 'avatar',
  fields: { userId: '123', visibility: 'private' }
})
选择单个文件
0% · 空闲
<script setup lang="ts">
const STATUS = {
  idle: { color: 'neutral', label: '空闲' },
  pending: { color: 'info', label: '上传中' },
  success: { color: 'success', label: '完成' },
  error: { color: 'error', label: '失败' },
  aborted: { color: 'warning', label: '已取消' }
} as const

const selected = ref<File | null>(null)
const single = useUploadWithProgress()
</script>

<template>
  <div class="flex flex-col gap-3">
    <UFileUpload v-model="selected" label="选择单个文件" />
    <div class="flex items-center gap-2">
      <UButton
        :loading="single.status.value === 'pending'"
        :disabled="!selected"
        icon="i-lucide-upload-cloud"
        @click="single.upload('/upload', selected!, {
          fields: { folder: 'avatars', visibility: 'private' }
        })"
      >
        上传文件(携带 fields)
      </UButton>
      <UBadge :color="STATUS[single.status.value].color" variant="subtle">
        {{ single.progress.value ?? 0 }}% · {{ STATUS[single.status.value].label }}
      </UBadge>
    </div>
    <UAlert
      v-if="single.error.value"
      color="error"
      variant="subtle"
      :description="single.error.value.message"
    />
    <UCard v-if="single.data.value">
      <pre class="text-xs overflow-auto">{{ single.data.value }}</pre>
    </UCard>
  </div>
</template>

Timeout

timeoutMs sets the upload timeout in milliseconds (0 or omitted means no timeout). A timeout triggers the failure branch and error.message contains the timeout information:

选择文件后模拟超时
空闲
<script setup lang="ts">
const STATUS = {
  idle: { color: 'neutral', label: '空闲' },
  pending: { color: 'info', label: '上传中' },
  success: { color: 'success', label: '完成' },
  error: { color: 'error', label: '失败' },
  aborted: { color: 'warning', label: '已取消' }
} as const

const selected = ref<File[] | null>(null)
const timeout = useUploadWithProgress()
</script>

<template>
  <div class="flex flex-col gap-3">
    <UFileUpload v-model="selected" multiple label="选择文件后模拟超时" />
    <div class="flex items-center gap-2">
      <UButton
        color="neutral"
        :loading="timeout.status.value === 'pending'"
        :disabled="!selected?.length"
        icon="i-lucide-timer"
        @click="timeout.upload('/upload?slow=2000', selected!, { timeoutMs: 500 })"
      >
        模拟超时
      </UButton>
      <UBadge :color="STATUS[timeout.status.value].color" variant="subtle">
        {{ STATUS[timeout.status.value].label }}
      </UBadge>
    </div>
    <UAlert
      v-if="timeout.error.value"
      color="error"
      variant="subtle"
      :description="timeout.error.value.message"
    />
  </div>
</template>

Cross-Origin Credentials

await upload('https://other.example/upload', file, { withCredentials: true })

Abort Upload

const { status, upload, abort } = useUploadWithProgress()

upload('/api/upload', largeFile)

function handleCancel() {
  if (status.value === 'pending') abort()
}

After aborting:

  • status changes to 'aborted'; progress resets to 0
  • error remains null (not treated as failure)
  • onSuccess / onError are not called; no Toast is shown
  • upload() returns { data: null, error: null, aborted: true }

Authentication

The token is automatically read from the session and injected into the Authorization header. You can override it with the same header name in headers (user value takes priority).

await upload('/api/upload', file, {
  headers: { Authorization: 'Bearer override' }
})

API

useUploadWithProgress<T>()

useUploadWithProgress<T>(): { progress, status, data, error, upload, abort }

Returns

progress
Ref<number | null>
Upload progress, 0–100. Aligned with the download-side type, null is retained as a possible value but does not occur in practice for uploads.
status
Ref<TransferStatus>
Transfer status: 'idle' | 'pending' | 'success' | 'error' | 'aborted'.
data
Ref<T | null>
Business data unwrapped by dataKey.
error
Ref<ApiError | Error | null>
Error object. Business errors are ApiError; HTTP/network errors are Error. null on abort.
upload()
(url: string, files: File | File[], options?: UploadWithProgressOptions<T>) => Promise<TransferResult<T>>
Execute the upload.
abort()
() => void
Abort the current upload; status switches to 'aborted'.

Changelog

No recent changes
Copyright © 2025 - 2026 YiXuan - MIT License