Skip to content

高级筛选(筛选管理器)

列筛选有两种入口,可同时使用:

  1. 表头快捷筛选 —— 点击表头筛选图标,按列快速勾选/输入(见 列筛选)。
  2. 筛选管理器 —— VtFilterBuilder,挂在表格之外由你指定的容器里,用 AND/OR 条件组表达 「分数 ≥ 80 (部门属于工程部/设计部 级别为 P8)」这类复合条件,支持任意层嵌套。

每个条件组左侧有一条括号线,线上的「且 / 或」胶囊既是当前逻辑的回显,也是切换按钮—— 点一下就在「且」「或」之间切换,线的颜色随之变化(且=主色 / 或=警示色),嵌套层级一眼可辨。

两者是独立状态,最终以 AND 组合:表头筛掉的行,管理器再筛一遍。

使用的 API

ts
import { VirtTable } from '@virt-table/vanilla';
import { VtFilterBuilder } from '@virt-table/vanilla/components';

// 条件树(可嵌套)
type FilterModel = {
  kind: 'group';
  logic: 'and' | 'or';
  children: Array<FilterModel | {
    kind: 'condition';
    colKey: string;         // 列 key
    operator: FilterOperator; // 算子,见下表
    value?: unknown;        // 单值 / in 用数组 / between 用 value + value2
    value2?: unknown;
  }>;
} | null;

type Column = {
  key: string;
  title: string;
  width: number;
  filterType?: 'enum' | 'text' | 'number-range' | 'date-range'; // 也决定高级筛选的值域
  filterValueKind?: 'text' | 'number' | 'date' | 'enum' | 'boolean'; // 显式指定值域
  filterOperators?: FilterOperator[];  // 收窄该列可用算子
  filters?: Array<{ label: string; value: unknown }>; // 枚举候选(in/notIn 的选项来源)
};

type Options = {
  filterModel?: FilterModel;                          // 初始条件树
  onFilterModelChange?: (model: FilterModel) => void; // 变化回调
};

// 表格实例方法
table.setFilterModel(model);   // 应用条件树
table.getFilterModel();        // 读取(深拷贝)
table.clearFilterModel();      // 仅清空高级筛选
table.hasActiveFilters();      // 表头筛选或高级筛选任一生效

// 构建器
const fb = new VtFilterBuilder({
  el: '#filter-bar',   // 挂载容器
  table,               // 传入则自动取可见列 + 自动应用
  autoApply: false,    // true 则每次改动即时生效
  onApply: (model) => {},
});
fb.setModel(model); fb.getModel(); fb.refresh(); fb.apply(); fb.clear(); fb.destroy();

算子

值域可用算子
text包含 / 不包含 / 等于 / 不等于 / 开头是 / 结尾是 / 为空 / 不为空
numberdate等于 / 不等于 / 大于 / 大于等于 / 小于 / 小于等于 / 介于 / 为空 / 不为空
enum属于 / 不属于 / 等于 / 不等于 / 为空 / 不为空
boolean等于 / 不等于

几点约定

  • 列的 filterMethod 只参与 in / notIn(它的签名 (value, row) 没有算子概念),其余算子按值域内建比较。
  • 「为空」= null / undefined / '',不含 0false。「介于」是闭区间,只填一端即当单边界。
  • 编辑中的半成品条件(缺值、指向已删除的列、空条件组)一律视为通过,不会把表格瞬间筛空。
  • 构建器挂在表格外部,样式在 @virt-table/vanilla/components/vt-filter-builder.css,暗色主题自动适配。

示例

微应用尚未挂载。

源码

点击查看源码
ts
import { faker } from '@faker-js/faker';
import {
  VirtTable,
} from '@virt-table/vanilla';
import {
  vtColumnFilter,
} from '@virt-table/vanilla/plugins';
import {
  VtFilterBuilder,
} from '@virt-table/vanilla/components';

/**
 * 高级筛选(筛选管理器):表格外部的 AND/OR 条件构建器。
 * 同时保留表头快捷筛选(城市列),两者是 AND 关系。
 */
