Files
KCGL/inventory-web/src/views/stock/inbound/inbound_summary.vue

270 lines
9.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>