✨ 将应收账单预览导出改为异步处理

This commit is contained in:
caojia
2026-11-11 17:55:57 +08:00
parent b5299e4490
commit 27715b3e13
7 changed files with 334 additions and 27 deletions
@@ -1,5 +1,20 @@
<template>
<div class="generate-bill-dialogs">
<Teleport to="body">
<div v-if="previewExportFloatVisible" class="preview-export-status-float">
<el-tooltip :content="previewExportMessage || '点击查看详情'" placement="bottom">
<el-button class="preview-export-status-button" type="warning" plain size="small" @click="previewExportProgressVisible = true">
<span class="preview-export-progress-ring" :style="{ '--export-progress': previewExportDegree + 'deg' }" aria-hidden="true">
<span class="preview-export-progress-value">{{ previewExportPercent }}%</span>
</span>
<span class="preview-export-status-text">
<strong>{{ previewExportFloatTitle }}</strong>
<small>{{ previewExportFloatSummary }}</small>
</span>
</el-button>
</el-tooltip>
</div>
</Teleport>
<el-dialog v-model="dialogOpen" class="generate-dialog" title="生成应收账单" width="1200px" append-to-body destroy-on-close>
<el-form ref="generateFormRef" :model="generateForm" :rules="generateRules" label-width="110px" class="generate-form">
<el-row :gutter="16">
@@ -28,7 +43,7 @@
<div class="preview-toolbar">
<span v-if="previewSearched" class="preview-summary">可选 {{ previewTotal }} 条订单,已选 <b>{{ displaySelectedCount }}</b> 条,应收合计 <b>{{ money(displaySelectedAmount) }}</b></span>
<span v-else class="preview-summary">请选择客户、币种和账单截止日后查询可生成的计费订单</span>
<el-button v-if="previewSearched && previewTotal > 0" :icon="Download" :loading="previewExportLoading" :disabled="previewLoading" @click="exportPreview">导出查询结果</el-button>
<el-button v-if="previewSearched && previewTotal > 0" type="warning" plain :icon="Download" :loading="previewExportLoading" :disabled="previewLoading || previewExportLoading" @click="exportPreview">导出查询结果</el-button>
<el-button type="primary" plain :loading="previewLoading" :disabled="!bankAccount" @click="queryGeneratePreview">查询</el-button>
</div>
<div v-if="previewSearched" class="preview-selection-toolbar">
@@ -95,6 +110,17 @@
</template>
</el-dialog>
<el-dialog v-model="previewExportProgressVisible" :title="previewExportDialogTitle" width="480px" append-to-body :close-on-click-modal="false">
<el-progress :percentage="previewExportPercent" :status="previewExportStatus" :stroke-width="14" striped :striped-flow="previewExportLoading" />
<div class="preview-export-progress__msg">{{ previewExportMessage }}</div>
<div class="preview-export-progress__text">导出行数 {{ previewExportRowCount }}</div>
<p v-if="previewExportLoading" class="preview-export-progress__hint">导出在后台进行,可收起弹窗继续操作;请勿关闭或刷新本页</p>
<template #footer>
<el-button v-if="previewExportLoading" @click="previewExportProgressVisible = false">收起</el-button>
<el-button v-else type="primary" @click="previewExportProgressVisible = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog v-model="excludeDialogOpen" title="选择排除订单" width="1100px" append-to-body destroy-on-close @close="handleExcludeDialogClose">
<div class="exclude-toolbar">
<div class="order-identifier-input exclude-search-input">
@@ -150,6 +176,7 @@
import { listArBillGenerateCustomerOptions, getDefaultArBankAccount, previewArBill, previewExcludedArBill, generateArBill } from '@/api/ar/bill'
import { Download, Refresh, Search, Select } from '@element-plus/icons-vue'
import { AR_BILL_SELECTION_MODE } from '@/constants/ar/enums'
import { exportAsync } from '@/utils/excelExport'
const props = defineProps({
modelValue: {
@@ -168,6 +195,29 @@ const billNameOpen = ref(false)
const generateLoading = ref(false)
const previewLoading = ref(false)
const previewExportLoading = ref(false)
const previewExportFinished = ref(false)
const previewExportProgressVisible = ref(false)
const previewExportPercent = ref(0)
const previewExportStatus = ref(undefined)
const previewExportMessage = ref('')
const previewExportRowCount = ref(0)
let previewExportHideTimer = null
const previewExportFloatVisible = computed(() => previewExportLoading.value || previewExportFinished.value)
const previewExportDegree = computed(() => Math.round(previewExportPercent.value / 100 * 360))
const previewExportFloatTitle = computed(() => {
if (previewExportLoading.value) return '导出中'
return previewExportStatus.value === 'exception' ? '导出失败' : '导出完成'
})
const previewExportFloatSummary = computed(() => previewExportRowCount.value > 0
? `${previewExportPercent.value}% / ${previewExportRowCount.value} 行`
: `${previewExportPercent.value}%`)
const previewExportDialogTitle = computed(() => {
if (previewExportLoading.value) return '异步导出进行中'
return previewExportStatus.value === 'exception' ? '异步导出失败' : '异步导出完成'
})
onBeforeUnmount(() => {
clearTimeout(previewExportHideTimer)
})
const previewSearched = ref(false)
const previewRows = ref([])
const previewTotal = ref(0)
@@ -352,13 +402,50 @@ function handlePreviewPageChange(page) {
fetchPreview()
}
function exportPreview() {
async function exportPreview() {
if (!previewSearched.value || !previewTotal.value || !previewQuery.value || previewExportLoading.value) return
clearTimeout(previewExportHideTimer)
previewExportLoading.value = true
proxy.download('/ar/bill/generate/preview/export', { ...previewQuery.value }, `ar_bill_generate_preview_${Date.now()}.xlsx`)
.finally(() => {
previewExportLoading.value = false
previewExportFinished.value = false
previewExportProgressVisible.value = false
previewExportPercent.value = 0
previewExportStatus.value = undefined
previewExportMessage.value = '正在提交导出任务'
previewExportRowCount.value = 0
try {
const result = await exportAsync({
baseUrl: '/ar/bill/generate/preview/export',
query: { ...previewQuery.value },
filePrefix: 'ar_bill_generate_preview',
showLoading: false,
onProgress: task => {
previewExportPercent.value = Math.max(0, Math.min(100, Number(task.percent) || 0))
previewExportRowCount.value = Number(task.rowCount) || 0
previewExportMessage.value = task.error || task.message || '导出中'
if (task.status === 'FAIL' || task.status === 'FAILED') previewExportStatus.value = 'exception'
}
})
if (result) {
previewExportPercent.value = 100
previewExportStatus.value = 'success'
previewExportMessage.value = '导出完成'
} else {
if (previewExportStatus.value !== 'exception') {
previewExportMessage.value = '导出失败,请查看提示后重试'
}
previewExportStatus.value = 'exception'
}
} catch (error) {
previewExportStatus.value = 'exception'
previewExportMessage.value = error?.message || '导出失败,请稍后重试'
} finally {
previewExportLoading.value = false
previewExportFinished.value = true
previewExportHideTimer = window.setTimeout(() => {
previewExportFinished.value = false
previewExportHideTimer = null
}, 1000)
}
}
// 当前页变化只更新当前页 ID,selectedRowMap 因此可以跨页保留手动选择。
@@ -562,6 +649,17 @@ function createRequestId() { return globalThis.crypto?.randomUUID?.() || `ar-${D
.preview-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 18px 0 10px; padding: 10px 14px; border-radius: 6px; background: var(--el-fill-color-light); color: var(--el-text-color-regular); }
.preview-summary { flex: 1; font-size: 14px; }
.preview-summary b { color: var(--el-color-primary); }
.preview-export-status-float { position: fixed; top: 92px; right: 24px; z-index: 3001; }
.preview-export-status-button { display: inline-flex; align-items: center; gap: 10px; min-width: 200px; min-height: 68px; padding: 6px 16px 6px 7px; border-radius: 36px; }
.preview-export-progress-ring { position: relative; display: inline-flex; align-items: center; justify-content: center; flex: 0 0 54px; width: 54px; height: 54px; border-radius: 50%; background: conic-gradient(var(--el-color-warning) 0deg var(--export-progress, 0deg), var(--el-color-warning-light-8) var(--export-progress, 0deg) 360deg); }
.preview-export-progress-ring::after { position: absolute; inset: 5px; content: ''; border-radius: 50%; background: var(--el-bg-color); }
.preview-export-progress-value { position: relative; z-index: 1; color: var(--el-color-warning); font-size: 12px; font-weight: 600; line-height: 1; }
.preview-export-status-text { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; line-height: 1.2; white-space: nowrap; }
.preview-export-status-text strong { font-size: 14px; font-weight: 600; }
.preview-export-status-text small { color: var(--el-text-color-secondary); font-size: 11px; }
.preview-export-progress__msg { margin-top: 12px; color: var(--el-text-color-regular); }
.preview-export-progress__text { margin-top: 8px; color: var(--el-text-color-secondary); font-size: 13px; }
.preview-export-progress__hint { margin-top: 12px; color: var(--el-color-warning); font-size: 12px; }
.preview-selection-toolbar { display: flex; align-items: center; gap: 6px; min-height: 32px; padding-left: 8px; }
.preview-selection-toolbar :deep(.el-checkbox) { margin-right: 6px; }
.preview-selection-toolbar :deep(.el-checkbox__label) { display: inline-flex; align-items: center; padding-left: 4px; font-size: 16px; }