eagleye

在 Quasar + TypeScript 前端应用中,要使用响应中的文件名进行文件下载,需要在前端处理 HTTP 响应时提取 `Content-Disposition` 头部中的文件名信息。以下是完整的解决方案

在 Quasar + TypeScript 前端应用中,要使用响应中的文件名进行文件下载,需要在前端处理 HTTP 响应时提取 `Content-Disposition` 头部中的文件名信息。以下是完整的解决方案:

### 步骤 1: 创建文件下载工具函数

```typescript
// src/utils/fileDownload.ts
import { api } from 'boot/axios';

/**
* 从 Content-Disposition 头部提取文件名
* @param contentDisposition Content-Disposition 头部值
* @returns 文件名或 null
*/
function extractFilename(contentDisposition: string | null): string | null {
if (!contentDisposition) return null;

const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
const matches = filenameRegex.exec(contentDisposition);

if (matches && matches[1]) {
// 移除引号
return matches[1].replace(/['"]/g, '');
}

return null;
}

/**
* 处理文件下载响应
* @param response Axios 响应对象
*/
function handleFileResponse(response: any): void {
// 1. 获取文件名
const contentDisposition = response.headers['content-disposition'];
const filename = extractFilename(contentDisposition) || 'download.xlsx';

// 2. 创建 Blob 对象
const blob = new Blob([response.data], {
type: response.headers['content-type']
});

// 3. 创建下载链接
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;

// 4. 触发下载
document.body.appendChild(link);
link.click();

// 5. 清理资源
setTimeout(() => {
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}, 100);
}

/**
* 下载文件
* @param url 下载地址
* @param params 请求参数
* @param config Axios 配置
*/
export async function downloadFile(
url: string,
params?: any,
config?: any
): Promise<void> {
try {
const response = await api.post(url, params, {
...config,
responseType: 'blob', // 关键:指定响应类型为 blob
});

handleFileResponse(response);
} catch (error) {
console.error('文件下载失败:', error);
throw error;
}
}
```

### 步骤 2: 在组合函数中使用下载工具

```typescript
// src/composables/useAuditLogExport.ts
import { ref } from 'vue';
import { downloadFile } from '@/utils/fileDownload';
import type { ExportParams } from '@/types/auditLogTypes';

export function useAuditLogExport() {
const exporting = ref(false);
const exportError = ref<string | null>(null);
const exportProgress = ref(0);

const exportLogs = async (params: ExportParams) => {
exporting.value = true;
exportError.value = null;
exportProgress.value = 0;

try {
// 使用下载工具函数
await downloadFile('/audit-logs/export/', params, {
onUploadProgress: (progressEvent: any) => {
exportProgress.value = Math.round(
(progressEvent.loaded * 100) / (progressEvent.total || 1)
);
},
onDownloadProgress: (progressEvent: any) => {
exportProgress.value = 50 + Math.round(
(progressEvent.loaded * 50) / (progressEvent.total || 1)
);
}
});
} catch (error: unknown) {
exportError.value = handleExportError(error);
} finally {
exporting.value = false;
}
};

// ... 其他代码 (错误处理等) ...

return {
exporting,
exportError,
exportProgress,
exportLogs
};
}

// 错误处理函数
function handleExportError(error: unknown): string {
// 实现错误处理逻辑...
return '导出失败';
}
```

### 步骤 3: 在 Vue 组件中使用

```vue
<template>
<q-page class="q-pa-md">
<!-- 导出按钮 -->
<q-btn
label="导出审计日志"
color="primary"
:loading="exporting"
@click="exportLogs"
/>

<!-- 进度显示 -->
<q-linear-progress
v-if="exporting"
:value="exportProgress / 100"
color="primary"
class="q-mt-md"
/>

<!-- 错误提示 -->
<q-banner
v-if="exportError"
class="bg-negative text-white q-mt-md"
>
{{ exportError }}
</q-banner>
</q-page>
</template>

<script setup lang="ts">
import { useAuditLogExport } from '@/composables/useAuditLogExport';
import type { ExportParams } from '@/types/auditLogTypes';

const {
exporting,
exportError,
exportProgress,
exportLogs
} = useAuditLogExport();

// 导出参数
const exportParams: ExportParams = {
start_date: '2023-01-01',
end_date: '2023-12-31',
event_types: ['LOGIN_SUCCESS', 'USER_LOGOUT'],
format: 'xlsx'
};

// 触发导出
const startExport = async () => {
await exportLogs(exportParams);
};
</script>
```

