基于 React + AntDesign4.x 实现的可编辑表格,可满足大部分使用场景

给大家推荐一个好用的程序员效率工具,代码生成、剪切板云同步、AI问答等实用功能在这里插入图片描述
以下为原文:
可编辑任意单元格,单元格类型可选:输入框、单选框、下拉列表、分组下拉列表

属性列表

参数说明类型默认值
style覆盖样式Object{}
columns表格列Array[]
onChange单元格内容修改回调function(Object)-
isAddRow是否允许新增行booleantrue
isDeleteRow是否允许删除行booleantrue
isSelectRows是否显示勾选框booleantrue
isSelections是否使用全选和反选单页勾选框booleanfalse
isSearch是否需要搜索booleanfalse
searchKeys搜索字段值Array[]
searchStyle搜索框样式Object{}
dataSource初始化数据Array{}
pagination是否分页booleantrue

columns中的属性列表

参数说明类型默认值
title单元格名称Object{}
dataIndex单元格属性名Array[]
type单元格类型input(输入框)/radio(单选框)/checkbox(复选框,对应列的值必须为boolean类型)/select(下拉列表)/groupSelect(分组下拉框)/button(按钮)-
defaultValue默认值Object{}
maxLength最大长度int100
data类型为radio/select等时需要设置选项的值Array[]
dataSource外部数据源[]-
width列宽number-
editable是否可编辑booleantrue

使用示例:


class Demo extends Component {

  search = data => {
    console.log(data);
  };

  render () {
        const radioData = [{
          label: '是',
          value: '1'
        }, {
          label: '否',
          value: '2'
        }];

        const selectData = [
          {
            label: '广东省', options: [
              { label: '广州市', value: '广州' },
              { label: '佛山市', value: '佛山' },
              { label: '深圳市', value: '深圳' }
            ]
          },
          {
            label: '湖北省', options: [
              { label: '武汉市', value: '武汉' },
              { label: '恩施市', value: '恩施' }
            ]
          }
        ];

        const columnsData = [
          {
            title: '姓名',
            dataIndex: 'name',
            type: 'input'
          },
          {
            title: '年龄',
            dataIndex: 'age',
            type: 'radio',
            defaultValue: '2',
            data: radioData,
          },
          {
            title: '地址',
            dataIndex: 'address',
            type: 'groupSelect',
            data: selectData,
            defaultValue: '广州',
          }
        ];

    return (
      <div style={{ margin: '40px', width: '100%' }}>
        <EditTable columns={columnsData}/>
      </div>
    );

  }
}


export default Demo;

如果觉得有用,顺便帮忙点个赞吧/爱你哟

源码如下:

1.EditTable.js
2.EditTable.less

import React, { useContext, useEffect, useRef, useState } from 'react';
import { Button, Checkbox, Form, Input, message, Popconfirm, Radio, Select, Table } from 'antd';
import styles from './EditTable.less';
import SearchInput from '@/components/Can/SearchInput';

const EditableContext = React.createContext();
const { Option, OptGroup } = Select;

const EditableRow = ({ index, ...props }) => {
  const [form] = Form.useForm();
  return (
    <Form form={form} component={false}>
      <EditableContext.Provider value={form}>
        <tr {...props} />
      </EditableContext.Provider>
    </Form>
  );
};

