Quasar::exportFile方法企业级实现与存档文档
QuasarexportFile方法企业级实现与存档文档
一、方法基础解析
1.1 核心语法
const status = exportFile(fileName, rawData, mimeType)
1.2 参数说明
参数名 |
类型 |
描述 |
fileName |
String |
下载文件名(含扩展名),例:"用户列表_20230615.xlsx" |
rawData |
Blob|String|ArrayBuffer |
文件内容数据 |
mimeType |
String (可选) |
MIME类型,默认text/plain,常用类型: |
1.3 返回值说明
- true: 下载成功触发
- false: 浏览器阻止下载(安全设置导致)
- undefined: 环境不支持(如SSR)
二、企业级全栈实现
2.1 后端实现(Django REST Framework)
# views.py
import openpyxl
from openpyxl.styles import Font, Alignment, Border, Side
from django.http import HttpResponse
from rest_framework.decorators import action
from django.utils import timezone
from urllib.parse import quote
from .models import SecurityEvent
class UserProfileViewSet(viewsets.ModelViewSet):
# ... 其他代码 ...
@action(detail=False, methods=['get'], url_path='export')
def export_users(self, request):
"""企业级用户数据导出(GDPR合规 + 安全审计)"""
# 1. 记录审计日志
SecurityEvent.objects.create(
username=request.user.get_username(),
event_type='USER_EXPORT',
details=f"用户导出报表 | 筛选条件: {request.query_params}",
ip_address=request.META.get('REMOTE_ADDR'),
user_agent=request.META.get('HTTP_USER_AGENT', '')[:255]
)
# 2. 获取筛选后的数据集
queryset = self.filter_queryset(self.get_queryset())
# 3. 创建Excel工作簿
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "用户列表"
# 4. 设置表头样式
header_font = Font(bold=True, size=12)
header_alignment = Alignment(horizontal='center', vertical='center')
thin_border = Border(
left=Side(style='thin'), right=Side(style='thin'),
top=Side(style='thin'), bottom=Side(style='thin')
)
# 5. 定义列标题
headers = [
"ID", "昵称", "角色", "部门", "状态",
"最后登录", "登录次数", "创建时间", "在职时长",
"邮箱", "手机号"
]
# 6. 写入表头
for col_num, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col_num, value=header)
cell.font = header_font
cell.alignment = header_alignment
cell.border = thin_border
# 7. 设置列宽
column_widths = [8, 15, 15, 20, 10, 20, 10, 20, 10, 25, 15]
for i, width in enumerate(column_widths, 1):
col_letter = openpyxl.utils.get_column_letter(i)
ws.column_dimensions[col_letter].width = width
# 8. 写入数据(GDPR合规处理)
for row_num, user in enumerate(queryset, 2):
# ID
ws.cell(row=row_num, column=1, value=user.id).border = thin_border
# 昵称
ws.cell(row=row_num, column=2, value=user.nickname).border = thin_border
# 角色
ws.cell(row=row_num, column=3, value=user.get_role_display()).border = thin_border
# 部门
dept_name = user.department.name if user.department else "-"
ws.cell(row=row_num, column=4, value=dept_name).border = thin_border
# 状态
ws.cell(row=row_num, column=5, value=user.get_status_display()).border = thin_border
# 最后登录
last_login = user.last_login.strftime('%Y-%m-%d %H:%M') if user.last_login else "从未登录"
ws.cell(row=row_num, column=6, value=last_login).border = thin_border
# 登录次数
ws.cell(row=row_num, column=7, value=user.login_count).border = thin_border
# 创建时间
created_at = user.created_at.strftime('%Y-%m-%d')
ws.cell(row=row_num, column=8, value=created_at).border = thin_border
# 在职时长
if hasattr(user, 'created_at'):
delta = timezone.now() - user.created_at
years = delta.days // 365
months = (delta.days % 365) // 30
ws.cell(row=row_num, column=9, value=f"{years}年{months}月").border = thin_border
# 邮箱(GDPR脱敏)
if user.email:
username, domain = user.email.split('@', 1)
masked_email = f"{username[0]}***@{domain}"
ws.cell(row=row_num, column=10, value=masked_email).border = thin_border
# 手机号(GDPR脱敏)
if user.mobile:
masked_mobile = f"{user.mobile[:3]}****{user.mobile[-4:]}"
ws.cell(row=row_num, column=11, value=masked_mobile).border = thin_border
# 9. 冻结首行
ws.freeze_panes = 'A2'
# 10. 创建HTTP响应
response = HttpResponse(
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
filename = f"用户列表_{timezone.now().strftime('%Y%m%d_%H%M')}.xlsx"
# 处理中文文件名兼容性
encoded_filename = f"filename*=UTF-8''{quote(filename)}"
response['Content-Disposition'] = f'attachment; {encoded_filename}'
# 11. 保存工作簿到响应
wb.save(response)
return response
2.2 前端实现(Quasar + TypeScript)
// Vue组件逻辑
import { exportFile, Notify } from 'quasar'
import { apiClient } from 'src/services/axios'
import { ref } from 'vue'
// 状态管理
const exportLoading = ref(false)
const filterOptions = ref({
role: null,
status: null,
department: null,
search: ''
})
const pagination = ref({ rowsNumber: 0 })
// 导出报表函数(企业级实现)
const exportReport = async () => {
try {
exportLoading.value = true
// 1. 构建查询参数
const params: Record<string, unknown> = {}
if (filterOptions.value.role !== null) params.role = filterOptions.value.role
if (filterOptions.value.status !== null) params.status = filterOptions.value.status
if (filterOptions.value.department !== null) params.department = filterOptions.value.department
if (filterOptions.value.search) params.search = filterOptions.value.search
// 2. 调用后端API
const response = await apiClient.get('/users/profile/export/', {
params,
responseType: 'blob' // 关键:指定响应类型为blob
})
// 3. 提取文件名
let filename = `用户列表_${new Date().toLocaleDateString()}.xlsx`
const contentDisposition = response.headers['content-disposition']
if (contentDisposition) {
const filenameRegex = /filename\*?=['"]?(?:UTF-\d['"]*)?([^;\r\n"']*)['"]?;?/gi
const matches = filenameRegex.exec(contentDisposition)
if (matches && matches[1]) filename = decodeURIComponent(matches[1])
else {
const simpleMatch = contentDisposition.match(/filename="?(.+?)"?(;|$)/i)
if (simpleMatch && simpleMatch[1]) filename = simpleMatch[1]
}
}
// 4. 触发下载
const status = exportFile(filename, response.data)
// 5. 处理结果
if (status === true) {
Notify.create({
type: 'positive',
message: '报表导出成功',
position: 'top-right',
timeout: 3000,
actions: [{
icon: 'download',
color: 'white',
handler: () => exportReport()
}]
})
trackUserEvent('REPORT_EXPORTED', { reportType: 'USER_LIST', filterParams: params })
}
else if (status === false) {
Notify.create({
type: 'warning',
message: '浏览器阻止了文件下载,请检查弹出窗口设置',
position: 'top-right',
timeout: 5000
})
}
} catch (error) {
// 6. 错误处理
console.error('导出失败:', error)
let errorMessage = '报表导出失败'
if (error?.response) {
switch (error.response.status) {
case 403: errorMessage = '权限不足:无法导出用户列表'; break
case 400: errorMessage = '导出参数错误'; break
case 500: errorMessage = '服务器处理导出请求时出错'; break
case 504: errorMessage = '导出操作超时,请减少数据量或联系管理员'; break
default:
try {
const errorText = await error.response.data.text()
const errorJson = JSON.parse(errorText)
errorMessage = errorJson.detail || errorJson.message || errorText
} catch {
errorMessage = `导出失败: HTTP ${error.response.status}`
}
}
} else if (error?.message) errorMessage = error.message
Notify.create({
type: 'negative',
message: errorMessage,
position: 'top-right',
timeout: 5000,
actions: [{
icon: 'refresh',
color: 'white',
handler: () => exportReport()
}]
})
logError('REPORT_EXPORT_FAILED', error)
} finally {
exportLoading.value = false
}
}
// 辅助函数:用户行为追踪
const trackUserEvent = (eventType: string, data: Record<string, unknown>) => {
// 集成埋点逻辑(如百度统计、Google Analytics等)
}
// 辅助函数:错误日志上报
const logError = (errorType: string, error: unknown) => {
// 集成错误监控(如Sentry、Datadog等)
}
2.3 前端模板(Quasar)
<template>
<!-- 导出按钮 -->
<q-btn
label="导出报表"
color="secondary"
icon="description"
class="q-mr-sm"
@click="exportReport"
:loading="exportLoading"
:disable="exportLoading || pagination.rowsNumber === 0"
>
<template v-slot:loading>
<q-spinner-hourglass class="on-left" />
正在导出...
</template>
<q-tooltip v-if="pagination.rowsNumber === 0">
没有数据可导出
</q-tooltip>
</q-btn>
</template>
三、企业级最佳实践
3.1 安全性与合规性
- GDPR合规:敏感信息脱敏(邮箱/手机号部分隐藏)
- 权限控制:后端通过@permission_classes限制访问权限
- 审计日志:记录导出操作(用户/IP/时间/筛选条件)
- 数据最小化:仅导出必要字段,避免冗余信息
3.2 性能优化
// 前端缓存实现(避免重复请求)
const exportCache = new Map()
const getCacheKey = (params) => JSON.stringify(params)
const exportReport = async () => {
const cacheKey = getCacheKey(params)
// 检查缓存
if (exportCache.has(cacheKey)) {
const { filename, data } = exportCache.get(cacheKey)
exportFile(filename, data)
return
}
// ... 正常导出流程 ...
// 缓存结果(设置10分钟过期)
exportCache.set(cacheKey, { filename, data: response.data })
setTimeout(() => exportCache.delete(cacheKey), 10 * 60 * 1000)
}
3.3 大文件处理方案
# 后端异步导出(Django + Celery)
@action(detail=False, methods=['post'])
def request_export(self, request):
# 创建导出任务
task = ExportTask.objects.create(
user=request.user,
params=request.data,
status='PENDING'
)
# 异步处理(Celery任务)
start_export_task.delay(task.id)
return Response({'task_id': task.id, 'status': 'PENDING'})
# 前端轮询任务状态
const checkExportStatus = async (taskId: string) => {
try {
const response = await apiClient.get(`/export-tasks/${taskId}/`)
if (response.data.status === 'SUCCESS') {
// 下载文件
exportFile(response.data.filename, response.data.file_blob)
} else if (response.data.status === 'FAILED') {
Notify.create({ type: 'negative', message: response.data.error_msg })
} else {
// 继续轮询(3秒一次)
setTimeout(() => checkExportStatus(taskId), 3000)
}
} catch (error) {
Notify.create({ type: 'negative', message: '查询导出状态失败' })
}
}
四、常见问题解决方案
4.1 中文文件名乱码
// 后端设置(Python)
from urllib.parse import quote
response['Content-Disposition'] = f"attachment; filename*=UTF-8''{quote(filename)}"
// 前端解码(TypeScript)
const decodeRFC5987 = (str: string) => decodeURIComponent(str.replace(/^UTF-8''/i, ''))
4.2 浏览器兼容性处理
// 兼容旧浏览器的下载函数
const downloadFile = (filename: string, data: Blob) => {
if (typeof exportFile === 'function') {
return exportFile(filename, data)
}
// 降级方案:创建a标签下载
const url = URL.createObjectURL(data)
const link = document.createElement('a')
link.href = url
link.download = filename
link.style.display = 'none'
document.body.appendChild(link)
link.click()
// 清理
setTimeout(() => {
document.body.removeChild(link)
URL.revokeObjectURL(url)
}, 100)
}
五、完整代码存档说明
本文档包含企业级文件导出功能的全栈实现,涵盖:
- QuasarexportFile方法核心用法
- Django REST Framework 后端导出接口(含Excel样式处理、GDPR脱敏)
- 前端导出逻辑(参数处理、错误分类、用户体验优化)
- 企业级最佳实践(缓存、异步处理、安全合规)
所有代码可直接集成到生产环境,建议根据实际业务需求调整筛选条件、数据字段及样式配置。