270 lines
9.4 KiB
Vue
270 lines
9.4 KiB
Vue
<template>
|
||
<div v-if="userStore.hasPermission('inbound_summary:view')" class="app-container">
|
||
<div class="filter-container">
|
||
<el-input
|
||
v-model="listQuery.keyword"
|
||
placeholder="SKU / 名称 / 规格 / 批次号 / 来源"
|
||
style="width: 300px;"
|
||
class="filter-item"
|
||
clearable
|
||
@keyup.enter="handleFilter"
|
||
/>
|
||
|
||
<el-select v-model="listQuery.source_type" placeholder="全部来源" clearable class="filter-item" style="width: 140px; margin-left: 10px;">
|
||
<el-option label="采购入库" value="buy" />
|
||
<el-option label="半成品生产" value="semi" />
|
||
<el-option label="成品完工" value="product" />
|
||
</el-select>
|
||
|
||
<el-date-picker
|
||
v-model="listQuery.dateRange"
|
||
type="daterange"
|
||
range-separator="至"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
class="filter-item"
|
||
style="margin-left: 10px;"
|
||
value-format="YYYY-MM-DD"
|
||
@change="handleFilter"
|
||
/>
|
||
<el-button type="primary" class="filter-item" style="margin-left: 10px;" @click="handleFilter">查询</el-button>
|
||
|
||
<el-button type="success" plain class="filter-item" style="margin-left: 10px;" @click="handleExport" :loading="exportLoading">
|
||
导出Excel
|
||
</el-button>
|
||
</div>
|
||
|
||
<el-table
|
||
:data="list"
|
||
v-loading="loading"
|
||
border
|
||
stripe
|
||
style="width: 100%; margin-top: 20px;"
|
||
:header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
|
||
>
|
||
<el-table-column prop="sku" label="SKU" min-width="140" fixed sortable show-overflow-tooltip />
|
||
|
||
<el-table-column prop="name" label="物品名称" min-width="160" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
<span style="font-weight: 500;">{{ row.name }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column prop="spec_model" label="规格型号" min-width="140" show-overflow-tooltip />
|
||
|
||
<el-table-column label="分类" min-width="120" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
<span>{{ row.category }}</span>
|
||
<span v-if="row.category && row.material_type"> / </span>
|
||
<span>{{ row.material_type }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column prop="type_label" label="入库来源" width="110" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="getSourceTag(row.source_type)" effect="plain">
|
||
{{ row.type_label }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column prop="inbound_date" label="入库/生产日期" width="120" align="center" sortable />
|
||
|
||
<el-table-column label="入库数量" width="110" align="center">
|
||
<template #default="{ row }">
|
||
<span style="font-weight: bold; color: #409EFF;">{{ row.quantity }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column prop="batch_number" label="批次/序列号" min-width="140" show-overflow-tooltip />
|
||
|
||
<el-table-column prop="source_info" label="供应商/负责人" min-width="140" show-overflow-tooltip />
|
||
|
||
<el-table-column prop="status" label="当前状态" width="100" align="center" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-tag size="small" :type="getStatusTag(row.status)">{{ row.status }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
</el-table>
|
||
|
||
<div style="margin-top: 20px; display: flex; justify-content: flex-end;">
|
||
<el-pagination
|
||
v-model:current-page="listQuery.page"
|
||
v-model:page-size="listQuery.per_page"
|
||
:total="total"
|
||
:page-sizes="[10, 20, 50, 100, 200]"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
@size-change="handleFilter"
|
||
@current-change="fetchData"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
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'
|
||
|
||
const userStore = useUserStore()
|
||
|
||
const list = ref([])
|
||
const total = ref(0)
|
||
const loading = ref(false)
|
||
const exportLoading = ref(false)
|
||
|
||
const listQuery = reactive({
|
||
page: 1,
|
||
per_page: 20, // 默认每页20
|
||
keyword: '',
|
||
source_type: '',
|
||
dateRange: null as any
|
||
})
|
||
|
||
const fetchData = async () => {
|
||
loading.value = true
|
||
try {
|
||
const params = {
|
||
page: listQuery.page,
|
||
per_page: listQuery.per_page,
|
||
keyword: listQuery.keyword,
|
||
source_type: listQuery.source_type,
|
||
start_date: listQuery.dateRange ? listQuery.dateRange[0] : null,
|
||
end_date: listQuery.dateRange ? listQuery.dateRange[1] : null
|
||
}
|
||
|
||
const res = await getInboundSummaryList(params)
|
||
if (res.data) {
|
||
list.value = res.data.items || []
|
||
total.value = res.data.total || 0
|
||
}
|
||
} catch (error) {
|
||
console.error('获取入库记录失败', error)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 查询操作重置页码
|
||
const handleFilter = () => {
|
||
listQuery.page = 1
|
||
fetchData()
|
||
}
|
||
|
||
// ============================================================
|
||
// 异步导出定时器(组件级别,需在组件销毁时强制清理)
|
||
// ============================================================
|
||
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,
|
||
start_date: listQuery.dateRange ? listQuery.dateRange[0] : null,
|
||
end_date: listQuery.dateRange ? listQuery.dateRange[1] : null
|
||
}
|
||
|
||
const loadingInstance = ElLoading.service({
|
||
text: '正在后台生成报表,请稍候...',
|
||
background: 'rgba(0, 0, 0, 0.6)'
|
||
})
|
||
|
||
submitExportTask(filters)
|
||
.then((res: any) => {
|
||
const taskId: string = res.data?.task_id
|
||
if (!taskId) {
|
||
loadingInstance.close()
|
||
exportLoading.value = false
|
||
ElMessage.error('任务提交失败,未获取到 task_id')
|
||
return
|
||
}
|
||
|
||
// 赋值给外部变量,供 onBeforeUnmount 清理
|
||
exportTimer = setInterval(() => {
|
||
checkExportStatus(taskId)
|
||
.then((statusRes: any) => {
|
||
const { status, progress, url, error } = statusRes.data || {}
|
||
|
||
// 实时更新 Loading 提示文字(显示后端返回的进度百分比)
|
||
if (progress != null) {
|
||
loadingInstance.setText(`正在生成报表... ${progress}%`)
|
||
}
|
||
|
||
if (status === 'completed') {
|
||
clearInterval(exportTimer!)
|
||
loadingInstance.close()
|
||
exportLoading.value = false
|
||
|
||
// 触发浏览器下载(注意:/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}`
|
||
const link = document.createElement('a')
|
||
link.href = downloadUrl
|
||
link.setAttribute('download', '')
|
||
link.style.display = 'none'
|
||
document.body.appendChild(link)
|
||
link.click()
|
||
document.body.removeChild(link)
|
||
|
||
ElMessage.success('报表已生成,正在下载')
|
||
} else if (status === 'failed') {
|
||
clearInterval(exportTimer!)
|
||
loadingInstance.close()
|
||
exportLoading.value = false
|
||
ElMessage.error(`生成失败:${error || '未知错误'}`)
|
||
}
|
||
// 'processing' → 继续轮询
|
||
})
|
||
.catch(() => {
|
||
// 轮询请求本身失败(网络中断),停止轮询,提示用户
|
||
clearInterval(exportTimer!)
|
||
loadingInstance.close()
|
||
exportLoading.value = false
|
||
ElMessage.error('查询进度失败,请检查网络或稍后重试')
|
||
})
|
||
}, 1500)
|
||
})
|
||
.catch((err: any) => {
|
||
console.error('提交导出任务失败', err)
|
||
loadingInstance.close()
|
||
exportLoading.value = false
|
||
ElMessage.error('提交导出任务失败')
|
||
})
|
||
}
|
||
|
||
// 来源类型的 Tag 颜色
|
||
const getSourceTag = (type: string) => {
|
||
if (type === 'buy') return 'success' // 绿色
|
||
if (type === 'semi') return 'warning' // 橙色
|
||
if (type === 'product') return 'primary' // 蓝色
|
||
return 'info'
|
||
}
|
||
|
||
// [新增] 状态的 Tag 颜色逻辑
|
||
const getStatusTag = (status: string) => {
|
||
if (!status) return 'info'
|
||
if (status.includes('已出库')) return 'info' // 灰色
|
||
if (status.includes('部分')) return 'warning' // 橙色
|
||
if (status.includes('不合格') || status.includes('异常')) return 'danger' // 红色
|
||
return 'success' // 默认(如:合格、在库)为绿色
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchData()
|
||
})
|
||
</script> |