Ant Design可编辑单元格表格改进版

基于ant design@4 版本

Posted by wangtiegang on March 1, 2020

之前在空闲的时候按照 antd 官方的可编辑单元格的示例改进实现过一个 demo ,然后马马虎虎能用,自己也弄明白了实现的原理。然后没有实际的项目,就没再继续改进了。

最近真的要用 antd pro 做项目了,突然发现几个月不看,已经变成 hook 风格了,前端变化也太快了,有点看不懂,又花了些时间学习才找回来感觉。。然后在实际项目中需要可编辑单元格,又去 ant 官网看最新 @4 版本的示例,弄明白之后,结合 hook 进行改造增强,实现了一个比上次好一丢丢的版本,虽然还存在很多可以改进的地方,但是项目进度赶,只能先出成果,后续再进行改进了。

实现

此次的可编辑表格具有以下特点

  • 采用函数式组件和 hook
  • 使用了最新 ant@4 版本的 Form 特性
  • 支持配置单元格编辑组件类型,目前支持字符串,数字,日期,下拉框,可根据需要扩展

EditableTable 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
import React, { useContext, useState, useEffect, useRef } from "react";
import { Table, Input, Form, Select, InputNumber } from "antd";
import styles from "./index.less";
import StringDatePicker from "../StringDatePicker";

const EditableContext = React.createContext();

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,
  editor,
  children,
  dataIndex,
  rules,
  record,
  handleSave,
  ...restProps
}) => {
  const [editing, setEditing] = useState(false);
  const inputRef = useRef();
  const form = useContext(EditableContext);
  useEffect(() => {
    if (editing) {
      if (inputRef.current) {
        inputRef.current.focus();
      }
    }
  }, [editing]);

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

  const save = async () => {
    try {
      const values = await form.validateFields();
      toggleEdit();
      handleSave({ ...record, ...values });
    } catch (errInfo) {
      // eslint-disable-next-line no-console
      console.log("行保存失败:", errInfo);
    }
  };

  let childNode = children;

  if (editable) {
    // 根据传递过来的 column 属性判断使用哪种输入组件
    let editorNode;
    if (editor.type === "string") {
      editorNode = (
        <Input
          ref={inputRef}
          style=
          onPressEnter={save}
          onBlur={save}
        />
      );
    } else if (editor.type === "number") {
      editorNode = (
        <InputNumber
          style=
          autoFocus
          onPressEnter={save}
          onBlur={save}
        />
      );
    } else if (editor.type === "dropdown") {
      editorNode = (
        <Select
          style=
          autoFocus
          showSearch
          optionFilterProp="label"
          options={editor.options}
          onSelect={save}
          onBlur={save}
        />
      );
    } else if (editor.type === "date") {
      editorNode = (
        // StringDatePicker 是自定义的日期选择组件,直接返回字符串类型的值
        <StringDatePicker
          style=
          format="YYYY/MM/DD"
          autoFocus
          onStringChange={save}
          onBlur={save}
        />
      );
    } else {
      editorNode = "";
    }

    childNode = editing ? (
      <Form.Item
        style=
        name={dataIndex}
        rules={rules}
      >
        {editorNode}
      </Form.Item>
    ) : (
      <div
        className={styles.editableCellValueWrap}
        style=
        onClick={toggleEdit}
      >
        {children}
      </div>
    );
  }

  return <td {...restProps}>{childNode}</td>;
};

const EditableTable = props => {
  const { columns, handleSave } = props;

  // 通过onCell设置单元格属性
  const newColumns = columns.map(col => {
    if (!col.editable) {
      return col;
    }

    return {
      ...col,
      onCell: record => ({
        record,
        editable: col.editable,
        editor: col.editor,
        dataIndex: col.dataIndex,
        title: col.title,
        rules: col.rules,
        handleSave
      })
    };
  });

  // 替换原生的row和cell组件
  const components = {
    body: {
      row: EditableRow,
      cell: EditableCell
    }
  };

  const newProps = { ...props, columns: newColumns, components };

  return <Table {...newProps} rowClassName={styles.editableRow} />;
};

export default EditableTable;

样式表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.editable-cell {
  position: relative;
}

.editableCellValueWrap {
  padding: 5px 12px;
  cursor: pointer;
}

.editableRow:hover .editableCellValueWrap {
  min-height: 32px;
  padding: 4px 11px;
  border: 1px solid #d9d9d9;
  border-radius: 4px;
}

[data-theme="dark"] .editableRow:hover .editableCellValueWrap {
  min-height: 32px;
  border: 1px solid #434343;
}

使用方式

由于没有使用 TypeScript ,不好继承和对属性进行限制,所以在使用的时候必须自己注意传递必须的属性,除了 columns 属性变化了,需要更多信息外,其他属性跟原生的 Table 组件一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
const TestComponent = () => {
  // 新增时赋值 id
  const [count, setCount] = useState(1);
  // 表格数据
  const [dataSource, setDataSource] = useState();

  // 新增行
  const handleAdd = () => {
    const newData = {
      id: `_add_${count}`,
      __status: "add"
    };
    setDataSource([newData, ...dataSource]);
    setCount(count + 1);
  };
  
  // 列的属性增加了 editable,editor,rules
  const columns = [
    {
      ellipsis: true,
      title: "公司名称",
      dataIndex: "companyName",
      width: "300px",
      editable: true, // 是否可编辑
      editor: {
        type: "string" // 支持 string,number,dorpdown,date,可自行扩展
      }, // 当可编辑时,必须提供 editor 属性
      rules: [
        {
          required: true,
          message: "公司名称必填"
        }
      ] // 用来编辑时校验,同 form rules配置
    },
    {
      ellipsis: true,
      title: "公司编码",
      dataIndex: "companyCode",
      width: "300px",
      editable: true,
      editor: {
        type: "string"
      }
    }
  ];

  // 单元格编辑后的保存函数
  const handleRowSave = row => {
    const newRow = row;
    if (row.__status !== "add") {
      newRow.__status = "update";
    }
    const newDataSource = [...dataSource];
    const index = newDataSource.findIndex(item => newRow.id === item.id);
    const item = newDataSource[index];
    newDataSource.splice(index, 1, { ...item, ...newRow });
    setDataSource(newDataSource);
  };

  return (
    // EditableTable 可以接受所有 Table 组件的属性
    <EditableTable
      size="small"
      rowKey="id"
      columns={columns}
      dataSource={dataSource}
      handleSave={handleRowSave}
      rowSelection={rowSelection}
      pagination={pagination}
    />
  );
};

export default TestComponent;

以上就是全部了,使用代码只是一个很简单粗糙的示例,实际使用中需要定义增删改查方法,结合 hook 就可实现从数据库读取数据进行操作了,同时也可以定义表格的翻页筛选组件,这个就跟原生的 Table 使用方式一样了。

可编辑单元格表格在实际项目中使用非常频繁,后续可以把增删改查封装进去,使用的时候只传递一组 url 和 dataSource 的 Fields 就好了,这样可以简化很多工作量,不过现阶段水平时间有限,只能更熟练后进行重构了