掌控大数据:Vue.js中的数据导出艺术组件功能介绍 1.动态任务队列管理:可以动态添加任务到队列中,并按顺序处理每个任
组件功能介绍
这个 Vue 组件旨在简化和优化数据导出到 Excel 文件的过程。它特别适用于需要处理大量数据并将其导出为 Excel 文件的场景。以下是该组件的主要功能:
-
1.动态任务队列管理:可以动态添加任务到队列中,并按顺序处理每个任务。无论是单个任务还是多个任务,都能轻松管理。
-
2.进度监控:每个任务的进度可以实时监控,并通过进度条显示。用户可以直观地看到每个任务的进度。
-
3.任务取消功能:在任务执行过程中,可以随时取消正在进行的任务。用户不必担心误操作,可以随时中止任务。
-
4.自定义工作表配置:支持通过回调函数自定义每个工作表的配置,如表头、列宽等。灵活的配置让导出的 Excel 文件更符合需求。
-
5.多工作表导出:支持将数据导出到多个工作表中,每个工作表可以有不同的配置。适用于需要将数据分组导出的场景。
-
6.错误处理和重试机制:支持请求重试和错误处理,确保数据的完整性和准确性。即使在网络不稳定的情况下,也能保证数据导出成功。
组件实现初衷
在日常开发中,我们经常需要将大量数据导出为 Excel 文件。然而,处理大数据量的导出任务往往会遇到性能瓶颈和用户体验问题。这个组件的设计初衷是为了解决以下问题:
-
1.接口超时问题:当数据量较大时,导出任务可能会因为接口超时而失败。
-
2.进度监控问题:在导出过程中,需要实时显示任务的进度,而普通的http接口是无法满足的。
-
3.任务取消问题:在导出过程中,如果用户需要中止任务,就需要手动处理。
-
4.用户等待体验:在导出过程中,大量数据的导出可能会花费很长时间,用户需要等待,而没有交互效果。
针对以上问题,这个组件提供了解决方案:
-
1.提升用户体验:通过进度条和通知弹窗,用户可以实时了解导出任务的进度,避免长时间等待的不确定性。
-
2.简化开发流程:提供一个开箱即用的解决方案,开发者只需简单配置即可实现复杂的数据导出功能。
-
3.提高性能:通过批量请求和并发控制,优化数据导出的性能,确保在处理大数据量时依然流畅。
-
4.增强灵活性:支持自定义工作表配置和多工作表导出,满足不同场景下的数据导出需求。
-
5.提供错误处理和重试机制:在请求失败时,可以自动进行重试,确保数据导出成功。
-
6.任务取消功能:在任务执行过程中,可以随时取消正在进行的任务。用户不必担心误操作,可以随时中止任务。
组件的核心结构
组件主要由批量请求函数、通用数据导出excel并下载函数和实现下载队列管理组件构成:
-
1.batchRequest:方法用于批量请求数据,并支持并发控制、请求重试、进度更新等功能。该方法特别适用于需要从服务器批量获取大量数据的场景。
-
batchRequest
方法接受三个参数:-
requestFn:用于批量请求每次触发时请求的函数,
(params: { page: number; pageSize: number; [key: string]: any }) => Promise<any[]>
。 -
totalDataCount:需要导出的总数据量,用于计算批次,通常数据总量是需要调用后台接口获取,
number
。 -
options:批量请求的置对象,包含并发控制、请求重试等参数(非必填)。
- pageSize(可选):每次查询的数据条数,默认:100,
number
。 - maxConcurrentRequests(可选):并发请求数,默认:5,
number
。 - extraParams(可选):接口请求除了页码之外的请求参数,
{ [key: string]: any }
。 - retries(可选):请求重试次数,默认:1,
number
。 - delay(可选):重试的延迟时间,默认:1000,
number
。 - initialPage(可选):接口查询的起始页码,默认:0,
number
。 - onProgress(可选):进度更新回调函数,
(progress: number) => void
。 - onRequestSuccess(可选):单次请求成功回调函数,
(page: number, data: any[]) => void
。
- pageSize(可选):每次查询的数据条数,默认:100,
-
-
batchRequest
方法返回一个对象,包含以下属性:- error:错误信息,
any
。 - startBatchRequest:调用此函数开始请求,返回最终的总数据,
() => Promise<any[]>
。 - cancelBatchRequest:取消当前正在进行的请求,
() => void
。
- error:错误信息,
-
-
2.exportToExcel:方法用于将数据导出为 Excel 文件。它支持多工作表导出、自动列宽、自定义表头、合并单元格等功能。该方法特别适用于需要处理大数据集并将其导出为 Excel 文件的场景。(依赖于
xlsx
和file-saver
这两个库) -
exportToExcel
方法接受一个配置对象,包含以下参数:-
sheets:多工作表的配置,每个工作表包含数据和导出选项,
Array<{ data: any[], options: ExportToExcelOptions }>
。-
ExportToExcelOptions
接口:- sheetName(可选):工作表名称,默认:
Sheet1
,string
。 - headers(可选):自定义表头,
string[]
。 - headerKeys(可选):表头对应的字段键名,用于对齐数据,
string[]
。 - autoWidth(可选):是否自动列宽,默认:
true
,boolean
。 - columnWidths(可选):自定义列宽,
number[]
。 - merges(可选):自定义合并单元格,
MergeRange[]
。 - onProgress(可选):进度更新回调函数,
(progress: number) => void
。
- sheetName(可选):工作表名称,默认:
-
-
filename(可选):导出文件名,默认值为当前日期,
string
。 -
pageSize(可选):每次处理的数据条目数,默认:1000,
number
。 -
onProgress(可选):总进度更新回调函数,
(progress: number) => void
。
-
-
3.BatchExportExcel.vue:实现下载队列管理组件,用于动态添加任务到队列中,并按顺序处理每个任务。同时支持配置默认进度弹窗是否展示和使用v-model传递队列状态,让使用者可以自定义进度弹窗的展示和关闭。
-
BatchExportExcel.vue
组件接受以下 props:-
exportInfo:导出任务的实时信息,通过
v-model
绑定,Record<string, Omit<IExportList, 'key'>>
。 -
showNotify(可选):是否显示通知弹窗,默认:
true
,boolean
。 -
showFilename(可选):是否显示文件名,默认:
false
,boolean
。 -
handlerWorksheetConfig(可选):自定义工作表配置的回调函数,
(data: any[]) => IWorksheetConfig[]
。 -
BatchExportExcel.vue
组件提供以下事件:- finish:导出任务取消或者结束时的回调,返回对应的
key
,(key: string | number) => void
。 - update:exportInfo:用于v-model的触发事件。
- finish:导出任务取消或者结束时的回调,返回对应的
-
BatchExportExcel.vue
组件提供以下方法:- addTask:添加任务到队列中,
组件的触发方式
,(task: IExportList) => void
。-
ITask
接口:- exportToExcelOptions:Excel 导出配置,
Omit<MultiSheetExportOptions, 'onProgress'>
。 - batchRequest:批量请求函数,
(params: { page: number; pageSize: number; [key: string]: any }) => Promise<any[]>
。 - total:获取数据总条数的函数,
() => number | Promise<number>
。 - batchRequestOptions:批量请求的配置选项,
BatchRequestOptions
。 - key:任务的唯一标识,
string | number
。
- exportToExcelOptions:Excel 导出配置,
-
- addTask:添加任务到队列中,
-
-
组件源码
batchRequest.ts
export interface BatchRequestOptions {
pageSize?: number // 每次查询的数据条数
maxConcurrentRequests?: number // 最大并发请求数
extraParams?: { [key: string]: any }// 扩展参数
retries?: number // 请求重试次数
delay?: number // 重试的延迟时间
initialPage?: number // 起始页码
onProgress?: (progress: number) => void // 进度回调函数
onRequestSuccess?: (page: number, data: any[]) => void // 单次请求成功回调函数
}
interface BatchRequestResult {
error: any // 错误信息
startBatchRequest: () => Promise<any[]> // 开始批量请求
cancelBatchRequest: () => void // 取消批量请求
}
export function batchRequest(
requestFn: (params: { page: number; pageSize: number; [key: string]: any }) => Promise<any[]>,
totalDataCount: number, // 接收动态的 totalDataCount
options?: BatchRequestOptions,
): BatchRequestResult {
let data: any[] = [] // 存储请求数据
let progress = 0 // 请求进度
let error = null // 错误信息
let isCancelled = false // 标记是否取消
const retries = options.retries || 1
const delay = options.delay || 1000
const initialPage = options.initialPage || 0 // 起始页码
// 重试机制
const retryRequest = async (fn: () => Promise<any>, retries: number, delay: number) => {
for (let attempt = 0; attempt < retries; attempt++) {
try {
return await fn()
}
catch (err) {
if (attempt === retries - 1)
throw err
await new Promise(resolve => setTimeout(resolve, delay))
}
}
}
// 取消批量请求函数
const cancelBatchRequest = () => {
isCancelled = true
console.log('批量请求已取消。')
}
// 开始批量请求的函数
const startBatchRequest = async () => {
console.log('开始批量请求...', options)
const { pageSize = 100, maxConcurrentRequests = 5, extraParams = {} } = options
const totalPages = Math.ceil(totalDataCount / pageSize)
let currentPage = initialPage // 使用自定义的起始页
const allData: any[] = new Array(totalPages) // 创建一个指定大小的数组来存储每页数据
const queue: Promise<void>[] = []
let completedRequests = 0
error = null // 清除之前的错误状态
progress = 0 // 重置进度
isCancelled = false // 重置取消状态
const requestBatch = async () => {
while (currentPage < totalPages + initialPage) {
if (isCancelled)
return
const page = currentPage++
try {
const params = { page, pageSize, ...extraParams }
const pageData = await retryRequest(() => requestFn(params), retries, delay)
allData[page - initialPage] = pageData
completedRequests++
// 调用单次请求成功后的回调
options.onRequestSuccess?.(page, pageData)
// 更新总进度
progress = Math.min((completedRequests / totalPages) * 100, 100)
options.onProgress?.(progress)
}
catch (err) {
error = err
console.error(`请求第 ${page} 页数据失败:`, err)
}
}
}
// 初始化并发请求
for (let i = 0; i < maxConcurrentRequests; i++) {
queue.push(requestBatch())
}
// 等待所有请求完成
await Promise.all(queue)
// 如果没有被取消,合并数据
if (!isCancelled) {
data = allData.flat()
}
return data
}
return {
error,
startBatchRequest,
cancelBatchRequest, // 返回取消请求函数
}
}
exportToExcel.ts
import { utils, write } from 'xlsx'
import { saveAs } from 'file-saver'
// 定义表格合并单元格的类型
interface MergeRange {
s: { r: number; c: number } // 起始位置,行和列
e: { r: number; c: number } // 结束位置,行和列
}
// 定义导出选项的接口
export interface ExportToExcelOptions {
sheetName?: string // 工作表名称
headers?: string[] // 自定义表头
headerKeys?: string[] // 表头对应的字段键名,用于对齐数据
autoWidth?: boolean // 是否自动列宽
columnWidths?: number[] // 自定义列宽
merges?: MergeRange[] // 自定义合并单元格
onProgress?: (progress: number) => void // 进度更新回调函数
}
// 定义多工作表的配置
export interface MultiSheetExportOptions {
sheets: Array<{
data: any[] // 数据直接传入
options: ExportToExcelOptions // 导出选项
}>
filename?: string // 导出的文件名
pageSize?: number // 每次处理的数据条目数,默认 1000
onProgress?: (progress: number) => void // 总进度更新回调
}
/**
* 导出多个工作表数据到 Excel 文件,支持大数据集优化
* @param {MultiSheetExportOptions} multiSheetOptions - 多工作表的配置
*/
export async function exportToExcel(
multiSheetOptions: MultiSheetExportOptions,
): Promise<void> {
const { sheets, filename = new Date().toLocaleDateString().replaceAll('/', '-'), pageSize = 1000, onProgress } = multiSheetOptions
try {
// 统一的进度更新函数
const updateProgress = (percentage: number) => {
if (onProgress)
onProgress(percentage)
}
updateProgress(10) // 开始导出进度
// 创建一个新的工作簿
const workbook = utils.book_new()
// 处理每个工作表
for (let i = 0; i < sheets.length; i++) {
const { data, options } = sheets[i]
const {
sheetName = `Sheet${i + 1}`, // 默认使用 Sheet1, Sheet2...
headers = [],
headerKeys = [],
autoWidth = true,
columnWidths = [],
merges = [],
} = options
const finalData: any[] = []
// 计算总页数
const totalPages = Math.ceil(data.length / pageSize)
// 分批次处理数据,减少内存压力
for (let page = 0; page < totalPages; page++) {
const start = page * pageSize
const end = Math.min(start + pageSize, data.length) // 每次最多处理 pageSize 条数据
const dataChunk = data.slice(start, end) // 直接从数据中切片
const mappedChunk = dataChunk.map((row) => {
const reorderedRow: Record<string, any> = {}
headerKeys.forEach((key) => {
reorderedRow[key] = row[key]
})
return reorderedRow
})
finalData.push(...mappedChunk)
// 更新进度
const chunkProgress = (page + 1) / totalPages * 80 / sheets.length
updateProgress(10 + chunkProgress)
}
// 插入表头
if (headers.length > 0) {
finalData.unshift(headers.reduce((obj, header, index) => {
obj[headerKeys[index]] = header
return obj
}, {}))
}
// 转换数据为工作表
const worksheet = utils.json_to_sheet(finalData, { skipHeader: headers.length > 0 })
// 自动列宽处理
if (autoWidth) {
worksheet['!cols'] = calculateColumnWidths(finalData)
}
// 自定义列宽
if (columnWidths.length > 0) {
worksheet['!cols'] = columnWidths.map(width => ({ wpx: width }))
}
// 合并单元格
if (merges.length > 0) {
worksheet['!merges'] = merges
}
// 添加工作表到工作簿
utils.book_append_sheet(workbook, worksheet, sheetName)
// 完成该工作表的进度更新
updateProgress(90 * (i + 1) / sheets.length)
}
// 导出为二进制文件
const excelBuffer = write(workbook, { bookType: 'xlsx', type: 'array' })
updateProgress(95) // 模拟进度到95%
// 使用 file-saver 保存文件
const blob = new Blob([excelBuffer], { type: 'application/octet-stream' })
saveAs(blob, `${filename}.xlsx`)
updateProgress(100) // 完成导出
}
catch (error) {
console.error('导出 Excel 时发生错误: ', error)
if (onProgress)
onProgress(-1) // 进度更新为 -1 表示出错
}
}
/**
* 计算列宽
* @param {Array<object>} data - 表格数据
* @returns {Array<object>} 列宽配置
*/
function calculateColumnWidths(data: any[]): { wch: number }[] {
const objectMaxLength: number[] = []
data.forEach((row) => {
Object.keys(row).forEach((key, index) => {
const value = row[key] || ''
const length = value.toString().length
// 考虑不同字符的显示宽度,中文字符一般较宽,设定系数为 2
const isChinese = /[\u4E00-\u9FA5]/.test(value)
const charWidth = isChinese ? length * 2 : length
// 设置最小列宽,避免某些列太窄无法完全展示内容
const minWidth = 10
objectMaxLength[index] = Math.max(minWidth, objectMaxLength[index] || 0, charWidth)
})
})
// 调整列宽系数为 1.2,使显示效果更好
return objectMaxLength.map(width => ({ wch: Math.ceil(width * 1.2) }))
}
typing.ts
import type { BatchRequestOptions } from '@galaxy-fe/utils/index'
import type { ExportToExcelOptions, MultiSheetExportOptions } from '@galaxy-fe/utils/exportToExcel'
export interface ITask {
exportToExcelOptions: Omit<MultiSheetExportOptions, 'onProgress'>
batchRequest: (params: { page: number; pageSize: number;[key: string]: any }) => Promise<any[]>
total: () => number | Promise<number>
batchRequestOptions: BatchRequestOptions
key: string | number
}
export interface IExportList {
key: string | number
data: any[]
isLoading: boolean
progress: number
isCanceled: boolean
cancelTask: ((index: number) => void) | null
}
export interface IWorksheetConfig { data: any[]; options: ExportToExcelOptions }
export interface IProps {
exportInfo?: Record<string, Omit<IExportList, 'key'>>
showNotify?: boolean
showFilename?: boolean
handlerWorksheetConfig?: (data: any[]) => IWorksheetConfig[]
}
BatchExportExcel.vue
<script lang="ts" setup name="BatchExportExcel">
import { defineEmits, defineProps, ref, watch, withDefaults } from 'vue'
import { Close, VideoPause } from '@element-plus/icons-vue'
import { batchRequest } from '@galaxy-fe/utils'
import { exportToExcel } from '@galaxy-fe/utils/exportToExcel'
import type {
IExportList,
IProps,
ITask,
IWorksheetConfig,
} from './typing'
const props = withDefaults(defineProps<IProps>(), {
showNotify: true,
showFilename: false,
exportInfo: () => ({}),
})
const emits = defineEmits(['update:exportInfo', 'finish'])
const model = ref<Record<string, Omit<IExportList, 'key'>>>({})
watch(() => model.value, (val) => {
emits('update:exportInfo', val)
}, { deep: true })
const showNotification = ref(false)
const taskQueue = ref<ITask[]>([]) // 动态任务队列
const taskProgress = ref<Array<number>>([]) // 每个任务的进度数组
const taskCancelFns = ref<Array<() => void>>([]) // 每个任务的取消函数
const isProcessing = ref(false) // 当前是否有任务在处理
const cancelExport = ref<boolean[]>([])
// 处理任务成功逻辑
const handleSuccess = async (task: ITask, taskIndex: number, requestData: any[], worksheetConfig: null | IWorksheetConfig[]) => {
// 生成 Excel 文件,并更新表格导出进度
const { sheets, ...excelConfig } = task.exportToExcelOptions
let finalSheets = sheets
if (worksheetConfig) {
finalSheets = worksheetConfig
}
else {
finalSheets[0].data = requestData
}
await exportToExcel({
...excelConfig,
sheets: finalSheets,
onProgress: (p: number) => {
const requestProgress = taskProgress.value[taskIndex]
const exportProgress = p / 20
taskProgress.value[taskIndex] = Math.floor(requestProgress + exportProgress)
model.value[taskQueue.value[taskIndex].key].progress = taskProgress.value[taskIndex]
},
})
taskProgress.value[taskIndex] = 100
model.value[taskQueue.value[taskIndex].key].progress = 100
model.value[taskQueue.value[taskIndex].key].isLoading = false
model.value[taskQueue.value[taskIndex].key].data = requestData
emits('finish', taskQueue.value[taskIndex].key)
}
// 取消单个任务
const cancelTask = (index: number) => {
cancelExport.value[index] = true // 更新任务状态为取消
taskCancelFns.value[index]() // 调用任务的取消函数
if (taskProgress.value[index] < 1) {
model.value[taskQueue.value[index].key].isCanceled = true
model.value[taskQueue.value[index].key].isLoading = false
taskProgress.value.splice(index, 1)
taskQueue.value.splice(index, 1)
taskCancelFns.value.splice(index, 1)
cancelExport.value.splice(index, 1)
emits('finish', taskQueue.value[index].key)
}
if (!taskQueue.value.length) {
taskProgress.value = []
showNotification.value = false
}
}
// 任务调度器:负责处理队列中的任务
const processTask = async (taskIndex: number) => {
console.log('-------taskIndex', taskIndex)
try {
const task = taskQueue.value[taskIndex]
if (!task)
return
const totalDataCount = await task.total() // 获取总数据量
const { startBatchRequest, cancelBatchRequest } = batchRequest(
task.batchRequest,
totalDataCount,
{
...task.batchRequestOptions,
onProgress: (p: number) => {
taskProgress.value[taskIndex] = Math.ceil(p * 0.95)
model.value[taskQueue.value[taskIndex].key].progress = taskProgress.value[taskIndex]
},
},
)
taskCancelFns.value[taskIndex] = cancelBatchRequest // 保存任务的取消函数
model.value[taskQueue.value[taskIndex].key].cancelTask = cancelTask
// 开始接口请求,并更新接口请求进度
const requestData = await startBatchRequest()
let worksheetConfig = null
if (props.handlerWorksheetConfig && typeof props.handlerWorksheetConfig === 'function') {
worksheetConfig = props.handlerWorksheetConfig(requestData) // 调用自定义数据转换函数
}
if (taskProgress.value[taskIndex] < 100 && cancelExport.value[taskIndex]) {
handleCancellation(taskIndex)
return Promise.reject(Error('取消下载'))
}
else {
await handleSuccess(task, taskIndex, requestData, worksheetConfig) // 处理成功逻辑
}
}
catch (error) {
console.error(`任务 ${taskIndex + 1} 执行失败:`, error)
taskProgress.value.splice(taskIndex, 1) // 任务失败或被取消
}
}
// 处理队列中所有任务
const processQueue = async () => {
if (isProcessing.value) {
return // 如果已经在处理任务,直接返回
}
isProcessing.value = true
showNotification.value = true
for (let i = 0; i < taskQueue.value.length; i++) {
if (taskProgress.value[i] < 100) {
await processTask(i) // 处理队列中的每个任务
}
}
isProcessing.value = false
// 所有任务完成后自动关闭通知弹窗
nextTick(() => {
if (!taskQueue.value.length || taskProgress.value.every(t => t === 100)) {
showNotification.value = false
taskQueue.value = []
taskProgress.value = []
taskCancelFns.value = []
isProcessing.value = false
cancelExport.value = []
}
})
}
// 处理任务取消逻辑
function handleCancellation(taskIndex: number) {
model.value[taskQueue.value[taskIndex].key].isCanceled = true
model.value[taskQueue.value[taskIndex].key].isLoading = false
taskQueue.value.splice(taskIndex, 1)
taskCancelFns.value.splice(taskIndex, 1)
cancelExport.value.splice(taskIndex, 1)
isProcessing.value = false
emits('finish', taskQueue.value[taskIndex].key)
processQueue()
}
// 添加任务到队列
const addTask = (task: ITask) => {
taskQueue.value.push(task) // 将任务添加到队列
taskProgress.value.push(0) // 添加任务的进度条
cancelExport.value.push(false) // 添加任务的进度条
taskCancelFns.value.push(() => { }) // 占位取消函数
model.value[task.key] = {
data: [],
isLoading: true,
progress: 0,
isCanceled: false,
cancelTask: null,
}
processQueue() // 每次添加任务后尝试处理队列
}
defineExpose({
addTask,
})
</script>
<template>
<teleport to="body">
<transition name="notification-fade">
<div v-if="props.showNotify && showNotification" class="notification">
<div class="notification__title">
数据导出中:
</div>
<div v-for="(task, index) in taskQueue" :key="index" class="notification__content">
<div v-show="showFilename" class="filename">
{{ task.exportToExcelOptions.filename }}:
</div>
<div class="notification__progress-wrapper">
<div class="notification__progress">
<el-progress class="progress" :text-inside="true" :stroke-width="12" striped :percentage="taskProgress[index]" />
</div>
<el-button class="cancel_btn" :icon="VideoPause" type="danger" plain link @click="cancelTask(index)" />
</div>
</div>
<el-button class="notification__close-btn" :icon="Close" @click="showNotification = false" />
</div>
</transition>
</teleport>
</template>
<style lang="scss" scoped>
.notification {
position: fixed;
top: 10px;
right: 10px;
width: 300px;
padding: 16px;
background-color: #fff;
border: 1px solid #d8d5d5;
border-radius: 8px;
box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
transition: opacity 0.3s ease, transform 0.3s ease;
.notification__title {
margin-bottom: 8px;
font-weight: bold;
}
.notification__content {
width: 100%;
font-size: 14px;
.filename {
width: 100%;
height: 32px;
overflow: hidden;
line-height: 32px;
text-overflow: ellipsis;
white-space: nowrap;
}
.notification__progress-wrapper {
display: flex;
align-items: center;
width: 100%;
padding: 0;
margin-left: 12px;
}
.notification__progress {
display: flex;
flex: 1;
align-items: center;
.progress {
flex: 1;
}
}
.cancel_btn {
padding: 0;
margin-left: 12px;
}
}
.notification__close-btn {
position: absolute;
top: 8px;
right: 8px;
padding: 0;
font-size: 16px;
cursor: pointer;
background: none;
border: none;
}
}
.notification-fade-enter-active,
.notification-fade-leave-active {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.notification-fade-enter-from,
.notification-fade-leave-to {
opacity: 0;
transform: translateY(-20px);
}
</style>
组件使用示例
<script setup lang="ts">
import { Download } from '@element-plus/icons-vue'
import { GaBatchExportExcel, GaSearchForm, GaSearchItem } from '../../src/index'
import type { ITask, IWorksheetConfig } from '../../src/BatchExportExcel/src/typing'
const useCompRef = <T extends abstract new (...args: any) => any>(_comp: T) => {
return ref<InstanceType<T>>()
}
const batchExportExcelRef = useCompRef(GaBatchExportExcel)
// 组件导出数据的实时数据,通过exportTaskOptions中定义的key来归类
const exportInfo = ref({})
// 是否正在导出数据
const isExport = ref(false)
function objectToQueryParams(obj: { [key: string]: any }) {
const keys = Object.keys(obj)
const queryParams = keys.map((key) => {
const value = obj[key]
if (Array.isArray(value)) {
return value.map(v => `${encodeURIComponent(key)}[]=${encodeURIComponent(v)}`).join('&')
}
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
})
return queryParams.join('&')
}
interface GetKidsbayAdminGetUserCheckListQuery {
/** 用户昵称 example: */
nickname?: string
/** 用户手机号 example: */
phone?: string
/** 测评开始时间 example: */
start_time?: string
/** 测评结束时间 example: */
end_time?: string
/** 页码 example: 1 */
page: number
/** 每页数量 example: 10 */
page_size: number
}
/**
* 获取数据(对应我们项目中封装的useWrv或者axios请求)
*
* @param params 查询参数
* @returns 返回用户检查列表数据
*/
const getData = async (params: GetKidsbayAdminGetUserCheckListQuery) => {
const requestHeaders = new Headers()
requestHeaders.append('Content-Type', 'application/json')
requestHeaders.append('token', 'XXXX')
const requestOptions = {
method: 'GET',
headers: requestHeaders,
mode: 'cors' as RequestMode,
cache: 'no-cache' as RequestCache,
}
const paramsStr = objectToQueryParams(params)
const response = await fetch(`https://XXXXX.com/admin/get-user-check-list?${paramsStr}`, requestOptions)
const data = await response.json()
return data.data
}
const model = ref({
nickname: undefined,
phone: undefined,
time: undefined,
})
/**
* 批量请求数据单次调用的回调,这里可以对数据进行转换处理,返回转换后的数据项数组
*
* @param params 请求参数对象,包含页码、每页数量等属性
* @param params.page 页码
* @param params.pageSize 每页数量
* @param params.[key: string] 其他参数
* @returns 返回请求结果,若请求成功,返回包含转换后的数据项的数组;若请求失败,返回 Promise.reject(error)
*/
const batchRequest = async (params: { page: number; pageSize: number;[key: string]: any }) => {
try {
const { pageSize, ...arg } = params
const allParams = {
...arg,
page_size: pageSize,
}
const { list } = await getData(allParams)
const data = list.map((item: Record<string, any>) => {
item.status_name = item.status === 1 ? '已完成' : '未完成'
return item
})
return Promise.resolve(data)
}
catch (error) {
return Promise.reject(error)
}
}
/**
* 获取需要导出的数据的总条数
*/
const dataTotal = async () => {
try {
const allParams = {
nickname: model.value.nickname || '',
phone: model.value.phone || '',
start_time: model.value.time?.[0] || '',
end_time: model.value.time?.[1] || '',
page: 1,
page_size: 1,
}
const { total } = await getData(allParams)
return Promise.resolve(total)
}
catch (error) {
return Promise.resolve(0)
}
}
const handlerWorksheetConfig = ref<((data: any[]) => IWorksheetConfig[]) | undefined>(undefined)
const startExportData = (multiple = false) => {
isExport.value = true
const sheetsOptions = {
headers: ['测评时间', '测评用户', '用户手机号', '测评内容', '宝宝昵称', '宝宝月龄', '测评时长', '测评状态', '综合得分', '粗大动作', '精细动作', '认知发展', '语言交流', '社会情感'],
headerKeys: ['check_time', 'nickname', 'phone', 'name', 'baby_name', 'baby_month_age', 'estimated_duration', 'status_name', 'score', 'domain_score_2', 'domain_score_1', 'domain_score_5', 'domain_score_4', 'domain_score_3'],
autoWidth: true,
sheetName: '表1',
}
if (multiple) {
/**
* 下面是模拟多工作表的情况,模拟需要将导出的数据根据手机号分组,每一个手机号的数据放到一个工作表中,并把手机号用作工作表名称
*/
handlerWorksheetConfig.value = (list: any[]) => {
console.log('----list', list)
const grouped = list.reduce((acc, item) => {
const phone = item.phone
// 如果这个 phone 还没有分组,则创建一个
if (!acc[phone]) {
acc[phone] = {
data: [], // 存放相同 phone 的数据
options: {
...sheetsOptions,
sheetName: phone, // sheetName 为 phone
},
}
}
// 将当前项加入对应的 phone 分组中
acc[phone].data.push(item)
return acc
}, {})
console.log('----Object.values(grouped)', Object.values(grouped))
// 返回一个数组形式的结果
return Object.values(grouped)
}
}
const exportToExcelOptions = {
filename: 'kidsbay用户测评列表',
sheets: [
{
data: [],
options: sheetsOptions,
},
],
}
const exportTaskOptions: ITask = {
batchRequest,
total: dataTotal,
batchRequestOptions: {
pageSize: 500,
maxConcurrentRequests: 5,
initialPage: 1,
retries: 1,
extraParams: {
nickname: model.value.nickname || '',
phone: model.value.phone || '',
start_time: model.value.time?.[0] || '',
end_time: model.value.time?.[1] || '',
},
},
exportToExcelOptions,
key: 'user',
}
batchExportExcelRef.value?.addTask(exportTaskOptions)
}
</script>
<template>
<div class="batch-export-excel">
<GaSearchForm label-width="0" :model="model">
<template #button>
<el-button type="primary" plain :icon="Download" :loading="isExport" @click="startExportData()">
导出数据
</el-button>
<el-button type="primary" plain :icon="Download" :loading="isExport" @click="startExportData(true)">
导出多工作表数据
</el-button>
</template>
<GaSearchItem label="" prop="nickname">
<el-input v-model="model.nickname" placeholder="请输入用户昵称" maxlength="50" clearable autocomplete="off" />
</GaSearchItem>
<GaSearchItem label="" prop="phone">
<el-input v-model="model.phone" placeholder="请输入用户手机号" maxlength="50" clearable autocomplete="off" />
</GaSearchItem>
<GaSearchItem label="" prop="time">
<el-date-picker
v-model="model.time"
type="daterange"
:default-time="[
new Date(2000, 1, 1, 0, 0, 0),
new Date(2000, 2, 1, 23, 59, 59),
]"
value-format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD"
start-placeholder="测评开始时间"
end-placeholder="测评结束时间"
/>
</GaSearchItem>
</GaSearchForm>
<GaBatchExportExcel ref="batchExportExcelRef" v-model:exportInfo="exportInfo" :handler-worksheet-config="handlerWorksheetConfig" @finish="isExport = false" />
</div>
</template>
总结
这个 Vue 组件是为了优化大量数据导出到 Excel 的过程而设计的。它通过动态任务队列管理、进度监控、任务取消、自定义工作表配置、多工作表导出以及错误处理和重试机制等功能,提高了大数据量导出的性能和用户体验。组件提供了详细的源码和使用示例,方便开发者在项目中集成和使用。
使用场景
这个 Vue 组件的使用场景主要包括但不限于以下几个方面:
-
1.企业报表导出:企业中经常需要将报表数据导出为 Excel 文件,以便于后续的分析和存档。
-
2.数据备份:需要定期将数据库中的数据导出备份,以防数据丢失或损坏。
-
3.客户信息管理:在客户关系管理系统中,可能需要将客户信息导出为 Excel 文件,以便进行整理或分享。
-
4.订单处理系统:电商平台或销售系统中,订单数据的导出是常见的需求,方便进行物流管理或财务核算。
-
5.数据分析:在进行数据分析时,可能需要将数据从系统导出到 Excel,利用 Excel 的图表和分析工具进行进一步处理。
-
6.教育和学术研究:学校或研究机构可能需要导出研究数据或学生成绩,以便于进行统计分析或撰写报告。
-
7.政府和公共部门:政府机构可能需要导出公民信息、投票结果或其他公共数据,以便于进行管理和发布。
-
8.医疗健康记录:医疗机构可能需要导出病人的健康记录或检查结果,以便于进行跨机构的会诊或研究。
-
9.项目管理工具:在项目管理软件中,可能需要导出项目进度、资源分配或时间表等信息。
-
10.人力资源管理:人力资源部门可能需要导出员工信息、工资单或出勤记录等。
-
11.库存管理:库存管理系统中,库存数据的导出可以帮助进行库存盘点或供应商结算。
-
12.自定义报表生成:在需要根据用户自定义条件生成报表的场景下,这个组件可以导出用户指定格式的数据。
-
13.数据迁移:在系统升级或更换时,需要将数据从一个系统迁移到另一个系统,导出为 Excel 文件是一种常见的中间步骤。
-
14.审计和合规性检查:为了满足审计或合规性要求,可能需要导出系统数据以供审查。
这个组件由于其灵活性和强大的功能,非常适合需要处理大量数据导出场景的应用程序。
转载自:https://juejin.cn/post/7418233427420823603