使用 antd@4 table 自定义筛选表头功能做一个联动搜索表头筛选

前言: 上篇文章是使用 antd@4 table 自定义表头筛选完成一个表格动态列的功能,这次需要完成一个表头联动条件筛选功能。

一、开始前

开始之前先去 Antd 官网看下「自定义的列筛选功能」的代码和逻辑:

插一句:

目前我做的是 PC 后台管理系统,系统里面涉及到大量带条件筛选的表头,项目中「table 自定义列筛选功能」这个组件是另一个小伙伴封装的,封装的还可以,就是没用到 Antd 「自定义的列筛选功能」提供的 API ,导致后面有很多效果需要自己去手动实现,例如: 筛选图标点亮、搜索框输入查询条件不点击确认自动清空效果等,因为项目比较急,没办法我只能全手动加上,结果造成代码非常的臃肿🧼。

二、模拟大量数据

项目中的接口肯定是不能直接拿来做 demo 演示的,而且就算我拿来了,大家也连不了,都是内网😂,还是老老实实的模拟接口吧。

模拟数据使用的模块 json-servermockjs ,详细使用参考我去年写的文章:学习使用 json-server 和 mockjs

看看我去年写的代码,想想过的真快,都一年整了,哎!那时候好连 ES6 都不是太会 😂😂😂,感觉进步好大,用写博客逼着自己成长,各位观众没事也来试试呗👍。

好了,看下本次模拟数据的逻辑代码和注释:

// 使用 Mock
const Mock = require('mockjs');
const pinyin = require("pinyin");
// 引入node内置的文件系统
const { writeFile } = require('fs');
// 使用Random这个api
const random = Mock.Random;

// 统计 national 、province、education、作为查询条件
let nationalArr = [], provinceArr = [], educationArr = [];


// 汉字转拼音
function han2pinyin(han) {
    return [].concat(...pinyin(han, {
        // 拼音不加音调
        style: pinyin.STYLE_NORMAL
    })).join("");
};

const tableData = [];
for (let i = 0;i < 100;i++) {
    // 随机56个民族
    const national = random.pick(["汉族","蒙古族","回族","藏族","维吾尔族","苗族","彝族","壮族","布依族","朝鲜族","满族","侗族","瑶族","白族","土家族","哈尼族","哈萨克族","傣族","黎族","僳僳族","佤族","畲族","高山族","拉祜族","水族","东乡族","纳西族","景颇族","柯尔克孜族","土族","达斡尔族","仫佬族","羌族","布朗族","撒拉族","毛南族","仡佬族","锡伯族","阿昌族","普米族","塔吉克族","怒族","乌孜别克族","俄罗斯族","鄂温克族","德昂族","保安族","裕固族","京族","塔塔尔族","独龙族","鄂伦春族","赫哲族","门巴族","珞巴族","基诺族"]);
    !nationalArr.includes(national) && nationalArr.push(national);

    // 随机省份
    let province;
    do {
        province = random.province();
    } while(province === "山西省");
    !provinceArr.includes(province) && provinceArr.push(province);

    // 随机出受教育程度
    const education = random.pick(["初中","高中","大专","本科","研究生"]);
    !educationArr.includes(education) && educationArr.push(education);

    // 数据放入数组arr
    tableData.push({
        "id" : i,
        province,
        education,
        national

    });
};

nationalArr = nationalArr.map(national => ({ title: national, value: han2pinyin(national)}));
provinceArr = provinceArr.map(province => ({ title: province, value: han2pinyin(province)})); /* 注意哦:陕西省和山西省的拼音一样的 */
educationArr = educationArr.map(education => ({ title: education, value: han2pinyin(education)}));

const db = { tableData, nationalArr, provinceArr, educationArr };

// 文件写入
writeFile("./db.json",JSON.stringify(db),function(err){
    if (err) {
        console.log(`写入错误,错误为:${err}`);
        return ;
    };
    console.log("一百条信息录入成功!");
});

我通过 npx json-server --watch db.json --port 3000 来启动接口,一共启用四个接口链接分别为🔗:

http://localhost:3000/tableData
http://localhost:3000/nationalArr
http://localhost:3000/provinceArr
http://localhost:3000/educationArr

浏览器打开即可看到数据。

为了证明俺没骗你,截图为证

三、table 用到的一些样式

写 CSS 也是挺费劲的,做人不能不厚道,样式也送给大家,注:是 less 文件。

/*多选框去掉三角和文字*/
.ant-select-tree {
    padding-left: 12px !important;
    span.ant-select-tree-switcher,
    .ant-select-tree-indent {
        display: none;
    }
}
.table-filter-dropdown {
    position: relative;
    padding: 6px;
    box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.15);
    .tree-select {
        width: 150px;
        margin-right: 5px;
        vertical-align: middle;
        .ant-select-selector {
            height: 26px;
            .ant-select-selection-item {
                display: none;
            }
        }
    }
    .ant-btn {
        height: 26px;
        width: 70px;
        vertical-align: middle;
    }
    .common-remove-filter {
        position: absolute;
        right: 86px;
        top: 50%;
        transform: translateY(-50%);
        cursor: pointer;
        color: #C2C2C2;
    }
    .common-treeSelect-dropdown {
        top: 32px !important;
        left: -6px !important;
        border-top: 1px solid #E8E8E8;
    }
}

