fix: 为 handleExport 添加 onBeforeUnmount 幽灵定时器防护,并补充轮询失败时的兜底处理

This commit is contained in:
DXC
2026-05-19 10:45:41 +08:00
parent e977ffc42d
commit e331236a6e

View File

@ -103,7 +103,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
import { getInboundSummaryList, submitExportTask, checkExportStatus } from '@/api/inbound/inbound_summary'
import { useUserStore } from '@/stores/user'
import { ElMessage, ElLoading } from 'element-plus'
@ -153,12 +153,24 @@ const handleFilter = () => {
fetchData()
}
// 导出 Excel异步轮询模式后端生成文件后下载
const handleExport = () => {
// 如果已有任务在执行中,跳过
if (exportLoading.value) return
// ============================================================
// 异步导出定时器(组件级别,需在组件销毁时强制清理)
// ============================================================
let exportTimer: ReturnType<typeof setInterval> | null = null
// 组件销毁前,强制清理"幽灵定时器"(防止用户切换路由后定时器仍在跑)
onBeforeUnmount(() => {
if (exportTimer) clearInterval(exportTimer)
})
// ============================================================
// 导出 Excel后端异步轮询模式
// ============================================================
const handleExport = () => {
// 防抖:已有任务在执行中直接跳过
if (exportLoading.value) return
exportLoading.value = true
// 第一步:构造过滤参数,提交导出任务
const filters = {
keyword: listQuery.keyword,
source_type: listQuery.source_type,
@ -166,7 +178,6 @@ const handleExport = () => {
end_date: listQuery.dateRange ? listQuery.dateRange[1] : null
}
// 开启全屏 Loading防止用户重复点击
const loadingInstance = ElLoading.service({
text: '正在后台生成报表,请稍候...',
background: 'rgba(0, 0, 0, 0.6)'
@ -176,37 +187,35 @@ const handleExport = () => {
.then((res: any) => {
const taskId: string = res.data?.task_id
if (!taskId) {
ElMessage.error('任务提交失败,未获取到 task_id')
loadingInstance.close()
exportLoading.value = false
ElMessage.error('任务提交失败,未获取到 task_id')
return
}
// 第二步:轮询任务状态,每 1.5 秒查一次
const POLL_INTERVAL_MS = 1500
const timer = setInterval(() => {
// 赋值给外部变量,供 onBeforeUnmount 清理
exportTimer = setInterval(() => {
checkExportStatus(taskId)
.then((statusRes: any) => {
const statusData = statusRes.data || {}
const { status, progress, url, error } = statusData
const { status, progress, url, error } = statusRes.data || {}
// 动态更新 Loading 文字显示当前进度
// 实时更新 Loading 提示文字显示后端返回的进度百分比)
if (progress != null) {
loadingInstance.setText(`正在生成报表... ${progress}%`)
}
if (status === 'completed') {
// 第三步:完成 → 停止轮询,关闭 Loading触发下载
clearInterval(timer)
clearInterval(exportTimer!)
loadingInstance.close()
exportLoading.value = false
// 拼接完整下载地址Vite 代理已配置 /api -> 后端)
// 触发浏览器下载(注意:/download/<taskId> 接口在 Flask 侧不过滤 JWT
// 若需要 Token 验证下载,请改用 window.open 或 iframe 下载方式)
const baseUrl = import.meta.env.VITE_API_BASE_URL || window.location.origin
const downloadUrl = url.startsWith('http') ? url : `${baseUrl}${url}`
// 动态创建 <a> 标签触发浏览器下载
const link = document.createElement('a')
link.href = downloadUrl
link.setAttribute('download', '') // 让浏览器自动从 Content-Disposition 取文件名
link.setAttribute('download', '')
link.style.display = 'none'
document.body.appendChild(link)
link.click()
@ -214,21 +223,26 @@ const handleExport = () => {
ElMessage.success('报表已生成,正在下载')
} else if (status === 'failed') {
// 失败 → 停止轮询,关闭 Loading弹出错误原因
clearInterval(timer)
clearInterval(exportTimer!)
loadingInstance.close()
exportLoading.value = false
ElMessage.error(`生成失败:${error || '未知错误'}`)
}
// processing → 继续轮询,不做任何操作
// 'processing' → 继续轮询
})
.catch(() => {
// 网络错误时继续轮询,不中断流程
// 轮询请求本身失败(网络中断),停止轮询,提示用户
clearInterval(exportTimer!)
loadingInstance.close()
exportLoading.value = false
ElMessage.error('查询进度失败,请检查网络或稍后重试')
})
}, POLL_INTERVAL_MS)
}, 1500)
})
.catch((err: any) => {
console.error('提交导出任务失败', err)
loadingInstance.close()
exportLoading.value = false
ElMessage.error('提交导出任务失败')
})
}