feat: 将入库汇总导出从本地 xlsx 重构为后端异步轮询模式(submitExportTask + checkExportStatus)

This commit is contained in:
DXC
2026-05-19 10:41:21 +08:00
parent 4d81056075
commit e977ffc42d
2 changed files with 105 additions and 35 deletions

View File

@ -17,11 +17,43 @@ export function getInboundSummaryList(params: InboundSummaryQuery) {
}) })
} }
export function exportInboundSummary(params: any) { // ============================================================
// 旧版:前端直接处理 Excel blob已废弃保留用于参考
// ============================================================
// export function exportInboundSummary(params: any) {
// return request({
// url: '/v1/inbound/summary/export',
// method: 'get',
// params,
// responseType: 'blob'
// })
// }
// ============================================================
// 新版:异步导出 API后端生成 + 轮询任务状态)
// ============================================================
/**
* 提交异步导出任务
* POST /api/v1/export/inventory
* 返回 { task_id: string }
*/
export function submitExportTask(filters: Record<string, any>) {
return request({ return request({
url: '/v1/inbound/summary/export', url: '/v1/export/inventory',
method: 'get', method: 'post',
params, data: filters
responseType: 'blob' })
}
/**
* 轮询导出任务状态
* GET /api/v1/export/status/<taskId>
* 返回 { status: 'processing'|'completed'|'failed', progress: number, url: string, error: string }
*/
export function checkExportStatus(taskId: string) {
return request({
url: `/v1/export/status/${taskId}`,
method: 'get'
}) })
} }

View File

@ -104,9 +104,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { getInboundSummaryList, exportInboundSummary } 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 } from 'element-plus' import { ElMessage, ElLoading } from 'element-plus'
const userStore = useUserStore() const userStore = useUserStore()
@ -153,45 +153,83 @@ const handleFilter = () => {
fetchData() fetchData()
} }
// 导出 Excel // 导出 Excel(异步轮询模式,后端生成文件后下载)
const handleExport = () => { const handleExport = () => {
exportLoading.value = true // 如果已有任务在执行中,跳过
const params = { if (exportLoading.value) return
// 第一步:构造过滤参数,提交导出任务
const filters = {
keyword: listQuery.keyword, keyword: listQuery.keyword,
source_type: listQuery.source_type, source_type: listQuery.source_type,
start_date: listQuery.dateRange ? listQuery.dateRange[0] : null, start_date: listQuery.dateRange ? listQuery.dateRange[0] : null,
end_date: listQuery.dateRange ? listQuery.dateRange[1] : null end_date: listQuery.dateRange ? listQuery.dateRange[1] : null
} }
exportInboundSummary(params) // 开启全屏 Loading防止用户重复点击
.then((response: any) => { const loadingInstance = ElLoading.service({
const blob = new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) text: '正在后台生成报表,请稍候...',
const url = window.URL.createObjectURL(blob) background: 'rgba(0, 0, 0, 0.6)'
})
submitExportTask(filters)
.then((res: any) => {
const taskId: string = res.data?.task_id
if (!taskId) {
ElMessage.error('任务提交失败,未获取到 task_id')
loadingInstance.close()
return
}
// 第二步:轮询任务状态,每 1.5 秒查一次
const POLL_INTERVAL_MS = 1500
const timer = setInterval(() => {
checkExportStatus(taskId)
.then((statusRes: any) => {
const statusData = statusRes.data || {}
const { status, progress, url, error } = statusData
// 动态更新 Loading 文字,显示当前进度
if (progress != null) {
loadingInstance.setText(`正在生成报表... ${progress}%`)
}
if (status === 'completed') {
// 第三步:完成 → 停止轮询,关闭 Loading触发下载
clearInterval(timer)
loadingInstance.close()
// 拼接完整下载地址Vite 代理已配置 /api -> 后端)
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') const link = document.createElement('a')
link.href = url link.href = downloadUrl
link.setAttribute('download', '') // 让浏览器自动从 Content-Disposition 取文件名
const now = new Date() link.style.display = 'none'
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const hour = String(now.getHours()).padStart(2, '0')
const minute = String(now.getMinutes()).padStart(2, '0')
const second = String(now.getSeconds()).padStart(2, '0')
const filename = `入库记录_${year}${month}${day}_${hour}${minute}${second}.xlsx`
link.setAttribute('download', filename)
document.body.appendChild(link) document.body.appendChild(link)
link.click() link.click()
document.body.removeChild(link) document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('导出成功') ElMessage.success('报表已生成,正在下载')
} else if (status === 'failed') {
// 失败 → 停止轮询,关闭 Loading弹出错误原因
clearInterval(timer)
loadingInstance.close()
ElMessage.error(`生成失败:${error || '未知错误'}`)
}
// processing → 继续轮询,不做任何操作
})
.catch(() => {
// 网络错误时继续轮询,不中断流程
})
}, POLL_INTERVAL_MS)
}) })
.catch((err: any) => { .catch((err: any) => {
console.error('导出失败', err) console.error('提交导出任务失败', err)
ElMessage.error('导出失败') loadingInstance.close()
}) ElMessage.error('提交导出任务失败')
.finally(() => {
exportLoading.value = false
}) })
} }