Refine v5 Ant Design FilterDropdown 组件全指南:表格列筛选的桥接、映射与源码剖析
2026/9/12 21:06:59 网站建设 项目流程

Refine v5 Ant Design FilterDropdown 组件全指南:表格列筛选的桥接、映射与源码剖析

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

<FilterDropdown>@refinedev/antd提供的一个辅助组件,用于打通 Ant Design<Table>列头筛选面板与 Refine 数据层之间的数据同步,并自动生成"过滤 / 清除"操作按钮。本文以该组件为主体,结合仓库源码与测试用例,从基础用法、useSelect集成、mapValue双向映射、rangePickerFilterMapper日期范围处理到getDefaultFiltersyncWithLocation的联动细节,给出可直接复制运行的完整方案。

FilterDropdown 是什么:连接表格筛选面板与 Refine 数据的桥梁

在 Ant Design 的<Table>中,每列可以通过filterDropdown属性自定义筛选面板。<FilterDropdown>正是为这种自定义筛选面板而生的助手组件,它充当桥梁:同步其子组件(如<Select><DatePicker>)的输入值与<Table>的筛选值

最基础的用法如下(完整示例位于examples/table-antd-use-table):

import { List, FilterDropdown, useTable, } from "@refinedev/antd"; import { Table, Select } from "antd"; const PostList: React.FC = (props) => { const { tableProps } = useTable<IPost>(); return ( <List> <Table {...tableProps} rowKey="id"> <Table.Column dataIndex="id" title="ID" /> <Table.Column dataIndex={["category", "id"]} title="Category" key="category.id" filterDropdown={(props) => ( <FilterDropdown {...props}> <Select mode="multiple" placeholder="Select Category" options={[ { label: "Ergonomic", value: "1" }, { label: "Island", value: "2" }, ]} /> </FilterDropdown> )} /> </Table> </List> ); }; interface IPost { id: number; category: { id: number; }; }

从下拉框中选择分类后,分类的id会作为筛选值发送给<Table>,Refine 会在底层自动完成数据更新——你不需要手动管理筛选状态与请求参数。

此处为了简洁,分类选项是手动写死的。实际上更推荐配合useSelectHook 动态填充,下文会专门讲解。

自动生成的操作按钮:源码中的 Filter 与 Clear

<FilterDropdown>会自动渲染两个按钮,分别执行"过滤"与"清除过滤"操作。查看其源码(packages/antd/src/components/table/components/filterDropdown/index.tsx)可以看到按钮区域的实际实现:

return ( <div style={{ padding: 10, display: "flex", flexDirection: "column", alignItems: "flex-end", }} > <div style={{ marginBottom: 15 }}>{childrenWithProps}</div> <Space> <Button type="primary" size="small" onClick={() => onFilter()}> <FilterOutlined /> {translate("buttons.filter", "Filter")} </Button> <Button danger size="small" onClick={() => clearFilter()}> {translate("buttons.clear", "Clear")} </Button> </Space> </div> );

关键行为(均有测试用例在 filterDropdown/index.spec.tsx 中验证):

  • Filter 按钮:触发onFilter(),内部会将selectedKeys做归一化处理——数字会被转为字符串,dayjs对象会被转为 ISO 字符串数组,随后调用setSelectedKeysconfirm?.()关闭筛选面板并应用筛选;
  • Clear 按钮:触发clearFilter(),调用 Ant Design 传入的clearFilters回调清空筛选;
  • 按钮文案:通过useTranslate读取 i18n 翻译键buttons.filterbuttons.clear,未配置时回退为 "Filter" / "Clear"。

组件还会通过React.cloneElement向子元素注入onChangevalue两个受控属性(源码 L75-L83),因此任意支持受控模式的输入组件都可以直接作为其子节点使用。

结合 useSelect 动态填充下拉选项

为了让分类选项来源于真实数据,可以把useSelect返回的selectProps展开到<Select>上。useSelect的详细说明可参考 useSelect Hook 文档:

const { selectProps: categorySelectProps } = useSelect<ICategory>({ resource: "categories", optionLabel: "title", optionValue: "id", }); <Select {...categorySelectProps} />;

配合mapValuegetDefaultFilter,还能让筛选面板在初始化时正确回显已有的筛选值,并处理数字类型映射,完整代码如下:

import { getDefaultFilter } from "@refinedev/antd"; import { useTable, FilterDropdown, useSelect } from "@refinedev/antd"; import { Table, Select } from "antd"; const { tableProps, filters } = useTable<IPost>({ filters: { initial: [ { field: "category.id", value: [1, 2], operator: "in", }, ], }, }); const { selectProps: categorySelectProps } = useSelect<ICategory>({ resource: "categories", optionLabel: "title", optionValue: "id", defaultValue: getDefaultFilter("category.id", filters, "in"), }); <Table> <Table.Column dataIndex="id" title="ID" /> <Table.Column dataIndex={["category", "id"]} title="Category" key="category.id" filterDropdown={(props) => ( <FilterDropdown {...props} mapValue={(selectedKeys) => selectedKeys.map((i) => parseInt(i.toString())) } > <Select style={{ minWidth: 200 }} mode="multiple" placeholder="Select Category" {...categorySelectProps} /> </FilterDropdown> )} defaultFilteredValue={getDefaultFilter("category.id", filters, "in")} /> </Table>;

mapValue:两种数据格式之间的双向映射

mapValueFilterDropdown提供的一个核心工具函数,用于根据事件类型转换selectedKeys的格式。其类型签名如下:

function mapValue(selectedKeys: React.Key[], event: "onChange" | "value"): any;
  • selectedKeys:下拉框中当前选中的键集合;
  • event:触发mapValue的事件类型:
    • "onChange":下拉框值发生变化时触发,用于把值映射成 Refine 期望的格式(供 data provider、syncWithLocation等使用);
    • "value":需要把值映射给子组件时触发。

典型的场景是配合useSelect使用<Select />时,把值映射为number类型。从源码(filterDropdown/index.tsx L55-L83)可以看到两个事件的分工:

  • 子组件触发onChange时,组件会把变更值交给mapValue(e, "onChange")处理后写入setSelectedKeys
  • 渲染子组件时,通过cloneElementvalue: mapValue(selectedKeys, "value")注入给子组件,保证面板回显。

源码中mapValue的默认实现是恒等函数(value) => value,即不传时原样透传。测试用例 filterDropdown/index.spec.tsx L156-L198 验证了onChangevalue两个事件分别会得到不同的映射结果,并分别作用于筛选状态与输入框回显。

rangePickerFilterMapper:日期范围筛选的完整解决方案

当筛选条件是日期范围时,会遇到一个典型矛盾:Refine 的 data provider 期望 ISO 8601 字符串格式,而 Ant Design 的<DatePicker.RangePicker />内部使用的是 Dayjs 对象。rangePickerFilterMapper正是为此提供的官方映射工具。

完整示例(过滤created_at在 2022-01-01 至 2022-01-31 之间的记录):

import { getDefaultFilter } from "@refinedev/antd"; import { DateField, FilterDropdown, rangePickerFilterMapper, useTable, } from "@refinedev/antd"; import { Table, DatePicker } from "antd"; export const Posts = () => { const { tableProps, filters } = useTable({ filters: { initial: [ { field: "created_at", value: ["2022-01-01", "2022-01-31"], operator: "between", }, ], }, }); return ( <Table {...tableProps} rowKey="id"> <Table.Column dataIndex="id" title="ID" /> <Table.Column dataIndex="title" title="Title" /> <Table.Column dataIndex="createdAt" title="Created At" filterDropdown={(props) => ( <FilterDropdown {...props} mapValue={(selectedKeys, event) => { return rangePickerFilterMapper(selectedKeys, event); }} > <DatePicker.RangePicker /> </FilterDropdown> )} defaultFilteredValue={getDefaultFilter( "created_at", filters, "between", )} /> </Table> ); };

源码剖析:双方向转换逻辑

rangePickerFilterMapper的实现位于 packages/antd/src/definitions/filter-mappers/index.ts:

import type { FilterDropdownProps, MapValueEvent, } from "@components/table/components"; import dayjs from "dayjs"; export const rangePickerFilterMapper = ( selectedKeys: FilterDropdownProps["selectedKeys"], event: MapValueEvent, ) => { if (!selectedKeys) { return selectedKeys; } if (event === "value") { return selectedKeys.map((key) => { if (typeof key === "string") { return dayjs(key); } return key; }); } if (event === "onChange") { if (selectedKeys.every(dayjs.isDayjs)) { return selectedKeys.map((date: any) => dayjs(date).toISOString()); } } return selectedKeys; };

