useDownloadWithProgress

View source
带进度监控的文件下载 composable,支持 JSON 业务校验、POST 导出、取消、不确定进度。

用法

useDownloadWithProgress 基于原生 fetch + ReadableStream 实现,复用 @movk/nuxt 的端点、认证、业务校验、Toast、movk:api:* hook 体系。

后端返回 application/json 时走业务校验,不会把错误响应当作二进制写入文件;返回二进制时按 content-length 计算进度,缺失时 progressnull,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() 中止流式读取:

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 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>

文件名提取

优先级:

  1. options.filename
  2. 响应头 Content-Disposition(支持 RFC 5987 编码)
  3. URL pathname 末段(自动去除 query string)
  4. 'download'
await download('/api/export?token=abc')
// 自动取得 export 作为 fallback 文件名,query 不会污染

不确定进度

当响应缺少 content-length(如分块传输),progressnull,通过 nullish-coalescing 映射到 UProgress:model-value 即自动进入 indeterminate:

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 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 重置为 0
  • error 保持 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

progress
Ref<number | null>
下载进度,0-100;缺 content-length 时为 null,UI 据此切 indeterminate。
status
Ref<TransferStatus>
传输状态:'idle' | 'pending' | 'success' | 'error' | 'aborted'
data
Ref<T | null>
二进制路径下为 Blob;JSON 路径下为解包后的业务数据。
error
Ref<ApiError | Error | null>
错误对象;业务错误为 ApiError,HTTP/网络错误为 Error。中止时为 null
download()
(url: string, options?: DownloadWithProgressOptions) => Promise<TransferResult<T>>
执行下载。
abort()
() => void
中止当前下载,status 切到 'aborted'

Changelog

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