fix(导出): 修复大批量 Excel 下载超时
This commit is contained in:
parent
a754472cd5
commit
373825dbfe
@ -385,7 +385,11 @@ test('工作簿应生成三张行政导出表并保留日期、金额和版式
|
||||
assert.ok(relationSheet.autoFilter)
|
||||
assert.equal(mainSheet.getCell('A1').fill.type, 'pattern')
|
||||
assert.equal(mainSheet.getCell('A1').border.top?.style, 'thin')
|
||||
assert.equal(mainSheet.getCell('H2').value, null)
|
||||
assert.equal(mainSheet.getCell('H2').border.top?.color?.argb, 'FFD1D5DB')
|
||||
assert.equal(mainSheet.getCell('S2').border.right?.style, 'thin')
|
||||
assert.strictEqual(mainSheet.getCell('A1').border, mainSheet.getCell('S2').border)
|
||||
assert.strictEqual(mainSheet.getCell('A1').border, relationSheet.getCell('J2').border)
|
||||
assert.equal(mainSheet.getCell('J2').alignment.wrapText, true)
|
||||
assert.equal(mainSheet.getCell('F2').value, null)
|
||||
assert.equal(mainSheet.getCell('L2').value, null)
|
||||
|
||||
@ -224,17 +224,26 @@ const applyHeaderStyle = (worksheet: import('exceljs').Worksheet) => {
|
||||
headerRow.height = 28
|
||||
}
|
||||
|
||||
const LIGHT_GRAY_BORDER_COLOR = Object.freeze({ argb: 'FFD1D5DB' })
|
||||
const LIGHT_THIN_BORDER_SIDE = Object.freeze({
|
||||
style: 'thin' as const,
|
||||
color: LIGHT_GRAY_BORDER_COLOR
|
||||
})
|
||||
const USED_RANGE_BORDER = Object.freeze({
|
||||
top: LIGHT_THIN_BORDER_SIDE,
|
||||
left: LIGHT_THIN_BORDER_SIDE,
|
||||
bottom: LIGHT_THIN_BORDER_SIDE,
|
||||
right: LIGHT_THIN_BORDER_SIDE
|
||||
})
|
||||
|
||||
const applyUsedRangeBorders = (worksheet: import('exceljs').Worksheet) => {
|
||||
for (let rowNumber = 1; rowNumber <= worksheet.rowCount; rowNumber += 1) {
|
||||
// ExcelJS 的 columnCount 会遍历所有行计算最大列数,必须在循环外只读取一次。
|
||||
const rowCount = worksheet.rowCount
|
||||
const columnCount = worksheet.columnCount
|
||||
for (let rowNumber = 1; rowNumber <= rowCount; rowNumber += 1) {
|
||||
const row = worksheet.getRow(rowNumber)
|
||||
for (let columnNumber = 1; columnNumber <= worksheet.columnCount; columnNumber += 1) {
|
||||
const cell = row.getCell(columnNumber)
|
||||
cell.border = {
|
||||
top: { style: 'thin', color: { argb: 'FFD1D5DB' } },
|
||||
left: { style: 'thin', color: { argb: 'FFD1D5DB' } },
|
||||
bottom: { style: 'thin', color: { argb: 'FFD1D5DB' } },
|
||||
right: { style: 'thin', color: { argb: 'FFD1D5DB' } }
|
||||
}
|
||||
for (let columnNumber = 1; columnNumber <= columnCount; columnNumber += 1) {
|
||||
row.getCell(columnNumber).border = USED_RANGE_BORDER
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,7 +18,8 @@ export const exportEquipment = (params?: {
|
||||
mount_status: params?.mountStatus,
|
||||
keyword: params?.keyword
|
||||
},
|
||||
responseType: 'blob'
|
||||
responseType: 'blob',
|
||||
timeout: 60000
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -84,8 +84,13 @@
|
||||
v-if="showMoreActions"
|
||||
class="absolute right-0 top-full z-20 mt-2 w-44 rounded-xl border border-border bg-bg-secondary p-2 shadow-[0_18px_44px_rgba(0,0,0,0.36)]"
|
||||
>
|
||||
<button class="w-full rounded-lg px-3 py-2 text-left text-sm text-text-secondary transition hover:bg-bg-hover hover:text-text-primary" @click="handleExportAndClose">
|
||||
导出 Excel
|
||||
<button
|
||||
type="button"
|
||||
class="w-full rounded-lg px-3 py-2 text-left text-sm text-text-secondary transition hover:bg-bg-hover hover:text-text-primary disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:disabled="isExporting"
|
||||
@click="handleExportAndClose"
|
||||
>
|
||||
{{ isExporting ? '正在导出...' : '导出 Excel' }}
|
||||
</button>
|
||||
<button class="w-full rounded-lg px-3 py-2 text-left text-sm text-text-secondary transition hover:bg-bg-hover hover:text-text-primary" @click="downloadTemplateAndClose">
|
||||
下载模板
|
||||
@ -127,6 +132,31 @@
|
||||
<button @click="handleReset" class="btn-secondary sm:col-span-2 lg:col-span-1">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="exportFeedback"
|
||||
class="panel mt-4 flex items-center justify-between gap-3"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<span class="badge" :class="getExportFeedbackBadgeClass(exportFeedback.type)">
|
||||
{{ exportFeedback.type === 'loading' ? '生成中' : exportFeedback.type === 'success' ? '已完成' : '失败' }}
|
||||
</span>
|
||||
<p class="text-sm" :class="getExportFeedbackTextClass(exportFeedback.type)">
|
||||
{{ exportFeedback.message }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
v-if="exportFeedback.type !== 'loading'"
|
||||
type="button"
|
||||
class="btn-icon h-8 w-8 shrink-0"
|
||||
aria-label="关闭导出提示"
|
||||
@click="exportFeedback = null"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 使用操作区 - 页面级核心工作区 -->
|
||||
@ -862,6 +892,11 @@ const showBatchReturnModal = ref(false)
|
||||
const showBatchTransferModal = ref(false)
|
||||
const showMoreActions = ref(false)
|
||||
const showAdvancedFilters = ref(false)
|
||||
const isExporting = ref(false)
|
||||
const exportFeedback = ref<{
|
||||
type: 'loading' | 'success' | 'error'
|
||||
message: string
|
||||
} | null>(null)
|
||||
const showBorrowAnomalyHandler = ref(false)
|
||||
const borrowAnomalyTarget = ref<{
|
||||
equipmentId: number
|
||||
@ -1429,9 +1464,51 @@ const formatExportTimestamp = (date = new Date()) => {
|
||||
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}_${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`
|
||||
}
|
||||
|
||||
const getExportFeedbackBadgeClass = (type: 'loading' | 'success' | 'error') => {
|
||||
if (type === 'loading') return 'badge-warning'
|
||||
if (type === 'success') return 'badge-success'
|
||||
return 'badge-error'
|
||||
}
|
||||
|
||||
const getExportFeedbackTextClass = (type: 'loading' | 'success' | 'error') => {
|
||||
if (type === 'success') return 'text-status-success'
|
||||
if (type === 'error') return 'text-status-error'
|
||||
return 'text-text-secondary'
|
||||
}
|
||||
|
||||
const isExportTimeoutError = (error: any) => {
|
||||
const code = error?.originalError?.code || error?.code
|
||||
const message = String(error?.message || error?.originalError?.message || '')
|
||||
return code === 'ECONNABORTED' || code === 'ETIMEDOUT' || /timeout|超时/i.test(message)
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
const res = await exportEquipment(filters.value)
|
||||
downloadFile(res.data as Blob, `资产列表_${formatExportTimestamp()}.xlsx`)
|
||||
if (isExporting.value) return
|
||||
|
||||
isExporting.value = true
|
||||
exportFeedback.value = {
|
||||
type: 'loading',
|
||||
message: '正在生成资产列表,数据较多时可能需要一些时间。'
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await exportEquipment(filters.value)
|
||||
const filename = `行政资产台账_${formatExportTimestamp()}.xlsx`
|
||||
downloadFile(res.data as Blob, filename)
|
||||
exportFeedback.value = {
|
||||
type: 'success',
|
||||
message: `导出成功,已下载 ${filename}`
|
||||
}
|
||||
} catch (error: any) {
|
||||
exportFeedback.value = {
|
||||
type: 'error',
|
||||
message: isExportTimeoutError(error)
|
||||
? '导出超时,资产较多时生成会比较慢,请稍后重试。'
|
||||
: '导出失败,请稍后重试;如持续失败,可缩小筛选范围。'
|
||||
}
|
||||
} finally {
|
||||
isExporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleExportAndClose = async () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user