四、数据渲染到表格

开始写代码之前我们确保知道 filterDropdown 函数四个形参的作用,不会回到标题一去看 Antd 官网:

  • setSelectedKeys 设置值
  • selectedKeys 存储值
  • confirm ok 时触发,清除搜索框输入的值和关闭筛选模块
  • clearFilters cancel 时触发,清除搜索框输入的值和关闭筛选模块

前端表头筛选

前端控制的表头筛选

对应的源代码和注释:

import React from 'react';
import 'antd/dist/antd.css';
import { Table, Button, Space, TreeSelect } from 'antd';
import { FilterOutlined } from '@ant-design/icons';
import Axios from 'axios';
import "./filterItem.less";

const SHOW_PARENT = TreeSelect.SHOW_PARENT;

export default class App extends React.Component {
    state = {
        // table 的 dataSource
        dataSource: [],
        // 表头三个下拉列表
        educationArr: [],
        nationalArr: [],
        provinceArr: []
    };

       // 请求数据
    async componentDidMount() {
        const { data: tableData } = await Axios.get("http://localhost:3000/tableData");
        const { data: provinceArr } = await Axios.get("http://localhost:3000/provinceArr");
        const { data: nationalArr } = await Axios.get("http://localhost:3000/nationalArr");
        const { data: educationArr } = await Axios.get("http://localhost:3000/educationArr");
        this.setState({
            dataSource: tableData,
            educationArr,
            nationalArr,
            provinceArr
        });
    }

    // treeSelect 组件 => 使用 treeData 把 JSON 数据生成树结构。
    itemSelection = (treeData, dataIndex, selectedKeys, setSelectedKeys) => {

        // 这些配置去 https://ant.design/components/tree-select-cn/ 查看
        const tProps = {
            treeData,
            value: selectedKeys,
            defaultValue: [],
            placeholder: `Select ${dataIndex}`,
            autoClearSearchValue: false,
            treeCheckable: true,
            maxTagCount: 0,
            treeNodeFilterProp: 'title',
            treeDefaultExpandAll: true,
            showCheckedStrategy: SHOW_PARENT,
            getPopupContainer: (triggerNode) => triggerNode.parentNode,
            size: 'small',
            className: 'tree-select',
            dropdownMatchSelectWidth: 217,
            dropdownClassName: 'common-treeSelect-dropdown'
        };

        tProps.onChange = value => {
            setSelectedKeys(value);
        };

        return <TreeSelect {...tProps} />;
    }

    // 格式化数据为 treeSelect 组件所需要的格式
    treeSelectData(ThreeData) {
        let tempArr = [];
        if (ThreeData?.length) {
            tempArr = [ { title: '全选', value: 'all', children: [] } ];
            ThreeData.forEach(({ title, value }) => {
                tempArr[0].children.push({ title, value: title });
            });
        };
        return tempArr;
    }

    // 自定义表头筛选函数
    getColumnSearchProps = (treeData, dataIndex) => ({
        filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => (
            <div className="table-filter-dropdown" >
                {this.itemSelection(treeData, dataIndex, selectedKeys, setSelectedKeys)}
                <Space>
                    <Button
                        onClick={() => this.handleReset(clearFilters)}
                        size="small"
                        style={{ width: 50 }}
                    >
                        清空
                    </Button>
                    <Button
                        type="primary"
                        onClick={() => this.handleSearch(confirm)}
                        size="small"
                        style={{ width: 60 }}
                    >
                        确认
                    </Button>
                </Space>
            </div>
        ),
        filterIcon: filtered => <FilterOutlined style={{ color: filtered ? '#1890ff' : undefined }} />,
        onFilter: (value, record) => {
            // 前端筛选
            if (value === "all") return true;
            return record[dataIndex] ? record[dataIndex].includes(value) : '';
        }
    });

    // 点击确定按钮🔘关闭筛选清空搜索
    handleSearch = confirm => {
        confirm();
    };

    // 点击清空按钮🔘关闭筛选清空搜索
    handleReset = clearFilters => {
        clearFilters();
    };

    render() {
        // 表头
        const columns = [
            {
                title: '序号',
                dataIndex: 'id',
                key: 'id',
                width: '30%'
            },
            {
                title: '省份',
                dataIndex: 'province',
                key: 'province',
                width: '20%',
                ...this.getColumnSearchProps(this.treeSelectData(this.state.provinceArr ), 'province')
            },
            {
                title: '受教育程度',
                dataIndex: 'education',
                key: 'education',
                ...this.getColumnSearchProps(this.treeSelectData(this.state.educationArr ), 'education')
            },
            {
                title: '民族',
                dataIndex: 'national',
                key: 'national',
                ...this.getColumnSearchProps(this.treeSelectData(this.state.nationalArr ), 'national')
            }
        ];
        const { dataSource } = this.state;
        return <Table columns={columns} dataSource={dataSource} rowKey="id" />;
    }
}

