Appearance
高级筛选(筛选管理器)
列筛选有两种入口,可同时使用:
- 表头快捷筛选 —— 点击表头筛选图标,按列快速勾选/输入(见 列筛选)。
- 筛选管理器 ——
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 | 包含 / 不包含 / 等于 / 不等于 / 开头是 / 结尾是 / 为空 / 不为空 |
number、date | 等于 / 不等于 / 大于 / 大于等于 / 小于 / 小于等于 / 介于 / 为空 / 不为空 |
enum | 属于 / 不属于 / 等于 / 不等于 / 为空 / 不为空 |
boolean | 等于 / 不等于 |
几点约定
- 列的
filterMethod只参与in/notIn(它的签名(value, row)没有算子概念),其余算子按值域内建比较。 - 「为空」=
null/undefined/'',不含0与false。「介于」是闭区间,只填一端即当单边界。 - 编辑中的半成品条件(缺值、指向已删除的列、空条件组)一律视为通过,不会把表格瞬间筛空。
- 构建器挂在表格外部,样式在
@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 = '';
};
}