使用 antd@4 table 自定義篩選表頭功能做一個(gè)聯(lián)動(dòng)搜索表頭篩選

前言: 上篇文章是使用 antd@4 table 自定義表頭篩選完成一個(gè)表格動(dòng)態(tài)列的功能,這次需要完成一個(gè)表頭聯(lián)動(dòng)條件篩選功能溺蕉。

一邻吞、開(kāi)始前

開(kāi)始之前先去 Antd 官網(wǎng)看下「自定義的列篩選功能」的代碼和邏輯:

插一句:

目前我做的是 PC 后臺(tái)管理系統(tǒng)耿眉,系統(tǒng)里面涉及到大量帶條件篩選的表頭鸣剪,項(xiàng)目中「table 自定義列篩選功能」這個(gè)組件是另一個(gè)小伙伴封裝的斤寇,封裝的還可以拥褂,就是沒(méi)用到 Antd 「自定義的列篩選功能」提供的 API 饺鹃,導(dǎo)致后面有很多效果需要自己去手動(dòng)實(shí)現(xiàn)悔详,例如: 篩選圖標(biāo)點(diǎn)亮茄螃、搜索框輸入查詢條件不點(diǎn)擊確認(rèn)自動(dòng)清空效果等归苍,因?yàn)轫?xiàng)目比較急,沒(méi)辦法我只能全手動(dòng)加上摇展,結(jié)果造成代碼非常的臃腫??盯孙。

二振惰、模擬大量數(shù)據(jù)

項(xiàng)目中的接口肯定是不能直接拿來(lái)做 demo 演示的报账,而且就算我拿來(lái)了,大家也連不了榜晦,都是內(nèi)網(wǎng)??,還是老老實(shí)實(shí)的模擬接口吧朽寞。

模擬數(shù)據(jù)使用的模塊 json-servermockjs 喻频,詳細(xì)使用參考我去年寫(xiě)的文章:學(xué)習(xí)使用 json-server 和 mockjs

看看我去年寫(xiě)的代碼甥温,想想過(guò)的真快姻蚓,都一年整了狰挡,哎!那時(shí)候好連 ES6 都不是太會(huì) ??????殉农,感覺(jué)進(jìn)步好大超凳,用寫(xiě)博客逼著自己成長(zhǎng)轮傍,各位觀眾沒(méi)事也來(lái)試試唄??创夜。

好了驰吓,看下本次模擬數(shù)據(jù)的邏輯代碼和注釋:

// 使用 Mock
const Mock = require('mockjs');
const pinyin = require("pinyin");
// 引入node內(nèi)置的文件系統(tǒng)
const { writeFile } = require('fs');
// 使用Random這個(gè)api
const random = Mock.Random;

// 統(tǒng)計(jì) national 姑廉、province桥言、education号阿、作為查詢條件
let nationalArr = [], provinceArr = [], educationArr = [];


// 漢字轉(zhuǎn)拼音
function han2pinyin(han) {
    return [].concat(...pinyin(han, {
        // 拼音不加音調(diào)
        style: pinyin.STYLE_NORMAL
    })).join("");
};