const EditableCell = ({
                        title,
                        editable,
                        type,
                        children,
                        dataIndex,
                        data,
                        width,
                        defaultValue,
                        maxLength,
                        record,
                        rowKey,
                        handleSave,
                        ...restProps
                      }) => {
  const [editing, setEditing] = useState(false);
  const inputRef = useRef();
  const form = useContext(EditableContext);
  useEffect(() => {
    if (editing) {
      inputRef.current.focus();
    }
  }, [editing]);

  const toggleEdit = () => {
    setEditing(!editing);
    form.setFieldsValue({
      [dataIndex]: record[dataIndex],
    });
  };

  const save = async e => {
    try {
      const values = await form.validateFields();
      handleSave({ ...record, ...values });
    } catch (errInfo) {
      console.log('保存单元格内容失败:', errInfo);
    }
  };

  const onChange = (value, dataIndex, maxLength) => {
    if (maxLength && value.length === maxLength) {
      message.error('输入内容达到最大可输入长度');
    }
    const data = {
      [dataIndex]: value,
    };
    handleSave({ ...record, ...data });
  };

  let childNode = children;
  if (editable) {
    if (record && record[dataIndex] !== undefined) {
      if ((typeof record[dataIndex]) === 'number') {
        defaultValue = parseInt(record[dataIndex]);
      } else {
        defaultValue = record[dataIndex];
      }
    }

    switch (type) {
      case 'input':
        childNode = (
          <Input
            defaultValue={defaultValue}
            style={width ? { width: width } : { display: 'flex' }}
            value={record[dataIndex]}
            maxLength={maxLength ? maxLength : 100}
            onPressEnter={save} onBlur={save}
            onChange={(e) => onChange(e.target.value, dataIndex, maxLength)}
          />);
        break;
      case 'select':
        const selectData = data;
        const selectChild = [];
        if (selectData && selectData.length > 0) {
          if (!defaultValue) {
            defaultValue = selectData[0].value;
          }
          selectData.forEach((item, index) => {
            selectChild.push(<Option value={item.value} key={index}>{item.label}</Option>);
          });
        }
        childNode = (<Select
          getPopupContainer={triggerNode => triggerNode.parentNode}
          defaultValue={defaultValue}
          style={width ? { width: width } : { display: 'flex' }}
          value={record[dataIndex]}
          onChange={(e) => onChange(e, dataIndex)}
        >
          {selectChild}
        </Select>);
        break;
      case 'groupSelect':
        const groupSelectData = data;
        const groupSelectChild = [];
        if (groupSelectData && groupSelectData.length > 0) {
          if (!defaultValue) {
            defaultValue = groupSelectData[0].options[0].value;
          }
          groupSelectData.forEach((item, index) => {
            const optGroup = <OptGroup label={item.label} key={`${item.label}_${index}`}>
              {item.options.map((option, index2) => {
                return <Option value={option.value} key={`${option.label}_${index}_${index2}`}>{option.label}</Option>;
              })}
            </OptGroup>;
            groupSelectChild.push(optGroup);
          });
        }
        childNode = (
          <Select
            getPopupContainer={triggerNode => triggerNode.parentNode}
            defaultValue={defaultValue}
            style={width ? { width: width } : { display: 'flex' }}
            value={record[dataIndex]}
            onChange={(e) => onChange(e, dataIndex)}
            showSearch
          >
            {groupSelectChild}
          </Select>
        );
        break;
      case 'radio':
        const radioData = data;
        const radioChild = [];
        if (radioData && radioData.length > 0) {
          if (defaultValue === undefined) {
            defaultValue = radioData[0].value;
          }
          radioData.forEach((item, index) => {
            radioChild.push(<Radio value={item.value} key={index}>{item.label}</Radio>);
          });
        }
        childNode = (<Radio.Group
          defaultValue={defaultValue}
          style={width ? { width: width } : { display: 'flex' }}
          value={record[dataIndex]}
          onChange={(e) =>
            onChange(e.target.value, dataIndex)}
        >
          {radioChild}
        </Radio.Group>);
        break;
      case 'checkbox':
        childNode = (<Checkbox
          checked={record[dataIndex]}
          style={width ? { width: width } : { display: 'flex' }}
          onChange={(e) =>
            onChange(e.target.checked, dataIndex)}
        />);
        break;
      case 'button':
        break;
      default:
        childNode = (<div
          className={styles['editable-cell-value-wrap']}
          style={{
            paddingRight: 24,
          }}
          onClick={toggleEdit}
        >
          {children}
        </div>);
    }
  }
  return <td {...restProps}>{childNode}</td>;
};

