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:
@ -175,6 +175,12 @@ def get_outbound_list():
|
|||||||
search_type = request.args.get('search_type', 'all')
|
search_type = request.args.get('search_type', 'all')
|
||||||
company = request.args.get('company', '')
|
company = request.args.get('company', '')
|
||||||
|
|
||||||
|
# ★ 高级筛选:JSON 字符串 → 条件列表(解析失败退化为空,不影响主查询)
|
||||||
|
from app.utils.advanced_filter import parse_advanced_filters
|
||||||
|
advanced_filters = parse_advanced_filters(
|
||||||
|
request.args.get('advancedFilters', '')
|
||||||
|
)
|
||||||
|
|
||||||
# ★ 数据权限:普通用户只看“领用人=本人姓名(不含账号前缀)”的出库记录;管理者看全部
|
# ★ 数据权限:普通用户只看“领用人=本人姓名(不含账号前缀)”的出库记录;管理者看全部
|
||||||
consumer_name = None
|
consumer_name = None
|
||||||
if not is_privileged_viewer():
|
if not is_privileged_viewer():
|
||||||
@ -189,7 +195,8 @@ def get_outbound_list():
|
|||||||
# ★ [修改] 调用分组查询服务,支持搜索类型
|
# ★ [修改] 调用分组查询服务,支持搜索类型
|
||||||
result = OutboundService.get_grouped_list(
|
result = OutboundService.get_grouped_list(
|
||||||
page, limit, keyword, search_type=search_type,
|
page, limit, keyword, search_type=search_type,
|
||||||
company=company, consumer_name=consumer_name
|
company=company, consumer_name=consumer_name,
|
||||||
|
advanced_filters=advanced_filters,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 字段级脱敏
|
# 字段级脱敏
|
||||||
|
|||||||
@ -327,12 +327,13 @@ class OutboundService:
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_grouped_list(page=1, per_page=10, keyword=None, search_type='all', start_date=None, end_date=None, company=None, consumer_name=None):
|
def get_grouped_list(page=1, per_page=10, keyword=None, search_type='all', start_date=None, end_date=None, company=None, consumer_name=None, advanced_filters=None):
|
||||||
"""
|
"""
|
||||||
查询出库记录(按出库单号分组),包含详细物品信息
|
查询出库记录(按出库单号分组),包含详细物品信息
|
||||||
支持跨表搜索:单号、领用人、SKU、物料名称、规格型号
|
支持跨表搜索:单号、领用人、SKU、物料名称、规格型号
|
||||||
search_type: all, no, name, sku, material_name, spec_model
|
search_type: all, no, name, sku, material_name, spec_model
|
||||||
company: 可选的公司过滤参数
|
company: 可选的公司过滤参数
|
||||||
|
advanced_filters: 高级筛选条件列表 [{'field','operator','value'}, ...]
|
||||||
"""
|
"""
|
||||||
# 日期补全:解决零点截断问题
|
# 日期补全:解决零点截断问题
|
||||||
if end_date and len(str(end_date).strip()) == 10:
|
if end_date and len(str(end_date).strip()) == 10:
|
||||||
@ -536,6 +537,51 @@ class OutboundService:
|
|||||||
if keyword_conditions is not None:
|
if keyword_conditions is not None:
|
||||||
stmt = stmt.filter(keyword_conditions)
|
stmt = stmt.filter(keyword_conditions)
|
||||||
|
|
||||||
|
# ====================================================================
|
||||||
|
# ★ 高级筛选:父级字段直接过滤,子级字段(SKU/物料名称)走
|
||||||
|
# 「命中单号子查询 → 按单号 IN」的 EXISTS 语义。
|
||||||
|
#
|
||||||
|
# 绝不能写成 stmt.filter(TransOutbound.sku.ilike(...)):
|
||||||
|
# 那样会在 GROUP BY 前收窄明细范围,展开行里的兄弟明细会丢失。
|
||||||
|
# ====================================================================
|
||||||
|
if advanced_filters:
|
||||||
|
from app.utils.advanced_filter import (
|
||||||
|
build_predicate, apply_child_condition,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 父级字段:单号/操作人本身就在流水表上,直接 filter(否定操作符
|
||||||
|
# 走标准 SQL != / NOT LIKE 即可,语义无歧义)
|
||||||
|
parent_field_map = {
|
||||||
|
'no': TransOutbound.outbound_no,
|
||||||
|
'operator': TransOutbound.operator_name,
|
||||||
|
'consumer_name': TransOutbound.consumer_name,
|
||||||
|
'outbound_type': TransOutbound.outbound_type,
|
||||||
|
}
|
||||||
|
# 子级字段:SKU 直接列 + 物料名称(需三表联查)
|
||||||
|
child_field_map = {'sku': TransOutbound.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_field_map:
|
||||||
|
p = build_predicate(cond, parent_field_map)
|
||||||
|
if p is not None:
|
||||||
|
stmt = stmt.filter(p)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if field in child_field_map or field == 'material_name':
|
||||||
|
# ★ 正/负操作符语义分派:否定 → 整单排除(NOT IN)
|
||||||
|
stmt = apply_child_condition(
|
||||||
|
stmt, TransOutbound.outbound_no, TransOutbound, cond,
|
||||||
|
child_field_map, material_stock_models,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
if start_date and end_date:
|
if start_date and end_date:
|
||||||
stmt = stmt.filter(TransOutbound.outbound_time.between(start_date, end_date))
|
stmt = stmt.filter(TransOutbound.outbound_time.between(start_date, end_date))
|
||||||
|
|
||||||
|
|||||||
259
inventory-backend/app/utils/advanced_filter.py
Normal file
259
inventory-backend/app/utils/advanced_filter.py
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
"""
|
||||||
|
高级筛选(Advanced Filters)共享解析与谓词构建工具。
|
||||||
|
|
||||||
|
设计背景
|
||||||
|
--------
|
||||||
|
记录类接口(出库 / 借还 / 报废)对外返回的是**按单号分组的订单**(父),
|
||||||
|
而用户筛选用的字段往往落在**明细行**上(子),例如「SKU 包含 123」。
|
||||||
|
|
||||||
|
若直接在分组查询上加 .filter(TransOutbound.sku.ilike('%123%')),分组范围会
|
||||||
|
被一起收窄——GROUP BY 之后只剩匹配的那条明细,展开行里的其它兄弟明细会
|
||||||
|
凭空消失。正确做法是:先求出「含匹配明细的单号集合」,再让主查询按该集合
|
||||||
|
过滤,即 IN / EXISTS 子查询语义。
|
||||||
|
|
||||||
|
本模块只负责保证三个模块的操作符语义不漂移:
|
||||||
|
1. parse_advanced_filters() 解析并规整前端传来的 JSON 字符串
|
||||||
|
2. build_predicate() 把单个条件翻译成 SQLAlchemy 谓词
|
||||||
|
3. build_material_name_subquery() 物料名称的三表联查子查询(三模块共用)
|
||||||
|
|
||||||
|
字段 → 列的映射由各调用方提供(allowed_fields),因为三个模块的操作人/单号
|
||||||
|
列名不同。
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 与前端 operatorOptions 保持一致的受支持操作符
|
||||||
|
SUPPORTED_OPERATORS = {
|
||||||
|
'eq', 'ne', 'contains', 'not_contains', 'ge', 'le',
|
||||||
|
}
|
||||||
|
|
||||||
|
# ★ 否定操作符 → 其等价的肯定形式。
|
||||||
|
# 子级字段(SKU/物料名称)的否定语义必须借「肯定形式求反」来实现,
|
||||||
|
# 不能直接用 SQL 的 != / NOT LIKE,原因见 apply_child_condition 的说明。
|
||||||
|
NEGATIVE_OPERATORS = {
|
||||||
|
'ne': 'eq',
|
||||||
|
'not_contains': 'contains',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def invert_condition(condition):
|
||||||
|
"""
|
||||||
|
把否定条件翻转为等价的肯定条件(ne→eq,not_contains→contains)。
|
||||||
|
|
||||||
|
非否定操作符原样返回。
|
||||||
|
"""
|
||||||
|
op = condition.get('operator')
|
||||||
|
if op in NEGATIVE_OPERATORS:
|
||||||
|
flipped = dict(condition)
|
||||||
|
flipped['operator'] = NEGATIVE_OPERATORS[op]
|
||||||
|
return flipped
|
||||||
|
return dict(condition)
|
||||||
|
|
||||||
|
|
||||||
|
def is_negative(condition):
|
||||||
|
return condition.get('operator') in NEGATIVE_OPERATORS
|
||||||
|
|
||||||
|
|
||||||
|
def parse_advanced_filters(raw):
|
||||||
|
"""
|
||||||
|
解析 advancedFilters 查询参数。
|
||||||
|
|
||||||
|
前端传的是 JSON.stringify 后的数组字符串;同时兼容已被解析为 list 的情况
|
||||||
|
(测试或内部调用)。任何解析失败都退化为空列表(不影响主查询),
|
||||||
|
并记 warning 便于排查。
|
||||||
|
"""
|
||||||
|
if not raw:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if isinstance(raw, str):
|
||||||
|
try:
|
||||||
|
raw = json.loads(raw)
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
logger.warning(f"[advanced_filter] JSON 解析失败,已忽略: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
if not isinstance(raw, list):
|
||||||
|
logger.warning(f"[advanced_filter] 期望 list,实际 {type(raw).__name__},已忽略")
|
||||||
|
return []
|
||||||
|
|
||||||
|
parsed = []
|
||||||
|
for cond in raw:
|
||||||
|
if not isinstance(cond, dict):
|
||||||
|
continue
|
||||||
|
field = str(cond.get('field') or '').strip()
|
||||||
|
operator = str(cond.get('operator') or '').strip()
|
||||||
|
value = cond.get('value')
|
||||||
|
|
||||||
|
# 字段与操作符必须有效;值允许为 0 / '0',故用 is None 判定而非真值
|
||||||
|
if not field or operator not in SUPPORTED_OPERATORS or value is None:
|
||||||
|
continue
|
||||||
|
if isinstance(value, str) and value.strip() == '':
|
||||||
|
continue
|
||||||
|
|
||||||
|
parsed.append({'field': field, 'operator': operator, 'value': value})
|
||||||
|
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def build_predicate(condition, allowed_fields):
|
||||||
|
"""
|
||||||
|
把单个条件翻译成 SQLAlchemy 谓词。
|
||||||
|
|
||||||
|
allowed_fields: {field_name: Column} —— 调用方给出的白名单映射,
|
||||||
|
未登记的字段返回 None(杜绝任意列注入)。
|
||||||
|
|
||||||
|
数值型操作符 ge/le 在值无法转 float 时返回 None 跳过该条件,
|
||||||
|
与既有 buy_service 的处理保持一致,避免整条查询 500。
|
||||||
|
"""
|
||||||
|
field = condition.get('field')
|
||||||
|
operator = condition.get('operator')
|
||||||
|
value = condition.get('value')
|
||||||
|
|
||||||
|
column = allowed_fields.get(field)
|
||||||
|
if column is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if operator == 'eq':
|
||||||
|
return column == value
|
||||||
|
if operator == 'ne':
|
||||||
|
return column != value
|
||||||
|
if operator == 'contains':
|
||||||
|
return column.ilike(f'%{value}%')
|
||||||
|
if operator == 'not_contains':
|
||||||
|
return ~column.ilike(f'%{value}%')
|
||||||
|
if operator == 'ge':
|
||||||
|
try:
|
||||||
|
return column >= float(value)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return None
|
||||||
|
if operator == 'le':
|
||||||
|
try:
|
||||||
|
return column <= float(value)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def build_material_name_select(model, order_no_column, stock_models, keyword,
|
||||||
|
operator='contains'):
|
||||||
|
"""
|
||||||
|
构造「物料名称命中」的单号查询(三表联查,出库/借还/报废共用)。
|
||||||
|
|
||||||
|
material_name 不在流水表上,需经 stock_buy / stock_semi / stock_product
|
||||||
|
三张库存表 JOIN material_base 才能取到。任一来源命中即算命中,故三路
|
||||||
|
union 后再交给调用方做 order_no_column.in_(...)。
|
||||||
|
|
||||||
|
这是"子级字段 → 父级订单"的核心:调用方对**单号**做 IN 过滤,
|
||||||
|
而不是对明细行做 filter,从而保住同单的其它兄弟明细。
|
||||||
|
|
||||||
|
参数
|
||||||
|
----
|
||||||
|
model : 流水模型(TransOutbound / TransBorrow / TransScrap)
|
||||||
|
order_no_column: 该模型上的单号列
|
||||||
|
stock_models : [(StockModel, source_table_value), ...]
|
||||||
|
keyword : 匹配值
|
||||||
|
operator : 'contains'(默认)或 'eq'
|
||||||
|
|
||||||
|
返回
|
||||||
|
----
|
||||||
|
可直接用于 order_no_column.in_(...) 的 select,或 None(无有效来源/无关键词)。
|
||||||
|
"""
|
||||||
|
if not keyword:
|
||||||
|
return None
|
||||||
|
if not stock_models:
|
||||||
|
return None
|
||||||
|
|
||||||
|
from sqlalchemy import and_
|
||||||
|
from app.models.base import MaterialBase
|
||||||
|
|
||||||
|
matched = MaterialBase.name == keyword if operator == 'eq' \
|
||||||
|
else MaterialBase.name.ilike(f'%{keyword}%')
|
||||||
|
|
||||||
|
selects = []
|
||||||
|
for StockModel, source_value in stock_models:
|
||||||
|
s = (
|
||||||
|
db_session_query(model, order_no_column)
|
||||||
|
.join(StockModel, and_(model.stock_id == StockModel.id,
|
||||||
|
model.source_table == source_value))
|
||||||
|
.join(MaterialBase, StockModel.base_id == MaterialBase.id)
|
||||||
|
.filter(matched)
|
||||||
|
)
|
||||||
|
selects.append(s)
|
||||||
|
|
||||||
|
combined = selects[0]
|
||||||
|
for extra in selects[1:]:
|
||||||
|
combined = combined.union(extra)
|
||||||
|
return combined
|
||||||
|
|
||||||
|
|
||||||
|
def db_session_query(model, *columns):
|
||||||
|
"""按需构造 Query(延迟导入 db,避免模块级导入环)"""
|
||||||
|
from app.extensions import db
|
||||||
|
return db.session.query(*columns)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_child_condition(stmt, order_no_column, model, condition,
|
||||||
|
child_field_map, material_stock_models=None):
|
||||||
|
"""
|
||||||
|
★ 把**子级**条件应用到主查询上,正/负操作符语义有别。
|
||||||
|
|
||||||
|
为什么否定操作符不能直接用 SQL 的 != / NOT LIKE
|
||||||
|
---------------------------------------------------
|
||||||
|
订单是父、明细是子。若对明细行写 `sku != 'X'`,得到的是
|
||||||
|
「本单存在某条 SKU 不等于 X 的明细」——多明细单几乎必然成立,
|
||||||
|
于是「SKU 不等于 X」会返回几乎全部订单,与用户直觉相悖。
|
||||||
|
|
||||||
|
用户点「不等于」时的真实意图是:**整单排除**——只要单内任意一条明细
|
||||||
|
命中 X,整张单就不该出现。即 NOT EXISTS / NOT IN 语义。
|
||||||
|
|
||||||
|
实现方式
|
||||||
|
--------
|
||||||
|
肯定操作符: order_no IN (含匹配明细的单号)
|
||||||
|
否定操作符: order_no NOT IN (含匹配明细的单号)
|
||||||
|
|
||||||
|
两者的子查询完全一致(都用肯定形式的谓词),只是外层取反,
|
||||||
|
因此 ne / not_contains 只需翻转为 eq / contains 后走同一条路径。
|
||||||
|
|
||||||
|
参数
|
||||||
|
----
|
||||||
|
stmt : 主查询(已 GROUP BY 或待过滤的 Query)
|
||||||
|
order_no_column : 单号列
|
||||||
|
model : 流水模型(用于物料名称的三表联查)
|
||||||
|
condition : 单个高级筛选条件
|
||||||
|
child_field_map : {field: Column} 明细级直接字段白名单(如 sku)
|
||||||
|
material_stock_models : [(StockModel, source_table_value), ...],
|
||||||
|
提供后可支持 material_name 字段
|
||||||
|
|
||||||
|
返回
|
||||||
|
----
|
||||||
|
过滤后的 stmt;条件不可用(字段未登记/值为非法类型)时原样返回。
|
||||||
|
"""
|
||||||
|
from app.extensions import db
|
||||||
|
|
||||||
|
negative = is_negative(condition)
|
||||||
|
probe = invert_condition(condition) # 一律以肯定形式构造子查询
|
||||||
|
field = probe.get('field')
|
||||||
|
|
||||||
|
inner = None
|
||||||
|
if field == 'material_name':
|
||||||
|
if not material_stock_models:
|
||||||
|
return stmt
|
||||||
|
sel = build_material_name_select(
|
||||||
|
model, order_no_column, material_stock_models,
|
||||||
|
probe.get('value'), operator=probe.get('operator', 'contains'),
|
||||||
|
)
|
||||||
|
if sel is None:
|
||||||
|
return stmt
|
||||||
|
inner = sel
|
||||||
|
else:
|
||||||
|
p = build_predicate(probe, child_field_map)
|
||||||
|
if p is None:
|
||||||
|
return stmt
|
||||||
|
sub = db.session.query(order_no_column).filter(p).distinct().subquery()
|
||||||
|
inner = sub.select().with_only_columns(sub.c[0])
|
||||||
|
|
||||||
|
# ★ 否定 → 整单排除(NOT IN);肯定 → 整单命中(IN)
|
||||||
|
return stmt.filter(~order_no_column.in_(inner) if negative
|
||||||
|
else order_no_column.in_(inner))
|
||||||
@ -43,6 +43,44 @@
|
|||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="fetchData">查询</el-button>
|
<el-button type="primary" @click="fetchData">查询</el-button>
|
||||||
<el-button @click="resetFilter">重置</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-button v-if="userStore.hasPermission('outbound_create:operation')" type="success" @click="$router.push('/outbound/create')">新建出库</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@ -230,16 +268,59 @@ const listQuery = reactive({
|
|||||||
keyword: '',
|
keyword: '',
|
||||||
search_type: 'all',
|
search_type: 'all',
|
||||||
dateRange: [],
|
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 () => {
|
const fetchData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
...listQuery,
|
...listQuery,
|
||||||
start_date: listQuery.dateRange && listQuery.dateRange[0] ? listQuery.dateRange[0] : null,
|
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)
|
const res = await getOutboundList(params)
|
||||||
@ -269,6 +350,9 @@ const resetFilter = () => {
|
|||||||
listQuery.keyword = ''
|
listQuery.keyword = ''
|
||||||
listQuery.search_type = 'all'
|
listQuery.search_type = 'all'
|
||||||
listQuery.dateRange = []
|
listQuery.dateRange = []
|
||||||
|
listQuery.advancedFilters = []
|
||||||
|
advancedConditions.value = [{ field: '', operator: '', value: '' }]
|
||||||
|
appliedConditions.value = []
|
||||||
listQuery.page = 1
|
listQuery.page = 1
|
||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
@ -322,6 +406,13 @@ onBeforeUnmount(() => {
|
|||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 高级筛选条件行 */
|
||||||
|
.condition-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.signature-cell {
|
.signature-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user