useDownloadWithProgress
Usage
useDownloadWithProgress is built on the native fetch + ReadableStream and reuses @movk/nuxt's endpoint, auth, business validation, Toast and movk:api:* hook system.
When the backend returns application/json, business validation is applied and error responses are not written to a file as binary. When binary data is returned, progress is calculated from content-length; when content-length is missing, progress is null and the UI can switch to indeterminate accordingly.
<script setup lang="ts">
const { progress, status, error, download, abort } = useDownloadWithProgress()
async function handleDownload() {
const { aborted, error: err } = await download('/api/export/report', {
filename: 'monthly-report.pdf',
onSuccess: name => console.log('Done', name)
})
if (aborted) return
if (err) console.error(err)
}
</script>
<template>
<UButton :loading="status === 'pending'" @click="handleDownload">Download</UButton>
<UProgress
v-if="status === 'pending'"
:model-value="progress ?? undefined"
:max="100"
/>
<UButton v-if="status === 'pending'" @click="abort">Cancel</UButton>
</template>
$api directly.Basic Download
When content-length is known, progress updates proportionally; abort() stops the stream read:
<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 basic = useDownloadWithProgress()
</script>
<template>
<div class="flex flex-col gap-3">
<div class="flex items-center gap-2">
<UButton
:loading="basic.status.value === 'pending'"
icon="i-lucide-download"
@click="basic.download('/download/large')"
>
开始下载
</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"
/>
</div>
</template>
Filename Resolution
Priority order:
options.filename- Response header
Content-Disposition(supports RFC 5987 encoding) - Last segment of the URL pathname (query string is automatically stripped)
'download'
await download('/api/export?token=abc')
// Automatically uses "export" as the fallback filename; query string does not pollute the name
Indeterminate Progress
When the response is missing content-length (e.g. chunked transfer encoding), progress is null. Using nullish-coalescing to map it to UProgress's :model-value automatically activates the indeterminate state:
<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 chunked = useDownloadWithProgress()
</script>
<template>
<div class="flex flex-col gap-3">
<div class="flex items-center gap-2">
<UButton
:loading="chunked.status.value === 'pending'"
icon="i-lucide-download"
@click="chunked.download('/download/chunked')"
>
开始下载
</UButton>
<UBadge :color="STATUS[chunked.status.value].color" variant="subtle">
{{ chunked.progress.value === null ? '不确定' : `${chunked.progress.value}%` }}
· {{ STATUS[chunked.status.value].label }}
</UBadge>
</div>
<UProgress :model-value="chunked.progress.value ?? undefined" :max="100" />
</div>
</template>
Custom Filename
options.filename overrides the response header and URL inference to force a specific download filename:
<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 custom = useDownloadWithProgress()
</script>
<template>
<div class="flex flex-col gap-3">
<div class="flex items-center gap-2">
<UButton
:loading="custom.status.value === 'pending'"
icon="i-lucide-file-down"
@click="custom.download('/download/large', { filename: 'custom-name.bin' })"
>
自定义文件名下载
</UButton>
<UBadge :color="STATUS[custom.status.value].color" variant="subtle">
{{ STATUS[custom.status.value].label }}
</UBadge>
</div>
<UAlert
v-if="custom.error.value"
color="error"
variant="subtle"
:description="custom.error.value.message"
/>
</div>
</template>
POST Export
method: 'POST' + body puts filter criteria in the request body; Content-Type is automatically set to application/json:
<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 postExport = useDownloadWithProgress()
</script>
<template>
<div class="flex flex-col gap-3">
<div class="flex items-center gap-2">
<UButton
:loading="postExport.status.value === 'pending'"
icon="i-lucide-package"
@click="postExport.download('/download/large', {
method: 'POST',
body: { ids: [1, 2, 3] }
})"
>
POST 导出
</UButton>
<UBadge :color="STATUS[postExport.status.value].color" variant="subtle">
{{ STATUS[postExport.status.value].label }}
</UBadge>
</div>
<UAlert
v-if="postExport.error.value"
color="error"
variant="subtle"
:description="postExport.error.value.message"
/>
</div>
</template>
Abort Download
const { status, download, abort } = useDownloadWithProgress()
download('/api/export/large.zip')
function handleCancel() {
if (status.value === 'pending') abort()
}
After aborting:
statuschanges to'aborted';progressresets to0errorremainsnull(not treated as failure)onErroris not called; no Toast is showndownload()returns{ data: null, error: null, aborted: true }
Authentication
The token is automatically read from the session and injected into the Authorization header. Override it in headers if needed (user value takes priority).
await download('/api/export/private.pdf', {
headers: { 'X-Custom': 'value' }
})
API
useDownloadWithProgress<T>()
useDownloadWithProgress<T = Blob>(): { progress, status, data, error, download, abort }
Returns
null when content-length is missing; the UI should switch to indeterminate in that case.'idle' | 'pending' | 'success' | 'error' | 'aborted'.Blob. In the JSON path, the unwrapped business data.ApiError; HTTP/network errors are Error. null on abort.Parameters
'GET'.Returns
Promise<TransferResult<T>>: { data: T | null, error: ApiError | Error | null, aborted: boolean }.
status switches to 'aborted'.