class EditTable extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      dataSource: props.dataSource ? props.dataSource : [],
      selectedRowKeys: [],
      searchValue: undefined,
      showSearch: false,
      searchResultKeys: [],
    };
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.dataSource !== this.props.dataSource) {
      this.state = {
        dataSource: nextProps.dataSource ? nextProps.dataSource : [],
        searchResultKeys: this.state.searchResultKeys,
        showSearch: this.state.showSearch,
        selectedRowKeys: this.state.selectedRowKeys,
        searchValue: this.state.searchValue,
      };
    }
  }

  handleDeletes = key => {
    const dataSource = [...this.state.dataSource];
    const newDataSource = [];
    dataSource.forEach(itemData => {
      let flag = false;
      key.forEach(keyItem => {
        if (itemData.key === keyItem) {
          flag = true;
        }
      });
      if (!flag) {
        newDataSource.push(itemData);
      }
    });
    this.setState({
      dataSource: newDataSource,
    });
    this.setState({
      selectedRowKeys: [],
    });
  };

  handleDelete = record => {
    if (this.props.handleDelete) {
      this.props.handleDelete(record);
    } else {
      const { key } = record;
      const dataSource = [...this.state.dataSource];
      const newDataSource = dataSource.filter(item => item.key !== key);
      this.setState({
        dataSource: newDataSource,
      });
      if (this.props.onChange) {
        this.props.onChange(newDataSource);
      }
    }
  };

  handleAdd = () => {
    if (this.props.handleAdd) {
      this.props.handleAdd();
    } else {
      const dataSource = [...this.state.dataSource];
      const newData = {};
      const newDataSource = [...dataSource, newData];
      this.setState({
        dataSource: newDataSource,
      });

      if (this.props.onChange) {
        this.props.onChange(newDataSource);
      }
    }
  };

  handleSave = row => {
    const { rowKey } = this.props;
    const newData = [...this.state.dataSource];
    const index = newData.findIndex(item => row[rowKey] === item[rowKey]);
    const item = newData[index];
    newData.splice(index, 1, { ...item, ...row });

    this.setState({
      dataSource: newData,
    });

    if (this.props.onChange) {
      this.props.onChange(newData);
    }
  };

  onSelectChange = keys => {
    this.setState({
      selectedRowKeys: keys,
    });

    if (this.props.onSelectChange) {
      this.props.onSelectChange(keys);
    }
  };

  onSearch = (value, opt) => {
    const { dataSource = [] } = this.state;
    const { rowKey } = this.props;

    if (value === undefined || value.trim().length === 0) {
      this.setState({
        showSearch: false,
        searchResultKeys: [],
      });
      return;
    }

    const result = [];
    if (dataSource && dataSource.length > 0) {
      dataSource.forEach(item => {
        const type = typeof item[opt];
        switch (type) {
          case 'string':
            if (item[opt] && item[opt].toLowerCase().indexOf(value.toLowerCase()) !== -1) {
              result.push(item[rowKey]);
            }
            break;
          case 'number':
            if (String(item[opt]).indexOf(value) !== -1) {
              result.push(item[rowKey]);
            }
        }
      });
    }

    this.setState({
      showSearch: true,
      searchResultKeys: result,
    });

  };

  render() {
    const {
      style,
      columns = [],
      isAddRow,
      isDeleteRow,
      isSelectRows = true,
      isSelections,
      isSearch,
      rowKey,
      searchKeys = [],
      searchStyle,
    } = this.props;

    const selectBefore = [];

    searchKeys.forEach(item => {
      const searchColumn = columns.filter(column => item === column.dataIndex);
      if (searchColumn && searchColumn.length > 0) {
        selectBefore.push({
          label: searchColumn[0].title,
          value: searchColumn[0].dataIndex,
        });
      }
    });

    const { searchResultKeys = [], showSearch } = this.state;
    let dataSource = [];
    if (showSearch) {
      searchResultKeys.forEach(key => {
        dataSource.push(this.state.dataSource.filter(item => item[rowKey] === key)[0]);
      });
    } else {
      this.state.dataSource.forEach(item => {
        dataSource.push(item);
      });
    }

    const components = {
      body: {
        row: EditableRow,
        cell: EditableCell,
      },
    };

    const columnsData = columns.map(col => {
      if (col.type === 'button') {
        return {
          ...col,
          onCell: record => ({
            record,
            width: col.width,
            editable: col.editable === undefined ? true : col.editable,
            title: col.title,
            type: col.type,
            render: col.render,
          }),
        };
      } else {
        return {
          ...col,
          onCell: record => ({
            record,
            rowKey: rowKey,
            width: col.width,
            maxLength: col.maxLength,
            editable: col.editable === undefined ? true : col.editable,
            dataIndex: col.dataIndex,
            title: col.title,
            handleSave: this.handleSave,
            type: col.type,
            data: col.data,
            defaultValue: col.defaultValue,
            ellipsis: col.ellipsis,
          }),
        };
      }
    });

    if (isDeleteRow) {
      columnsData.push({
        title: '操作',
        dataIndex: 'operation',
        width: 80,
        render: (text, record) =>
          dataSource.length > 0 ? (
            <Popconfirm title="确定要删除吗?" onConfirm={() => this.handleDelete(record)}>
              <a>删除</a>
            </Popconfirm>
          ) : null,
      });
    }

    const selectedRowKeys = this.state.selectedRowKeys;

    const rowSelection = {
      selectedRowKeys,
      onChange: this.onSelectChange,
      columnWidth: isSelections ? 65 : 50,
      preserveSelectedRowKeys: true,
      selections: isSelections ? [
          Table.SELECTION_ALL,
          Table.SELECTION_INVERT,
        ]
        : undefined,
    };

    return (
      <div style={{ ...style }} className={styles['table-wrapper']}>
        <div style={{ display: 'flex', justifyContent: 'space-between' }}>
          <div>
            {
              isAddRow && isAddRow === true ?
                <Button
                  onClick={this.handleAdd}
                  type="primary"
                  style={{
                    margin: '0px 10px 10px 0px',
                  }}
                >
                  添加
                </Button>
                : null
            }
            {
              isDeleteRow && selectedRowKeys && selectedRowKeys.length > 0 ?
                <Popconfirm title="确定删除吗?" onConfirm={() => this.handleDeletes(selectedRowKeys)}>
                  <Button
                    style={{
                      marginBottom: 10,
                    }}
                    danger
                  >
                    删除
                  </Button>
                </Popconfirm>
                : null
            }
          </div>
          <div style={{ textAlign: 'right', display: isSearch && searchKeys.length > 0 ? '' : 'none' }}>
            <SearchInput
              style={{ width: '400px', marginBottom: '10px', ...searchStyle }}
              addonBefore={selectBefore}
              placeholder="关键字搜索"
              onSearch={this.onSearch}
            />
          </div>
        </div>
        <Table
          rowSelection={isSelectRows ? rowSelection : null}
          rowKey={rowKey}
          components={components}
          rowClassName={() => styles['editable-row']}
          bordered
          dataSource={dataSource}
          columns={columnsData}
          style={{ width: '100%' }}
          size={this.props.size || 'middle'}
          pagination={this.props.pagination === false ? false
            : (this.props.pagination || {
              size: 'middle',
              hideOnSinglePage: true,
              showSizeChanger: false,
            })}
          scroll={this.props.scroll}
        />
      </div>
    );
  }

}