const tableData = [];
for (let i = 0;i < 100;i++) {
    // 隨機(jī)56個(gè)民族
    const national = random.pick(["漢族","蒙古族","回族","藏族","維吾爾族","苗族","彝族","壯族","布依族","朝鮮族","滿族","侗族","瑤族","白族","土家族","哈尼族","哈薩克族","傣族","黎族","僳僳族","佤族","畬族","高山族","拉祜族","水族","東鄉(xiāng)族","納西族","景頗族","柯?tīng)柨俗巫?,"土族","達(dá)斡爾族","仫佬族","羌族","布朗族","撒拉族","毛南族","仡佬族","錫伯族","阿昌族","普米族","塔吉克族","怒族","烏孜別克族","俄羅斯族","鄂溫克族","德昂族","保安族","裕固族","京族","塔塔爾族","獨(dú)龍族","鄂倫春族","赫哲族","門(mén)巴族","珞巴族","基諾族"]);
    !nationalArr.includes(national) && nationalArr.push(national);

    // 隨機(jī)省份
    let province;
    do {
        province = random.province();
    } while(province === "山西省");
    !provinceArr.includes(province) && provinceArr.push(province);

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

    // 數(shù)據(jù)放入數(shù)組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 };

// 文件寫(xiě)入
writeFile("./db.json",JSON.stringify(db),function(err){
    if (err) {
        console.log(`寫(xiě)入錯(cuò)誤,錯(cuò)誤為:${err}`);
        return ;
    };
    console.log("一百條信息錄入成功扰柠!");
});

我通過(guò) npx json-server --watch db.json --port 3000 來(lái)啟動(dòng)接口蝙泼,一共啟用四個(gè)接口鏈接分別為??:

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

瀏覽器打開(kāi)即可看到數(shù)據(jù)汤踏。

為了證明俺沒(méi)騙你搂擦,截圖為證

三瀑踢、table 用到的一些樣式

寫(xiě) CSS 也是挺費(fèi)勁的橱夭,做人不能不厚道棘劣,樣式也送給大家首昔,注:是 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;
    }
}

四、數(shù)據(jù)渲染到表格

開(kāi)始寫(xiě)代碼之前我們確保知道 filterDropdown 函數(shù)四個(gè)形參的作用巨税,不會(huì)回到標(biāo)題一去看 Antd 官網(wǎng):

  • setSelectedKeys 設(shè)置值
  • selectedKeys 存儲(chǔ)值
  • confirm ok 時(shí)觸發(fā)草添,清除搜索框輸入的值和關(guān)閉篩選模塊
  • clearFilters cancel 時(shí)觸發(fā)远寸,清除搜索框輸入的值和關(guān)閉篩選模塊

前端表頭篩選

前端控制的表頭篩選

對(duì)應(yīng)的源代碼和注釋:

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: [],
        // 表頭三個(gè)下拉列表
        educationArr: [],
        nationalArr: [],
        provinceArr: []
    };

       // 請(qǐng)求數(shù)據(jù)
    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 數(shù)據(jù)生成樹(shù)結(jié)構(gòu)。
    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} />;
    }

    // 格式化數(shù)據(jù)為 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;
    }

    // 自定義表頭篩選函數(shù)
    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 }}
                    >
                        確認(rèn)
                    </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) : '';
        }
    });

    // 點(diǎn)擊確定按鈕??關(guān)閉篩選清空搜索
    handleSearch = confirm => {
        confirm();
    };

    // 點(diǎn)擊清空按鈕??關(guān)閉篩選清空搜索
    handleReset = clearFilters => {
        clearFilters();
    };

    render() {
        // 表頭
        const columns = [
            {
                title: '序號(hào)',
                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" />;
    }
}

搜索功能如果不受控的話唉韭,當(dāng) autoClearSearchValue: true, 時(shí)女器,搜索之后選中驾胆,搜索值會(huì)被立刻清空俏拱,回到未搜索的狀態(tài)下锅必。所以 treeSelect 的搜索功能??要受控驹愚,但是搜索一旦受控就又引發(fā)兩個(gè)問(wèn)題逢捺,即1. 搜索選中狀態(tài)下劫瞳,選中之后點(diǎn)擊篩選模塊之外的地方來(lái)關(guān)閉篩選框志于,無(wú)法清除搜索值伺绽;2. 搜索選中狀態(tài)下奈应,未選則,點(diǎn)擊篩選模塊之外的地方來(lái)關(guān)閉篩選框程梦,無(wú)法再次打開(kāi)。解決辦法:在篩選關(guān)閉的時(shí)候清除搜索值哥童,也就是在 onFilterDropdownVisibleChange 參數(shù)為 false 的時(shí)候清空搜索值贮懈。

當(dāng) autoClearSearchValue: true, 時(shí)朵你,搜索之后選中抡医,搜索值會(huì)被立刻清空演示:

當(dāng) `autoClearSearchValue: true,` 時(shí)大脉,搜索之后選中镰矿,搜索值會(huì)被立刻清空
  1. 搜索選中狀態(tài)下,選中之后點(diǎn)擊篩選模塊之外的地方來(lái)關(guān)閉篩選框苍姜,無(wú)法清除搜索值怖现;
  2. 搜索選中狀態(tài)下屈嗤,未選則饶号,點(diǎn)擊篩選模塊之外的地方來(lái)關(guān)閉篩選框茫船,無(wú)法再次打開(kāi)算谈。
引發(fā)的另外兩個(gè)問(wèn)題

以上,邏輯是對(duì)的高每,但是不用這么麻煩鲸匿,請(qǐng)把 treeSelect 組件的 autoClearSearchValue 設(shè)置為 false 即可带欢;這時(shí)候我們的搜索受控徒坡,只是用來(lái)負(fù)責(zé)全選功能喇完,不在負(fù)責(zé)清除搜索值锦溪。

解決辦法對(duì)應(yīng)的源代碼:

onFilterDropdownVisibleChange: visible => {
    // requestAnimationFrame用來(lái)控制關(guān)閉之后在清空搜索值
    !visible && requestAnimationFrame(() => { this.setState({ [`${dataIndex}SearchValue`]: "" }); });
}

好了,這時(shí)候就差一個(gè)核心功能了牺丙,即全選功能應(yīng)該是全選搜索之后的結(jié)果∷谂校現(xiàn)在的全選是所有子項(xiàng)的父親档礁,所以不管你使沒(méi)使用搜索功能呻澜,全選都是選擇所有羹幸。

解決辦法:使用 treeSelect 組件的 onSelect 事件來(lái)處理睹欲。

需要變動(dòng)部分的源代碼,一共兩處:

  1. itemSelection 函數(shù)添加 onSelect 事件
  2. 確認(rèn)按鈕??需要增加邏輯處理冀墨,因?yàn)橹挥?selectedKeys 值里面出現(xiàn) all 項(xiàng)诽嘉,全選圖標(biāo)才會(huì)變成?骄酗,所以部分全選時(shí)趋翻,需要增加額外參數(shù)區(qū)分是全選還是部分全選
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={() => {
        // 部分全選時(shí),partialAll字段必須唯一
        selectedKeys[1] === "partialAll" && setSelectedKeys(selectedKeys[2])
        this.handleSearch(confirm)}
    }
    size="small"
    style={{ width: 60 }}