搜索功能如果不受控的话,当 autoClearSearchValue: true, 时,搜索之后选中,搜索值会被立刻清空,回到未搜索的状态下。所以 treeSelect 的搜索功能🔍要受控,但是搜索一旦受控就又引发两个问题,即1. 搜索选中状态下,选中之后点击筛选模块之外的地方来关闭筛选框,无法清除搜索值;2. 搜索选中状态下,未选则,点击筛选模块之外的地方来关闭筛选框,无法再次打开。解决办法:在筛选关闭的时候清除搜索值,也就是在 onFilterDropdownVisibleChange 参数为 false 的时候清空搜索值。

autoClearSearchValue: true, 时,搜索之后选中,搜索值会被立刻清空演示:

当 `autoClearSearchValue: true,` 时,搜索之后选中,搜索值会被立刻清空
  1. 搜索选中状态下,选中之后点击筛选模块之外的地方来关闭筛选框,无法清除搜索值;
  2. 搜索选中状态下,未选则,点击筛选模块之外的地方来关闭筛选框,无法再次打开。
引发的另外两个问题

以上,逻辑是对的,但是不用这么麻烦,请把 treeSelect 组件的 autoClearSearchValue 设置为 false 即可;这时候我们的搜索受控,只是用来负责全选功能,不在负责清除搜索值

解决办法对应的源代码:

onFilterDropdownVisibleChange: visible => {
    // requestAnimationFrame用来控制关闭之后在清空搜索值
    !visible && requestAnimationFrame(() => { this.setState({ [`${dataIndex}SearchValue`]: "" }); });
}

好了,这时候就差一个核心功能了,即全选功能应该是全选搜索之后的结果。现在的全选是所有子项的父亲,所以不管你使没使用搜索功能,全选都是选择所有。

解决办法:使用 treeSelect 组件的 onSelect 事件来处理。

需要变动部分的源代码,一共两处:

  1. itemSelection 函数添加 onSelect 事件
  2. 确认按钮🔘需要增加逻辑处理,因为只有 selectedKeys 值里面出现 all 项,全选图标才会变成✅,所以部分全选时,需要增加额外参数区分是全选还是部分全选
itemSelection = (treeData, dataIndex, selectedKeys, setSelectedKeys) => {

    // 这些配置去 https://ant.design/components/tree-select-cn/ 查看
    const tProps = {
        treeData,
        value: selectedKeys,
        defaultValue: [],
        placeholder: `Select ${dataIndex}`,
        searchValue: this.state[`${dataIndex}SearchValue`],
        autoClearSearchValue: true,
        treeCheckable: true,
        maxTagCount: 0,
        treeNodeFilterProp: 'title',
        treeDefaultExpandAll: true,
        showCheckedStrategy: SHOW_PARENT,
        getPopupContainer: (triggerNode) => triggerNode.parentNode,
        size: 'small',
        className: 'tree-select',
        dropdownMatchSelectWidth: 217,
        dropdownClassName: 'common-treeSelect-dropdown'
    };

    tProps.onChange = value => {
        console.log(value);
        setSelectedKeys(value);
    };

    tProps.onSearch = searchValue => {
        this.setState({
            [`${dataIndex}SearchValue`]: searchValue
        });
    };

    tProps.onSelect = (value, item) => {
        // all {title: "全选", key: "all", value: "all", children: Array(34)}
        const searchValue = this.state[ `${dataIndex}SearchValue` ];
        if ( value === "all" && searchValue) {
            const selectedItems = item.children.filter(({ title }) => title.includes(searchValue) );
            const selectedKeys = selectedItems.map(({ title }) => title);
            setSelectedKeys([ "all", "partialAll", selectedKeys ]);
        };
    };

    return <TreeSelect {...tProps} />;
}
{/* 华丽丽的分割线 */}
<Button
    type="primary"
    onClick={() => {
        // 部分全选时,partialAll字段必须唯一
        selectedKeys[1] === "partialAll" && setSelectedKeys(selectedKeys[2])
        this.handleSearch(confirm)}
    }
    size="small"
    style={{ width: 60 }}
>
    确认
</Button>

此时自定义筛选功能做的差不多了,所有的技术难点均已攻破,唯一的使用痛点就是多页面使用了,不可能每个页面都复制一份,多傻 X,没错我现在写项目这块就是这么做的,只怪程序耦合性太高,我也没得办法,真是写死人了,不注意的话还会经常出 Bug。

五、组件分离

设计要求: 设计成一个公共函数,喂给函数展示数据,函数吐出选中的值,选中的值可以用于和后端交互例如带条件查询。

OK , 提取组件这步我想应该没啥难的了,就是把公用的模块提取出来而已。