export default EditTable;

.editable-cell {
  position: relative;
}

.editable-cell-value-wrap {
  padding: 5px 12px;
  cursor: pointer;
}

.editable-row:hover .editable-cell-value-wrap {
  min-height: 40px;
  border: 1px solid #d9d9d9;
  border-radius: 4px;
  padding: 4px 11px;
}

[data-theme='dark'] .editable-row:hover .editable-cell-value-wrap {
  min-height: 40px;
  border: 1px solid #434343;
}

.table-wrapper {

  :global {
    .ant-table.ant-table-middle .ant-table-tbody > tr > td {
      padding: 6px 6px !important;
      height: 41px;
    }

  }
}

  • 6
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
好的,下面是一个简单的实现示例: ```javascript import React from "react"; import { Table } from "antd"; const columns1 = [ { title: "姓名", dataIndex: "name", key: "name" }, { title: "年龄", dataIndex: "age", key: "age" }, { title: "住址", dataIndex: "address", key: "address" }, ]; const data1 = [ { key: "1", name: "John Brown", age: 32, address: "New York No. 1 Lake Park" }, { key: "2", name: "Jim Green", age: 42, address: "London No. 1 Lake Park" }, { key: "3", name: "Joe Black", age: 32, address: "Sidney No. 1 Lake Park" }, ]; const columns2 = [ { title: "科目", dataIndex: "subject", key: "subject" }, { title: "成绩", dataIndex: "score", key: "score" }, ]; const data2 = [ { key: "1", subject: "数学", score: 90 }, { key: "2", subject: "英语", score: 80 }, { key: "3", subject: "语文", score: 95 }, ]; const expandedRowRender = (record) => { const subTableData = [ { key: "1", subject: "数学", score: 90 }, { key: "2", subject: "英语", score: 80 }, { key: "3", subject: "语文", score: 95 }, ]; const subTableColumns = [ { title: "科目", dataIndex: "subject", key: "subject" }, { title: "成绩", dataIndex: "score", key: "score" }, ]; return <Table columns={subTableColumns} dataSource={subTableData} pagination={false} />; }; const TableWithNestedTable = () => { return ( <Table columns={columns1} dataSource={data1} pagination={false} expandedRowRender={expandedRowRender} /> ); }; export default TableWithNestedTable; ``` 首先,我们先定义两个表格的列和数据,分别为 `columns1` 和 `data1`,以及嵌套的表格的列和数据,分别为 `columns2` 和 `data2`。 然后,我们定义一个 `expandedRowRender` 函数,该函数接受一个参数 `record`,表示当前行的数据。在该函数中,我们返回一个嵌套的表格,该表格的列和数据为 `columns2` 和 `data2`。 最后,我们在主表格中添加 `expandedRowRender` 属性,并将其值设置为 `expandedRowRender` 函数,这样就可以实现表格嵌套两层表格的效果了。 注意,我们在嵌套的表格中将 `pagination` 属性设置为 `false`,以禁用分页功能。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值