聊聊如何在React项目中使用Antd的Table组件实现Echarts的热力图效果?

1,808 阅读17分钟

「这是我参与2022首次更文挑战的第11天,活动详情查看:2022首次更文挑战

前言

公司才开始上班,后端人员请假调休了,所以导致我们的工作没有可对接人员,需求人员也还没有理出完整的产品文档,这个难得闲暇时间咋办呢?思去想来,还是不要碌碌无为的浪费时间吧(说好的不卷还是得卷了,头发不保啊),那就只有写一篇聊聊如何在React项目中使用Antd的Table组件实现Echarts热力图效果的文章了(码住你一定会用上的!)。

需求背景

原始需求就是做一个页面统计部门或者分公司的所有人员在每年52个周的工时强度表,需要直观的展示每个人员的工时情况强弱对比关系。咋样?需求很简单吧,注意这只是原始需求,这个需求我们迭代了至少10个版本。

老规矩,先上设计图,直观了解需求:

微信截图_20220211155522.png

原始需求的设计图就是这样的,表头给缺省了列并要求固定不动,底部两行也需要固定,并且还有两列是有排序功能的。是不是我们的需求都是不走寻常路的,怎么难就给你怎么上,别人给你挖好了坑就等着你来跳呢。

迭代中的其中一版:

微信截图_20220211155609.png

就颜色变了?仔细看表的上方,是不是多了一个滑块,滑块出现的意义就是我只要随意拖动,下方的颜色块就只保留滑块区间的色系,是不是很合理?不容你辩驳,反正按我的需求来绝对没问题。

test1.gif

这就完了?不可能的,期间我们做数据可视化时,Boss看到了上面Echarts热力图效果,来来来,我们的工时强度表能不能改成Echarts的那样的热力图效果呢?咳咳,现在用Echarts重构哪怕是原有的数据结构这些怕是不能支持,要和后端商量才能有准确改造计划,老板就让下来研究。哎,看到我就要,能有啥办法,打工人的生活不易就在这些点滴。

看下面就是上线之后的最终效果:

test2.gif

思考

思考什么?我们拿到这个需求,我们并不是直接上手撸代码,而是应该思考这个页面有什么难度,难点在什么地方?我们代码的可扩展性该怎么考虑?和后端交互的数据结构是怎样的?怎么才能让前后端改动都小而不影响整体交付时间等等问题我们都要思考。

就上面的需求我总结有以下难点:

  1. Antd的Table组件支不支持表头列可缺省?不支持该怎么实现设计图设计的效果?不支持怎么固定前两行做成表头样式?排序列的排序功能没用Antd表头提供的排序API如何实现?
  2. 表尾的固定行怎么是现实?
  3. 图例枚举、色值枚举怎么定义?
  4. 数据结构是怎么样的?热力图效果要不要在现有基础上重新开发?热力图是否满足现有的页面功能?能不能用其他方式实现热力图效果呢?
  5. 滑块使用Antd提供的Slider组件还是自己封装?使用Slider组件需要做什么改造?如何实现鼠标移入移出表格色块跟随变化的效果?怎么计算鼠标在Slider组件上移动距离去找色值的枚举值?
  6. 图例说明如何添加事件支持查看色值变化功能?

这就是我拿到这个需求之后做出的思考,也是我在开发过程中觉得会有难度的点,当把这些点想透彻,那问题就不是问题,都会迎刃而解,做起事来也会事半功倍。

开发

既然开发中会遇到问题都列出来了,那我们就来针对问题一个个解决就欧克了。

问题一 ★★★☆☆

首先针对第一个思考我的做法是:

我查看了AntdTable组件,发现它不支持表头列缺省,那要实现这样的效果不可能自己用div写一个表格出来吧,那就太荒谬了,表头不支持?那我不要表头,把它当成数据行,然后根据序号判断它是第一行,如果有数据就黑色,没数据就白色;第二行样式按设计图的灰底白字样式编写;非第一行第二行那就走色值枚举颜色逻辑不就行了。对,这样还靠谱一点,比起用表头还简单多了,现在唯一的缺点就是得自己写排序功能得逻辑。至于如何固定不动那就交给样式控制了,我使用得是position的新属性sticky。先上代码:

布局代码:

<Table
    className="custom-table strength-table h ofa"
    rowKey="id"
    rowClassName={(record: any, index: number) => {
        let className: any = [];
        // 第一行
        if (index === 0) {
            className= ['first-tr-line', 'table-first-tr-sticky'];
        }
        // 第二行
        if (index === 1) {
            className= ['second-tr-line', 'table-second-tr-sticky'];
        }
        return className.join(' ');
    }}
    bordered
    sticky={true}
    dataSource={dataSource}
    columns={columns}
    loading={loading}
    showHeader={false} // 隐藏表头
    pagination={false}
</>

逻辑代码:

/**
 * 设置表格行单元格类名
 * @param index
 * @param value
 * @param style
 * @param workHour
 */
const turnListColsClassName = (index: number, value: any, style: number, workHour: number): string => {
    // 如果是第一行并且没有值
    if (index === 0 && !value) {
        return 'no-data-td';
    }
    // 如果是第一行并且有值
    if (index === 0 && value) {
        return 'has-data-td';
    }
    // 第二行
    if (index === 1) {
        return 'second-tr-line';
    }
    // 非第一行和第二行
    return isTdCellBgColor(style, workHour);
}

/**
 * 判断单元格背景颜色
 * @param style
 * @param workHour
 */
const isTdCellBgColor = (style: number, workHour: number): string => {
    if (!workHour) {
        return '';
    }
    // 不在范围内的色块消失不见,默认背景颜色是白色的
    if (!sliderArr.includes(style)) {
        return;
    }
    let colorArr: any[] = [
        'bit-bg-0to37',
        'bit-bg-37to50',
        'bit-bg-50to55',
        'bit-bg-55to60',
        'bit-bg-60to65',
        'bit-bg-65to70',
        'bit-bg-70to80',
        'bit-bg-80over'
    ];
    let cssStyle: string = '';
    // 如果滑块值或图例值等于色值,就加上阴影突出显示
    if (hoverNum == style) {
        cssStyle = 'bit-num-shadow'
    }
    return [cssStyle, colorArr[style]].join(' ');
}

样式代码:

// 固定第一行
.table-first-tr-sticky {
    td {
        position: sticky !important;
        top: 0px;
        z-index: 1 !important;
        background-color: #fff;
    }
}
// 固定第二行
.table-second-tr-sticky {
    td {
        position: sticky !important;
        top: 24px;
        z-index: 1 !important;
        background-color: #647386;
    }
}

.first-tr-line {
    height: 24px !important;

    td {
        height: 24px;
        color: #fff;
        background-color: #647386 !important;
    }

    .no-data-td {
        background-color: #fff !important;
        border-right-color: #fff !important;
    }

    .has-data-td {
        background-color: #263238 !important;
        border-right-color: #fff !important;
    }

    .no-sort-color {
        color: #fff;
    }

    .has-sort-color {
        color: #409EFF
    }
}

.second-tr-line {
    color: #fff;
    background-color: #647386 !important;
}

第一个问题还剩如何编写排序功能,我并没有把它提成一个组件来写,因为其它地方用到的排序功能都是表格提供的,就这个页面需要自己写所以不具有共用性就没有提出单独成一个组件;还有就是看图可以知道合计列周数列是将第一行和第二行合并了的,所以这两列在做的时候也要考虑合并单元格。具体实现方式见如下代码:

/**
 * 合计列和周数列内容
 * @param index
 * @param value
 * @param orderFiled
 */
const turnTotalAndWeekColContent = (index: number, value: any, orderFiled: string): any => {
    let className: string = '';
    let children: any = null;
    let rowSpan: number = null;
    // 通过行序号判断并合并单元格
    if (index === 0) {
        rowSpan = 2;
        className = 'first-tr-td';
        children = (
            <div>
                <div>{value}</div>
                <div className='flexc crp' onClick={() => onSortHandle(orderFiled)}>
                    <CaretUpOutlined
                        className={[
                            'no-sort-color',
                            isSorterStyle(orderFiled, 1)
                        ].join(' ')}
                    />
                    <CaretDownOutlined
                        style={{ marginTop: '-5px' }}
                        className={[
                            'no-sort-color',
                            isSorterStyle(orderFiled, 2)
                        ].join(' ')}
                    />
                </div>
            </div>
        )
    }
    if (index === 1) {
        rowSpan = 0;
    }
    if (index !== 0 && index !== 1) {
        className = 'td-bold-col';
    }
    if (index !== 0) {
        children = <div>{value}</div>;
    }
    return {children, props: {className, rowSpan}};
}


