React 可编辑表格,动态列与合并单元格

368 阅读5分钟

工作中遇到的问题特此记录一下

技术难点一: 动态列

...dateList.map(dateStr => {
    const weekStr = getWeekByDate(dateStr, dateFormat);
    return {
        title: `${dateStr}(周${weekStr})`,
        dataIndex: `${dateStr}${weekStr}`,
        align: 'center',
        key: `${dateStr}${weekStr}`,
        editable: true,
        width: '130px',
        render: (text) => <span>{text}</span>,
    };
}),

技术难点二: 合并单元格

根据 antd 官网给的 合并单元格 给的例子实现不了
如下解决的例子
const columns = [
    
    {
        title: '地区',
        dataIndex: '地区',
        align: 'center',
        key: '地区',
        editable: false,
        width: '80px',
        fixed: 'left',
        render: (text, record, index) => {
            const rowSpan = this.calculateRowSpan(this.state[dataSourceKey], text)[index];
            return {
                children: <span>{text}</span>,
                props: {
                    rowSpan,
                },
            };
        }
    },
]

如下是页面的全部代码

import React, {Component} from 'react';
import style from './index.less';
import EditableCell from '../components/EditableCell';
import {Button, DatePicker, Table, Form, InputNumber, message, Modal, Radio, Checkbox} from 'antd';
import {getAllDate, getWeekByDate} from '../../../utils/utils';
import moment from 'moment';
import {CalculatorOutlined, SearchOutlined, ExperimentOutlined, FieldTimeOutlined} from '@ant-design/icons';
import ButtonBar from '../../../components/ButtonBar';
import DevideBar from '../../../components/DevideBar';
import {
    getTableData,
    updateTableData,
    computeTableData,
    initTableData,
    updateSimultaneousRate,
    getSimultaneousRate,
    deleteTableData
} from '../../../axios/UnitManage/FutureTenDayForecastLoad';

const dateFormat = 'YYYY-MM-DD';


class FutureTenDayForecastLoad extends Component {
    constructor(props) {
        super(props);
        this.formRef = React.createRef();
        this.buttonItems = [
            {
                icon: <SearchOutlined/>,
                name: "查询",
                style: {background: 'rgb(255, 255, 255)', borderColor: 'rgb(33, 142, 147)', color: 'rgb(33, 142, 147)'},
                btnFunction: () => this.onSearch(),
            },
            {
                icon: <ExperimentOutlined/>,
                name: "初始化",
                style: {background: "rgb(33, 142, 147)", borderColor: "#7ccdc7"},
                btnFunction: () => {
                    this.setState({initModalVisible: true})
                },
            },
            {
                icon: (<CalculatorOutlined style={{fontSize: '16px',}}/>),
                name: '计算',
                style: {background: '#FFA800', borderColor: '#FFA800'},
                btnFunction: () => this.compute()
            },
            {
                icon: <FieldTimeOutlined/>,
                name: "同时率",
                style: {background: "rgb(33, 142, 147)", borderColor: "#7ccdc7"},
                btnFunction: () => {
                    this.setState({simultaneousRateModalVisible: true})
                },
            },
        ];
        this.state = {
            orgCode: localStorage.getItem(FN.COOKIE_KEY_PREFIX + 'deptcode'),
            startDate: moment('2024-07-10').format(dateFormat),
            startDate1: moment().format(dateFormat),
            endDate: moment('2024-07-11').add(9, 'days').format(dateFormat),
            endDate1: moment().format(dateFormat),
            dataSource1: [],
            dataSource2: [],
            dataSource3: [],
            dataSource4: [],
            columns1: [],
            columns2: [],
            columns3: [],
            columns4: [],
            // 豫南
            yuNan: false,
            editingKey: '',
            originalRecord: {},
            // 初始化模态框
            initModalVisible: false,
            // 同时率模态框
            simultaneousRateModalVisible: false,
            simultaneousRate: 0.995,
            simultaneousRateId: null,
            // 预计划/计划
            selectedOption: '预计划',
            // 确认删除模态框
            confirmModalVisible: false,
            // 确认删除记录
            deleteRecord: {},
            // 为每个表单创建单独的 formRef
            formRefs: {
                form1: React.createRef(),
                form2: React.createRef(),
                form3: React.createRef(),
                form4: React.createRef(),
            }
        };
    }

    componentDidMount() {
        this.getSimultaneousRate();
        this.createTable();
    }