>
    確認(rèn)
</Button>

此時(shí)自定義篩選功能做的差不多了讨惩,所有的技術(shù)難點(diǎn)均已攻破,唯一的使用痛點(diǎn)就是多頁(yè)面使用了处面,不可能每個(gè)頁(yè)面都復(fù)制一份鸳君,多傻 X或颊,沒(méi)錯(cuò)我現(xiàn)在寫(xiě)項(xiàng)目這塊就是這么做的囱挑,只怪程序耦合性太高平挑,我也沒(méi)得辦法通熄,真是寫(xiě)死人了唇辨,不注意的話還會(huì)經(jīng)常出 Bug亡驰。

五凡辱、組件分離

設(shè)計(jì)要求: 設(shè)計(jì)成一個(gè)公共函數(shù)透乾,喂給函數(shù)展示數(shù)據(jù)续徽,函數(shù)吐出選中的值钦扭,選中的值可以用于和后端交互例如帶條件查詢客情。

OK , 提取組件這步我想應(yīng)該沒(méi)啥難的了痹雅,就是把公用的模塊提取出來(lái)而已摔蓝。

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={() => {
                            // 部分全選時(shí)赌渣,partialAll字段必須唯一
                            selectedKeys[1] === "partialAll" && setSelectedKeys(selectedKeys[2]);
                            cb({ confirm });
                        }}
                        size="small"
                        style={{ width: 60 }}
                    >
                        確認(rèn)
                    </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用來(lái)控制關(guān)閉之后在清空搜索值
            !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: []
    };

    // 請(qǐng)求數(shù)據(jù)
    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
        });
    }

    // 篩選回調(diào)
    handleFilterCallback(query){
        const { clearFilters, confirm } = query;
        clearFilters && clearFilters();
        confirm && confirm();
    }

    render() {
    
        const columns = [
            {
                title: '序號(hào)',
                dataIndex: 'id',
                key: 'id',
                width: '30%'
            },
            {
                title: '省份',
                dataIndex: 'province',
                key: 'province',
                width: '20%',
                ...fetchColumnSearchProps.call(this, this.state.provinceArr, 'province', this.handleFilterCallback)
            },
            {
                title: '學(xué)歷',
                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" />;
    }
}

到這就完成了簡(jiǎn)單的前端表頭篩選,并且沒(méi)啥 bug路操。

六屯仗、篩選列表默認(rèn)展開(kāi)

再來(lái)一個(gè)優(yōu)化魁袜,要求點(diǎn)擊篩選 icon 的時(shí)候峰弹,treeSelect 自動(dòng)聚焦鞠呈,但是因?yàn)?table 表頭自定義篩選功能的時(shí)候蚁吝,篩選組件的 HTML 給渲染到了全局所以不能簡(jiǎn)單的通過(guò) ref 來(lái)獲取 treeSelectDOM窘茁,這是第一個(gè)難點(diǎn);第二個(gè)難點(diǎn)就是 treeSelect 的展開(kāi)是用 mousedown 事件來(lái)做的邢羔,屬于鼠標(biāo)???事件砂蔽,我們不能像 選中事件 一樣直接通過(guò) JS 用 select() 方法來(lái)觸發(fā)左驾,DOM 沒(méi)有 mousedown() 事件诡右;兩大難題。