/**
 * 操作排序图标进行排序函数
 * @param orderField 
 */
const onSortHandle = (orderField: string) => {
    let obj: any = sorterArray.find((v: any) => v.orderField === orderField);
    let newSorterArray: any[] = [...sorterArray];
    let newDataSource: any[] = JSON.parse(JSON.stringify(dataSource)) || [];
    newSorterArray.forEach((v: any) => {
        if (v.orderField === orderField) {
            v.isSort = true;
            if (v.orderMode != 2) {
                obj.orderMode += 1;
            } else {
                obj.orderMode = 0;
            }
        } else {
            v.orderMode = 0;
            v.isSort = false;
        }
    });
    setSorterArray(newSorterArray);
    // 如果取消排序则还原表格数据为未排序之前的数据,否则排除前两行数据进行升序或者降序排序
    if (obj.orderMode === 0) {
        setDataSource(dataSourceBak);
        return;
    }
    let tempList: any[] = newDataSource.slice(0, 2);
    let aray: any[] = newDataSource.slice(2).sort(compare(orderField, obj.orderMode));
    setDataSource([...tempList, ...aray]);
}


/**
 * 排序比较函数
 * @param orderField
 * @param orderMode 1升序, 2降序
 */
const compare = (orderField: string, orderMode: number) => {
    return function (a: any, b: any) {
        // 升序
        if (orderMode === 1) {
            return a[orderField] - b[orderField];
        }
        // 降序
        if (orderMode === 2) {
            return b[orderField] - a[orderField];
        }
        return 0;
    }
}


/**
 * 判断排序样式
 * @param orderField
 * @param orderMode 1升序, 2降序
 */
const isSorterStyle = (orderField: string, orderMode: number): string => {
    let sorterClassName: string = '';
    sorterArray.forEach((v: any) => {
        if (v.isSort && v.orderField === orderField && v.orderMode === orderMode) {
            sorterClassName = 'has-sort-color';
        }
    });
    return sorterClassName;
}

/**
 * 构建表格列
 */
const columns= [
    {
        dataIndex: 'name',
        className: 'table-right-border',
        align: 'center',
        width: 66,
        render: (text: any, record: any, index: number) => {
            const {id} = record;
            let className: string = '';
            if (index === 0) {
                className = 'first-tr-td';
            }
            if (index === 1) {
                className = 'second-tr-td';
            }
            if (index !== 0 && index !== 1) {
                className = 'td-first-col';
            }
            if (selectUserId === id) {
                className = 'bit-first-td-active';
            }
            let children: any = (
                <div
                    title={text}
                    className={[index !== 0 && index!== 1 ? "bit-name-div hcp": "", "w eps"].join(' ')}
                    onClick={() => clickNameHandle(id, index)}
                >{text}</div>
            )
            return {children, props: {className}};
        }
    },
    {
        dataIndex: 'total',
        className: 'table-right-border',
        align: 'center',
        width: 41,
        render: (text: any, record: any, index: number) => {
            return turnTotalAndWeekColContent(index, text, 'total');
        }
    },
    {
        dataIndex: 'weekTotal',
        className: 'table-right-border',
        align: 'center',
        width: 41,
        render: (text: any, record: any, index: number) => {
            return turnTotalAndWeekColContent(index, text, 'weekTotal');
        }
    },
    ...buildListColumns('tableVos')
];

好了,第一个问题相关的疑问就解释清了,解决问题的方法还是需要掌握一定的知识面的,这都要得益于平时项目的累积,所以好记性不如烂笔头,遇到问题还是记录起来免得下次遇到又懵了。

问题二 ★★★☆☆

表尾的固定行怎么实现?这个问题可以使用问题一中的定位来解决,但是我没有选择那样的方式,因为我做表尾的固定都是使用的Table组件提供的summary(总结栏)的API方式加问题一中定位方式实现的,我坚持的观念就是:有就用,没有就改,改了不行就写。其实就是在Table组件上加上如下代码:

布局代码:

