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> </template>
<script setup lang="ts"> <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 { getInboundSummaryList, submitExportTask, checkExportStatus } from '@/api/inbound/inbound_summary'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { ElMessage, ElLoading } from 'element-plus' import { ElMessage, ElLoading } from 'element-plus'
@ -153,12 +153,24 @@ const handleFilter = () => {
fetchData() 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 = { const filters = {
keyword: listQuery.keyword, keyword: listQuery.keyword,
source_type: listQuery.source_type, source_type: listQuery.source_type,
@ -166,7 +178,6 @@ const handleExport = () => {
end_date: listQuery.dateRange ? listQuery.dateRange[1] : null end_date: listQuery.dateRange ? listQuery.dateRange[1] : null
} }
// 开启全屏 Loading防止用户重复点击
const loadingInstance = ElLoading.service({ const loadingInstance = ElLoading.service({
text: '正在后台生成报表,请稍候...', text: '正在后台生成报表,请稍候...',
background: 'rgba(0, 0, 0, 0.6)' background: 'rgba(0, 0, 0, 0.6)'
@ -176,37 +187,35 @@ const handleExport = () => {
.then((res: any) => { .then((res: any) => {
const taskId: string = res.data?.task_id const taskId: string = res.data?.task_id
if (!taskId) { if (!taskId) {
ElMessage.error('任务提交失败,未获取到 task_id')
loadingInstance.close() loadingInstance.close()
exportLoading.value = false
ElMessage.error('任务提交失败,未获取到 task_id')
return return
} }
// 第二步:轮询任务状态,每 1.5 秒查一次 // 赋值给外部变量,供 onBeforeUnmount 清理
const POLL_INTERVAL_MS = 1500 exportTimer = setInterval(() => {
const timer = setInterval(() => {
checkExportStatus(taskId) checkExportStatus(taskId)
.then((statusRes: any) => { .then((statusRes: any) => {
const statusData = statusRes.data || {} const { status, progress, url, error } = statusRes.data || {}
const { status, progress, url, error } = statusData
// 动态更新 Loading 文字显示当前进度 // 实时更新 Loading 提示文字显示后端返回的进度百分比)
if (progress != null) { if (progress != null) {
loadingInstance.setText(`正在生成报表... ${progress}%`) loadingInstance.setText(`正在生成报表... ${progress}%`)
} }
if (status === 'completed') { if (status === 'completed') {
// 第三步:完成 → 停止轮询,关闭 Loading触发下载 clearInterval(exportTimer!)
clearInterval(timer)
loadingInstance.close() 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 baseUrl = import.meta.env.VITE_API_BASE_URL || window.location.origin
const downloadUrl = url.startsWith('http') ? url : `${baseUrl}${url}` const downloadUrl = url.startsWith('http') ? url : `${baseUrl}${url}`
// 动态创建 <a> 标签触发浏览器下载
const link = document.createElement('a') const link = document.createElement('a')
link.href = downloadUrl link.href = downloadUrl
link.setAttribute('download', '') // 让浏览器自动从 Content-Disposition 取文件名 link.setAttribute('download', '')
link.style.display = 'none' link.style.display = 'none'
document.body.appendChild(link) document.body.appendChild(link)
link.click() link.click()
@ -214,21 +223,26 @@ const handleExport = () => {
ElMessage.success('报表已生成,正在下载') ElMessage.success('报表已生成,正在下载')
} else if (status === 'failed') { } else if (status === 'failed') {
// 失败 → 停止轮询,关闭 Loading弹出错误原因 clearInterval(exportTimer!)
clearInterval(timer)
loadingInstance.close() loadingInstance.close()
exportLoading.value = false
ElMessage.error(`生成失败:${error || '未知错误'}`) ElMessage.error(`生成失败:${error || '未知错误'}`)
} }
// processing → 继续轮询,不做任何操作 // 'processing' → 继续轮询
}) })
.catch(() => { .catch(() => {
// 网络错误时继续轮询,不中断流程 // 轮询请求本身失败(网络中断),停止轮询,提示用户
clearInterval(exportTimer!)
loadingInstance.close()
exportLoading.value = false
ElMessage.error('查询进度失败,请检查网络或稍后重试')
}) })
}, POLL_INTERVAL_MS) }, 1500)
}) })
.catch((err: any) => { .catch((err: any) => {
console.error('提交导出任务失败', err) console.error('提交导出任务失败', err)
loadingInstance.close() loadingInstance.close()
exportLoading.value = false
ElMessage.error('提交导出任务失败') ElMessage.error('提交导出任务失败')
}) })
} }