eagleye

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,常用类型:
- Excel:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
- CSV:text/csv
- PDF:application/pdf

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脱敏)
  • 前端导出逻辑(参数处理、错误分类、用户体验优化)
  • 企业级最佳实践(缓存、异步处理、安全合规)

所有代码可直接集成到生产环境,建议根据实际业务需求调整筛选条件、数据字段及样式配置。

 

posted on 2025-08-05 23:09  GoGrid  阅读(9)  评论(0)    收藏  举报

导航