useDownloadWithProgress
用法
useDownloadWithProgress 基于原生 fetch + ReadableStream 实现,复用 @movk/nuxt 的端点、认证、业务校验、Toast、movk:api:* hook 体系。
后端返回 application/json 时走业务校验,不会把错误响应当作二进制写入文件;返回二进制时按 content-length 计算进度,缺失时 progress 为 null,UI 据此切 indeterminate。
<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('完成', name)
})
if (aborted) return
if (err) console.error(err)
}
</script>
<template>
<UButton :loading="status === 'pending'" @click="handleDownload">下载</UButton>
<UProgress
v-if="status === 'pending'"
:model-value="progress ?? undefined"
:max="100"
/>
<UButton v-if="status === 'pending'" @click="abort">取消</UButton>
</template>
$api 即可。基础下载
content-length 已知时进度按比例更新,abort() 中止流式读取:
<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>
文件名提取
优先级:
options.filename- 响应头
Content-Disposition(支持 RFC 5987 编码) - URL pathname 末段(自动去除 query string)
'download'
await download('/api/export?token=abc')
// 自动取得 export 作为 fallback 文件名,query 不会污染
不确定进度
当响应缺少 content-length(如分块传输),progress 为 null,通过 nullish-coalescing 映射到 UProgress 的 :model-value 即自动进入 indeterminate:
<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>
自定义文件名
options.filename 覆盖响应头与 URL 推断,强制指定下载文件名:
<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 导出
method: 'POST' + body 把筛选条件放进请求体,Content-Type 自动补 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>
取消下载
const { status, download, abort } = useDownloadWithProgress()
download('/api/export/large.zip')
function handleCancel() {
if (status.value === 'pending') abort()
}
取消后:
status变为'aborted',progress重置为0error保持null(不视为失败)- 不触发
onError,不弹 Toast download()返回{ data: null, error: null, aborted: true }
认证
自动从 session 读取 token 注入到 Authorization 头;可在 headers 中覆盖(用户优先)。
await download('/api/export/private.pdf', {
headers: { 'X-Custom': 'value' }
})
API
useDownloadWithProgress<T>()
useDownloadWithProgress<T = Blob>(): { progress, status, data, error, download, abort }
Returns
content-length 时为 null,UI 据此切 indeterminate。'idle' | 'pending' | 'success' | 'error' | 'aborted'。Blob;JSON 路径下为解包后的业务数据。ApiError,HTTP/网络错误为 Error。中止时为 null。Parameters
'GET'。Returns
Promise<TransferResult<T>>:{ data: T | null, error: ApiError | Error | null, aborted: boolean }。
status 切到 'aborted'。