在解決這兩個(gè)難題之前咙边,先來(lái)看看 Antd 自定義的列篩選功能,并實(shí)現(xiàn)一個(gè)搜索列的示例淑蔚,這個(gè)搜索示例是自動(dòng)聚焦的。

Antd 自定義的列篩選功能带迟,并實(shí)現(xiàn)一個(gè)搜索列的示例

核心實(shí)現(xiàn)代碼是這句:

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

由此我們發(fā)問(wèn):

為什么有些事件能通過(guò) API 來(lái)觸發(fā)仓犬,有些事件需要事件發(fā)射器來(lái)派發(fā)婶肩?
答案:一些事件是由用戶觸發(fā)的,例如鼠標(biāo)或鍵盤(pán)事件啡专;而其他事件常由 API 生成险毁,例如指示動(dòng)畫(huà)已經(jīng)完成運(yùn)行的事件,視頻已被暫停等等们童。事件也可以通過(guò)腳本代碼觸發(fā)畔况,例如對(duì)元素調(diào)用 HTMLElement.click() 方法,或者定義一些自定義事件慧库,再使用 EventTarget.dispatchEvent() 方法將自定義事件派發(fā)往指定的目標(biāo)(target)跷跪。
摘抄自:https://developer.mozilla.org/zh-CN/docs/Web/API/Event

弄清楚這個(gè)我們就能派發(fā)鼠標(biāo)???事件了吵瞻,先看如何派發(fā)济舆,分為兩種一種過(guò)時(shí)的签夭,另一種通過(guò) event 構(gòu)造函數(shù)。

第一種,參考鏈接:https://developer.mozilla.org/zh-CN/docs/Web/API/Event/initEvent
注:該特性已經(jīng)從 Web 標(biāo)準(zhǔn)中刪除

用戶觸發(fā) 和 派發(fā)
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>派發(fā)鼠標(biāo)???事件</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">派發(fā)</button>
        <p id="paragraph"></p>
    </section>
    <script>
        // 設(shè)置事件監(jiān)聽(tīng).
        ipt.addEventListener("mousedown", function() {
            paragraph.textContent = "我被觸發(fā)了";
        });
        btn.onclick = function() {
            // 創(chuàng)建事件.
            const mouseEvent = document.createEvent("MouseEvent");
            // 初始化一個(gè)鼠標(biāo)按下事件之宿,可以冒泡,可以被取消
            mouseEvent.initEvent("mousedown", true, true);
            // 觸發(fā)事件監(jiān)聽(tīng)
            ipt.dispatchEvent(mouseEvent);
        }
    </script>
</body>
</html>

