feat: 将入库汇总导出从本地 xlsx 重构为后端异步轮询模式(submitExportTask + checkExportStatus)
This commit is contained in:
@ -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'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -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)'
|
||||||
const link = document.createElement('a')
|
})
|
||||||
link.href = url
|
|
||||||
|
|
||||||
const now = new Date()
|
submitExportTask(filters)
|
||||||
const year = now.getFullYear()
|
.then((res: any) => {
|
||||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
const taskId: string = res.data?.task_id
|
||||||
const day = String(now.getDate()).padStart(2, '0')
|
if (!taskId) {
|
||||||
const hour = String(now.getHours()).padStart(2, '0')
|
ElMessage.error('任务提交失败,未获取到 task_id')
|
||||||
const minute = String(now.getMinutes()).padStart(2, '0')
|
loadingInstance.close()
|
||||||
const second = String(now.getSeconds()).padStart(2, '0')
|
return
|
||||||
const filename = `入库记录_${year}${month}${day}_${hour}${minute}${second}.xlsx`
|
}
|
||||||
|
|
||||||
link.setAttribute('download', filename)
|
// 第二步:轮询任务状态,每 1.5 秒查一次
|
||||||
document.body.appendChild(link)
|
const POLL_INTERVAL_MS = 1500
|
||||||
link.click()
|
const timer = setInterval(() => {
|
||||||
document.body.removeChild(link)
|
checkExportStatus(taskId)
|
||||||
window.URL.revokeObjectURL(url)
|
.then((statusRes: any) => {
|
||||||
ElMessage.success('导出成功')
|
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')
|
||||||
|
link.href = downloadUrl
|
||||||
|
link.setAttribute('download', '') // 让浏览器自动从 Content-Disposition 取文件名
|
||||||
|
link.style.display = 'none'
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
|
||||||
|
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
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user