feat(scrap): 报废记录按单号分组 + 可展开明细 + 高级搜索
一、后端按「订单」分组(原为平铺明细列表) 有 scrap_request_no → 按单号分组; 无单号(历史直接报废)→ 按 操作时间(精确到分钟) + 操作人 虚拟分组, 生成 LEGACY-<yyyyMMddHHmm>-<操作人> 形式的虚拟单号,避免历史数据成为无主记录。 每单返回 items 明细数组、损失合计、报废总数、申请人(取自 ScrapApproval)。 二、物料解析改为批量预加载 原实现逐条记录 N+1 查询,且 5 条来源路径(stock_buy/semi/product、 trans_borrow、trans_repair)各自 query.get()。改为按 (source_table, stock_id) 收集 ID → 批量 joinedload 查询 → 内存拼装。 三、损失金额改为按权限可见 原 /records 无条件剥离 total_loss,导致持有 scrap_list:loss_amount 的角色 也看不到金额,与权限元素的存在相矛盾。改为对齐 outbound 的 filter_item_by_permissions:无权限置 None,前端 v-if 隐藏整列。 四、新增 keyword / search_type 高级搜索参数 (单号/SKU 在 SQL 层过滤,操作人/申请人/物料名在分组后过滤) 五、修复申请人显示 ScrapApproval._user_name() 返回 '杜邢宸/duxingchen' 全名,统一经 _display_name() 去掉 '/账号' 后缀。 前端 scrap/index.vue 重写为 el-table type=expand 嵌套表格,对齐出库记录版式, 搜索区改用统一 el-form inline 版式(含重置按钮)。 实测:3 单(1 张申请单 2 明细 + 1 组 legacy 合并 2 条),申请人/损失合计/ 虚拟单号均正确。
This commit is contained in:
@ -1,48 +1,125 @@
|
||||
<template>
|
||||
<div v-if="userStore.hasPermission('scrap_list')" class="app-container">
|
||||
<div class="filter-container">
|
||||
<el-input v-model="sku" placeholder="SKU" style="width: 200px" @keyup.enter="fetchData" />
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="margin-left: 10px; width: 240px"
|
||||
/>
|
||||
<el-button type="primary" @click="fetchData">查询</el-button>
|
||||
<el-button @click="resetFilter">重置</el-button>
|
||||
</div>
|
||||
<!-- ★ 高级搜索:与出库记录 / 借还记录统一版式 -->
|
||||
<el-form :inline="true" class="filter-form" @submit.prevent>
|
||||
<el-form-item label="搜索">
|
||||
<el-input
|
||||
v-model="listQuery.keyword"
|
||||
placeholder="输入关键词"
|
||||
style="width: 240px"
|
||||
clearable
|
||||
@input="debouncedSearch"
|
||||
@clear="handleClearSearch"
|
||||
>
|
||||
<template #prepend>
|
||||
<el-select v-model="listQuery.search_type" placeholder="搜索类型" style="width: 110px">
|
||||
<el-option label="全部" value="all" />
|
||||
<el-option label="单号" value="no" />
|
||||
<el-option label="操作人" value="name" />
|
||||
<el-option label="SKU" value="sku" />
|
||||
<el-option label="物料名称" value="material_name" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="报废日期">
|
||||
<el-date-picker
|
||||
v-model="listQuery.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="fetchData">查询</el-button>
|
||||
<el-button @click="resetFilter">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table
|
||||
:data="list"
|
||||
border
|
||||
stripe
|
||||
style="margin-top: 20px"
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
border
|
||||
v-loading="loading"
|
||||
style="width: 100%; margin-top: 20px;"
|
||||
:header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
|
||||
>
|
||||
<el-table-column prop="sku" label="SKU" width="140" show-overflow-tooltip />
|
||||
<el-table-column label="物料名称" min-width="160">
|
||||
<template #default="{ row }">
|
||||
{{ row.material_name || '-' }}
|
||||
<!-- ★ 展开行:报废明细 -->
|
||||
<el-table-column type="expand">
|
||||
<template #default="props">
|
||||
<div style="padding: 10px 40px; background: #fafafa;">
|
||||
<h4 style="margin: 0 0 10px 0; color: #F56C6C;">报废明细 ({{ props.row.items?.length || 0 }} 项)</h4>
|
||||
<el-table :data="props.row.items" border size="small">
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="sku" label="SKU" width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="material_name" label="物料名称" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.material_name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="spec_model" label="规格型号" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.spec_model || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库位" width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.warehouse_location || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="批次/序列号" width="130" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.batch_number || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="quantity" label="报废数量" width="100" align="right" />
|
||||
<el-table-column v-if="canViewLoss" label="损失金额" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.loss_amount !== null && row.loss_amount !== undefined" style="color:#F56C6C;">
|
||||
¥{{ row.loss_amount.toFixed(2) }}
|
||||
</span>
|
||||
<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>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="规格型号" min-width="140">
|
||||
|
||||
<el-table-column label="报废单号" min-width="220" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.spec_model || '-' }}
|
||||
<span>{{ row.scrap_request_no }}</span>
|
||||
<el-tag v-if="row.is_legacy" size="small" type="info" style="margin-left:6px;">历史</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="quantity" label="报废数量" width="100" align="right" />
|
||||
<el-table-column prop="reason" label="报废原因" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="operator_name" label="操作人" width="100" />
|
||||
<el-table-column prop="operation_time" label="报废时间" width="160" />
|
||||
<el-table-column label="损失金额" width="100" align="right">
|
||||
|
||||
<el-table-column prop="applicant_name" label="申请人" width="110" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.applicant_name || '-' }}</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="operator_name" label="操作人" width="110" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.operator_name || '-' }}</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="报废时间" width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.total_loss ? `¥${row.total_loss}` : '-' }}
|
||||
<span>{{ row.scrap_time ? row.scrap_time.substring(0, 16) : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="approval_status" label="审批状态" width="100" align="center">
|
||||
|
||||
<el-table-column label="报废总数" width="100" align="right">
|
||||
<template #default="{ row }">{{ row.total_quantity }}</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column v-if="canViewLoss" label="总损失金额" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.total_loss !== null && row.total_loss !== undefined" style="color:#F56C6C; font-weight:bold; font-size:15px;">
|
||||
¥{{ Number(row.total_loss).toFixed(2) }}
|
||||
</span>
|
||||
<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)">
|
||||
{{ getStatusText(row.approval_status) }}
|
||||
@ -51,20 +128,21 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next, total"
|
||||
:total="total"
|
||||
:page-size="pageSize"
|
||||
:current-page="page"
|
||||
@current-change="handlePage"
|
||||
style="margin-top: 10px; text-align: right"
|
||||
/>
|
||||
<div style="margin-top: 20px; text-align: right;">
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next, total"
|
||||
:total="total"
|
||||
:page-size="listQuery.pageSize"
|
||||
:current-page="listQuery.page"
|
||||
@current-change="handlePage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { getScrapRecords } from '@/api/scrap'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
@ -72,70 +150,111 @@ const userStore = useUserStore()
|
||||
|
||||
const list = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(50)
|
||||
const sku = ref('')
|
||||
const dateRange = ref<[string, string] | null>(null)
|
||||
const loading = ref(false)
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const listQuery = reactive({
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
keyword: '',
|
||||
search_type: 'all',
|
||||
dateRange: [] as string[],
|
||||
})
|
||||
|
||||
// 损失金额可见性:与后端 scrap_list:loss_amount 权限一致
|
||||
const canViewLoss = computed(() =>
|
||||
userStore.role === 'SUPER_ADMIN' || userStore.hasPermission('scrap_list:loss_amount')
|
||||
)
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
sku: sku.value
|
||||
page: listQuery.page,
|
||||
pageSize: listQuery.pageSize,
|
||||
keyword: listQuery.keyword,
|
||||
search_type: listQuery.search_type,
|
||||
}
|
||||
if (dateRange.value && dateRange.value.length === 2) {
|
||||
params.start_date = dateRange.value[0]
|
||||
params.end_date = dateRange.value[1]
|
||||
if (listQuery.dateRange && listQuery.dateRange.length === 2) {
|
||||
params.start_date = listQuery.dateRange[0]
|
||||
params.end_date = listQuery.dateRange[1]
|
||||
}
|
||||
const res = await getScrapRecords(params)
|
||||
list.value = res.data.list || []
|
||||
total.value = res.data.total || 0
|
||||
const res: any = await getScrapRecords(params)
|
||||
list.value = res.data?.list || []
|
||||
total.value = res.data?.total || 0
|
||||
} catch (e) {
|
||||
console.error('加载报废记录失败', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedSearch = () => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(() => {
|
||||
listQuery.page = 1
|
||||
fetchData()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const handleClearSearch = () => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
listQuery.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handlePage = (val: number) => {
|
||||
page.value = val
|
||||
listQuery.page = val
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const resetFilter = () => {
|
||||
sku.value = ''
|
||||
dateRange.value = null
|
||||
page.value = 1
|
||||
listQuery.keyword = ''
|
||||
listQuery.search_type = 'all'
|
||||
listQuery.dateRange = []
|
||||
listQuery.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 报废台账写在执行/提交时,approval_status 的取值为这两个
|
||||
const getStatusType = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
executed: 'success',
|
||||
approved: 'success',
|
||||
rejected: 'danger',
|
||||
pending: 'warning'
|
||||
pending: 'warning',
|
||||
}
|
||||
return map[status] || 'info'
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
approved: '已审批',
|
||||
executed: '已执行',
|
||||
approved: '已报废',
|
||||
rejected: '已拒绝',
|
||||
pending: '待审批'
|
||||
pending: '待审批',
|
||||
}
|
||||
return map[status] || status
|
||||
return map[status] || status || '-'
|
||||
}
|
||||
|
||||
onMounted(fetchData)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer)
|
||||
debounceTimer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-container {
|
||||
.filter-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
.filter-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user