<Table
    className="custom-table strength-table h ofa"
    rowKey="id"
    rowClassName={(record: any, index: number) => {
        let className: any = [];
        if (index === 0) {
            className= ['first-tr-line', 'table-first-tr-sticky'];
        }
        if (index === 1) {
            className= ['second-tr-line', 'table-second-tr-sticky'];
        }
        return className.join(' ');
    }}
    bordered
    sticky={true}
    dataSource={dataSource}
    columns={columns}
    loading={loading}
    showHeader={false}
    pagination={false}
    // 总结栏
    summary={() => {
        const {total: avgTotal, weekTotal: avgWeekTotal, tableVos: avgList = []} = avgLine || {};
        const {total: sumTotal, weekTotal: sumWeekTotal, tableVos: sumList = []} = sumLine || {};
        return (
            <>
                {
                    ObjTest.isNotEmptyArray(dataSource)
                        ?
                        <>
                            {/*平均行*/}
                            <Table.Summary.Row>
                                <Table.Summary.Cell index={0} className="bit-summary-line1 table-right-border">
                                    <div className="bit-sumline-total">平均</div>
                                </Table.Summary.Cell>
                                <Table.Summary.Cell index={1} className="bit-summary-line1 table-right-border">
                                    <div className="bit-sumline-font">{avgTotal}</div>
                                </Table.Summary.Cell>
                                <Table.Summary.Cell index={2} className="bit-summary-line1 table-right-border">
                                    <div className="bit-sumline-font">{avgWeekTotal}</div>
                                </Table.Summary.Cell>
                                {
                                    avgList.map((v: any, index: number) =>
                                        <Table.Summary.Cell
                                            key={index}
                                            index={index+10}
                                            className={
                                                [
                                                    "bit-summary-line1",
                                                    v.isLine ? "table-right-border" : "",
                                                    isDescOrCellBgColor1(v.colorEnum - 1, v.workHour)
                                                ].join(' ')}>
                                            <div className="bit-sumline-font">{v.workHour}</div>
                                        </Table.Summary.Cell>
                                    )
                                }
                            </Table.Summary.Row>
                            {/*汇总行*/}
                            <Table.Summary.Row>
                                <Table.Summary.Cell index={0} className="bit-summary-line2 table-right-border">
                                    <div className="bit-sumline-total">
                                        汇总
                                        <Tooltip title={<div>总人数:{userCount}</div>}>
                                            (<span>{userCount}</span>)
                                        </Tooltip>
                                    </div>
                                </Table.Summary.Cell>
                                <Table.Summary.Cell index={1} className="bit-summary-line2 table-right-border">
                                    <div className="bit-sumline-font">{sumTotal}</div>
                                </Table.Summary.Cell>
                                <Table.Summary.Cell index={2} className="bit-summary-line2 table-right-border">
                                    <div className="bit-sumline-font">{sumWeekTotal}</div>
                                </Table.Summary.Cell>
                                {
                                    sumList.map((v: any, index: number) =>
                                        <Table.Summary.Cell
                                            key={index}
                                            index={index+10}
                                            className={
                                                [
                                                    "bit-summary-line2",
                                                    v.isLine ? "table-right-border" : ""
                                                ].join(' ')}>
                                            <div className="bit-sumline-font">{v.workHour}</div>
                                        </Table.Summary.Cell>
                                    )
                                }
                            </Table.Summary.Row>
                        </>
                        :
                        null
                }
            </>
        );
    }}
/>

样式代码:

.bit-summary-line1, .bit-summary-line2 {
    position: sticky;
    bottom: 28px;
    height: 28px;
    text-align: center;
    color: rgba(0, 0, 0, 0.85);
    background-color: #fff;
    border-top: 1px solid #CFD8DB;

    .bit-sumline-total {
        text-align: center;
        font-size: 12px;
        font-weight: bold;
        color: rgba(0, 0, 0, 0.85);
    }

    .bit-sumline-font {
        text-align: center;
        font-weight: bold;
    }
}

.bit-summary-line2 {
    bottom: 0px;
    border-top: 0;
}

问题三 ★☆☆☆☆

枚举值的定义这个要和后端同学商量好来,避免双方都按照自己的来到对接时谁都不想改就更尴尬了,所以这些都是不可忽略的细节。

// 颜色枚举
const ColorEnum: any = [
    {style: 1, label: '0~37.5'},
    {style: 2, label: '37.5~50'},
    {style: 3, label: '50~55'},
    {style: 4, label: '55~60'},
    {style: 5, label: '60~65'},
    {style: 6, label: '65~70'},
    {style: 7, label: '70~80'},
    {style: 8, label: '80以上'}
];