第二種:推薦使用特定的 event 構(gòu)造器函數(shù),參考鏈接: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>派發(fā)鼠標(biāo)???事件</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">派發(fā)</button>
        <p id="paragraph"></p>
    </section>
    <script>
        function triggerMouseEvent (node, eventType) {  
            // 創(chuàng)建并初始化一個(gè)點(diǎn)擊事件
            const clickEvent = new Event(eventType, {"bubbles":true, "cancelable":false});;
            node.dispatchEvent (clickEvent);
        };

        // 設(shè)置事件監(jiān)聽(tīng).
        ipt.addEventListener("mousedown", function() {
            paragraph.textContent = "我被觸發(fā)了";
        });
        btn.onclick = function() {
            // 獲取 DOM
            const targetNode = document.querySelector("#ipt");
            if (targetNode) {
                // 調(diào)用函數(shù)
                triggerMouseEvent (targetNode, "mousedown");
            }
            else
                console.log ("*** Target node not found!"); 
        };
    </script>
</body>
</html>

學(xué)會(huì)了上面的內(nèi)容冒掌,現(xiàn)在我們來(lái)做篩選列表默認(rèn)展開(kāi),直接看修改部分代碼內(nèi)容:

<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>
篩選列表默認(rèn)展開(kāi)演示效果

bug: 我想了一下午也沒(méi)弄明白為啥輸入框沒(méi)自動(dòng)聚焦,不過(guò)我們這個(gè)篩選說(shuō)實(shí)話,弄了自動(dòng)展開(kāi)反而有些變扭毒姨,我就不研究為啥 input 輸入框沒(méi)自動(dòng)聚焦了??。

七腥沽、聯(lián)動(dòng)篩選

上硬菜了茅信,上面扯了這么多妖谴,最后這章才是重點(diǎn)。表頭聯(lián)動(dòng)查詢的思路,點(diǎn)擊清空或確認(rèn)按鈕,在回調(diào)里面獲取選中的結(jié)果作為查詢條件崭篡,但是這個(gè)查詢條件應(yīng)該存儲(chǔ)在哪里呢砸彬,很明顯要存儲(chǔ)在父組件里,因?yàn)槊總€(gè)篩選都是一個(gè)組件滴某,又因?yàn)椴恍枰?state,所以不把它放在 state 里面。

先說(shuō)一個(gè)敗筆悼潭,因?yàn)?json-server 的限制??,導(dǎo)致條件查詢不能聯(lián)動(dòng)软族,例如我先篩選完「河北省」初茶,學(xué)歷的篩選條件應(yīng)該是在「河北省」篩選之后在篩選螺戳,而現(xiàn)在是篩選全部沒(méi)取交集爽待。早知道用 node 自己寫(xiě)了膏燃,哎禁炒!大成若缺,還是算了吧。??

tableHeadFilter.js文件變動(dòng)兩處,注釋已經(jīng)標(biāo)出:

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數(shù)據(jù)的格式:
    [
        selectedData: [],
        partialAllKeys: []
    ]

    cb 反回的回調(diào):
    {
        setSelectedKeys,
        selectedKeys,
        confirm,
        clearFilters
    }

*/

export function fetchColumnSearchProps(listArr, type, cb) {
    const itemSelection = (data, dataIndex, selectedKeys, setSelectedKeys) => {
        const treeData = [ ...data ];
        /* ??因?yàn)閟electedKeys數(shù)據(jù)的格式數(shù)據(jù)格式變了,所以需要調(diào)整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` ];
             /* ?? 調(diào)整部分全選的邏輯 */
            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 }}
                    >
                        確認(rèn)
                    </Button>
                </Space>
            </div>
        ),
        filterIcon: filtered => <FilterOutlined style={{ color: filtered ? '#1890ff' : undefined }} />,
        onFilterDropdownVisibleChange: visible => {
            // requestAnimationFrame用來(lái)控制關(guān)閉之后在清空搜索值
            !visible && requestAnimationFrame(() => { this.setState({ [`${dataIndex}SearchValue`]: "" }); });
        }
    });

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