tableHeadFilter.js 用于提起公用逻辑代码。

import React from 'react';
import { Table, Button, Space, TreeSelect } from 'antd';
import { FilterOutlined } from '@ant-design/icons';
import "./filterItem.less";

const SHOW_PARENT = TreeSelect.SHOW_PARENT;

export function fetchColumnSearchProps(listArr, type, cb) {
    const itemSelection = (data, dataIndex, selectedKeys, setSelectedKeys) => {
        const treeData = [ ...data ];
        const tProps = {
            treeData,
            value: selectedKeys,
            defaultValue: [],
            placeholder: `Select ${dataIndex}`,
            searchValue: this.state[`${dataIndex}SearchValue`],
            autoClearSearchValue: false,
            treeCheckable: true,
            maxTagCount: 0,
            treeNodeFilterProp: 'title',
            treeDefaultExpandAll: true,
            showCheckedStrategy: SHOW_PARENT,
            getPopupContainer: (triggerNode) => triggerNode.parentNode,
            size: 'small',
            className: 'tree-select',
            dropdownMatchSelectWidth: 217,
            dropdownClassName: 'common-treeSelect-dropdown'
        };

        tProps.onChange = value => {
            setSelectedKeys(value);
        };

        tProps.onSearch = searchValue => {
            this.setState({
                [`${dataIndex}SearchValue`]: searchValue
            });
        };

        tProps.onSelect = (value, item) => {
            // all {title: "全选", key: "all", value: "all", children: Array(34)}
            const searchValue = this.state[ `${dataIndex}SearchValue` ];
            if ( value === "all" && searchValue) {
                const selectedItems = item.children.filter(({ title }) => title.includes(searchValue) );
                const selectedKeys = selectedItems.map(({ title }) => title);
                setSelectedKeys([ "all", "partialAll", selectedKeys ]);
            };
        };
        return <TreeSelect {...tProps} />;
    }

    function treeSelectData(ThreeData) {
        let tempArr = [];
        if (ThreeData?.length) {
            tempArr = [ { title: '全选', value: 'all', children: [] } ];
            ThreeData.forEach(({ title, value }) => {
                tempArr[0].children.push({ title, value: title });
            });
        };
        return tempArr;
    }

    const getColumnSearchProps = (treeData, dataIndex) => ({
        filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => (
            <div className="table-filter-dropdown">
                {itemSelection(treeData, dataIndex, selectedKeys, setSelectedKeys)}
                <Space>
                    <Button
                        onClick={() => cb({ clearFilters })}
                        size="small"
                        style={{ width: 50 }}
                    >
                        清空
                    </Button>
                    <Button
                        type="primary"
                        onClick={() => {
                            // 部分全选时,partialAll字段必须唯一
                            selectedKeys[1] === "partialAll" && setSelectedKeys(selectedKeys[2]);
                            cb({ confirm });
                        }}
                        size="small"
                        style={{ width: 60 }}
                    >
                        确认
                    </Button>
                </Space>
            </div>
        ),
        filterIcon: filtered => <FilterOutlined style={{ color: filtered ? '#1890ff' : undefined }} />,
        onFilter: (value, record) => {
            // 前端筛选
            if (value === "all") return true;
            return record[dataIndex] ? record[dataIndex].includes(value) : '';
        },
        onFilterDropdownVisibleChange: visible => {
            // requestAnimationFrame用来控制关闭之后在清空搜索值
            !visible && requestAnimationFrame(() => { this.setState({ [`${dataIndex}SearchValue`]: "" }); });
        }
    });

    return getColumnSearchProps(treeSelectData(listArr), type, cb);
}

在组件里面如何使用:

import React from 'react';
import 'antd/dist/antd.css';
import { Table } from 'antd';
import Axios from 'axios';
import "./filterItem.less";
import { fetchColumnSearchProps } from "./tableHeadFilter";

export default class App extends React.Component {
    state = {
        dataSource: []
    };

    // 请求数据
    async componentDidMount() {
        const { data: tableData } = await Axios.get("http://localhost:3000/tableData");
        const { data: provinceArr } = await Axios.get("http://localhost:3000/provinceArr");
        const { data: nationalArr } = await Axios.get("http://localhost:3000/nationalArr");
        const { data: educationArr } = await Axios.get("http://localhost:3000/educationArr");
        this.setState({
            dataSource: tableData,
            educationArr,
            nationalArr,
            provinceArr
        });
    }

    // 筛选回调
    handleFilterCallback(query){
        const { clearFilters, confirm } = query;
        clearFilters && clearFilters();
        confirm && confirm();
    }