问题四 ★★★☆☆

数据结构也是要和后端同学商量着来的,最好提前定义好结构和字段名,但是对于我来说把结构定义好已经可以了,至于字段名的定义是不怎么匹配的含义才会去提出让后端修改,最主要就是结构一定不要变。我们这个页面的数据结构是这样的:

微信截图_20220211195842.png

后端把表尾固定的两行分别给独立为balanceRowtotalRow两个对象出来了,中间表格数据为deptWorkingIntensityDetailsVos数组,其实dunk不必这样的,可以把表尾两行数据融进数组的,有可能他是为了我们方便吧^_^

需求背景不是有提到在我们做数据可视化时,老板要我们将做好的页面改成Echarts的热力图效果么。其实我们思考过这个问题,做了相关的调研和对比,发现Echarts的热力图有局限性,像需求提到的表头缺省、固定、排序效果;表尾固定效果;数据多时可以滚动查看等效果热力图都不能实现,就拿数据很多时,表格可以滚动查看,而热力图是用Canvas画出来的,就在一屏上显示,到时的页面效果很拥挤不直观,所以我们思考再三就没有进行重构;当然原有的数据结构也会不支持热力图展现,所以页面就将是重新开发,推到重来,老板也不接受这个开发时间,但就是想要那样的效果。那咋办呢?我就建议我们在现有基础上改动,加一个滑块可以左右拖动查看相应的色块情况,然后就做了中间那一版本,只是可以拖动不带鼠标移入移出的效果;上线几天之后发现还是念念不忘,还是想要鼠标移入移出的效果,所以我们只有再去思考怎样才能达到那样的效果,终于在最后实现了想要的效果。

问题五 ★★★★☆

其实这个思考是该需求中最重要的一个,一切的交互效果都由它产生。

滑块效果首先想到的是Antd有提供一个Slider滑动输入条组件,它就能左右来回滑动,和我们业务还挺切合的,我们能不能拿来用用?既然有了这个想法就去考究,发现它的范围可拖拽效果还挺符合我们期望的,如下图:

微信截图_20220211201925.png

它的值是0~100,我把它改成我们的枚举值0~7不就欧克了么。样式不对?改一改不就行了么,改是我们的强项呀,分分钟就能搞定,先用用再说。哎呀,得来全不费工夫^_^。

这里我是重新封装了一下Slider组件的,因为它提供的事件不足以满足我们的需求,我还需要给它额外加上鼠标移入移出等事件,页面相对复杂,我就把它独立为一个组件方便引用处理逻辑。

Slider组件本身没有提供鼠标事件,那我们如何添加呢?那就是在外层给它套一个div,给div加上鼠标的移入移出点击松开事件,并且设定一个默认宽度,用于计算鼠标移到位置的枚举值为多少并传出给父组件,让枚举值对应的色块加上阴影突出显示,从而达到热力图效果。

思路就是如上,代码实现如下:

创建CustomSlider.tsx文件:

import React, {useEffect, useRef, useState} from 'react';
import {Slider} from 'antd';
import Decimal from "decimal.js";

interface CustomSliderProps {
    flag: number; // 是否重置默认值
    onMouseMoveHandle: (value: number) => void; // 鼠标移入事件
    onMouseLeaveHandle: (value: number) => void; // 鼠标离开事件
    onAfterChangeHandle: (arr: number[], flag: number) => void; // slider改变后事件
}


/**
 * 自定义Slider组件
 * @param props
 * @constructor
 */