    getSimultaneousRate = async () => {
        try {
            let res = await getSimultaneousRate(this.state.simultaneousRate);
            if (res.code === 200 && res.data.length > 0) {
                this.setState({
                    simultaneousRate: res.data[0]["confValue"],
                    simultaneousRateId: res.data[0]["id"],
                });
            } else {
                message.error('获取同时率失败');
            }
        } catch (e) {
            message.error('获取同时率失败');
        }
    };
    // 初始化按钮
    initTableData = async () => {
        try {
            this.setState({initModalVisible: false});
            let res = await initTableData(this.state.startDate1, this.state.endDate1, this.state.selectedOption,);
            if (res.code === 200) {
                message.success('初始化成功');
            } else {
                message.error('初始化失败');
            }
        } catch (e) {
            message.error('初始化失败');
        }
    }
    handleChange = (e) => {
        this.setState({selectedOption: e.target.value});
    };
    handleSimultaneousRate = async () => {
        try {
            this.setState({simultaneousRateModalVisible: false});
            let res = await updateSimultaneousRate(this.state.simultaneousRateId, this.state.simultaneousRate,);
            if (res.code === 200) {
                message.success('同时率计算成功');
                this.getTableData();
            } else {
                message.error('同时率计算失败');
            }
        } catch (e) {
            message.error('同时率计算失败');
        }
    };
    // 同时率修改事件
    simultaneousRateChange = (value) => {
        this.setState({
            simultaneousRate: value
        })
    }
    // 计算按钮
    compute = async () => {
        try {
            let res = await computeTableData(this.state.startDate, this.state.endDate, this.state.orgCode,);
            if (res.code === 200) {
                message.success('计算成功');
            } else {
                message.error('计算失败');
            }
        } catch (e) {
            message.error('计算失败');
        }

    }
    // 查询按钮
    onSearch = () => {
        this.createTable();
    }
    calculateRowSpan = (data, region) => {
        const rowSpan = [];
        let currentRowSpan = 1;

        for (let i = 0; i < data.length; i++) {
            if (i === 0 || data[i]['地区'] !== data[i - 1]['地区']) {
                currentRowSpan = 1;
                for (let j = i + 1; j < data.length; j++) {
                    if (data[j]['地区'] === data[i]['地区']) {
                        currentRowSpan++;
                    } else {
                        break;
                    }
                }
                rowSpan[i] = currentRowSpan;
            } else {
                rowSpan[i] = 0;
            }
        }
        return rowSpan;
    };

    createTable = () => {
        const {startDate, endDate, formRefs} = this.state;
        const dateList = getAllDate(startDate, endDate);

        const createColumns = (dataSourceKey, formRef) => [
            {
                title: '地区',
                dataIndex: '地区',
                align: 'center',
                key: '地区',
                editable: false,
                width: '80px',
                fixed: 'left',
                render: (text, record, index) => {
                    const rowSpan = this.calculateRowSpan(this.state[dataSourceKey], text)[index];
                    return {
                        children: <span>{text}</span>,
                        props: {
                            rowSpan,
                        },
                    };
                }
            },
            {
                title: '',
                dataIndex: '类型名称',
                key: '类型名称',
                width: 100,
                fixed: 'left',
                editable: false,
            },
            ...dateList.map(dateStr => {
                const weekStr = getWeekByDate(dateStr, dateFormat);
                return {
                    title: `${dateStr}(周${weekStr})`,
                    dataIndex: `${dateStr}${weekStr}`,
                    align: 'center',
                    key: `${dateStr}${weekStr}`,
                    editable: true,
                    width: '130px',
                    render: (text) => <span>{text}</span>,
                };
            }),
            {
                title: '编辑',
                dataIndex: 'operation',
                width: "200px",
                align: "center",
                fixed: 'right',
                render: (_, record) => {
                    const editable = this.isEditing(record);
                    return editable ? (
                        <span>
                            <a onClick={() => this.save(record, formRef)} style={{marginRight: 8}}>
                                <img alt="" className="img_icon" src={require('../../../images/sys/save_icon.png')}/>
                            </a>
                            <a onClick={() => this.cancel(record)}>取消</a>
                        </span>
                    ) : (
                        <span>
                            <Button
                                size="small"
                                style={{
                                    borderColor: '#2CAFE6',
                                    color: '#2CAFE6',
                                    display: 'inline-flex',
                                    alignItems: 'center'
                                }}
                                icon={<img alt="" style={{width: '14px', marginRight: '6px'}}
                                           src={require('../../../images/icon/edit.png')}/>}
                                disabled={this.state.editingKey !== ''}
                                onClick={() => this.edit(record, formRef)}
                            >
                                编辑
                            </Button>
                            <Button
                                size="small"
                                style={{
                                    borderColor: '#EC4758',
                                    color: '#EC4758',
                                    display: 'inline-flex',
                                    alignItems: 'center'
                                }}
                                icon={<img alt="" style={{width: '14px', marginRight: '6px'}}
                                           src={require('../../../images/icon/delete.png')}/>}
                                disabled={this.state.editingKey !== ''}
                                onClick={() => this.setState({confirmModalVisible: true, deleteRecord: record})}
                            >
                                删除
                            </Button>
                        </span>
                    );
                },
            },
        ];

        this.setState({
            columns1: createColumns('dataSource1', formRefs['form1']),
            columns2: createColumns('dataSource2', formRefs['form2']),
            columns3: createColumns('dataSource3', formRefs['form3']),
            columns4: createColumns('dataSource4', formRefs['form4'])
        }, () => {
            this.getTableData();
        });
    };