    render() {
    
        const columns = [
            {
                title: '序号',
                dataIndex: 'id',
                key: 'id',
                width: '30%'
            },
            {
                title: '省份',
                dataIndex: 'province',
                key: 'province',
                width: '20%',
                ...fetchColumnSearchProps.call(this, this.state.provinceArr, 'province', this.handleFilterCallback)
            },
            {
                title: '学历',
                dataIndex: 'education',
                key: 'education',
                ...fetchColumnSearchProps.call(this, this.state.educationArr , 'education', this.handleFilterCallback )
            },
            {
                title: '民族',
                dataIndex: 'national',
                key: 'national',
                ...fetchColumnSearchProps.call(this, this.state.nationalArr, 'national', this.handleFilterCallback)
            }
        ];
        const { dataSource } = this.state;
        return <Table columns={columns} dataSource={dataSource} rowKey="id" />;
    }
}

到这就完成了简单的前端表头筛选,并且没啥 bug。

六、筛选列表默认展开

再来一个优化,要求点击筛选 icon 的时候,treeSelect 自动聚焦,但是因为 table 表头自定义筛选功能的时候,筛选组件的 HTML 给渲染到了全局所以不能简单的通过 ref 来获取 treeSelectDOM,这是第一个难点;第二个难点就是 treeSelect 的展开是用 mousedown 事件来做的,属于鼠标🖱️事件,我们不能像 选中事件 一样直接通过 JS 用 select() 方法来触发,DOM 没有 mousedown() 事件;两大难题。

在解决这两个难题之前,先来看看 Antd 自定义的列筛选功能,并实现一个搜索列的示例,这个搜索示例是自动聚焦的。

Antd 自定义的列筛选功能,并实现一个搜索列的示例

核心实现代码是这句:

onFilterDropdownVisibleChange: visible => {
    if (visible) {
        setTimeout(() => this.searchInput.select(), 100);
    }
}

由此我们发问:

为什么有些事件能通过 API 来触发,有些事件需要事件发射器来派发?
答案:一些事件是由用户触发的,例如鼠标或键盘事件;而其他事件常由 API 生成,例如指示动画已经完成运行的事件,视频已被暂停等等。事件也可以通过脚本代码触发,例如对元素调用 HTMLElement.click() 方法,或者定义一些自定义事件,再使用 EventTarget.dispatchEvent() 方法将自定义事件派发往指定的目标(target)。
摘抄自:https://developer.mozilla.org/zh-CN/docs/Web/API/Event

弄清楚这个我们就能派发鼠标🖱️事件了,先看如何派发,分为两种一种过时的,另一种通过 event 构造函数。

第一种,参考链接:https://developer.mozilla.org/zh-CN/docs/Web/API/Event/initEvent
注:该特性已经从 Web 标准中删除

用户触发 和 派发
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>派发鼠标🖱️事件</title>
    <style>
        body,html {
            width: 100%;
            height: 100%;
            display: flex;
            justify-content: center;
            align-items: center;
        }
    </style>
</head>
<body>
    <section>
        <input type="text" id="ipt">
        <button id="btn">派发</button>
        <p id="paragraph"></p>
    </section>
    <script>
        // 设置事件监听.
        ipt.addEventListener("mousedown", function() {
            paragraph.textContent = "我被触发了";
        });
        btn.onclick = function() {
            // 创建事件.
            const mouseEvent = document.createEvent("MouseEvent");
            // 初始化一个鼠标按下事件,可以冒泡,可以被取消
            mouseEvent.initEvent("mousedown", true, true);
            // 触发事件监听
            ipt.dispatchEvent(mouseEvent);
        }
    </script>
</body>
</html>

第二种:推荐使用特定的 event 构造器函数,参考链接:https://developer.mozilla.org/zh-CN/docs/Web/API/Event/Event

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>派发鼠标🖱️事件</title>
    <style>
        body,html {
            width: 100%;
            height: 100%;
            display: flex;
            justify-content: center;
            align-items: center;
        }
    </style>
</head>
<body>
    <section>
        <input type="text" id="ipt">
        <button id="btn">派发</button>
        <p id="paragraph"></p>
    </section>
    <script>
        function triggerMouseEvent (node, eventType) {  
            // 创建并初始化一个点击事件
            const clickEvent = new Event(eventType, {"bubbles":true, "cancelable":false});;
            node.dispatchEvent (clickEvent);
        };

        // 设置事件监听.
        ipt.addEventListener("mousedown", function() {
            paragraph.textContent = "我被触发了";
        });
        btn.onclick = function() {
            // 获取 DOM
            const targetNode = document.querySelector("#ipt");
            if (targetNode) {
                // 调用函数
                triggerMouseEvent (targetNode, "mousedown");
            }
            else
                console.log ("*** Target node not found!"); 
        };
    </script>
</body>
</html>

学会了上面的内容,现在我们来做筛选列表默认展开,直接看修改部分代码内容:

<div className="table-filter-dropdown" ref={parentNode => {
                if (parentNode) {
                    const childNode = parentNode.getElementsByTagName("input")[0];
                    const mouseEvent = new Event("mousedown", { "bubbles": true, "cancelable": false });;
                    childNode.dispatchEvent(mouseEvent);
                };
            }}>
 省略部分
</div>
筛选列表默认展开演示效果

