---
title: "useDownloadWithProgress"
description: "带进度监控的文件下载 composable，支持 JSON 业务校验、POST 导出、取消、不确定进度。"
seo_title: "useDownloadWithProgress"
seo_description: "Stream-download files with real-time progress, abort support, JSON business-code parsing and shared endpoint/auth handling with the API system."
canonical_url: "https://nuxt.mhaibaraai.cn/docs/composables/use-download-with-progress"
---
# useDownloadWithProgress

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

## 用法

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

后端返回 `application/json` 时走业务校验，**不会**把错误响应当作二进制写入文件；返回二进制时按 `content-length` 计算进度，缺失时 `progress` 为 `null`，UI 据此切 indeterminate。

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

> \[\!NOTE\]
> 
> 适用于大文件导出、批量下载等场景。对于无需进度的小文件，直接使用 
> 
> $api
> 
>  即可。

## 基础下载

`content-length` 已知时进度按比例更新，`abort()` 中止流式读取：

```vue [ComposablesUseDownloadProgressBasicExample.vue]
<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'`

```ts
await download('/api/export?token=abc')
// 自动取得 export 作为 fallback 文件名，query 不会污染
```

## 不确定进度

当响应缺少 `content-length`（如分块传输），`progress` 为 `null`，通过 nullish-coalescing 映射到 `UProgress` 的 `:model-value` 即自动进入 indeterminate：

```vue [ComposablesUseDownloadProgressChunkedExample.vue]
<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 推断，强制指定下载文件名：

```vue [ComposablesUseDownloadProgressCustomExample.vue]
<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`：

```vue [ComposablesUseDownloadProgressPostExample.vue]
<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>
```

## 取消下载

```ts
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` 中覆盖（用户优先）。

```ts
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>>`): 执行下载。Parameters默认 'GET'。非 GET 时序列化为 JSON。用户优先于自动 auth 头。ReturnsPromise<TransferResult<T>>：{ data: T | null, error: ApiError | Error | null, aborted: boolean }。

**abort()** (`() => void`): 中止当前下载，status 切到 'aborted'。

## Changelog

See commit history for [src/runtime/composables/useDownloadWithProgress.ts](https://github.com/mhaibaraai/movk-nuxt/commits/main/src/runtime/composables/useDownloadWithProgress.ts).


## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