    getTableData = async () => {
        try {
            let res = await getTableData(this.state.orgCode, this.state.startDate, this.state.endDate, this.state.yuNan);
            if (res.code === 200) {
                this.setState({
                    dataSource1: res.data['晚高峰'] || [],
                    dataSource2: res.data['腰荷'] || [],
                    dataSource3: res.data['早高峰'] || [],
                    dataSource4: res.data['低谷'] || [],
                });
            } else {
                message.error('获取数据失败');
            }
        } catch (e) {
            message.error('获取数据失败');
            console.error('获取数据失败:', e);
        }
    };
    // 豫南 checkbox
    onCheckbox = (e) => {
        this.setState({yuNan: e.target.checked});
    };
    datePickerChange = (date, dateString) => {
        this.setState({startDate: dateString[0], endDate: dateString[1]}, this.createTable);
    };
    futureThreeDay = () => {
        this.setState({
            startDate1: moment().add(1, 'days').format(dateFormat),
            endDate1: moment().add(3, 'days').format(dateFormat),
        })
    }
    futureTenDay = () => {
        this.setState({
            startDate1: moment().add(1, 'days').format(dateFormat),
            endDate1: moment().add(10, 'days').format(dateFormat),
        })
    }
    dateStartChange = (date, dateString) => {
        this.setState({startDate1: dateString})
    }
    dateEndChange = (date, dateString) => {
        this.setState({endDate1: dateString})
    }
    isEditing = (record) => record.UUID === this.state.editingKey;

    edit = (record, formRef) => {
        this.setState({editingKey: record.UUID, originalRecord: {...record}});
        formRef.current.setFieldsValue(record);
    };

    cancel = () => {
        this.setState({
            editingKey: '',
            originalRecord: {},
        });
    };


    save = async (record, formRef) => {
        try {
            const values = await formRef.current.validateFields();
            if (!values.UUID) {
                values.UUID = record.UUID;
            }

            const changes = {};
            for (const key in values) {
                if (values[key] !== this.state.originalRecord[key]) {
                    changes[key] = values[key].trim();
                }
            }

            if (Object.keys(changes).length === 0) {
                message.info('没有检测到变化。');
                return;
            }

            const dataToUpdate = {...this.state.originalRecord, ...changes};
            dataToUpdate['areaId'] = this.state.orgCode;
            dataToUpdate['measTypeCode'] = record['类型'];

            let res;
            if (record.UUID) {
                res = await updateTableData(dataToUpdate);
            }

            if (res.code === 200) {
                message.success('修改成功');
                this.getTableData();
                this.cancel();
            } else {
                message.error('更新失败');
            }
        } catch (error) {
            console.error("保存数据时出错:", error);
            message.error('保存失败');
        }
    };