組件中主要是增加了但骨,篩選回調(diào)函數(shù)的邏輯补履,代碼和注釋如下:

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: []
    };

    // 請(qǐng)求數(shù)據(jù)
    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("&");
        /* 
            查詢結(jié)果為河北省和江西省例子??
            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);
        };
    }

    // 每一個(gè)篩選都是一個(gè)組件,所以需要混總篩選條件讼稚,因?yàn)椴恍枰滤圆环旁?state赠摇。
    #queryCriteria = {};

    // 篩選回調(diào)
    handleFilterCallback = query => {
        const { confirm, clearFilters, selectedKeys, setSelectedKeys, dataIndex } = query;

        // 匯總的請(qǐng)求條件
        const queryCriteria = { ...this.#queryCriteria };
       
        // 不是全選的結(jié)果 || 條件清空邏輯
        const selectedData = clearFilters ? [] : selectedKeys[0];
        // 全部選擇和部分選擇全選下的結(jié)果 || 條件清空邏輯
        const partialAllKeys = clearFilters ? [] : selectedKeys[1];

        /* 更新請(qǐng)求參數(shù) */
        if (selectedData[0] === "all") {
            // 請(qǐng)求參數(shù) partialAllKeys
            queryCriteria[`${dataIndex}Codes`] = partialAllKeys;
        } else {
            console.log(selectedData, "selectedKeys");
            // 請(qǐng)求參數(shù) selectedData
            queryCriteria[`${dataIndex}Codes`] = selectedData;
        };

        this.#queryCriteria = queryCriteria;

        this.fetchTableData(queryCriteria);
        /* 必須在 clearFilters/confirm 函數(shù)執(zhí)行前使用 setSelectedKeys */
        setSelectedKeys([ selectedData ]);
        
        console.log(queryCriteria, "queryCriteria");
        /* 清空并關(guān)閉搜索模塊 */
        clearFilters && clearFilters();
        confirm && confirm();
    }

    render() {
    
        const columns = [
            {
                title: '序號(hào)',
                dataIndex: 'id',
                key: 'id',
                width: '30%'
            },
            {
                title: '省份',
                dataIndex: 'province',
                key: 'province',
                width: '20%',
                ...fetchColumnSearchProps.call(this, this.state.provinceArr, 'province', this.handleFilterCallback)
            },
            {
                title: '學(xué)歷',
                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" />;
    }
}

差不多了,唯一的一個(gè)遺憾就是被 json-server 坑了一把,篩選條件接口沒(méi)有一起聯(lián)動(dòng),只有單個(gè)篩選是聯(lián)動(dòng)的。

這個(gè)聯(lián)動(dòng)說(shuō)到底就是篩選列表是全部清空重新去拉取查詢列表:即取交集,還是不根據(jù)篩選條件全部展示:即取并集把敞。這個(gè)需要看項(xiàng)目要求了盛霎,不過(guò)我們項(xiàng)目取的是交集规个。
總結(jié): 聯(lián)動(dòng) => 交集 不聯(lián)動(dòng) => 并集

其他功能完成的都挺完美缤苫,比現(xiàn)在在項(xiàng)目中用的篩選好多了翼虫,代碼明確邏輯清晰死陆,項(xiàng)目里面的篩選我也懶得去改了规哪,牽涉頁(yè)面和邏輯太多??杯巨。大成之后動(dòng)圖演示:

使用 antd@4 table 自定義篩選表頭功能做一個(gè)聯(lián)動(dòng)表頭篩選

補(bǔ)充:等一下镜会,今天周五镣屹,剛剛?cè)ラ_(kāi)了一個(gè)例會(huì),既然我模擬不了交集的情況,但是可以完美模擬取并集的情況呀??魏割。而且取并集維護(hù)的變量也少钱豁,更加簡(jiǎn)單溢豆。

tableHeadFilter.js 文件修改兩部分內(nèi)容:

  1. 篩選頭的內(nèi)容,由 state 提取到類的靜態(tài)屬性上
  2. 刪除聯(lián)動(dòng)篩選頭接口