bug: 我想了一下午也没弄明白为啥输入框没自动聚焦,不过我们这个筛选说实话,弄了自动展开反而有些变扭,我就不研究为啥 input 输入框没自动聚焦了😂。

七、联动筛选

上硬菜了,上面扯了这么多,最后这章才是重点。表头联动查询的思路,点击清空或确认按钮,在回调里面获取选中的结果作为查询条件,但是这个查询条件应该存储在哪里呢,很明显要存储在父组件里,因为每个筛选都是一个组件,又因为不需要更新 state,所以不把它放在 state 里面。

先说一个败笔,因为 json-server 的限制🚫,导致条件查询不能联动,例如我先筛选完「河北省」,学历的筛选条件应该是在「河北省」筛选之后在筛选,而现在是筛选全部没取交集。早知道用 node 自己写了,哎!大成若缺,还是算了吧。😂

tableHeadFilter.js文件变动两处,注释已经标出:

import React from 'react';
import { Table, Button, Space, TreeSelect } from 'antd';
import { FilterOutlined } from '@ant-design/icons';
import "./filterItem.less";

const SHOW_PARENT = TreeSelect.SHOW_PARENT;

/* 

    selectedKeys数据的格式:
    [
        selectedData: [],
        partialAllKeys: []
    ]

    cb 反回的回调:
    {
        setSelectedKeys,
        selectedKeys,
        confirm,
        clearFilters
    }

*/

export function fetchColumnSearchProps(listArr, type, cb) {
    const itemSelection = (data, dataIndex, selectedKeys, setSelectedKeys) => {
        const treeData = [ ...data ];
        /* 𝟏因为selectedKeys数据的格式数据格式变了,所以需要调整treeSelect的格式 */
        const selectedData = selectedKeys[0];
        const tProps = {
            treeData,
            value: selectedData,
            defaultValue: [],
            placeholder: `Select ${dataIndex}`,
            searchValue: this.state[`${dataIndex}SearchValue`],
            autoClearSearchValue: false,
            treeCheckable: true,
            maxTagCount: 0,
            treeNodeFilterProp: 'title',
            treeDefaultExpandAll: true,
            showCheckedStrategy: SHOW_PARENT,
            getPopupContainer: (triggerNode) => triggerNode.parentNode,
            size: 'small',
            className: 'tree-select',
            dropdownMatchSelectWidth: 217,
            dropdownClassName: 'common-treeSelect-dropdown'
        };

        tProps.onChange = value => {
            setSelectedKeys([ value ]);
        };

        tProps.onSearch = searchValue => {
            this.setState({
                [`${dataIndex}SearchValue`]: searchValue
            });
        };

        tProps.onSelect = (value, item) => {
            // all {title: "全选", key: "all", value: "all", children: Array(34)}
            const searchValue = this.state[ `${dataIndex}SearchValue` ];
             /* 𝟐 调整部分全选的逻辑 */
            if ( value === "all" && searchValue) {
                const selectedItems = item.children.filter(({ title }) => title.includes(searchValue) );
                const partialAllKeys = selectedItems.map(({ value }) => value);
                setSelectedKeys([ [ "all" ], partialAllKeys ]);
            } else if (value === "all") {
                const partialAllKeys = item.children.map(({ value }) => value);
                setSelectedKeys([ [ "all" ], partialAllKeys ]);
            };
        };
        return <TreeSelect {...tProps} />;
    }

    function treeSelectData(ThreeData) {
        let tempArr = [];
        if (ThreeData?.length) {
            tempArr = [ { title: '全选', value: 'all', children: [] } ];
            ThreeData.forEach(({ title, value }) => {
                tempArr[0].children.push({ title, value });
            });
        };
        return tempArr;
    }

    const getColumnSearchProps = (treeData, dataIndex) => ({
        filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => (
            <div className="table-filter-dropdown">
                {itemSelection(treeData, dataIndex, selectedKeys, setSelectedKeys)}
                <Space>
                    <Button
                        onClick={() => cb({ setSelectedKeys, selectedKeys, clearFilters, dataIndex })}
                        size="small"
                        style={{ width: 50 }}
                    >
                        清空
                    </Button>
                    <Button
                        type="primary"
                        onClick={() =>  cb({ setSelectedKeys, selectedKeys, confirm, dataIndex })}
                        size="small"
                        style={{ width: 60 }}
                    >
                        确认
                    </Button>
                </Space>
            </div>
        ),
        filterIcon: filtered => <FilterOutlined style={{ color: filtered ? '#1890ff' : undefined }} />,
        onFilterDropdownVisibleChange: visible => {
            // requestAnimationFrame用来控制关闭之后在清空搜索值
            !visible && requestAnimationFrame(() => { this.setState({ [`${dataIndex}SearchValue`]: "" }); });
        }
    });

    return getColumnSearchProps(treeSelectData(listArr), type, cb);
}

组件中主要是增加了,筛选回调函数的逻辑,代码和注释如下:

import React from 'react';
import 'antd/dist/antd.css';
import { Table } from 'antd';
import Axios from 'axios';
import "./filterItem.less";
import { fetchColumnSearchProps } from "./tableHeadFilter";