    onDelete = async () => {
        try {
            this.state.deleteRecord['areaId'] = this.state.orgCode;
            this.state.deleteRecord['measTypeCode'] = this.state.deleteRecord['类型'];
            const res = await deleteTableData(this.state.deleteRecord);
            if (res.code === 200) {
                message.success('删除成功');
                this.getTableData();
            } else {
                message.error('删除失败');
            }
            this.setState({confirmModalVisible: false});
        } catch (e) {
            message.error('删除失败');
        }
    };
    renderTable = (title, columns, dataSource, formRef) => {
        const mergedColumns = columns.map(col => ({
            ...col,
            onCell: record => ({
                record,
                inputType: col.dataIndex === 'number' ? 'number' : 'text',
                dataIndex: col.dataIndex,
                title: col.title,
                editing: (col.editable && (this.isEditing(record) || record.isAdd)) || (record.isAdd && col.editable),
            }),
        }));

        return (
            <div className='table-item'>
                <div className='table-header'>
                    <DevideBar className='DevideBar' title={title}/>
                </div>
                <div className='table-body'>
                    <Form ref={formRef} component={false}>
                        <Table
                            components={{body: {cell: EditableCell}}}
                            columns={mergedColumns}
                            dataSource={dataSource}
                            scroll={{x: 'auto'}}
                            pagination={false}
                            rowKey='UUID'
                        />
                    </Form>
                </div>
            </div>
        );
    };

    render() {
        return (
            <div className={style.container}>
                <div className='page-header'>
                    <div style={{float: 'left', width: '12%'}}>
                        <DevideBar className='DevideBar2' title={"同时率:" + this.state.simultaneousRate}/>
                    </div>
                    <Checkbox onChange={this.onCheckbox}
                              style={{float: 'right', width: '8%', color: '#218e93'}}>豫南</Checkbox>
                    <div className='text'>日期:</div>
                    <DatePicker.RangePicker
                        style={{marginRight: '10px'}}
                        allowClear={false}
                        defaultValue={[moment(this.state.startDate), moment(this.state.endDate)]}
                        onChange={this.datePickerChange}
                        format='YYYY-MM-DD'
                    />
                    <ButtonBar buttonItems={this.buttonItems}/>
                </div>
                <div className='table-container'>
                    {this.renderTable('未来10天晚高峰预测(万千瓦)', this.state.columns1, this.state.dataSource1, this.state.formRefs.form1)}
                    {this.renderTable('未来10天腰荷预测(万千瓦)', this.state.columns2, this.state.dataSource2, this.state.formRefs.form2)}
                    {this.renderTable('未来10天早高峰预测(万千瓦)', this.state.columns3, this.state.dataSource3, this.state.formRefs.form3)}
                    {this.renderTable('未来10天低谷预测(万千瓦)', this.state.columns4, this.state.dataSource4, this.state.formRefs.form4)}
                </div>
                {/*  初始化弹窗  */}
                <Modal
                    title="初始化"
                    onOk={this.initTableData}
                    visible={this.state.initModalVisible}
                    onCancel={() => {
                        this.setState({initModalVisible: false})
                    }}
                    okText="确认获取"
                    cancelText="取消"
                >
                    <Radio.Group onChange={this.handleChange} value={this.state.selectedOption}>
                        <Radio value="预计划">预计划</Radio>
                        <Radio value="计划">计划</Radio>
                    </Radio.Group>
                    <Button type="primary"
                            onClick={this.futureThreeDay.bind(this)}>未来三天</Button> &nbsp; &nbsp; &nbsp;
                    <Button type="primary" onClick={this.futureTenDay.bind(this)}>未来十天</Button><br/><br/>
                    起始日期:
                    <DatePicker key="start-date-datepicker"
                                value={moment(this.state.startDate1, dateFormat)}
                                format={dateFormat}
                                onChange={this.dateStartChange}
                                allowClear={false}
                    /><br/><br/>
                    结束日期:
                    <DatePicker key="end-date-datepicker"
                                value={moment(this.state.endDate1, dateFormat)}
                                format={dateFormat}
                                onChange={this.dateEndChange}
                                allowClear={false}
                    />
                </Modal>
                <Modal
                    key="simultaneous-modal"
                    title="同时率数据维护"
                    onOk={this.handleSimultaneousRate}
                    visible={this.state.simultaneousRateModalVisible}
                    onCancel={() => {
                        this.setState({simultaneousRateModalVisible: false})
                    }}
                    okText="保存"
                    cancelText="取消"
                >
                    <div style={{textAlign: "center"}}>
                        同时率:
                        <InputNumber value={this.state.simultaneousRate} onChange={this.simultaneousRateChange} step={0.001}></InputNumber>
                    </div>
                </Modal>
                <Modal
                    key="confirm-modal"
                    title="确认删除"
                    visible={this.state.confirmModalVisible}
                    onOk={this.onDelete}
                    onCancel={() => {
                        this.setState({confirmModalVisible: false})
                    }}
                    okText="确认"
                    cancelText="取消"
                >
                    确认删除该记录吗?
                </Modal>
            </div>
        );
    }
}

export default FutureTenDayForecastLoad;