| | |
| | | <!-- 导出 Excel 文件 --> |
| | | <template> |
| | | <ElButton |
| | | :type="type" |
| | |
| | | <ElIcon class="is-loading"> |
| | | <Loading /> |
| | | </ElIcon> |
| | | {{ loadingText }} |
| | | {{ resolvedLoadingText }} |
| | | </template> |
| | | <slot>{{ buttonText }}</slot> |
| | | <slot>{{ resolvedButtonText }}</slot> |
| | | </ElButton> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import * as XLSX from 'xlsx' |
| | | import FileSaver from 'file-saver' |
| | | import { ref, computed, nextTick } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { ref, computed, nextTick, readonly } from 'vue' |
| | | import { Loading } from '@element-plus/icons-vue' |
| | | import { useThrottleFn } from '@vueuse/core' |
| | | import { useI18n } from 'vue-i18n' |
| | | defineOptions({ name: 'ArtExcelExport' }) |
| | | const { t } = useI18n() |
| | | const props = defineProps({ |
| | | filename: { |
| | | required: false, |
| | |
| | | type: { required: false, default: 'primary' }, |
| | | size: { required: false, default: 'default' }, |
| | | disabled: { required: false, default: false }, |
| | | buttonText: { required: false, default: '导出 Excel' }, |
| | | loadingText: { required: false, default: '导出中...' }, |
| | | buttonText: { required: false, default: '' }, |
| | | loadingText: { required: false, default: '' }, |
| | | autoIndex: { required: false, default: false }, |
| | | indexColumnTitle: { required: false, default: '序号' }, |
| | | indexColumnTitle: { required: false, default: '' }, |
| | | columns: { required: false, default: () => ({}) }, |
| | | headers: { required: false, default: () => ({}) }, |
| | | maxRows: { required: false, default: 1e5 }, |
| | |
| | | } |
| | | const isExporting = ref(false) |
| | | const hasData = computed(() => Array.isArray(props.data) && props.data.length > 0) |
| | | const resolvedButtonText = computed(() => props.buttonText || t('common.actions.export')) |
| | | const resolvedLoadingText = computed(() => props.loadingText || t('common.actions.exporting')) |
| | | const resolvedIndexColumnTitle = computed(() => props.indexColumnTitle || t('table.index')) |
| | | const validateData = (data) => { |
| | | if (!Array.isArray(data)) { |
| | | throw new ExportError('数据必须是数组格式', 'INVALID_DATA_TYPE') |
| | | throw new ExportError(t('message.exportInvalidDataType'), 'INVALID_DATA_TYPE') |
| | | } |
| | | if (data.length === 0) { |
| | | throw new ExportError('没有可导出的数据', 'NO_DATA') |
| | | throw new ExportError(t('message.exportNoData'), 'NO_DATA') |
| | | } |
| | | if (data.length > props.maxRows) { |
| | | throw new ExportError(`数据行数超过限制(${props.maxRows}行)`, 'EXCEED_MAX_ROWS', { |
| | | throw new ExportError(t('message.exportExceedMaxRows', { maxRows: props.maxRows }), 'EXCEED_MAX_ROWS', { |
| | | currentRows: data.length, |
| | | maxRows: props.maxRows |
| | | }) |
| | |
| | | return value.toLocaleDateString('zh-CN') |
| | | } |
| | | if (typeof value === 'boolean') { |
| | | return value ? '是' : '否' |
| | | return value ? t('common.status.yes') : t('common.status.no') |
| | | } |
| | | return String(value) |
| | | } |
| | |
| | | const processedData = data.map((item, index) => { |
| | | const processedItem = {} |
| | | if (props.autoIndex) { |
| | | processedItem[props.indexColumnTitle] = String(index + 1) |
| | | processedItem[resolvedIndexColumnTitle.value] = String(index + 1) |
| | | } |
| | | Object.entries(item).forEach(([key, value]) => { |
| | | let columnTitle = key |
| | |
| | | if (props.workbookOptions) { |
| | | workbook.Props = { |
| | | Title: filename, |
| | | Subject: '数据导出', |
| | | Subject: t('message.exportWorkbookSubject'), |
| | | Author: props.workbookOptions.creator || 'Art Design Pro', |
| | | Manager: props.workbookOptions.lastModifiedBy || '', |
| | | Company: '系统导出', |
| | | Category: '数据', |
| | | Keywords: 'excel,export,data', |
| | | Comments: '由系统自动生成', |
| | | Company: t('message.exportWorkbookCompany'), |
| | | Category: t('message.exportWorkbookCategory'), |
| | | Keywords: t('message.exportWorkbookKeywords'), |
| | | Comments: t('message.exportWorkbookComments'), |
| | | CreatedDate: props.workbookOptions.created || /* @__PURE__ */ new Date(), |
| | | ModifiedDate: props.workbookOptions.modified || /* @__PURE__ */ new Date() |
| | | } |
| | |
| | | await nextTick() |
| | | return Promise.resolve() |
| | | } catch (error) { |
| | | throw new ExportError(`Excel 导出失败: ${error.message}`, 'EXPORT_FAILED', error) |
| | | throw new ExportError( |
| | | t('message.exportExcelFailed', { message: error.message }), |
| | | 'EXPORT_FAILED', |
| | | error |
| | | ) |
| | | } |
| | | } |
| | | const handleExport = useThrottleFn(async () => { |
| | |
| | | emit('export-success', props.filename, props.data.length) |
| | | if (props.showSuccessMessage) { |
| | | ElMessage.success({ |
| | | message: `成功导出 ${props.data.length} 条数据`, |
| | | message: t('message.exportSuccessWithCount', { count: props.data.length }), |
| | | duration: 3e3 |
| | | }) |
| | | } |
| | |
| | | const exportError = |
| | | error instanceof ExportError |
| | | ? error |
| | | : new ExportError(`导出失败: ${error.message}`, 'UNKNOWN_ERROR', error) |
| | | : new ExportError( |
| | | t('message.exportFailedUnknown', { message: error.message }), |
| | | 'UNKNOWN_ERROR', |
| | | error |
| | | ) |
| | | emit('export-error', exportError) |
| | | if (props.showErrorMessage) { |
| | | ElMessage.error({ |