通过封装FilterPopover组件实现表格头部的高级筛选功能,支持多条件组合查询。该组件采用受控模式设计,可灵活集成到各类表格组件中。

核心功能实现原理

FilterPopover组件接收field(字段名)、onFilterChange(回调函数)和existingFilters(已有筛选条件)三个主要props。内部使用useState管理条件组和逻辑关系,通过useEffect处理已有条件的回显。

条件存储结构采用数组形式:

[
  { operator: 'eq', value: '', logic: 'and' }
]

多条件组合查询实现

组件支持动态添加/删除条件项,每个条件包含运算符选择器和值输入框。运算符支持等于、不等于、包含等多种匹配方式。通过Radio.Group组件提供"且"/"或"的逻辑关系选择。

条件更新采用immutable方式:

const updateCondition = (index, key, value) => {
  const newConditions = [...conditions];
  newConditions[index][key] = value;
  setConditions(newConditions);
};

与表格的集成方式

在表格列配置中通过render函数集成筛选图标:

const processedColumns = columns.map(column => {
  if (column.filter) {
    return {
      ...column,
      title: (
        <FilterPopover 
          field={column.dataIndex}
          onFilterChange={handleFilterChange}
        />
      )
    }
  }
  return column;
});

筛选结果处理逻辑

点击筛选按钮时进行验证并格式化参数:

const filters = validConditions.map(cond => ({
  field,
  operator: cond.operator,
  value: cond.value
}));

onFilterChange({
  field,
  logic: fieldLogic,
  filters
});

实际应用场景

该组件特别适合需要复杂查询的后台管理系统,例如:

  • 电商平台订单多条件筛选
  • CRM系统的客户高级查询
  • 数据分析平台的多维度过滤

组件设计考虑了扩展性,可通过修改operators配置支持更多比较运算符,或通过customRender支持自定义输入组件。

完整代码如下 


