"""筛选条容器(通用可复用 UI 构件,归 widgets 包)。
管理 N 个 :class:`FilterRowWidget` + 「添加条件」按钮 + 可选快查框,
对外发出 ``conditions_changed`` 信号,并汇总为统一条件契约
``{field: {value, operator}}``。
设计要点(对齐 docs/BLUEPRINT.md §五 wf5):
- **数据源无关**:行所需的下拉加载 / 日期选择 / 样式回调通过构造参数透传给
每个 ``FilterRowWidget``;容器本身不触碰具体视图或 Controller。
- **统一条件契约**:``get_conditions()`` 与 ``query_view.get_filters`` 输出格式一致,
便于等价替换。
- **快查框**:可选 ``QuickSearch``(复用导入对话框的模糊匹配交互),选中项通过
``quick_search_callback`` 回调上抛,由宿主视图决定如何定位(如级联下拉 / 内存过滤)。
"""
from __future__ import annotations
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QComboBox, QGridLayout, QHBoxLayout, QPushButton, QWidget
from certflow.widgets.filter_row import FilterRowWidget
from certflow.widgets.quick_search import QuickSearch
[文档]
class FilterBar(QWidget):
"""筛选条件容器。"""
conditions_changed = Signal()
def __init__(
self,
parent: QWidget | None = None,
*,
columns: list[dict] | None = None,
field_op_map: dict | None = None,
field_choices_map: dict | None = None,
operators: dict | None = None,
is_dark_theme_fn=None,
load_combo_values_fn=None,
open_date_picker_fn=None,
date_button_style_fn=None,
grid_cols: int = 2,
show_quick_search: bool = False,
quick_search_placeholder: str = "",
quick_search_callback=None,
quick_search_typing_callback=None,
show_preset_selector: bool = False,
presets: list | None = None,
preset_changed_callback=None,
) -> None:
super().__init__(parent)
self._columns = list(columns or [])
self._field_op_map = field_op_map or {}
self._field_choices_map = field_choices_map or {}
self._operators = operators or {}
self._is_dark_theme_fn = is_dark_theme_fn
self._load_combo_values_fn = load_combo_values_fn
self._open_date_picker_fn = open_date_picker_fn
self._date_button_style_fn = date_button_style_fn
self._grid_cols = max(1, grid_cols)
self._presets = list(presets or [])
self._rows: list[FilterRowWidget] = []
root = QHBoxLayout(self)
root.setContentsMargins(0, 0, 0, 0)
self._rows_widget = QWidget()
self._grid = QGridLayout(self._rows_widget)
self._grid.setContentsMargins(0, 0, 0, 0)
root.addWidget(self._rows_widget, 1)
# 预设选择器(preset 下拉,方案 B):切换 preset 改变快查框字段集
if show_preset_selector and self._presets:
self._preset_combo = QComboBox()
self._preset_combo.addItems([p.get("name", "") for p in self._presets])
self._preset_combo.currentTextChanged.connect(
lambda name: preset_changed_callback(name) if preset_changed_callback else None
)
root.addWidget(self._preset_combo)
else:
self._preset_combo = None
if show_quick_search:
self._quick_search = QuickSearch(
None,
placeholder=quick_search_placeholder,
min_width=260,
)
self._quick_search.activated.connect(
lambda text: quick_search_callback(text) if quick_search_callback else None
)
# 模糊模式:用户键入时实时转发(驱动下拉模糊匹配 + 宿主防抖初步筛选)
self._quick_search.typing.connect(
lambda text: (
quick_search_typing_callback(text) if quick_search_typing_callback else None
)
)
root.addWidget(self._quick_search)
else:
self._quick_search = None
self._add_btn = QPushButton("+ 添加条件")
self._add_btn.setFixedHeight(24)
self._add_btn.clicked.connect(lambda: self.add_row())
root.addWidget(self._add_btn)
# ============================================================
# 列定义(随视图切换更新)
# ============================================================
[文档]
def set_columns(self, columns: list[dict]) -> None:
"""更新可用字段列表(视图切换时调用),并刷新所有行的字段下拉。"""
self._columns = list(columns)
for row in self._rows:
row.set_columns(self._columns)
# ============================================================
# 行管理
# ============================================================
[文档]
def add_row(self, field_name=None, operator=None, value=None) -> FilterRowWidget:
"""添加一行筛选条件,返回新建的 ``FilterRowWidget``。"""
row = FilterRowWidget(
None,
columns=self._columns,
field_op_map=self._field_op_map,
field_choices_map=self._field_choices_map,
operators=self._operators,
is_dark_theme_fn=self._is_dark_theme_fn,
load_combo_values_fn=self._load_combo_values_fn,
open_date_picker_fn=self._open_date_picker_fn,
date_button_style_fn=self._date_button_style_fn,
field_name=field_name,
operator=operator,
value=value,
)
row.value_changed.connect(self._on_row_value_changed)
row.structure_changed.connect(self._on_row_structure_changed)
row.remove_requested.connect(lambda: self._remove_row_widget(row))
self._rows.append(row)
self._reposition()
self.conditions_changed.emit()
return row
def _on_row_value_changed(self) -> None:
self.conditions_changed.emit()
def _on_row_structure_changed(self) -> None:
self.conditions_changed.emit()
def _remove_row_widget(self, row: FilterRowWidget) -> None:
if row in self._rows:
self._rows.remove(row)
self._grid.removeWidget(row)
row.deleteLater()
self._reposition()
self.ensure_at_least_one_row()
self.conditions_changed.emit()
[文档]
def remove_row(self, widget: QWidget) -> None:
"""按控件引用删除一行(供宿主视图快筛逻辑调用)。"""
if isinstance(widget, FilterRowWidget):
self._remove_row_widget(widget)
[文档]
def clear_rows(self) -> None:
"""清空所有筛选行。"""
for row in list(self._rows):
self._grid.removeWidget(row)
row.deleteLater()
self._rows = []
[文档]
def ensure_at_least_one_row(self) -> None:
"""保证至少有一个空筛选行。"""
if not self._rows:
self.add_row()
def _reposition(self) -> None:
for i, row in enumerate(self._rows):
r = i // self._grid_cols
c = i % self._grid_cols
self._grid.removeWidget(row)
self._grid.addWidget(row, r, c)
# ============================================================
# 条件汇总
# ============================================================
[文档]
def rows_data(self) -> list[dict]:
"""返回所有行的 row_data 字典列表(迁移期兼容)。"""
return [r.row_data() for r in self._rows]
[文档]
def rows(self) -> list[FilterRowWidget]:
return list(self._rows)
[文档]
def get_conditions(self) -> dict:
"""汇总为统一条件契约 ``{field: {value, operator}}``。"""
conditions: dict = {}
for row in self._rows:
cond = row.get_condition()
if cond:
conditions.update(cond)
return conditions
[文档]
def collect_conditions(self) -> list[dict]:
"""汇总为列表形式 ``[{field, operator, value}, ...]``(保存 / 加载用)。"""
out: list[dict] = []
for row in self._rows:
cond = row.to_condition_dict()
if cond:
out.append(cond)
return out
[文档]
def find_row_by_field(self, field_name: str) -> FilterRowWidget | None:
"""查找指定字段的筛选行。"""
for row in self._rows:
if row.field_combo.currentData() == field_name:
return row
return None
# ============================================================
# 快查框
# ============================================================
[文档]
def set_quick_search_items(self, items: list[str]) -> None:
"""填充快查框候选项(editable + completer 模糊匹配)。"""
if self._quick_search is None:
return
self._quick_search.set_items(items)
# ============================================================
# 预设选择器(preset 下拉,方案 B)
# ============================================================
[文档]
def set_quick_search_placeholder(self, text: str) -> None:
"""设置快查框占位提示(随 active preset 变化)。"""
if self._quick_search is not None:
self._quick_search.setPlaceholderText(text)
[文档]
def set_presets(self, presets: list) -> None:
"""动态更新预设列表(视图切换时)。"""
self._presets = list(presets or [])
if self._preset_combo is not None:
self._preset_combo.blockSignals(True)
self._preset_combo.clear()
self._preset_combo.addItems([p.get("name", "") for p in self._presets])
self._preset_combo.blockSignals(False)
[文档]
def active_preset(self) -> dict | None:
"""返回当前选中的预设 dict(无预设选择器时返回 None)。"""
if self._preset_combo is None or not self._presets:
return None
name = self._preset_combo.currentText()
for p in self._presets:
if p.get("name") == name:
return p
return None