export function bootstrapTableFilterBuilder(root: HTMLElement): () => void {
  const departments = ['工程部', '设计部', '市场部', '财务部', '人事部'];
  const levels = ['P5', 'P6', 'P7', 'P8'];
  const cities = ['北京', '上海', '杭州', '深圳'];

  const list = Array.from({ length: 2000 }, (_, i) => ({
    id: i,
    name: faker.person.fullName(),
    department: departments[i % departments.length],
    level: levels[i % levels.length],
    city: cities[i % cities.length],
    score: Math.floor(Math.random() * 100),
    hiredAt: faker.date.between({ from: '2019-01-01', to: '2025-06-30' }).toISOString().slice(0, 10),
  }));

  const columns = [
    { key: 'name', title: '姓名', width: 180, filterType: 'text' as const },
    {
      key: 'department',
      title: '部门',
      width: 130,
      filters: departments.map((d) => ({ label: d, value: d })),
    },
    {
      key: 'level',
      title: '级别',
      width: 100,
      filters: levels.map((l) => ({ label: l, value: l })),
    },
    {
      // 表头快捷筛选(枚举勾选)与高级筛选并存
      key: 'city',
      title: '城市',
      width: 110,
      filters: cities.map((c) => ({ label: c, value: c })),
    },
    { key: 'score', title: '分数', width: 100, filterType: 'number-range' as const, summary: 'avg' as const },
    { key: 'hiredAt', title: '入职日期', width: 140, filterType: 'date-range' as const },
  ];

  root.innerHTML = `
    <div class="virt-table-controls">
      <label>
        <input type="checkbox" id="cbAuto" /> 即时应用(autoApply)
      </label>
      <button class="virt-table-btn virt-table-btn-primary" id="btnPreset">载入示例条件</button>
      <span class="demo-note">下面的条件构建器在表格外部 · 与表头「城市」筛选是「且」关系</span>
    </div>
    <div id="filterBar" style="margin-bottom:10px;max-width:900px;"></div>
    <div id="status" class="status-text"></div>
    <div style="width:900px;height:520px;" class="demo-container" id="tableContainer"></div>
  `;

  const container = root.querySelector('#tableContainer') as HTMLElement;
  const status = root.querySelector('#status') as HTMLElement;
  const listeners: (() => void)[] = [];

  const table = new VirtTable(container, {
    plugins: [vtColumnFilter()],
    list,
    columns,
    itemKey: 'id',
    estimatedSize: 40,
    buffer: 4,
    border: true,
    showSummary: true,
    summaryText: '平均',
    onFilterModelChange: () => updateStatus(),
    onFilterChange: () => updateStatus(),
  });

  function updateStatus(): void {
    const rendered = (table.core.state as unknown as { renderTotal?: number }).renderTotal ?? list.length;
    const model = table.getFilterModel();
    const headerFilters = Object.keys(table.getActiveFilters());
    status.textContent =
      `结果 ${rendered} / ${list.length} 行 · 高级筛选:${model ? '已启用' : '未启用'}` +
      (headerFilters.length ? ` · 表头筛选:${headerFilters.join('、')}` : '');
  }

  let builder = new VtFilterBuilder({
    el: root.querySelector('#filterBar') as HTMLElement,
    table,
    autoApply: false,
    onApply: () => updateStatus(),
  });

  updateStatus();

  // 切换即时应用:重建实例(构造项变化)
  const cbAuto = root.querySelector('#cbAuto') as HTMLInputElement;
  const onToggleAuto = () => {
    const model = builder.getModel();
    builder.destroy();
    builder = new VtFilterBuilder({
      el: root.querySelector('#filterBar') as HTMLElement,
      table,
      model,
      autoApply: cbAuto.checked,
      onApply: () => updateStatus(),
    });
  };
  cbAuto.addEventListener('change', onToggleAuto);
  listeners.push(() => cbAuto.removeEventListener('change', onToggleAuto));

  // 预设:分数 ≥ 80 且(部门属于工程部/设计部 或 级别为 P8)
  const btnPreset = root.querySelector('#btnPreset') as HTMLElement;
  const onPreset = () => {
    builder.setModel({
      kind: 'group',
      logic: 'and',
      children: [
        { kind: 'condition', colKey: 'score', operator: 'gte', value: 80 },
        {
          kind: 'group',
          logic: 'or',
          children: [
            { kind: 'condition', colKey: 'department', operator: 'in', value: ['工程部', '设计部'] },
            { kind: 'condition', colKey: 'level', operator: 'eq', value: 'P8' },
          ],
        },
      ],
    });
    builder.apply();
  };
  btnPreset.addEventListener('click', onPreset);
  listeners.push(() => btnPreset.removeEventListener('click', onPreset));

  return () => {
    builder.destroy();
    table.destroy();
    listeners.forEach((off) => off());
    root.innerHTML = '';
  };
}