feat(scrap): 报废记录按原因分类筛选 + Excel 导出 + 显示分类
- 记录页与审批页展示「原因分类」:分类数据一直有下发,只是前端没渲染, 页面上一个地方都看不到。 - 记录页加分类筛选。★ 选「库存报废」时 SQL 一并兜住空值:本列上线前的 历史台账没有分类,而它们全是 MOM 自身流程产生的;不兜会漏掉全部历史单, 用户会以为数据丢了。 - 新增 GET /scrap/records/export 导出 xlsx:与列表同一套筛选,但导**全部 匹配结果**而不是当前页 —— 只导当前页会让人以为数据被截断。 金额按 scrap_list:loss_amount 权限决定可不可见(与列表同口径), 否则导出就成了绕过字段级权限的后门。行数上限 20000 且截断会写进文件名。
This commit is contained in:
@ -29,6 +29,25 @@ export function getScrapRecords(params: any) {
|
||||
})
|
||||
}
|
||||
|
||||
// 4. 报废记录导出 Excel
|
||||
//
|
||||
// ★ 与列表页**同一套筛选参数**,但后端导出的是**全部匹配结果**而不是当前页 ——
|
||||
// 用户点「导出」要的就是全量,只导当前页会让他以为数据被截断了。
|
||||
// ★ responseType: 'blob' 不能省,否则 axios 会把二进制当文本解析,文件打不开。
|
||||
// ★ 损失金额由后端按 scrap_list:loss_amount 权限决定可不可见(与列表页同口径),
|
||||
// 前端不做二次处理 —— 否则导出会变成绕过字段级权限的后门。
|
||||
export function exportScrapRecords(params: any) {
|
||||
return request({
|
||||
url: '/v1/scrap/records/export',
|
||||
method: 'get',
|
||||
params,
|
||||
responseType: 'blob' as any,
|
||||
headers: {
|
||||
'Accept': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 3.1 报废申请专用库存列表(独立于出库选单权限,Fail-Closed 剥离价格)
|
||||
export function getScrapStockList(params: { page?: number; pageSize?: number; keyword?: string }) {
|
||||
return request({
|
||||
|
||||
@ -73,6 +73,19 @@
|
||||
<template #default="{ row }">{{ row.applicant_name || getApplicantName(row.applicant_id) }}</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 原因分类:审批人得知道批的是哪个环节的损失。
|
||||
「生产损耗」= 好料领用后变坏;「库存/采购」= 在库或买来就有问题。
|
||||
存量单没有分类(列是后加的),显示 '-' 而不是猜一个 -->
|
||||
<el-table-column label="原因分类" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.reason_category_label" size="small"
|
||||
:type="row.reason_category === 'PRODUCTION' ? 'warning' : 'info'">
|
||||
{{ row.reason_category_label }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="remark" label="申请原因" min-width="180" show-overflow-tooltip />
|
||||
|
||||
<el-table-column label="报废种类" width="100" align="center">
|
||||
|
||||
@ -35,9 +35,24 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 原因分类:两种互斥口径 ——
|
||||
生产报废 = **所有走 Track 的**(料领到产线后在生产中变坏);
|
||||
库存报废 = **MOM 自身流程走下来的**(不看 Track、直接选库存行报废等)。
|
||||
⚠️ 选「库存报废」时后端会**一并兜住空值**:本列上线前的历史台账没有
|
||||
分类,而它们全都是 MOM 自身流程产生的,不兜会漏掉全部历史单 -->
|
||||
<el-form-item label="原因分类">
|
||||
<el-select v-model="listQuery.reason_category" clearable
|
||||
placeholder="全部" style="width: 130px">
|
||||
<el-option label="生产报废" value="PRODUCTION" />
|
||||
<el-option label="库存报废" value="STOCK" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="fetchData">查询</el-button>
|
||||
<el-button @click="resetFilter">重置</el-button>
|
||||
<!-- 导出:与当前筛选条件一致,但导的是**全部匹配结果**而不是当前页 -->
|
||||
<el-button type="success" :loading="exporting" @click="handleExport">导出 Excel</el-button>
|
||||
|
||||
<!-- ★ 高级筛选:对齐 material/list.vue 既有模式 -->
|
||||
<el-popover
|
||||
@ -114,6 +129,21 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 报废原因分类:回答「这笔损失出在哪个环节」。
|
||||
生产损耗(好料领用后变坏)vs 库存/采购(在库或买来就有问题)。
|
||||
★ 只展示,不做任何业务判断 —— 分类由提交方**显式**写入,
|
||||
不能从来源推导(Track 的生产报废与手工的不良品退回共用
|
||||
同一张 trans_defective_goods 表,推导会把生产损失算成库存损失)。
|
||||
⚠️ 存量单没有分类(列是后加的),显示 '-' 而不是猜一个 -->
|
||||
<el-table-column label="原因分类" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.reason_category_label" size="small"
|
||||
:type="row.reason_category === 'PRODUCTION' ? 'warning' : 'info'">
|
||||
{{ row.reason_category_label }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="reason" label="报废原因" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.reason || '-' }}</template>
|
||||
</el-table-column>
|
||||
@ -156,6 +186,17 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 原因分类也放主行:不用展开就能一眼看出这笔损失出在哪个环节 -->
|
||||
<el-table-column label="原因分类" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.reason_category_label" size="small"
|
||||
:type="row.reason_category === 'PRODUCTION' ? 'warning' : 'info'">
|
||||
{{ row.reason_category_label }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="审批状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.approval_status)">
|
||||
@ -180,7 +221,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { getScrapRecords } from '@/api/scrap'
|
||||
import { getScrapRecords, exportScrapRecords } from '@/api/scrap'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
@ -197,6 +238,8 @@ const listQuery = reactive({
|
||||
search_type: 'all',
|
||||
dateRange: [] as string[],
|
||||
advancedFilters: [] as any[],
|
||||
// '' = 全部;PRODUCTION = 生产报废;STOCK = 库存报废
|
||||
reason_category: '',
|
||||
})
|
||||
|
||||
// --- ★ 高级筛选 ---
|
||||
@ -255,6 +298,8 @@ const fetchData = async () => {
|
||||
search_type: listQuery.search_type,
|
||||
// ★ 高级筛选:后端约定参数名为 advancedFilters,值为 JSON 字符串
|
||||
advancedFilters: JSON.stringify(listQuery.advancedFilters || []),
|
||||
// 原因分类:空串 = 不筛
|
||||
reason_category: listQuery.reason_category || '',
|
||||
}
|
||||
if (listQuery.dateRange && listQuery.dateRange.length === 2) {
|
||||
params.start_date = listQuery.dateRange[0]
|
||||
@ -294,12 +339,53 @@ const resetFilter = () => {
|
||||
listQuery.search_type = 'all'
|
||||
listQuery.dateRange = []
|
||||
listQuery.advancedFilters = []
|
||||
listQuery.reason_category = ''
|
||||
advancedConditions.value = [{ field: '', operator: '', value: '' }]
|
||||
appliedConditions.value = []
|
||||
listQuery.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// ---- 导出 Excel ----
|
||||
// 与列表**同一套筛选条件**,但导的是全部匹配结果(后端不限当前页)。
|
||||
// 下载是浏览器的静默行为,页面不会跳转 —— 所以要给一条「导出中」的反馈,
|
||||
// 否则大表导出时用户会以为按钮没反应。
|
||||
const exporting = ref(false)
|
||||
const handleExport = async () => {
|
||||
exporting.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
keyword: listQuery.keyword,
|
||||
search_type: listQuery.search_type,
|
||||
advancedFilters: JSON.stringify(listQuery.advancedFilters || []),
|
||||
reason_category: listQuery.reason_category || '',
|
||||
}
|
||||
if (listQuery.dateRange && listQuery.dateRange.length === 2) {
|
||||
params.start_date = listQuery.dateRange[0]
|
||||
params.end_date = listQuery.dateRange[1]
|
||||
}
|
||||
const res: any = await exportScrapRecords(params)
|
||||
const blob = new Blob([res], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
})
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
const ymd = new Date().toISOString().split('T')[0]
|
||||
link.download = `报废记录_${ymd}.xlsx`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
ElMessage.success('导出成功')
|
||||
} catch (e) {
|
||||
console.error('导出报废记录失败', e)
|
||||
ElMessage.error('导出失败,请重试')
|
||||
} finally {
|
||||
exporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 报废台账写在执行/提交时,approval_status 的取值为这两个
|
||||
const getStatusType = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
|
||||
Reference in New Issue
Block a user