<!-- 导出 Excel 文件 -->
|
<template>
|
<ElButton
|
:type="type"
|
:size="size"
|
:loading="isExporting"
|
:disabled="disabled || !hasData"
|
v-ripple
|
@click="handleExport"
|
>
|
<template #loading>
|
<ElIcon class="is-loading">
|
<Loading />
|
</ElIcon>
|
{{ loadingText }}
|
</template>
|
<slot>{{ buttonText }}</slot>
|
</ElButton>
|
</template>
|
|
<script setup>
|
import * as XLSX from 'xlsx'
|
import FileSaver from 'file-saver'
|
import { ref, computed, nextTick } from 'vue'
|
import { Loading } from '@element-plus/icons-vue'
|
import { useThrottleFn } from '@vueuse/core'
|
defineOptions({ name: 'ArtExcelExport' })
|
const props = defineProps({
|
filename: {
|
required: false,
|
default: () => `export_${/* @__PURE__ */ new Date().toISOString().slice(0, 10)}`
|
},
|
sheetName: { required: false, default: 'Sheet1' },
|
type: { required: false, default: 'primary' },
|
size: { required: false, default: 'default' },
|
disabled: { required: false, default: false },
|
buttonText: { required: false, default: '导出 Excel' },
|
loadingText: { required: false, default: '导出中...' },
|
autoIndex: { required: false, default: false },
|
indexColumnTitle: { required: false, default: '序号' },
|
columns: { required: false, default: () => ({}) },
|
headers: { required: false, default: () => ({}) },
|
maxRows: { required: false, default: 1e5 },
|
showSuccessMessage: { required: false, default: true },
|
showErrorMessage: { required: false, default: true },
|
workbookOptions: { required: false, default: () => ({}) }
|
})
|
const emit = defineEmits(['before-export', 'export-success', 'export-error', 'export-progress'])
|
class ExportError extends Error {
|
constructor(message, code, details) {
|
super(message)
|
this.code = code
|
this.details = details
|
this.name = 'ExportError'
|
}
|
}
|
const isExporting = ref(false)
|
const hasData = computed(() => Array.isArray(props.data) && props.data.length > 0)
|
const validateData = (data) => {
|
if (!Array.isArray(data)) {
|
throw new ExportError('数据必须是数组格式', 'INVALID_DATA_TYPE')
|
}
|
if (data.length === 0) {
|
throw new ExportError('没有可导出的数据', 'NO_DATA')
|
}
|
if (data.length > props.maxRows) {
|
throw new ExportError(`数据行数超过限制(${props.maxRows}行)`, 'EXCEED_MAX_ROWS', {
|
currentRows: data.length,
|
maxRows: props.maxRows
|
})
|
}
|
}
|
const formatCellValue = (value, key, row, index) => {
|
const column = props.columns[key]
|
if (column?.formatter) {
|
return column.formatter(value, row, index)
|
}
|
if (value === null || value === void 0) {
|
return ''
|
}
|
if (value instanceof Date) {
|
return value.toLocaleDateString('zh-CN')
|
}
|
if (typeof value === 'boolean') {
|
return value ? '是' : '否'
|
}
|
return String(value)
|
}
|
const processData = (data) => {
|
const processedData = data.map((item, index) => {
|
const processedItem = {}
|
if (props.autoIndex) {
|
processedItem[props.indexColumnTitle] = String(index + 1)
|
}
|
Object.entries(item).forEach(([key, value]) => {
|
let columnTitle = key
|
if (props.columns[key]?.title) {
|
columnTitle = props.columns[key].title
|
} else if (props.headers[key]) {
|
columnTitle = props.headers[key]
|
}
|
processedItem[columnTitle] = formatCellValue(value, key, item, index)
|
})
|
return processedItem
|
})
|
return processedData
|
}
|
const calculateColumnWidths = (data) => {
|
if (data.length === 0) return []
|
const sampleSize = Math.min(data.length, 100)
|
const columns = Object.keys(data[0])
|
return columns.map((column) => {
|
const configWidth = Object.values(props.columns).find((col) => col.title === column)?.width
|
if (configWidth) {
|
return { wch: configWidth }
|
}
|
const maxLength = Math.max(
|
column.length,
|
...data.slice(0, sampleSize).map((row) => String(row[column] || '').length)
|
)
|
const width = Math.min(Math.max(maxLength + 2, 8), 50)
|
return { wch: width }
|
})
|
}
|
const exportToExcel = async (data, filename, sheetName) => {
|
try {
|
emit('export-progress', 10)
|
const processedData = processData(data)
|
emit('export-progress', 30)
|
const workbook = XLSX.utils.book_new()
|
if (props.workbookOptions) {
|
workbook.Props = {
|
Title: filename,
|
Subject: '数据导出',
|
Author: props.workbookOptions.creator || 'Art Design Pro',
|
Manager: props.workbookOptions.lastModifiedBy || '',
|
Company: '系统导出',
|
Category: '数据',
|
Keywords: 'excel,export,data',
|
Comments: '由系统自动生成',
|
CreatedDate: props.workbookOptions.created || /* @__PURE__ */ new Date(),
|
ModifiedDate: props.workbookOptions.modified || /* @__PURE__ */ new Date()
|
}
|
}
|
emit('export-progress', 50)
|
const worksheet = XLSX.utils.json_to_sheet(processedData)
|
worksheet['!cols'] = calculateColumnWidths(processedData)
|
emit('export-progress', 70)
|
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName)
|
emit('export-progress', 85)
|
const excelBuffer = XLSX.write(workbook, {
|
bookType: 'xlsx',
|
type: 'array',
|
compression: true
|
})
|
const blob = new Blob([excelBuffer], {
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
})
|
emit('export-progress', 95)
|
const timestamp = /* @__PURE__ */ new Date().toISOString().replace(/[:.]/g, '-')
|
const finalFilename = `${filename}_${timestamp}.xlsx`
|
FileSaver.saveAs(blob, finalFilename)
|
emit('export-progress', 100)
|
await nextTick()
|
return Promise.resolve()
|
} catch (error) {
|
throw new ExportError(`Excel 导出失败: ${error.message}`, 'EXPORT_FAILED', error)
|
}
|
}
|
const handleExport = useThrottleFn(async () => {
|
if (isExporting.value) return
|
isExporting.value = true
|
try {
|
validateData(props.data)
|
emit('before-export', props.data)
|
await exportToExcel(props.data, props.filename, props.sheetName)
|
emit('export-success', props.filename, props.data.length)
|
if (props.showSuccessMessage) {
|
ElMessage.success({
|
message: `成功导出 ${props.data.length} 条数据`,
|
duration: 3e3
|
})
|
}
|
} catch (error) {
|
const exportError =
|
error instanceof ExportError
|
? error
|
: new ExportError(`导出失败: ${error.message}`, 'UNKNOWN_ERROR', error)
|
emit('export-error', exportError)
|
if (props.showErrorMessage) {
|
ElMessage.error({
|
message: exportError.message,
|
duration: 5e3
|
})
|
}
|
console.error('Excel 导出错误:', exportError)
|
} finally {
|
isExporting.value = false
|
emit('export-progress', 0)
|
}
|
}, 1e3)
|
defineExpose({
|
exportData: handleExport,
|
isExporting: readonly(isExporting),
|
hasData
|
})
|
</script>
|
|
<style scoped>
|
.is-loading {
|
animation: rotating 2s linear infinite;
|
}
|
|
@keyframes rotating {
|
0% {
|
transform: rotate(0deg);
|
}
|
|
100% {
|
transform: rotate(360deg);
|
}
|
}
|
</style>
|