const CustomSlider = (props: CustomSliderProps) => {
    const {
        flag = 0,
        onMouseMoveHandle = (value: number) => {},
        onMouseLeaveHandle = (value: number) => {},
        onAfterChangeHandle = (arr: number[], flag: number) => {}
    } = props;
    const [sliderValue, setSliderValue] = useState<[number, number]>([0, 7]);
    const sliderRef = useRef<any>(null);
    const boolRef = useRef<boolean>(false);

    useEffect(() => {
        if(flag === 1) {
            setSliderValue([0, 7]);
        }
    }, [flag]);


    return (
        <div
            className="bit-slider-box"
            ref={sliderRef}
            onMouseMove={(e: any) => {
                if (boolRef.current) {
                    return;
                }
                if (sliderRef.current) {
                    // 根据移动的距离计算对应的枚举值并传出
                    let mouseOffsetX: string = e.clientX;
                    let boxOffsetX: string = sliderRef.current.offsetLeft;
                    let digital: number = new Decimal(new Decimal(mouseOffsetX).sub(boxOffsetX).toNumber()).div(sliderRef.current.clientWidth).toNumber();
                    let value: number = Number(new Decimal(digital).div(0.125).toFixed(0));
                    onMouseMoveHandle(value);
                }
            }}
            onMouseDown={() => boolRef.current = true}
            onMouseUp={() => boolRef.current = false}
            onMouseLeave={() => onMouseLeaveHandle(-1)}
        >
            <Slider
                range
                min={0}
                max={7}
                value={sliderValue}
                tooltipVisible={false}
                onAfterChange={(value: [number, number]) => {
                    let arr: number[] = [];
                    for (let i = value[0]; i <= value[1]; i++) {
                        arr.push(i);
                    }
                    boolRef.current = false;
                    onAfterChangeHandle(arr, 0);
                }}
                onChange={(value: [number, number]) => setSliderValue(value)}
            />
        </div>
    )
}


export default CustomSlider;

样式代码:

.bit-slider-box {
    width: 160px;
    margin-right: 20px;
    .ant-slider{
        height: 14px;
        margin: 0;
        padding: 0;
        .ant-slider-rail {
            height: 14px;
            background: linear-gradient(90deg, #60AA3C 0%, #FFD3D3 47%, #900908 100%);
        }
        .ant-slider-track {
            height: 14px;
            background-color: transparent;
        }
        .ant-slider-step {
            height: 14px;
        }
        .ant-slider-handle {
            margin-top: 0;
        }
    }
}

可以看到我们在计算时是用鼠标移动的X轴坐标距离减去Slider组件外层盒子相对于左边界的距离来除以盒子的实际宽度得到一个比例,然后用这个比例来除以每一份所占的比例(1 / 8 = 0.125)得到的值取整作为枚举值进行传出。当然这个取整得值不是那么准确,但是可以忽略,毕竟我们滑块的背景色选用的是渐变色,就是为了解决近似值的情况;还有我定义了一个布尔值boolRef来作为开关,只有当鼠标点下之后才能拖动,不然鼠标移上去就会遭成Silder跟随拖动从而形成卡顿,故用boolRef做开关来控制到底是移入移出还是点击拖动事件。

使用组件:

<div className="search-form-desc-wrap flexic">
    <CustomSlider
        flag={flag}
        onAfterChangeHandle={(arr: number[], flag: number) => {
            setSliderArr(arr);
            setFlag(flag);
        }}
        onMouseMoveHandle={(value: number) => {setHoverNum(value)}}
        onMouseLeaveHandle={(value: number) => {setHoverNum(value)}}
    />
</div>

逻辑代码:

/**
 * 构造动态列
 * @param dataIndex
 */
const buildListColumns = (dataIndex): any[] => {
    let column: any[] = [];
    if (ObjTest.isNotEmptyArray(dataSource)) {
        let firstList: any[] = dataSource[0][dataIndex] || [];
        let temp: any[] = firstList.map((v: any, i: number) => {
            return {
                className: `${v.isLine ? 'table-right-border': ''}`,
                align: 'center',
                width: 31,
                render: (text: any, record: any, index: number) => {
                    let {month, week, workHour, beginTime, endTime, colorEnum} = record[dataIndex][i];
                    let beginDate: string = beginTime ? beginTime.substring(5) : '';
                    let endDate: string = endTime ? endTime.substring(5) : '';
                    let className: string = turnListColsClassName(index, month, colorEnum - 1, workHour);
                    let children: any = (
                        <div className="bit-tr-cell">
                            {
                                index === 0
                                    ?
                                    <div>{month}</div>
                                    :
                                    null
                            }
                            {
                                index === 1
                                    ?
                                    <Tooltip title={<div>{beginDate}~{endDate}</div>}>
                                        <div>{week}</div>
                                    </Tooltip>
                                    :
                                    null
                            }
                            {
                                index !== 0 && index !== 1 && sliderArr.includes(colorEnum - 1)
                                    ?
                                    <div
                                        data-value={colorEnum - 1}
                                        className={[index !== 0 && index !== 1 ? "bit-num-hover" : ""].join(' ')}
                                    >{workHour}</div>
                                    :
                                    null
                            }
                        </div>
                    );
                    return {children, props: {className}};
                }
            }
        });
        column = [...temp];
    }
    return column;
}

问题六 ★★☆☆☆

Slider组件那么难加上鼠标事件的我们都加上,一般的div我们还加不上么?不可能的三,趁热打铁,直接上代码:

布局代码:

<div className="search-form-desc-wrap flexic">
    <CustomSlider
        flag={flag}
        onAfterChangeHandle={(arr: number[], flag: number) => {
            setSliderArr(arr);
            setFlag(flag);
        }}
        onMouseMoveHandle={(value: number) => {setHoverNum(value)}}
        onMouseLeaveHandle={(value: number) => {setHoverNum(value)}}
    />
    <div className="bit-pic-box flexic">
        工时颜色说明:
        {
            ColorEnum.map(v =>
                <div
                    className="ml6 flexic"
                    style={{cursor: "pointer"}}
                    key={v.style}
                    onMouseOver={() => {
                        if (hoverNum === v.style - 1) {
                            return;
                        }
                        setHoverNum(v.style - 1);
                    }}
                    onMouseOut={() => {
                        setHoverNum(-1);
                    }}
                >
                    <div className={["bit-color-block", isDescOrCellBgColor(v.style - 1)].join(' ')}></div>
                    <div className="ml4">{v.label}</div>
                </div>
            )
        }
    </div>
</div>

逻辑代码:

/**
 * 图例说明颜色
 * @param style
 */
const isDescOrCellBgColor = (style: number): string => {
    let colorArr: any[] = [
        'bit-bg-0to37',
        'bit-bg-37to50',
        'bit-bg-50to55',
        'bit-bg-55to60',
        'bit-bg-60to65',
        'bit-bg-65to70',
        'bit-bg-70to80',
        'bit-bg-80over'
    ];
    return colorArr[style];
}

上述实现鼠标移入图例上表格内的色块会高亮显示的逻辑主要是根据setHoverNum()设置,然后根据问题一展示代码中isTdCellBgColor()函数去判断是否加上阴影效果,从而达到跟随变化高亮显示效果。

样式代码:

.bit-tr-cell {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 100%;
    height: 100%;
    .bit-num-hover {
        width: 100%;
        height: 32px;
        line-height: 31px;
        cursor: pointer;
    }
    .bit-num-hover:hover {
        box-shadow: #666 0px 0px 10px;
    }
}

.bit-num-shadow {
    .bit-num-hover {
        width: 100%;
        height: 32px;
        line-height: 31px;
        cursor: pointer;
        box-shadow: #000000BF 0px 0px 10px;
    }
    .bit-num-hover:hover {
        box-shadow: #000000BF 0px 0px 10px;
    }
}

.bit-bg-0to37 {
    background-color: #60AA3C;
}

.bit-bg-37to50 {
    background-color: #9EBA77;
}

.bit-bg-50to55 {
    background-color: #D2E7C8;
}

.bit-bg-55to60 {
    background-color: #FFD5D5;
    border: 1px solid #FFD5D5;
}

.bit-bg-60to65 {
    background-color: #EAADAD;
}

.bit-bg-65to70 {
    background-color: #D28180;
}

.bit-bg-70to80 {
    background-color: #B85252;
}

.bit-bg-80over {
    background-color: #900908;
}

至此,关于如何实现Echarts热力图效果的相关思考问题就说得差不多了,这就是从拿到需求到对需求做出思考然后实现的过程。各位小伙伴可以参考思路和实现逻辑,当然上述的每个问题给出的解决办法都不一定是最优解,小伙伴们肯定也有其它更好的解决方法或我没有思考到的问题,所以希望大家可以发出来一起讨论讨论。

说个题外话,看到上面的gif图没有,基本上我们每星期的工时基本都是55左右及以上,所以之前有些文章提到的朝九晚九并不是没有根据的,只能感叹生活的不易。话说回来若需要完整页面代码的可以先联系我,当然最好还是大家自己去撸一遍,那样才会印象深刻。

往期精彩文章

后语

伙伴们,如果觉得本文对你有些许帮助,点个👍或者➕个关注在走呗^_^ 。另外如果本文章有问题或有不理解的部分,欢迎大家在评论区评论指出,我们一起讨论共勉。