源代碼和注釋如下:

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;

    // 請(qǐng)求數(shù)據(jù)
    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("&");
        /* 
            查詢結(jié)果為河北省和江西省例子??
            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);
        };
    }

    // 每一個(gè)篩選都是一個(gè)組件锚扎,所以需要混總篩選條件,因?yàn)椴恍枰滤圆环旁?state克伊。
    #queryCriteria = {};

    // 篩選回調(diào)
    handleFilterCallback = query => {

        /* 《《《《《《《《《 ======= 由此開(kāi)始 ===== 》》》》》》》》》》》》》》 */
        const { confirm, clearFilters, selectedKeys, setSelectedKeys, dataIndex } = query;

        // 匯總的請(qǐng)求條件
        const queryCriteria = { ...this.#queryCriteria };
       
        // 不是全選的結(jié)果 || 條件清空邏輯
        const selectedData = clearFilters ? [] : selectedKeys[0];
        // 全部選擇和部分選擇全選下的結(jié)果 || 條件清空邏輯
        const partialAllKeys = clearFilters ? [] : selectedKeys[1];

        /* 更新請(qǐng)求參數(shù) */
        if (selectedData[0] === "all") {
            // 請(qǐng)求參數(shù) partialAllKeys
            queryCriteria[`${dataIndex}Codes`] = partialAllKeys;
        } else {
            console.log(selectedData, "selectedKeys");
            // 請(qǐng)求參數(shù) selectedData
            queryCriteria[`${dataIndex}Codes`] = selectedData;
        };

        this.#queryCriteria = queryCriteria;

        /* 必須在 clearFilters/confirm 函數(shù)執(zhí)行前使用 setSelectedKeys */
        setSelectedKeys([ selectedData ]);
        
        /* 清空并關(guān)閉搜索模塊 */
        clearFilters && clearFilters();
        confirm && confirm();
        /* 《《《《《《《《《 ======= 到此結(jié)束枫耳,可進(jìn)一步提出 ===== 》》》》》》》》》》》》》》 */

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

    render() {
    
        const columns = [
            {
                title: '序號(hào)',
                dataIndex: 'id',
                key: 'id',
                width: '30%'
            },
            {
                title: '省份',
                dataIndex: 'province',
                key: 'province',
                width: '20%',
                ...fetchColumnSearchProps.call(this, this.#provinceArr, 'province', this.handleFilterCallback)
            },
            {
                title: '學(xué)歷',
                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" />;
    }
}

完美的并集表格篩選演示,缺點(diǎn)是會(huì)出現(xiàn)無(wú)數(shù)據(jù)的情況吟温,但是很常用:

完美的并集表格篩選,缺點(diǎn)是會(huì)出現(xiàn)無(wú)數(shù)據(jù)的情況

完~

搬到六道口的第七天扛邑,當(dāng)前時(shí)間: Friday, September 18, 2020 02:01:20

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末樟氢,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子朴爬,更是在濱河造成了極大的恐慌,老刑警劉巖冈闭,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)仁期,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)赊级,“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了翘魄?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)鳖悠。 經(jīng)常有香客問(wèn)我,道長(zhǎng)优妙,這世上最難降的妖魔是什么乘综? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮套硼,結(jié)果婚禮上卡辰,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好九妈,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布反砌。 她就那樣靜靜地躺著,像睡著了一般萌朱。 火紅的嫁衣襯著肌膚如雪宴树。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,079評(píng)論 1 285
  • 那天晶疼,我揣著相機(jī)與錄音酒贬,去河邊找鬼。 笑死翠霍,一個(gè)胖子當(dāng)著我的面吹牛锭吨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播寒匙,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼零如,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了锄弱?” 一聲冷哼從身側(cè)響起埠况,我...
    開(kāi)封第一講書(shū)人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎棵癣,沒(méi)想到半個(gè)月后辕翰,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡狈谊,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年喜命,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片河劝。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡壁榕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出赎瞎,到底是詐尸還是另有隱情牌里,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布务甥,位于F島的核電站牡辽,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏敞临。R本人自食惡果不足惜态辛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望挺尿。 院中可真熱鬧奏黑,春花似錦炊邦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至蹂匹,卻和暖如春蜗细,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背怒详。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留踪区,地道東北人昆烁。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像缎岗,于是被迫代替她去往敵國(guó)和親静尼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345