const baseURL = "http://localhost:3000/";
export default class App extends React.Component {
    state = {
        dataSource: []
    };

    // 请求数据
    async componentDidMount() {
        const { data: tableData } = await Axios.get(`${baseURL}tableData`);
        const { data: provinceArr } = await Axios.get(`${baseURL}provinceArr`);
        const { data: nationalArr } = await Axios.get(`${baseURL}nationalArr`);
        const { data: educationArr } = await Axios.get(`${baseURL}educationArr`);
        this.setState({
            dataSource: tableData,
            educationArr,
            nationalArr,
            provinceArr
        });
    }

    async fetchTableData(queryCriteria) {
        let { provinceCodes = [], nationalCodes = [], educationCodes = [] }  = queryCriteria;
        const province = provinceCodes.map(item => `provinceCodes=${item}`).join("&");
        const national = nationalCodes.map(item => `nationalCodes=${item}`).join("&");
        const education = educationCodes.map(item => `educationCodes=${item}`).join("&");
        const provinceValue = provinceCodes.map(item => `value=${item}`).join("&");
        const nationalValue = nationalCodes.map(item => `value=${item}`).join("&");
        const educationValue = educationCodes.map(item => `value=${item}`).join("&");
        /* 
            查询结果为河北省和江西省例子🌰
            http://localhost:3000/tableData?provinceCodes=jiangxisheng&provinceCodes=hebeisheng
        */
        try {
            const { data: tableData } = await Axios.get(`${baseURL}tableData?${education}&${national}&${province}`);
            const { data: provinceArr } = await Axios.get(`${baseURL}provinceArr?${provinceValue}`);
            const { data: nationalArr } = await Axios.get(`${baseURL}nationalArr?${nationalValue}`);
            const { data: educationArr } = await Axios.get(`${baseURL}educationArr?${educationValue}`);
            this.setState({ dataSource: tableData, educationArr, nationalArr, provinceArr });
        } catch (error) {
            console.log(error);
        };
    }

    // 每一个筛选都是一个组件,所以需要混总筛选条件,因为不需要更新所以不放在 state。
    #queryCriteria = {};

    // 筛选回调
    handleFilterCallback = query => {
        const { confirm, clearFilters, selectedKeys, setSelectedKeys, dataIndex } = query;

        // 汇总的请求条件
        const queryCriteria = { ...this.#queryCriteria };
       
        // 不是全选的结果 || 条件清空逻辑
        const selectedData = clearFilters ? [] : selectedKeys[0];
        // 全部选择和部分选择全选下的结果 || 条件清空逻辑
        const partialAllKeys = clearFilters ? [] : selectedKeys[1];

        /* 更新请求参数 */
        if (selectedData[0] === "all") {
            // 请求参数 partialAllKeys
            queryCriteria[`${dataIndex}Codes`] = partialAllKeys;
        } else {
            console.log(selectedData, "selectedKeys");
            // 请求参数 selectedData
            queryCriteria[`${dataIndex}Codes`] = selectedData;
        };

        this.#queryCriteria = queryCriteria;

        this.fetchTableData(queryCriteria);
        /* 必须在 clearFilters/confirm 函数执行前使用 setSelectedKeys */
        setSelectedKeys([ selectedData ]);
        
        console.log(queryCriteria, "queryCriteria");
        /* 清空并关闭搜索模块 */
        clearFilters && clearFilters();
        confirm && confirm();
    }

    render() {
    
        const columns = [
            {
                title: '序号',
                dataIndex: 'id',
                key: 'id',
                width: '30%'
            },
            {
                title: '省份',
                dataIndex: 'province',
                key: 'province',
                width: '20%',
                ...fetchColumnSearchProps.call(this, this.state.provinceArr, 'province', this.handleFilterCallback)
            },
            {
                title: '学历',
                dataIndex: 'education',
                key: 'education',
                ...fetchColumnSearchProps.call(this, this.state.educationArr , 'education', this.handleFilterCallback )
            },
            {
                title: '民族',
                dataIndex: 'national',
                key: 'national',
                ...fetchColumnSearchProps.call(this, this.state.nationalArr, 'national', this.handleFilterCallback)
            }
        ];
        const { dataSource } = this.state;
        return <Table columns={columns} dataSource={dataSource} rowKey="id" />;
    }
}

差不多了,唯一的一个遗憾就是被 json-server 坑了一把,筛选条件接口没有一起联动,只有单个筛选是联动的。

这个联动说到底就是筛选列表是全部清空重新去拉取查询列表:即取交集,还是不根据筛选条件全部展示:即取并集。这个需要看项目要求了,不过我们项目取的是交集。
总结: 联动 => 交集 不联动 => 并集

其他功能完成的都挺完美,比现在在项目中用的筛选好多了,代码明确逻辑清晰,项目里面的筛选我也懒得去改了,牵涉页面和逻辑太多🐷。大成之后动图演示:

使用 antd@4 table 自定义筛选表头功能做一个联动表头筛选

补充:等一下,今天周五,刚刚去开了一个例会,既然我模拟不了交集的情况,但是可以完美模拟取并集的情况呀🦑。而且取并集维护的变量也少,更加简单。

tableHeadFilter.js 文件修改两部分内容:

  1. 筛选头的内容,由 state 提取到类的静态属性上
  2. 删除联动筛选头接口

源代码和注释如下:

import React from 'react';
import 'antd/dist/antd.css';
import { Table } from 'antd';
import Axios from 'axios';
import "./filterItem.less";
import { fetchColumnSearchProps } from "./tableHeadFilter";

const baseURL = "http://localhost:3000/";
export default class App extends React.Component {
    state = {
        dataSource: []
    };
    #provinceArr;
    #educationArr;
    #nationalArr;

    // 请求数据
    async componentDidMount() {
        const { data: tableData } = await Axios.get(`${baseURL}tableData`);
        const { data: provinceArr } = await Axios.get(`${baseURL}provinceArr`);
        const { data: nationalArr } = await Axios.get(`${baseURL}nationalArr`);
        const { data: educationArr } = await Axios.get(`${baseURL}educationArr`);
        this.#educationArr = educationArr;
        this.#nationalArr = nationalArr;
        this.#provinceArr = provinceArr;

        this.setState({ dataSource: tableData });
    }

    async fetchTableData(queryCriteria) {
        let { provinceCodes = [], nationalCodes = [], educationCodes = [] }  = queryCriteria;
        const province = provinceCodes.map(item => `provinceCodes=${item}`).join("&");
        const national = nationalCodes.map(item => `nationalCodes=${item}`).join("&");
        const education = educationCodes.map(item => `educationCodes=${item}`).join("&");
        /* 
            查询结果为河北省和江西省例子🌰
            http://localhost:3000/tableData?provinceCodes=jiangxisheng&provinceCodes=hebeisheng
        */
        try {
            const { data: tableData } = await Axios.get(`${baseURL}tableData?${education}&${national}&${province}`);
            this.setState({ dataSource: tableData });
        } catch (error) {
            console.log(error);
        };
    }

    // 每一个筛选都是一个组件,所以需要混总筛选条件,因为不需要更新所以不放在 state。
    #queryCriteria = {};

    // 筛选回调
    handleFilterCallback = query => {

        /* 《《《《《《《《《 ======= 由此开始 ===== 》》》》》》》》》》》》》》 */
        const { confirm, clearFilters, selectedKeys, setSelectedKeys, dataIndex } = query;

        // 汇总的请求条件
        const queryCriteria = { ...this.#queryCriteria };
       
        // 不是全选的结果 || 条件清空逻辑
        const selectedData = clearFilters ? [] : selectedKeys[0];
        // 全部选择和部分选择全选下的结果 || 条件清空逻辑
        const partialAllKeys = clearFilters ? [] : selectedKeys[1];

        /* 更新请求参数 */
        if (selectedData[0] === "all") {
            // 请求参数 partialAllKeys
            queryCriteria[`${dataIndex}Codes`] = partialAllKeys;
        } else {
            console.log(selectedData, "selectedKeys");
            // 请求参数 selectedData
            queryCriteria[`${dataIndex}Codes`] = selectedData;
        };

        this.#queryCriteria = queryCriteria;

        /* 必须在 clearFilters/confirm 函数执行前使用 setSelectedKeys */
        setSelectedKeys([ selectedData ]);
        
        /* 清空并关闭搜索模块 */
        clearFilters && clearFilters();
        confirm && confirm();
        /* 《《《《《《《《《 ======= 到此结束,可进一步提出 ===== 》》》》》》》》》》》》》》 */

        // 只保留这一部分就OK了
        this.fetchTableData(queryCriteria);
    }

    render() {
    
        const columns = [
            {
                title: '序号',
                dataIndex: 'id',
                key: 'id',
                width: '30%'
            },
            {
                title: '省份',
                dataIndex: 'province',
                key: 'province',
                width: '20%',
                ...fetchColumnSearchProps.call(this, this.#provinceArr, 'province', this.handleFilterCallback)
            },
            {
                title: '学历',
                dataIndex: 'education',
                key: 'education',
                ...fetchColumnSearchProps.call(this, this.#educationArr , 'education', this.handleFilterCallback )
            },
            {
                title: '民族',
                dataIndex: 'national',
                key: 'national',
                ...fetchColumnSearchProps.call(this, this.#nationalArr, 'national', this.handleFilterCallback)
            }
        ];
        const { dataSource } = this.state;
        return <Table columns={columns} dataSource={dataSource} rowKey="id" />;
    }
}

完美的并集表格筛选演示,缺点是会出现无数据的情况,但是很常用:

完美的并集表格筛选,缺点是会出现无数据的情况

完~

搬到六道口的第七天,当前时间: Friday, September 18, 2020 02:01:20

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335