其行为完全对应两个事件方向:

  • event === "value":把字符串形式的日期转换为 Dayjs 对象,供<DatePicker.RangePicker />正确回显(如果已经是 Dayjs 对象则原样返回);
  • event === "onChange":当所有值都是 Dayjs 对象时,统一转换为 ISO 8601 字符串,交给 Refine 的 data provider /syncWithLocation使用;
  • 兜底逻辑selectedKeys为空、事件类型无效或值类型不符合预期时,原样返回,保证组件在异常输入下不崩溃。

该行为在 filter-mappers/index.spec.ts 中有完整覆盖,包括:空值原样返回、无效事件类型原样返回、字符串日期转 Dayjs、Dayjs 转 ISO 字符串等 6 组断言。其中测试注释特别说明:虽然 Ant Design 的类型定义把selectedKeys声明为React.Key[],但实际运行时它可以是任意类型(包括 Dayjs 元组),因此测试使用了as any——这也解释了为什么mapValue的返回类型被设计为宽泛的any

getDefaultFilter 与 syncWithLocation 的联动注意点

在筛选面板的初始化回显中,getDefaultFilter扮演关键角色,但有两个容易踩坑的细节值得注意。

从 URL 解析出的筛选值一定是字符串

如果启用了syncWithLocation,页面刷新时筛选值会从 URL 查询参数中解析出来,因此类型一定是string。这可能会与来自 API 的非字符串类型筛选数据产生不兼容——例如分类id在数据库中可能是number,而从 URL 恢复后变成了"1"。此时就需要mapValue配合类型转换(如parseInt)来消解差异。

关于syncWithLocation的行为与默认值(默认关闭,需显式开启),可参考 Refine 核心组件文档。

getDefaultFilter 的取值逻辑

getDefaultFilter用于从给定的filters中按字段与操作符取出默认筛选值。在上面的示例中,传给它的filters来自useTable的返回值,其中已包含从 URL 解析出的筛选值,因此defaultFilteredValueuseSelectdefaultValue能够正确回显当前筛选状态。

仓库中@refinedev/antd对它的实现(packages/antd/src/definitions/table/index.ts L35-L41)是直接委托给@refinedev/core的同名函数,并提供了operatorType参数(默认"eq"):

export const getDefaultFilter = ( columnName: string, filters?: CrudFilters, operatorType: CrudOperators = "eq", ): CrudFilter["value"] | undefined => { return getDefaultFilterCore(columnName, filters, operatorType); };

因此在多操作符(如同时存在eqinbetween)的筛选场景中,务必像示例那样显式传入正确的操作符("in""between"),否则会因默认"eq"而取不到值。

属性一览

<FilterDropdown>的完整属性定义见 packages/antd/src/components/table/components/filterDropdown/index.tsx L13-L16,核心属性如下:

属性类型说明
selectedKeysReact.Key[]当前选中的键,来自<Table.Column>filterDropdownprop
setSelectedKeys(keys: any) => void更新选中键的回调,同样来自filterDropdownprop
confirm() => void应用筛选并关闭面板的回调,由 Ant Design 注入
clearFilters() => void清除筛选的回调,由 Ant Design 注入
mapValue(selectedKeys: React.Key[], event: "onChange" \| "value") => any可选,双向格式映射函数,默认恒等返回
childrenReact.JSX.Element筛选输入组件,会由内部cloneElement注入onChangevalue

其中selectedKeyssetSelectedKeysconfirmclearFilters四个属性必须从<Table.Column>filterDropdownprop 透传(即示例中的{...props}),它们是 Ant Design 表格与筛选面板通信的契约。

测试用例如何验证组件行为

如果希望深入理解组件行为,仓库中的测试是很好的教材:

  • filterDropdown/index.spec.tsx:验证渲染出 "Filter"/"Clear" 按钮、点击 Filter 触发confirmsetSelectedKeys、点击 Clear 触发clearFiltersmapValue在输入变化与回显两个方向分别生效,以及InputSelectDatePicker三种输入控件的组合场景;
  • filter-mappers/index.spec.ts:针对rangePickerFilterMapper的 6 组边界断言。

完整可运行示例

组件文档配套的完整示例为table-antd-use-table,对应仓库目录 examples/table-antd-use-table,其中包含上述全部用法的可运行代码(App.tsx中的列表页与筛选逻辑),可以在本地按示例目录的package.json脚本启动查看实际效果。

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询