fix: 为 handleExport 添加 onBeforeUnmount 幽灵定时器防护,并补充轮询失败时的兜底处理
This commit is contained in:
@ -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('提交导出任务失败')
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user