const FilterPopover = ({ field, onFilterChange, existingFilters = [] }) => {
    const [conditions, setConditions] = useState([
        { operator: 'eq', value: '', logic: 'and' }
    ]);
    const [fieldLogic, setFieldLogic] = useState('and');

    useEffect(() => {
        // 回显已有筛选条件
        if (existingFilters.length > 0) {
            const fieldFilter = existingFilters.find(f => f.field === field);
            if (fieldFilter) {
                setFieldLogic(fieldFilter.logic);
                setConditions(fieldFilter.filters.map(f => ({
                    operator: f.operator,
                    value: f.value,
                    logic: 'and' // 初始逻辑关系
                })));
            }
        }
    }, [existingFilters, field]);

    const addCondition = () => {
        setConditions([...conditions, { operator: 'eq', value: '', logic: 'and' }]);
    };

    const removeCondition = (index) => {
        const newConditions = [...conditions];
        newConditions.splice(index, 1);
        setConditions(newConditions);
    };

    const updateCondition = (index, key, value) => {
        const newConditions = [...conditions];
        newConditions[index][key] = value;
        setConditions(newConditions);
    };

    const handleFilter = () => {
        const validConditions = conditions.filter(cond => cond.value.trim() !== '');
        if (validConditions.length === 0) {
            message.warning('请至少输入一个有效的筛选条件');
            return;
        }

        const filters = validConditions.map(cond => ({
            field,
            operator: cond.operator,
            value: cond.value
        }));

        onFilterChange({
            field,
            logic: fieldLogic,
            filters
        });
    };

    const handleClear = () => {
        setConditions([{ operator: 'eq', value: '', logic: 'and' }]);
        setFieldLogic('and');
        onFilterChange(null, field);
    };

    return (
        <div style={{ width: 300 }}>
            <div style={{ marginBottom: 12 }}>
                <span>条件关系: </span>
                <Select
                    value={fieldLogic}
                    onChange={setFieldLogic}
                    size="small"
                    style={{ width: 80 }}
                >
                    <Option value="and">并且</Option>
                    <Option value="or">或者</Option>
                </Select>
            </div>

            {conditions.map((condition, index) => (
                <div key={index} style={{ marginBottom: 8, display: 'flex', alignItems: 'center' }}>
                    <Select
                        value={condition.operator}
                        onChange={value => updateCondition(index, 'operator', value)}
                        size="small"
                        style={{ width: 100, marginRight: 8 }}
                    >
                        <Option value="eq">等于</Option>
                        <Option value="neq">不等于</Option>
                        <Option value="startswith">开始包含</Option>
                        <Option value="contains">包含</Option>
                        <Option value="endswith">结束包含</Option>
                    </Select>

                    <Input
                        value={condition.value}
                        onChange={e => updateCondition(index, 'value', e.target.value)}
                        size="small"
                        style={{ marginRight: 8 }}
                        placeholder="输入值"
                    />

                    {conditions.length > 1 && (
                        <Button
                            type="link"
                            danger
                            size="small"
                            onClick={() => removeCondition(index)}
                        >
                            删除
                        </Button>
                    )}
                </div>
            ))}

            <Button type="dashed" size="small" onClick={addCondition} style={{ marginBottom: 12 }}>
                添加条件
            </Button>

            <div style={{ display: 'flex', justifyContent: 'space-between' }}>
                <Button type="primary" size="small" onClick={handleFilter}>
                    筛选
                </Button>
                <Button size="small" onClick={handleClear}>
                    清除条件
                </Button>
            </div>
        </div>
    );
};
const SearchTable = forwardRef((props, ref) => {
    const {
        searchFields = [],
        operations,
        columns = [],
        style = {},
        tableHeight = 400,
        onRowClick,
        watchData = undefined,
        TableUrl = null,
        initialFetch = true,
        rowKey = 'id',
        initialWatch = false,
        emptyText = '暂无数据',
        showSearch = true,
        showPagination = true,
        render = null,
        renderStyle = null,
        clearSelection = true,
        onSelectionChange = null,
        params = null,
    } = props;
    const [form] = Form.useForm();
    const [data, setData] = useState([]);
    const [loading, setLoading] = useState(false);
    const [pagination, setPagination] = useState({
        current: 1,
        pageSize: 10,
        total: 0,
    });
    const [selectedRowKeys, setSelectedRowKeys] = useState([]);

    const [filters, setFilters] = useState([]);
    const filtersRef = useRef(filters);

    const [selectedRow, setSelectedRow] = useState(null);
    // 抛出的 方法 
    useImperativeHandle(ref, () => ({
        getFilters: () => filters,
        setFilters: (newFilters) => setFilters(newFilters),
        clearFilters: () => setFilters([]),
        /**
        * table 搜索查询 page 默认为1 
        **/
        handleSearch,
        /**
         *   table 重置 查询
         **/
        handleReset,
        /**
        *   前置查询 ,page,pagesize 为之前的查询条件
        **/
        getData,
        /**
        *  查询当前table search 需配合注入search
        **/
        getFormValues: () => form.getFieldsValue(),
        /**
         *  更改当前table search 需配合注入search
         **/
        setFormValues: (values) => form.setFieldsValue(values),
        getSelectedRow: () => selectedRow,
        getSelectedRowKeys: () => selectedRowKeys,
        /**
         *  table search 查询 指定page,pagesize,
         **/
        refresh: () => getData(pagination.current, pagination.pageSize),
        /**
         *  分页更改
         **/
        setPagination: (newPagination) => setPagination(newPagination),
        /**
         *  获取当前form
        **/
        getFormInstance: () => form
    }));


    // 使用ref标记是否已初始化
    const initializedRef = useRef(false);

    // 使用ref存储上一次的watchData值
    const prevWatchDataRef = useRef(null);

    const handleFilterChange = (filterData, fieldToRemove) => {
        let newFilters;
        if (filterData === null && fieldToRemove) {
            // 清除指定字段的筛选
            newFilters = filtersRef.current.filter(f => f.field !== fieldToRemove);
        } else {
            // 更新或添加筛选条件
            const otherFilters = filtersRef.current.filter(f => f.field !== filterData.field);
            newFilters = [...otherFilters, filterData];
        }

        // 立即更新状态和ref
        setFilters(newFilters);
        filtersRef.current = newFilters; // 手动更新ref

        // 调用getData,此时getData内部使用filtersRef.current就是最新的
        getData(1, pagination.pageSize);
    };


    const fetchData = async (data) => {
        try {
            // 调用 API 服务
            console.log(data);
            console.log(data);

            const result = await getAllTable(TableUrl, data);

            return result
        } catch (error) {
            console.error('获取数据失败:', error);
            return {
                data: [],
                total: 0,
            };
        }
    };

    // 获取数据
    const getData = async (page = pagination.current, pageSize = pagination.pageSize, data = null) => {
        setLoading(true);
        try {
            const values = form.getFieldsValue();

            const filterParams = filtersRef.current.length > 0
                ? { filters: filtersRef.current }
                : {};

            const result = await fetchData({
                ...values,
                ...data,
                ...filterParams,
                page,
                pageSize
            })
            if (result) {
                const { items, total } = result
                console.log(items);

                setData(items || []);
                setPagination({
                    current: page,
                    pageSize: pageSize,
                    total: total || 0,
                });
                // 数据刷新后清空选择
                if (clearSelection && typeof onSelectionChange === 'function') {
                    setSelectedRowKeys([]);
                    onSelectionChange([], []);
                }
            }
        } catch (error) {
            if (error.name !== 'AbortError') {
                console.error('获取数据失败:', error);
                message.error('数据加载失败,请稍后重试');
            }
        } finally {
            setLoading(false);
        }
    };

    // 初始化加载数据
    useEffect(() => {
        if (initialFetch && !initializedRef.current) {
            initializedRef.current = true;
            getData();
        }
    }, [initialFetch]);


    const getCurrentWatchData = () => {
        if (typeof watchData === 'function') {
            return watchData();
        }
        return watchData;
    };
    const hasWatchDataChanged = () => {
        const currentWatchData = getCurrentWatchData();
        const prevWatchData = prevWatchDataRef.current;
        return JSON.parse(JSON.stringify(currentWatchData)) !== JSON.parse(JSON.stringify(prevWatchData))
    };
    // 监听watchData变化
    useEffect(() => {
        // 如果watchData未定义或为null,则不触发
        if (watchData === undefined) return;

        // 如果是初始化且不需要初始化监听,则跳过
        if (!initializedRef.current && !initialWatch) {
            prevWatchDataRef.current = getCurrentWatchData();
            initializedRef.current = true;
            return;
        }

        // 检查watchData是否变化
        const hasChanged = hasWatchDataChanged();

        if (hasChanged) {
            // 重置分页到第一页
            setSelectedRow(null)
            setPagination(prev => ({
                ...prev,
                current: 1,
            }));
            getData(1);
        }

        // 更新上一次的值
        prevWatchDataRef.current = getCurrentWatchData();
    }, [watchData]);
    // 处理分页变化
    const handlePageChange = (page, pageSize) => {
        getData(page, pageSize);
    };

    // 处理查询
    const handleSearch = (data = null) => {
        console.log(data);

        getData(1, 10, data);
    };

    // 处理重置
    const handleReset = () => {
        form.resetFields();
        getData(1);
    };

    // 处理行点击
    const handleRowClick = (record) => {
        // 设置当前选中的行
        setSelectedRow(record);

        // 调用父组件回调函数
        if (onRowClick) {
            onRowClick(record);
        }
    };



    useEffect(() => {

    }, [params])


    // 修改columns渲染,添加筛选图标
    const processedColumns = columns.map(column => {
        if (column.filter) {
            const hasActiveFilter = filters.some(f => f.field === column.dataIndex);

            return {
                ...column,
                title: (
                    <div style={{ display: 'flex', alignItems: 'center' }}>
                        <span>{column.title}</span>
                        <Popover
                            content={
                                <FilterPopover
                                    field={column.dataIndex}
                                    onFilterChange={handleFilterChange}
                                    existingFilters={filters}
                                />
                            }
                            trigger="click"
                            placement="bottom"
                        >
                            <FilterOutlined
                                style={{
                                    color: hasActiveFilter ? '#ff4d4f' : undefined,
                                    marginLeft: 8,
                                    cursor: 'pointer'
                                }}
                            />
                        </Popover>
                    </div>
                )
            };
        }
        return column;
    });



    // 渲染查询表单
    const renderSearchForm = () => {
        if (!showSearch || searchFields.length === 0) return null;

        return (
            <div className="search-form-container">
                <Form
                    form={form}
                    layout="horizontal"
                    labelAlign="right"
                >
                    <Row gutter={16} align="middle">
                        {searchFields.map(field => (
                            <Col key={field.name} span={6}>

                                <Form.Item
                                    name={field.name}
                                    label={field.label}
                                    rules={field.rules || []}
                                    style={{ marginBottom: 12 }}
                                >

                                    {
                                        field.type === 'SearchSelect' &&
                                        // 使用 SearchSelect 组件
                                        <SearchSelect
                                            fetchData={field.fetchData}
                                            placeholder={field.placeholder || `请选择${field.label}`}
                                            style={{ width: '100%' }}
                                            debounceTime={field.debounceTime || 300}
                                            loadOnEmpty={field.loadOnEmpty !== false}
                                            initialLoad={field.initialLoad || false}
                                        />
                                    }
                                    {
                                        field.type === 'select' &&
                                        // 使用 SearchSelect 组件
                                        <Select {...field}></Select>
                                    }
                                    {
                                        field.type === undefined && <Input
                                            placeholder={field.label ? `请输入${field.label}` : field.placeholder || ''}
                                            {...field?.config}
                                            size="middle"
                                        />
                                    }

                                    {/* <Input
                                        placeholder={field.label ? `请输入${field.label}` : field.placeholder || ''}
                                        {...field?.config}
                                        size="middle"
                                    /> */}


                                </Form.Item>
                            </Col>
                        ))}

                        {/* 操作按钮紧跟最后一个搜索条件 */}
                        <Col span={6}>
                            <Space style={{ height: '44px', alignItems: 'start' }}>
                                <Button
                                    type="primary"
                                    icon={<SearchOutlined />}
                                    onClick={() => {
                                        handleSearch()
                                    }}
                                >
                                    查询
                                </Button>
                                <Button onClick={handleReset} icon={<ReloadOutlined />}>
                                    重置
                                </Button>
                            </Space>
                        </Col>
                    </Row>
                </Form>
            </div>
        );
    };

    // 渲染操作按钮区域
    const renderOperations = () => {
        if (!operations) return null;

        return (
            <div className="operations-container">
                {operations}
            </div>
        );
    };

    // 渲染表格
    const renderTable = () => {
        // 计算表格滚动区域高度
        const scrollY = tableHeight - (showPagination ? 55 : 0) - 55; // 减去表头高度和分页高度
        let rowSelectionConfig = null;
        if (typeof onSelectionChange === 'function') {
            rowSelectionConfig = {
                selectedRowKeys,
                onChange: (selectedKeys, selectedRows) => {
                    setSelectedRowKeys(selectedKeys);
                    if (onSelectionChange) {
                        onSelectionChange(selectedKeys, selectedRows);
                    }
                },
            };
        }

        return (
            <div className="table-container">
                <Spin spinning={loading} tip="加载中...">
                    <Table
                        rowKey={record => record[rowKey]}
                        rowSelection={rowSelectionConfig}
                        // columns={columns}
                        columns={processedColumns}

                        dataSource={data}
                        pagination={false}
                        loading={loading}
                        scroll={{
                            x: 'max-content',
                            y: scrollY
                        }}
                        size="middle"
                        style={{ flex: 1 }}
                        bordered={false}
                        onRow={(record) => ({
                            onClick: (event) => {
                                // @ts-ignore
                                if (event.target.closest('.ant-table-selection-column')) {
                                    return;
                                }
                                console.log(selectedRow);

                                handleRowClick(record)
                            }
                        })}
                        rowClassName={(record) =>
                            selectedRow && selectedRow[rowKey] === record[rowKey] ? 'highlight-row' : ''
                        }
                    />
                </Spin>

                {showPagination && data.length > 0 && (
                    <div className="pagination-container">
                        <Pagination
                            current={pagination.current}
                            pageSize={pagination.pageSize}
                            total={pagination.total}
                            showSizeChanger
                            showQuickJumper
                            showTotal={(total, range) => (
                                `第 ${range[0]}-${range[1]} 条,共 ${total} 条`
                            )}
                            onChange={handlePageChange}
                            onShowSizeChange={handlePageChange}
                            size="small"
                        />
                        <div className='pagination-iocn'>
                            <ReloadOutlined onClick={() => {
                                getData(1)
                            }} />
                        </div>
                    </div>
                )}
            </div>
        );
    };


    const renderCust = () => {

        return (
            <div className="table-container" style={{ height: tableHeight, overflow: 'hidden', overflowY: 'scroll', ...renderStyle }}>
                <Spin spinning={loading} tip="加载中...">
                    {render()}
                </Spin>
            </div>
        );
    };
    return (
        <div className="search-table-wrapper" style={style}>
            {renderSearchForm()}
            {renderOperations()}
            {!render ? renderTable() : renderCust()}
        </div>
    );
})



export default SearchTable;

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