### 关键实现细节说明:

1. **文件名提取**:
```typescript
function extractFilename(contentDisposition: string | null): string | null {
const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
const matches = filenameRegex.exec(contentDisposition || '');
return matches?.[1]?.replace(/['"]/g, '') || null;
}
```
- 使用正则表达式解析 `Content-Disposition` 头部
- 处理带引号和不带引号的文件名
- 返回清理后的文件名

2. **Blob 处理**:
```typescript
const blob = new Blob([response.data], {
type: response.headers['content-type']
});
```
- 根据响应内容类型创建 Blob
- 确保正确处理 Excel 文件类型

3. **下载触发**:
```typescript
const link = document.createElement('a');
link.href = url;
link.download = filename; // 使用后端提供的文件名
link.click();
```
- 创建隐藏的 `<a>` 标签触发下载
- 使用 `download` 属性设置文件名

4. **资源清理**:
```typescript
setTimeout(() => {
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}, 100);
```
- 确保及时清理创建的 URL 和 DOM 元素
- 防止内存泄漏

5. **响应类型**:
```typescript
responseType: 'blob' // 关键配置
```
- 必须设置 `responseType: 'blob'` 才能正确处理文件响应

### 企业级增强功能:

1. **下载状态管理**:
```typescript
// 在下载工具中添加状态回调
export async function downloadFile(
url: string,
params?: any,
config?: any,
progressCallback?: (progress: number) => void
): Promise<void> {
// ...
onDownloadProgress: (progressEvent) => {
const progress = Math.round(
(progressEvent.loaded * 100) / (progressEvent.total || 1)
);
progressCallback?.(progress);
}
// ...
}
```

2. **取消下载支持**:
```typescript
import { CancelToken } from 'axios';

// 在下载函数中
const source = CancelToken.source();
const response = await api.post(url, params, {
cancelToken: source.token,
// ...
});

// 返回取消函数
return () => source.cancel('下载已取消');
```

3. **文件类型验证**:
```typescript
// 在 handleFileResponse 中添加
const expectedType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
if (response.headers['content-type'] !== expectedType) {
throw new Error('无效的文件类型');
}
```

4. **大文件处理**:
```typescript
// 添加文件大小验证
const contentLength = parseInt(response.headers['content-length'] || '0', 10);
if (contentLength > 100 * 1024 * 1024) { // 100MB
console.warn('下载大文件:', contentLength);
}
```

5. **下载完成通知**:
```typescript
// 在下载完成后
const event = new CustomEvent('file-downloaded', {
detail: { filename, size: blob.size }
});
window.dispatchEvent(event);
```

### 使用注意事项:

1. **CORS 配置**:
```javascript
// 后端必须暴露 Content-Disposition 头部
// Django 示例
CORS_EXPOSE_HEADERS = ['Content-Disposition', 'Content-Length']
```

2. **文件名编码**:
- 如果文件名包含中文等特殊字符,后端应进行 URL 编码:
```python
from urllib.parse import quote

filename = "审计日志.xlsx"
response['Content-Disposition'] = f'attachment; filename*=UTF-8\'\'{quote(filename)}'
```

3. **安全考虑**:
```typescript
// 在提取文件名后
const sanitizedFilename = filename.replace(/[^a-zA-Z0-9._-]/g, '_');
```
- 清理文件名防止路径遍历攻击

这个实现确保了:
1. 使用后端提供的文件名进行下载
2. 正确处理各种文件类型
3. 提供进度反馈
4. 完善的错误处理
5. 资源安全清理
6. 良好的用户体验

通过这种企业级实现,你的 Quasar + TypeScript 应用可以无缝处理从 Django 后端返回的文件下载响应,并正确使用后端设置的文件名。

posted on 2025-07-31 16:31  GoGrid  阅读(6)  评论(0)    收藏  举报

导航