feat(records): 报废记录接入高级筛选

复用 app/utils/advanced_filter.py,但报废有两处与出库/借还不同,需特殊处理:

一、scrap_request_no 可为 NULL(历史直接报废)
  出库/借还可直接对单号列做 IN,报废不行——「单号 IN」永远匹配不上 NULL 行,
  会把历史单整体漏掉。此处统一用「行级等价键」表达同单关系:
    有单号 → scrap_request_no 相等
    无单号 → 同一分钟(date_trunc) + 同一操作人(与 Python 侧分组逻辑完全一致)

二、物料名不能用单号传递结果
  物料名需三表联查,但联查结果若用「单号 IN」回接,同样会漏掉 NULL 单号行。
  改用 tuple_(source_table, stock_id).in_(pairs) 元组定位报废行。

三、否定操作符
  命中行 → 折算同单等价键谓词 → 否定时整体取反(~pred),实现整单排除。

前端 scrap/index.vue 新增「高级筛选」el-popover,与出库/借还版式一致。

验证(库中当前唯一报废单含 SKU 0000000272):
  sku contains 0000000272 → 1 单
  sku ne 0000000272       → 0 单(整单排除,符合预期)
  material_name not_contains 白板 → 0 单(该单物料名含白板,被排除)
  单号 ne(父级)          → 1 单(父级否定语义不变)
This commit is contained in:
yueli
2026-09-10 13:06:07 +08:00
parent fd0bfd3d9c
commit 1ef9ae4ad9
2 changed files with 201 additions and 1 deletions

View File

@ -38,6 +38,43 @@
<el-form-item>
<el-button type="primary" @click="fetchData">查询</el-button>
<el-button @click="resetFilter">重置</el-button>
<!-- 高级筛选对齐 material/list.vue 既有模式 -->
<el-popover
v-model:visible="advancedFilterVisible"
placement="bottom"
title="高级筛选"
width="600"
trigger="manual"
>
<template #reference>
<el-button plain @click="advancedFilterVisible = !advancedFilterVisible">
高级筛选
<el-badge v-if="appliedConditions.length" :value="appliedConditions.length" style="margin-left:6px" />
</el-button>
</template>
<div class="advanced-filter">
<div
v-for="(condition, index) in advancedConditions"
:key="index"
class="condition-row"
>
<el-select v-model="condition.field" placeholder="字段" style="width:180px" :teleported="false">
<el-option v-for="f in fieldOptions" :key="f.value" :label="f.label" :value="f.value" />
</el-select>
<el-select v-model="condition.operator" placeholder="操作符" style="width:120px; margin-left:8px" :teleported="false">
<el-option v-for="op in operatorOptions" :key="op.value" :label="op.label" :value="op.value" />
</el-select>
<el-input v-model="condition.value" placeholder="值" style="width:180px; margin-left:8px" />
<el-button v-if="advancedConditions.length > 1" type="danger" link @click="removeCondition(index)" style="margin-left:8px">删除</el-button>
</div>
<div style="margin-top:12px">
<el-button type="primary" link @click="addCondition">添加条件</el-button>
<el-button type="primary" @click="applyAdvancedFilter">应用筛选</el-button>
<el-button @click="resetAdvancedFilter">重置</el-button>
</div>
</div>
</el-popover>
</el-form-item>
</el-form>
@ -159,8 +196,50 @@ const listQuery = reactive({
keyword: '',
search_type: 'all',
dateRange: [] as string[],
advancedFilters: [] as any[],
})
// --- ★ 高级筛选 ---
const advancedFilterVisible = ref(false)
const advancedConditions = ref([{ field: '', operator: '', value: '' }])
const appliedConditions = ref<any[]>([]) // 已生效的条件(用于按钮角标)
const fieldOptions = [
{ value: 'no', label: '单号' },
{ value: 'sku', label: 'SKU' },
{ value: 'material_name', label: '物料名称' },
{ value: 'operator', label: '操作人' },
]
const operatorOptions = [
{ value: 'contains', label: '包含' },
{ value: 'eq', label: '等于' },
{ value: 'not_contains', label: '不包含' },
{ value: 'ne', label: '不等于' },
]
const addCondition = () => {
advancedConditions.value.push({ field: '', operator: '', value: '' })
}
const removeCondition = (index: number) => {
advancedConditions.value.splice(index, 1)
}
const applyAdvancedFilter = () => {
// 过滤掉不完整的条件行
const valid = advancedConditions.value.filter(c => c.field && c.operator && c.value !== '')
listQuery.advancedFilters = valid
appliedConditions.value = valid
advancedFilterVisible.value = false
listQuery.page = 1
fetchData()
}
const resetAdvancedFilter = () => {
advancedConditions.value = [{ field: '', operator: '', value: '' }]
listQuery.advancedFilters = []
appliedConditions.value = []
advancedFilterVisible.value = false
listQuery.page = 1
fetchData()
}
// 损失金额可见性:与后端 scrap_list:loss_amount 权限一致
const canViewLoss = computed(() =>
userStore.role === 'SUPER_ADMIN' || userStore.hasPermission('scrap_list:loss_amount')
@ -174,6 +253,8 @@ const fetchData = async () => {
pageSize: listQuery.pageSize,
keyword: listQuery.keyword,
search_type: listQuery.search_type,
// ★ 高级筛选:后端约定参数名为 advancedFilters值为 JSON 字符串
advancedFilters: JSON.stringify(listQuery.advancedFilters || []),
}
if (listQuery.dateRange && listQuery.dateRange.length === 2) {
params.start_date = listQuery.dateRange[0]
@ -212,6 +293,9 @@ const resetFilter = () => {
listQuery.keyword = ''
listQuery.search_type = 'all'
listQuery.dateRange = []
listQuery.advancedFilters = []
advancedConditions.value = [{ field: '', operator: '', value: '' }]
appliedConditions.value = []
listQuery.page = 1
fetchData()
}
@ -257,4 +341,11 @@ onBeforeUnmount(() => {
margin-bottom: 0;
margin-right: 12px;
}
/* 高级筛选条件行 */
.condition-row {
display: flex;
align-items: center;
margin-bottom: 10px;
}
</style>