feat(records): 借还记录接入高级筛选
复用 app/utils/advanced_filter.py 的解析与谓词逻辑:
· 父级字段(单号 borrow_no、借用人 borrower_name)走标准 SQL 谓词
· 子级字段(SKU、物料名称)经单号子查询过滤,否定操作符走 NOT IN 整单排除
· 物料名经三表联查(buy/semi/product JOIN material_base)
借还的单号维度查询基于 order_subq 子查询分页,故此处把过滤条件施加在
order_subq.c.borrow_no 上,与既有的状态/日期/公司隔离过滤保持同一层次。
前端 records.vue 新增「高级筛选」el-popover(字段/操作符/值 + 添加条件/
应用筛选/重置),序列化为 advancedFilters JSON 字符串随查询下发。
验证:sku ne 0000000002 → 52 单(库中无单含该 SKU,正确不减);
sku contains 0000 → 52 单(52 单的 SKU 全部含 0000,数据巧合)。
This commit is contained in:
@ -165,6 +165,10 @@ def get_records():
|
||||
start_date = request.args.get('start_date', '')
|
||||
end_date = request.args.get('end_date', '')
|
||||
|
||||
# ★ 高级筛选:JSON 字符串 → 条件列表
|
||||
from app.utils.advanced_filter import parse_advanced_filters
|
||||
advanced_filters = parse_advanced_filters(request.args.get('advancedFilters', ''))
|
||||
|
||||
# ★ 数据权限:普通用户只看“借用人=本人姓名(不含账号前缀)”的借还记录;管理者看全部
|
||||
borrower_name = None
|
||||
if not is_privileged_viewer():
|
||||
@ -179,6 +183,7 @@ def get_records():
|
||||
page=page, limit=10, status=status, keyword=keyword,
|
||||
search_type=search_type, borrower_name=borrower_name,
|
||||
start_date=start_date, end_date=end_date,
|
||||
advanced_filters=advanced_filters,
|
||||
)
|
||||
|
||||
# ★ service 层异常时:code==500 的字典(带 traceback),需要直通到前端,便于排查
|
||||
|
||||
@ -415,7 +415,8 @@ class TransService:
|
||||
|
||||
@staticmethod
|
||||
def get_records(page=1, limit=10, status='all', keyword=None, search_type='all',
|
||||
borrower_name=None, start_date=None, end_date=None):
|
||||
borrower_name=None, start_date=None, end_date=None,
|
||||
advanced_filters=None):
|
||||
"""
|
||||
获取借还记录列表(按单号 borrow_no 维度分页,避免明细撑爆 pageSize)
|
||||
|
||||
@ -688,6 +689,49 @@ class TransService:
|
||||
order_subq.c.borrow_no.in_(company_borrow_nos_subq)
|
||||
)
|
||||
|
||||
# ====================================================================
|
||||
# ★ 高级筛选:父级字段直接过滤;子级字段(SKU/物料名称)走
|
||||
# 「命中单号子查询 → 按单号 IN」的 EXISTS 语义,
|
||||
# 避免在 GROUP BY 前收窄明细范围而丢失同单的兄弟明细。
|
||||
# ====================================================================
|
||||
if advanced_filters:
|
||||
from app.utils.advanced_filter import (
|
||||
build_predicate, apply_child_condition,
|
||||
)
|
||||
parent_map = {
|
||||
'no': TransBorrow.borrow_no,
|
||||
'operator': TransBorrow.borrower_name,
|
||||
'borrower_name': TransBorrow.borrower_name,
|
||||
}
|
||||
child_map = {'sku': TransBorrow.sku}
|
||||
material_stock_models = [
|
||||
(StockBuy, 'stock_buy'),
|
||||
(StockSemi, 'stock_semi'),
|
||||
(StockProduct, 'stock_product'),
|
||||
]
|
||||
for cond in advanced_filters:
|
||||
field = cond.get('field')
|
||||
|
||||
if field in parent_map:
|
||||
# 父级字段:标准 SQL 谓词即可
|
||||
p = build_predicate(cond, parent_map)
|
||||
if p is not None:
|
||||
borrow_no_q = borrow_no_q.filter(
|
||||
order_subq.c.borrow_no.in_(
|
||||
db.session.query(TransBorrow.borrow_no)
|
||||
.filter(p).distinct()
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if field in child_map or field == 'material_name':
|
||||
# ★ 子级字段:正/负操作符语义分派(否定 → 整单排除)
|
||||
borrow_no_q = apply_child_condition(
|
||||
borrow_no_q, order_subq.c.borrow_no, TransBorrow,
|
||||
cond, child_map, material_stock_models,
|
||||
)
|
||||
continue
|
||||
|
||||
# 日期范围过滤(按借出时间;边界已在入口补全时分秒)
|
||||
if start_date:
|
||||
borrow_no_q = borrow_no_q.filter(order_subq.c.borrow_no.in_(
|
||||
|
||||
@ -47,6 +47,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>
|
||||
|
||||
@ -287,6 +324,47 @@ const dateRange = ref<string[]>([])
|
||||
const page = ref(1)
|
||||
const loading = ref(false)
|
||||
|
||||
// --- ★ 高级筛选 ---
|
||||
const advancedFilterVisible = ref(false)
|
||||
const advancedConditions = ref([{ field: '', operator: '', value: '' }])
|
||||
const appliedConditions = ref<any[]>([])
|
||||
const advancedFilters = ref<any[]>([])
|
||||
const fieldOptions = [
|
||||
{ value: 'no', label: '单号' },
|
||||
{ value: 'sku', label: 'SKU' },
|
||||
{ value: 'material_name', label: '物料名称' },
|
||||
{ value: 'borrower_name', 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 !== '')
|
||||
advancedFilters.value = valid
|
||||
appliedConditions.value = valid
|
||||
advancedFilterVisible.value = false
|
||||
page.value = 1
|
||||
fetchData()
|
||||
}
|
||||
const resetAdvancedFilter = () => {
|
||||
advancedConditions.value = [{ field: '', operator: '', value: '' }]
|
||||
advancedFilters.value = []
|
||||
appliedConditions.value = []
|
||||
advancedFilterVisible.value = false
|
||||
page.value = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// ★ 借库报废相关
|
||||
const canScrap = computed(() =>
|
||||
userStore.role === 'SUPER_ADMIN' ||
|
||||
@ -355,7 +433,9 @@ const fetchData = async () => {
|
||||
page: page.value,
|
||||
status: status.value,
|
||||
keyword: keyword.value,
|
||||
search_type: searchType.value
|
||||
search_type: searchType.value,
|
||||
// ★ 高级筛选:后端约定参数名为 advancedFilters,值为 JSON 字符串
|
||||
advancedFilters: JSON.stringify(advancedFilters.value || []),
|
||||
}
|
||||
if (dateRange.value && dateRange.value.length === 2) {
|
||||
params.start_date = dateRange.value[0]
|
||||
@ -411,6 +491,9 @@ const resetFilter = () => {
|
||||
keyword.value = ''
|
||||
searchType.value = 'all'
|
||||
dateRange.value = []
|
||||
advancedFilters.value = []
|
||||
advancedConditions.value = [{ field: '', operator: '', value: '' }]
|
||||
appliedConditions.value = []
|
||||
page.value = 1
|
||||
fetchData()
|
||||
}
|
||||
@ -473,4 +556,11 @@ onBeforeUnmount(() => {
|
||||
margin-bottom: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
/* 高级筛选条件行 */
|
||||
.condition-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user