feat(records): 高级筛选引擎 + 出库记录接入
一、新增共享工具 app/utils/advanced_filter.py
系统内已有该模式(material/list.vue、stock/inbound/buy.vue),
沿用其既有约定:参数名 advancedFilters、值为 JSON 字符串、
操作符 eq/ne/contains/not_contains/ge/le。
· parse_advanced_filters() 解析并规整,坏输入退化为空列表不影响主查询
· build_predicate() 单条件 → SQLAlchemy 谓词,未登记字段返回 None 杜绝列注入
· build_material_name_select() 物料名三表联查(buy/semi/product JOIN material_base)
二、★ 父子关系处理(本次核心)
记录接口返回的是**按单号分组的订单**,而用户筛选字段多落在**明细行**上。
若直接 .filter(TransOutbound.sku.ilike(...)),会在 GROUP BY 前收窄明细范围,
展开行里的兄弟明细会凭空消失。正确做法是先求「含匹配明细的单号集合」
再让主查询按单号 IN 过滤。
实测对照(单 OUT-20260811-1519-0003,21 条明细):
按其中一条 SKU 筛选 → 子查询法保住全部 21 条;直接 filter 只剩 1 条。
三、★ 否定操作符语义(NOT IN)
子级字段的 ne / not_contains 不能直接用 SQL != / NOT LIKE —— 那表达的是
「本单存在某条不等于 X 的明细」,多明细单几乎必然成立,等于筛选失效。
用户意图是**整单排除**,故 apply_child_condition() 统一:
肯定 → order_no IN (含匹配明细的单号)
否定 → order_no NOT IN (含匹配明细的单号)
两者子查询完全一致(都用肯定形式谓词),仅外层取反。
父级字段(单号/操作人)仍走标准 SQL 谓词,语义无歧义。
四、出库记录接入(前端弹窗 + 后端接线)
验证:
eq 0000000002 → 1 单;material_name contains 白板 → 16 单
sku ne 0000000002 → 394 = 395-1,含该 SKU 的单被整体排除
material_name not_contains 白板 → 379 = 395-16
This commit is contained in:
@ -43,6 +43,44 @@
|
||||
<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-button v-if="userStore.hasPermission('outbound_create:operation')" type="success" @click="$router.push('/outbound/create')">新建出库</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@ -230,16 +268,59 @@ const listQuery = reactive({
|
||||
keyword: '',
|
||||
search_type: 'all',
|
||||
dateRange: [],
|
||||
company: '' as string
|
||||
company: '' 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()
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
...listQuery,
|
||||
start_date: listQuery.dateRange && listQuery.dateRange[0] ? listQuery.dateRange[0] : null,
|
||||
end_date: listQuery.dateRange && listQuery.dateRange[1] ? listQuery.dateRange[1] : null
|
||||
end_date: listQuery.dateRange && listQuery.dateRange[1] ? listQuery.dateRange[1] : null,
|
||||
// ★ 高级筛选:后端约定参数名为 advancedFilters,值为 JSON 字符串
|
||||
advancedFilters: JSON.stringify(listQuery.advancedFilters || []),
|
||||
}
|
||||
|
||||
const res = await getOutboundList(params)
|
||||
@ -269,6 +350,9 @@ const resetFilter = () => {
|
||||
listQuery.keyword = ''
|
||||
listQuery.search_type = 'all'
|
||||
listQuery.dateRange = []
|
||||
listQuery.advancedFilters = []
|
||||
advancedConditions.value = [{ field: '', operator: '', value: '' }]
|
||||
appliedConditions.value = []
|
||||
listQuery.page = 1
|
||||
fetchData()
|
||||
}
|
||||
@ -322,6 +406,13 @@ onBeforeUnmount(() => {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
/* 高级筛选条件行 */
|
||||
.condition-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.signature-cell {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
Reference in New Issue
Block a user